chore(release): 版本发布 - publish v1.0.11
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -521,6 +522,48 @@ func RegisterAdminRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
}
|
||||
ok(c, logs)
|
||||
})
|
||||
r.GET("/admin/api-logs/:id/media", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
stream, err := svc.OpenAdminAPICallLogMediaRange(user, c.Param("id"), c.GetHeader("Range"))
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
defer stream.Body.Close()
|
||||
resource := stream.Resource
|
||||
mimeType := resource.MimeType
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
c.Header("Cache-Control", "private, no-cache")
|
||||
c.Header("Accept-Ranges", "bytes")
|
||||
c.Header("X-Content-Type-Options", "nosniff")
|
||||
if c.Query("download") == "1" {
|
||||
extension := filepath.Ext(resource.ObjectKey)
|
||||
if len(extension) > 12 {
|
||||
extension = ""
|
||||
}
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=api-log-media%s", extension))
|
||||
}
|
||||
if resource.Provider == "local" {
|
||||
if seeker, ok := stream.Body.(io.ReadSeeker); ok {
|
||||
c.Header("Content-Type", mimeType)
|
||||
http.ServeContent(c.Writer, c.Request, resource.ID, resource.UpdatedAt, seeker)
|
||||
return
|
||||
}
|
||||
}
|
||||
if stream.ContentRange != "" {
|
||||
c.Header("Content-Range", stream.ContentRange)
|
||||
}
|
||||
if stream.AcceptRanges != "" {
|
||||
c.Header("Accept-Ranges", stream.AcceptRanges)
|
||||
}
|
||||
c.DataFromReader(stream.StatusCode, stream.ContentLength, mimeType, stream.Body, nil)
|
||||
})
|
||||
r.GET("/admin/api-logs/:id", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
|
||||
@@ -253,6 +253,8 @@ type ApiCallLog struct {
|
||||
CachedTokens int64 `json:"cachedTokens"`
|
||||
UsageAvailable bool `json:"usageAvailable"`
|
||||
MediaCount int `json:"mediaCount"`
|
||||
MediaPreviewURL string `json:"mediaPreviewUrl,omitempty" gorm:"-"`
|
||||
MediaPreviewKind string `json:"mediaPreviewKind,omitempty" gorm:"-"`
|
||||
VideoSeconds int `json:"videoSeconds"`
|
||||
ProviderRequestID string `json:"providerRequestId" gorm:"size:160"`
|
||||
EstimatedCostMicros int64 `json:"estimatedCostMicros"`
|
||||
|
||||
@@ -113,6 +113,30 @@ func (r *Repository) QueryAPICallLogs(filter APICallLogFilter) ([]model.ApiCallL
|
||||
if filter.Limit <= 0 || filter.Limit > 200 {
|
||||
filter.Limit = 50
|
||||
}
|
||||
var total int64
|
||||
// GORM 会把 Distinct 状态保留在同一查询链;计数和分页必须分别构造,避免 PostgreSQL 的 DISTINCT + ORDER BY 冲突。
|
||||
if err := r.filteredAPICallLogQuery(filter).Model(&model.ApiCallLog{}).Distinct("api_call_logs.id").Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var logs []model.ApiCallLog
|
||||
err := r.filteredAPICallLogQuery(filter).Omit("RequestBody", "ResponseBody").Order("api_call_logs.created_at desc").Offset((filter.Page - 1) * filter.Limit).Limit(filter.Limit).Find(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
func (r *Repository) ExportAPICallLogs(filter APICallLogFilter, limit int) ([]model.ApiCallLog, error) {
|
||||
if limit <= 0 || limit > 10_000 {
|
||||
limit = 10_000
|
||||
}
|
||||
query := r.filteredAPICallLogQuery(filter)
|
||||
if len(filter.IDs) > 0 {
|
||||
query = query.Where("api_call_logs.id IN ?", filter.IDs)
|
||||
}
|
||||
var logs []model.ApiCallLog
|
||||
err := query.Omit("RequestBody", "ResponseBody").Order("api_call_logs.created_at desc").Limit(limit).Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
||||
|
||||
func (r *Repository) filteredAPICallLogQuery(filter APICallLogFilter) *gorm.DB {
|
||||
query := visibleAPICallLogQuery(r.apiCallLogQuery(filter.AnalyticsFilter))
|
||||
if value := strings.TrimSpace(filter.Keyword); value != "" {
|
||||
pattern := "%" + strings.ToLower(value) + "%"
|
||||
@@ -127,36 +151,16 @@ func (r *Repository) QueryAPICallLogs(filter APICallLogFilter) ([]model.ApiCallL
|
||||
if filter.Status != "" {
|
||||
query = query.Where("api_call_logs.status = ?", filter.Status)
|
||||
}
|
||||
var total int64
|
||||
if err := query.Model(&model.ApiCallLog{}).Distinct("api_call_logs.id").Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var logs []model.ApiCallLog
|
||||
err := query.Omit("RequestBody", "ResponseBody").Order("api_call_logs.created_at desc").Offset((filter.Page - 1) * filter.Limit).Limit(filter.Limit).Find(&logs).Error
|
||||
return logs, total, err
|
||||
return query
|
||||
}
|
||||
|
||||
func (r *Repository) ExportAPICallLogs(filter APICallLogFilter, limit int) ([]model.ApiCallLog, error) {
|
||||
if limit <= 0 || limit > 10_000 {
|
||||
limit = 10_000
|
||||
func (r *Repository) APICallLogTasks(ids []string) ([]model.Task, error) {
|
||||
if len(ids) == 0 {
|
||||
return []model.Task{}, nil
|
||||
}
|
||||
query := visibleAPICallLogQuery(r.apiCallLogQuery(filter.AnalyticsFilter))
|
||||
if len(filter.IDs) > 0 {
|
||||
query = query.Where("api_call_logs.id IN ?", filter.IDs)
|
||||
}
|
||||
if value := strings.TrimSpace(filter.Keyword); value != "" {
|
||||
pattern := "%" + strings.ToLower(value) + "%"
|
||||
query = query.Joins("LEFT JOIN users ON users.id = api_call_logs.user_id").Joins("LEFT JOIN model_channels ON model_channels.id = api_call_logs.channel_id").Where(
|
||||
"lower(api_call_logs.user_id) LIKE ? OR lower(users.username) LIKE ? OR lower(users.display_name) LIKE ? OR lower(api_call_logs.channel_id) LIKE ? OR lower(model_channels.name) LIKE ? OR lower(api_call_logs.model) LIKE ? OR lower(api_call_logs.path) LIKE ? OR lower(api_call_logs.provider_request_id) LIKE ?",
|
||||
pattern, pattern, pattern, pattern, pattern, pattern, pattern, pattern,
|
||||
)
|
||||
}
|
||||
if filter.Status != "" {
|
||||
query = query.Where("api_call_logs.status = ?", filter.Status)
|
||||
}
|
||||
var logs []model.ApiCallLog
|
||||
err := query.Omit("RequestBody", "ResponseBody").Order("api_call_logs.created_at desc").Limit(limit).Find(&logs).Error
|
||||
return logs, err
|
||||
var tasks []model.Task
|
||||
err := r.db.Select("id", "user_id", "type", "result_json").Where("id IN ?", ids).Find(&tasks).Error
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
func visibleAPICallLogQuery(query *gorm.DB) *gorm.DB {
|
||||
|
||||
@@ -228,6 +228,26 @@ func (s *Service) decorateAPICallLogs(logs []model.ApiCallLog) error {
|
||||
for _, user := range users {
|
||||
userByID[user.ID] = user
|
||||
}
|
||||
taskIDs := make([]string, 0, len(logs))
|
||||
seenTaskIDs := make(map[string]struct{}, len(logs))
|
||||
for _, log := range logs {
|
||||
if (log.Capability != "image" && log.Capability != "video") || log.TaskID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seenTaskIDs[log.TaskID]; exists {
|
||||
continue
|
||||
}
|
||||
seenTaskIDs[log.TaskID] = struct{}{}
|
||||
taskIDs = append(taskIDs, log.TaskID)
|
||||
}
|
||||
tasks, err := s.repo.APICallLogTasks(taskIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskByID := make(map[string]model.Task, len(tasks))
|
||||
for _, task := range tasks {
|
||||
taskByID[task.ID] = task
|
||||
}
|
||||
for index := range logs {
|
||||
if logs[index].StartedAt.IsZero() {
|
||||
logs[index].StartedAt = logs[index].CreatedAt
|
||||
@@ -243,10 +263,51 @@ func (s *Service) decorateAPICallLogs(logs []model.ApiCallLog) error {
|
||||
logs[index].UserDisplayName = user.DisplayName
|
||||
logs[index].UserAccount = user.Username
|
||||
}
|
||||
if task, exists := taskByID[logs[index].TaskID]; exists && task.UserID == logs[index].UserID {
|
||||
previewURL, previewKind := taskMediaPreview(task.ResultJSON, task.Type)
|
||||
if canvasResourceID(previewURL) != "" {
|
||||
logs[index].MediaPreviewURL = "/api/admin/api-logs/" + logs[index].ID + "/media"
|
||||
logs[index].MediaPreviewKind = previewKind
|
||||
} else if strings.HasPrefix(previewURL, "https://") || strings.HasPrefix(previewURL, "http://") {
|
||||
logs[index].MediaPreviewURL = previewURL
|
||||
logs[index].MediaPreviewKind = previewKind
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 管理员媒体读取必须同时校验日志、任务和资源归属,不能绕过用户资源边界按资源 ID 任意读取。
|
||||
func (s *Service) OpenAdminAPICallLogMediaRange(actor *model.User, logID string, rangeHeader string) (*ResourceStream, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log, err := s.repo.APICallLog(strings.TrimSpace(logID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if log.TaskID == "" || (log.Capability != "image" && log.Capability != "video") {
|
||||
return nil, BadAuthRequest("该请求没有可预览媒体")
|
||||
}
|
||||
task, err := s.repo.Task(log.TaskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if task.UserID != log.UserID {
|
||||
return nil, BadAuthRequest("请求与媒体归属不一致")
|
||||
}
|
||||
previewURL, _ := taskMediaPreview(task.ResultJSON, task.Type)
|
||||
resourceID := canvasResourceID(previewURL)
|
||||
if resourceID == "" {
|
||||
return nil, BadAuthRequest("该请求没有已持久化媒体")
|
||||
}
|
||||
resource, err := s.repo.ResourceForUser(log.UserID, resourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.openResourceRange(log.UserID, resource, rangeHeader)
|
||||
}
|
||||
|
||||
func (s *Service) AdminAPICallLog(actor *model.User, id string) (*model.ApiCallLog, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -165,6 +165,10 @@ func (s *Service) OpenResourceRange(userID string, id string, rangeHeader string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.openResourceRange(userID, resource, rangeHeader)
|
||||
}
|
||||
|
||||
func (s *Service) openResourceRange(userID string, resource *model.Resource, rangeHeader string) (*ResourceStream, error) {
|
||||
if resource.Status != model.ResourceStatusReady {
|
||||
return nil, BadAuthRequest("资源尚未上传完成")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user