From 4d1a01ec71a4fa3dde685ff2fd6360f906ccd59c Mon Sep 17 00:00:00 2001 From: wangbo Date: Sun, 7 Jun 2026 19:29:04 +0800 Subject: [PATCH] Expire local static assets after 24 hours --- .../api/internal/httpapi/local_temp_assets.go | 34 ++- .../httpapi/request_preparation_test.go | 49 ++-- apps/api/internal/runner/upload.go | 233 +++++++++++++++++- apps/api/internal/runner/upload_test.go | 99 ++++++++ .../src/pages/admin/SystemSettingsPanel.tsx | 10 +- 5 files changed, 395 insertions(+), 30 deletions(-) diff --git a/apps/api/internal/httpapi/local_temp_assets.go b/apps/api/internal/httpapi/local_temp_assets.go index de71e86..1a31ce9 100644 --- a/apps/api/internal/httpapi/local_temp_assets.go +++ b/apps/api/internal/httpapi/local_temp_assets.go @@ -29,9 +29,35 @@ func (s *Server) startLocalTempAssetCleanup(ctx context.Context) { } func (s *Server) cleanupExpiredLocalTempAssets(ctx context.Context, now time.Time) int { - storageDir := strings.TrimSpace(s.cfg.LocalUploadedStorageDir) + targets := []localTempAssetCleanupTarget{ + { + StorageDir: s.cfg.LocalGeneratedStorageDir, + FallbackDir: config.DefaultLocalGeneratedStorageDir, + MarkRequestAsset: false, + }, + { + StorageDir: s.cfg.LocalUploadedStorageDir, + FallbackDir: config.DefaultLocalUploadedStorageDir, + MarkRequestAsset: true, + }, + } + deleted := 0 + for _, target := range targets { + deleted += s.cleanupExpiredLocalTempAssetsInDir(ctx, now, target) + } + return deleted +} + +type localTempAssetCleanupTarget struct { + StorageDir string + FallbackDir string + MarkRequestAsset bool +} + +func (s *Server) cleanupExpiredLocalTempAssetsInDir(ctx context.Context, now time.Time, target localTempAssetCleanupTarget) int { + storageDir := strings.TrimSpace(target.StorageDir) if storageDir == "" { - storageDir = config.DefaultLocalUploadedStorageDir + storageDir = target.FallbackDir } entries, err := os.ReadDir(storageDir) if err != nil { @@ -44,7 +70,7 @@ func (s *Server) cleanupExpiredLocalTempAssets(ctx context.Context, now time.Tim expiredBefore := now.Add(-ttl) deleted := 0 for _, entry := range entries { - if entry.IsDir() || !strings.HasPrefix(entry.Name(), requestAssetFilePrefix) { + if entry.IsDir() { continue } info, err := entry.Info() @@ -60,7 +86,7 @@ func (s *Server) cleanupExpiredLocalTempAssets(ctx context.Context, now time.Tim continue } deleted++ - if s.store != nil { + if target.MarkRequestAsset && strings.HasPrefix(entry.Name(), requestAssetFilePrefix) && s.store != nil { if err := s.store.MarkRequestAssetExpiredByLocalPath(ctx, localPath, now); err != nil && !store.IsUndefinedDatabaseObject(err) { s.logger.Warn("mark local temp asset expired failed", "path", localPath, "error", err) } diff --git a/apps/api/internal/httpapi/request_preparation_test.go b/apps/api/internal/httpapi/request_preparation_test.go index 61dbe0e..dd8bda3 100644 --- a/apps/api/internal/httpapi/request_preparation_test.go +++ b/apps/api/internal/httpapi/request_preparation_test.go @@ -75,42 +75,51 @@ func TestCanonicalConversationMessageHashUsesTextAndAssetRefs(t *testing.T) { } } -func TestCleanupExpiredLocalTempAssetsOnlyDeletesExpiredPrefixedFiles(t *testing.T) { - storageDir := t.TempDir() - oldTemp := filepath.Join(storageDir, requestAssetFilePrefix+"old.png") - freshTemp := filepath.Join(storageDir, requestAssetFilePrefix+"fresh.png") - oldGenerated := filepath.Join(storageDir, "gateway-result-old.png") - for _, path := range []string{oldTemp, freshTemp, oldGenerated} { +func TestCleanupExpiredLocalTempAssetsDeletesExpiredStaticFiles(t *testing.T) { + uploadedDir := t.TempDir() + generatedDir := t.TempDir() + oldUploaded := filepath.Join(uploadedDir, requestAssetFilePrefix+"old.png") + freshUploaded := filepath.Join(uploadedDir, requestAssetFilePrefix+"fresh.png") + oldGenerated := filepath.Join(generatedDir, "gateway-result-old.png") + freshGenerated := filepath.Join(generatedDir, "gateway-result-fresh.png") + for _, path := range []string{oldUploaded, freshUploaded, oldGenerated, freshGenerated} { if err := os.WriteFile(path, []byte("asset"), 0o644); err != nil { t.Fatalf("write fixture %s: %v", path, err) } } now := time.Now() - if err := os.Chtimes(oldTemp, now.Add(-25*time.Hour), now.Add(-25*time.Hour)); err != nil { - t.Fatalf("touch old temp: %v", err) + for _, path := range []string{oldUploaded, oldGenerated} { + if err := os.Chtimes(path, now.Add(-25*time.Hour), now.Add(-25*time.Hour)); err != nil { + t.Fatalf("touch old static asset %s: %v", path, err) + } } - if err := os.Chtimes(freshTemp, now.Add(-23*time.Hour), now.Add(-23*time.Hour)); err != nil { - t.Fatalf("touch fresh temp: %v", err) - } - if err := os.Chtimes(oldGenerated, now.Add(-25*time.Hour), now.Add(-25*time.Hour)); err != nil { - t.Fatalf("touch old generated: %v", err) + for _, path := range []string{freshUploaded, freshGenerated} { + if err := os.Chtimes(path, now.Add(-23*time.Hour), now.Add(-23*time.Hour)); err != nil { + t.Fatalf("touch fresh static asset %s: %v", path, err) + } } server := &Server{ - cfg: config.Config{LocalUploadedStorageDir: storageDir, LocalTempAssetTTLHours: 24}, + cfg: config.Config{ + LocalGeneratedStorageDir: generatedDir, + LocalUploadedStorageDir: uploadedDir, + LocalTempAssetTTLHours: 24, + }, logger: slog.New(slog.NewTextHandler(io.Discard, nil)), } deleted := server.cleanupExpiredLocalTempAssets(context.Background(), now) - if deleted != 1 { - t.Fatalf("expected one expired temp asset delete, got %d", deleted) + if deleted != 2 { + t.Fatalf("expected two expired static asset deletes, got %d", deleted) } - if _, err := os.Stat(oldTemp); !os.IsNotExist(err) { - t.Fatalf("old prefixed temp asset should be deleted, stat err=%v", err) + for _, path := range []string{oldUploaded, oldGenerated} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("old static asset should be deleted %s, stat err=%v", path, err) + } } - for _, path := range []string{freshTemp, oldGenerated} { + for _, path := range []string{freshUploaded, freshGenerated} { if _, err := os.Stat(path); err != nil { - t.Fatalf("non-expired or non-prefixed file should remain %s: %v", path, err) + t.Fatalf("fresh static asset should remain %s: %v", path, err) } } } diff --git a/apps/api/internal/runner/upload.go b/apps/api/internal/runner/upload.go index 1eb6ad1..642f9f2 100644 --- a/apps/api/internal/runner/upload.go +++ b/apps/api/internal/runner/upload.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/rand" + "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" @@ -76,6 +77,7 @@ func defaultGeneratedAssetUploadPolicy() generatedAssetUploadPolicy { func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, taskKind string, result map[string]any) (map[string]any, error) { data, _ := result["data"].([]any) if len(data) == 0 { + redactGeneratedResultRawData(result) return result, nil } policy, err := s.generatedAssetUploadPolicy(ctx) @@ -103,6 +105,7 @@ func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, task } } if !needsUpload && !changed { + redactGeneratedResultRawData(result) return result, nil } var channels []store.FileStorageChannel @@ -165,6 +168,9 @@ func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, task if kind == "image" { merged["image_url"] = urlValue } + if kind == "audio" { + merged["audio_url"] = urlValue + } } if kind != "" && stringFromAny(merged["type"]) == "" { merged["type"] = kind @@ -176,9 +182,185 @@ func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, task nextData = append(nextData, merged) } next["data"] = nextData + redactGeneratedResultRawData(next) return next, nil } +func redactGeneratedResultRawData(result map[string]any) bool { + if result == nil { + return false + } + changed := false + for _, key := range []string{"raw_data", "rawData"} { + value, ok := result[key] + if !ok || value == nil { + continue + } + next, nextChanged := redactGeneratedRawDataValue(value, key, nil) + if nextChanged { + result[key] = next + changed = true + } + } + return changed +} + +func redactGeneratedRawDataValue(value any, key string, siblings map[string]any) (any, bool) { + switch typed := value.(type) { + case map[string]any: + next := make(map[string]any, len(typed)) + changed := false + for childKey, childValue := range typed { + redacted, childChanged := redactGeneratedRawDataValue(childValue, childKey, typed) + next[childKey] = redacted + if childChanged { + changed = true + } + } + if changed { + return next, true + } + return value, false + case []any: + next := make([]any, len(typed)) + changed := false + for index, item := range typed { + redacted, itemChanged := redactGeneratedRawDataValue(item, key, siblings) + next[index] = redacted + if itemChanged { + changed = true + } + } + if changed { + return next, true + } + return value, false + case string: + redacted, ok := generatedRawDataMediaRedaction(key, typed, siblings) + if ok { + return redacted, true + } + return value, false + default: + return value, false + } +} + +func generatedRawDataMediaRedaction(key string, value string, siblings map[string]any) (map[string]any, bool) { + raw := strings.TrimSpace(value) + if raw == "" { + return nil, false + } + contentType := firstNonEmptyString(mediaContentTypeFromItem(siblings), defaultContentTypeForRawMediaKey(key)) + if strings.HasPrefix(strings.ToLower(raw), "data:") { + declared, encoded, ok, err := parseBase64DataURL(raw) + if err != nil || !ok || !generatedContentTypeIsMedia(declared) { + return nil, false + } + payload, err := decodeBase64Payload(encoded) + if err != nil { + return nil, false + } + return generatedRawDataRedaction("data_url", declared, payload, raw), true + } + if len(raw) < 128 { + return nil, false + } + keyLooksLikeMediaPayload := generatedRawDataMediaPayloadKey(key) + if !keyLooksLikeMediaPayload && !generatedContentTypeIsMedia(contentType) { + return nil, false + } + if keyLooksLikeMediaPayload && looksHexMediaPayload(raw) { + normalized := removeASCIIWhitespace(raw) + payload, err := hex.DecodeString(normalized) + if err == nil && len(payload) > 0 { + return generatedRawDataRedaction("hex", contentType, payload, raw), true + } + } + if payload, err := decodeBase64Payload(raw); err == nil && len(payload) > 0 { + return generatedRawDataRedaction("base64", contentType, payload, raw), true + } + if keyLooksLikeMediaPayload && len(raw) > 2048 { + return generatedRawDataRedaction("unknown", contentType, nil, raw), true + } + return nil, false +} + +func generatedRawDataMediaPayloadKey(key string) bool { + normalized := strings.ToLower(strings.TrimSpace(key)) + normalized = strings.ReplaceAll(normalized, "-", "_") + return normalized == "audio" || + normalized == "image" || + normalized == "video" || + normalized == "b64_json" || + normalized == "base64" || + normalized == "b64" || + normalized == "binary_data_base64" || + strings.Contains(normalized, "base64") || + strings.Contains(normalized, "_b64") || + strings.Contains(normalized, "audio_data") || + strings.Contains(normalized, "image_data") || + strings.Contains(normalized, "video_data") +} + +func defaultContentTypeForRawMediaKey(key string) string { + normalized := strings.ToLower(strings.TrimSpace(key)) + if strings.Contains(normalized, "audio") { + return "audio/mpeg" + } + if strings.Contains(normalized, "video") { + return "video/mp4" + } + if strings.Contains(normalized, "image") || strings.Contains(normalized, "b64_json") { + return "image/png" + } + return "" +} + +func generatedRawDataRedaction(encoding string, contentType string, payload []byte, raw string) map[string]any { + out := map[string]any{ + "redacted": true, + "reason": "inline_media_payload", + "encoding": encoding, + } + if contentType = normalizeGeneratedContentType(contentType); contentType != "" { + out["contentType"] = contentType + } + if len(payload) > 0 { + digest := sha256.Sum256(payload) + out["size"] = len(payload) + out["sha256"] = hex.EncodeToString(digest[:]) + } else { + out["size"] = len(raw) + } + return out +} + +func looksHexMediaPayload(value string) bool { + normalized := removeASCIIWhitespace(value) + if len(normalized) < 128 || len(normalized)%2 != 0 { + return false + } + for _, item := range normalized { + if (item >= '0' && item <= '9') || (item >= 'a' && item <= 'f') || (item >= 'A' && item <= 'F') { + continue + } + return false + } + return true +} + +func removeASCIIWhitespace(value string) string { + return strings.Map(func(r rune) rune { + switch r { + case '\n', '\r', '\t', ' ': + return -1 + default: + return r + } + }, value) +} + func (s *Service) generatedAssetUploadPolicy(ctx context.Context) (generatedAssetUploadPolicy, error) { settings, err := s.store.GetFileStorageSettings(ctx) if err != nil { @@ -282,11 +464,13 @@ func (s *Service) storeFileLocally(payload FileUploadPayload, storageDir string, _ = os.Remove(targetPath) return nil, &clients.ClientError{Code: "local_static_store_failed", Message: closeErr.Error(), Retryable: true} } + expiresAt := time.Now().Add(time.Duration(s.localStaticAssetTTLHours()) * time.Hour).UTC().Format(time.RFC3339) return map[string]any{ "url": s.localStaticFileURL(fileName, pathPrefix), "fileName": fileName, "contentType": payload.ContentType, "size": len(payload.Bytes), + "expiresAt": expiresAt, "storageChannel": map[string]any{ "id": "local-static", "channelKey": "local-static", @@ -296,6 +480,13 @@ func (s *Service) storeFileLocally(payload FileUploadPayload, storageDir string, }, nil } +func (s *Service) localStaticAssetTTLHours() int { + if s.cfg.LocalTempAssetTTLHours <= 0 { + return 24 + } + return s.cfg.LocalTempAssetTTLHours +} + func (s *Service) localStaticFileURL(fileName string, pathPrefix string) string { if strings.TrimSpace(pathPrefix) == "" { pathPrefix = localStaticUploadedPathPrefix @@ -643,6 +834,9 @@ func inlineAssetFromItem(taskKind string, item map[string]any) (*generatedInline if !ok || value == nil { continue } + if !inlineMediaKeyAllowedForItem(item, key, value) { + continue + } strictBase64 := inlineMediaKeyIsStrictBase64(key) payload, contentType, ok, err := inlineMediaPayload(value, strictBase64) if err != nil { @@ -818,6 +1012,9 @@ func inlineMediaKeys(item map[string]any) []string { if !ok || value == nil { continue } + if !inlineMediaKeyAllowedForItem(item, key, value) { + continue + } strictBase64 := inlineMediaKeyIsStrictBase64(key) if strictBase64 && stringFromAny(value) != "" { keys = append(keys, key) @@ -835,6 +1032,8 @@ func inlineMediaCandidateKeys() []string { "b64_json", "image_base64", "image_b64", + "audio_base64", + "audio_b64", "video_base64", "video_b64", "base64", @@ -842,24 +1041,56 @@ func inlineMediaCandidateKeys() []string { "url", "image_url", "imageUrl", + "audio_url", + "audioUrl", "video_url", "videoUrl", "output_url", "outputUrl", + "output_audio_url", + "outputAudioUrl", "output_video_url", "outputVideoUrl", "image", + "audio", "video", "image_buffer", "image_bytes", + "audio_buffer", + "audio_bytes", "video_buffer", "video_bytes", "buffer", "bytes", "data", + "content", } } +func inlineMediaKeyAllowedForItem(item map[string]any, key string, value any) bool { + if !strings.EqualFold(strings.TrimSpace(key), "content") { + return true + } + raw, ok := value.(string) + if !ok { + return generatedContentTypeIsMedia(mediaContentTypeFromItem(item)) || generatedResultItemIsMedia(item) + } + raw = strings.TrimSpace(raw) + if raw == "" { + return false + } + if strings.HasPrefix(strings.ToLower(raw), "data:") { + contentType, _, ok, err := parseBase64DataURL(raw) + return err == nil && ok && generatedContentTypeIsMedia(contentType) + } + return generatedContentTypeIsMedia(mediaContentTypeFromItem(item)) || generatedResultItemIsMedia(item) +} + +func generatedResultItemIsMedia(item map[string]any) bool { + itemType := strings.ToLower(strings.TrimSpace(stringFromAny(item["type"]))) + return strings.Contains(itemType, "image") || strings.Contains(itemType, "video") || strings.Contains(itemType, "audio") +} + func inlineMediaKeyIsStrictBase64(key string) bool { lower := strings.ToLower(key) return lower == "b64_json" || lower == "base64" || lower == "b64" || strings.Contains(lower, "base64") || strings.Contains(lower, "_b64") @@ -887,7 +1118,7 @@ func mediaURLKeys(item map[string]any) []string { } func mediaURLCandidateKeys() []string { - return []string{"url", "image_url", "imageUrl", "video_url", "videoUrl", "output_url", "outputUrl", "output_video_url", "outputVideoUrl", "download_url", "downloadUrl", "file_url", "fileUrl"} + return []string{"url", "image_url", "imageUrl", "audio_url", "audioUrl", "video_url", "videoUrl", "output_url", "outputUrl", "output_audio_url", "outputAudioUrl", "output_video_url", "outputVideoUrl", "download_url", "downloadUrl", "file_url", "fileUrl"} } func mediaURLString(value string) bool { diff --git a/apps/api/internal/runner/upload_test.go b/apps/api/internal/runner/upload_test.go index 1354cf5..5c69997 100644 --- a/apps/api/internal/runner/upload_test.go +++ b/apps/api/internal/runner/upload_test.go @@ -93,6 +93,43 @@ func TestGeneratedAssetDecisionUploadsDataURL(t *testing.T) { } } +func TestGeneratedAssetDecisionUploadsAudioContentDataURL(t *testing.T) { + item := map[string]any{ + "type": "audio", + "content": "data:audio/mpeg;base64," + base64.StdEncoding.EncodeToString([]byte("inline audio")), + "mime_type": "audio/mpeg", + } + + decision, err := generatedAssetDecisionForItem("speech.generations", item, defaultGeneratedAssetUploadPolicy()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if decision.Inline == nil { + t.Fatalf("expected inline audio content to be uploaded") + } + if decision.Inline.Kind != "audio" || decision.Inline.ContentType != "audio/mpeg" || decision.Inline.SourceKey != "content" { + t.Fatalf("unexpected inline audio metadata: %+v", decision.Inline) + } + if !containsString(decision.StripKeys, "content") { + t.Fatalf("uploaded audio content should be stripped: %+v", decision.StripKeys) + } +} + +func TestGeneratedAssetDecisionDoesNotUploadPlainTextContent(t *testing.T) { + item := map[string]any{ + "type": "text", + "content": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("plain text ", 20))), + } + + decision, err := generatedAssetDecisionForItem("speech.generations", item, defaultGeneratedAssetUploadPolicy()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if decision.Inline != nil || len(decision.StripKeys) > 0 { + t.Fatalf("plain text content should not be treated as generated media: %+v", decision) + } +} + func TestGeneratedAssetDecisionUploadsURLWhenPolicyUploadAll(t *testing.T) { item := map[string]any{ "type": "video", @@ -216,6 +253,9 @@ func TestUploadGeneratedAssetStoresLocalWhenNoChannels(t *testing.T) { if !strings.HasPrefix(urlValue, "/static/generated/gateway-result-task-123-01-") || !strings.HasSuffix(urlValue, ".png") { t.Fatalf("unexpected local static URL: %s", urlValue) } + if stringFromAny(upload["expiresAt"]) == "" { + t.Fatalf("local static upload should expose expiresAt: %+v", upload) + } entries, err := os.ReadDir(storageDir) if err != nil { t.Fatalf("failed to read local static dir: %v", err) @@ -232,6 +272,39 @@ func TestUploadGeneratedAssetStoresLocalWhenNoChannels(t *testing.T) { } } +func TestUploadGeneratedAssetStoresAudioLocalWhenNoChannels(t *testing.T) { + storageDir := t.TempDir() + service := &Service{cfg: config.Config{LocalGeneratedStorageDir: storageDir}} + asset := &generatedInlineAsset{ + Bytes: []byte("inline audio payload"), + ContentType: "audio/mpeg", + Kind: "audio", + SourceKey: "content", + } + + upload, contentType, kind, strategy, err := service.uploadGeneratedAsset(context.Background(), "task-tts", asset, 0, nil, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if contentType != "audio/mpeg" || kind != "audio" || strategy != "local_static_inline_media" { + t.Fatalf("unexpected local audio metadata: contentType=%s kind=%s strategy=%s", contentType, kind, strategy) + } + urlValue := stringFromAny(upload["url"]) + if !strings.HasPrefix(urlValue, "/static/generated/gateway-result-task-tts-01-") || !strings.HasSuffix(urlValue, ".mp3") { + t.Fatalf("unexpected local audio URL: %s", urlValue) + } + if stringFromAny(upload["expiresAt"]) == "" { + t.Fatalf("local audio static upload should expose expiresAt: %+v", upload) + } + entries, err := os.ReadDir(storageDir) + if err != nil { + t.Fatalf("failed to read local static dir: %v", err) + } + if len(entries) != 1 || !strings.HasSuffix(entries[0].Name(), ".mp3") { + t.Fatalf("expected one MP3 file in local static dir, got %+v", entries) + } +} + func TestUploadFileStoresLocalWhenNoChannels(t *testing.T) { storageDir := t.TempDir() service := &Service{cfg: config.Config{ @@ -254,6 +327,9 @@ func TestUploadFileStoresLocalWhenNoChannels(t *testing.T) { if !strings.HasPrefix(urlValue, "/static/uploaded/") || !strings.HasSuffix(urlValue, ".pdf") { t.Fatalf("unexpected uploaded local static URL: %s", urlValue) } + if stringFromAny(upload["expiresAt"]) == "" { + t.Fatalf("local uploaded static file should expose expiresAt: %+v", upload) + } storageChannel, _ := upload["storageChannel"].(map[string]any) if stringFromAny(storageChannel["provider"]) != "local_static" { t.Fatalf("expected local static provider metadata, got %+v", upload["storageChannel"]) @@ -277,3 +353,26 @@ func TestUploadFileStoresLocalWhenNoChannels(t *testing.T) { t.Fatalf("stored uploaded payload does not match source payload") } } + +func TestRedactGeneratedResultRawDataAudioPayload(t *testing.T) { + audioHex := strings.Repeat("49443304", 40) + result := map[string]any{ + "raw_data": map[string]any{ + "base_resp": map[string]any{"status_code": float64(0)}, + "data": map[string]any{"audio": audioHex}, + }, + } + + if !redactGeneratedResultRawData(result) { + t.Fatalf("expected raw audio payload to be redacted") + } + rawData, _ := result["raw_data"].(map[string]any) + data, _ := rawData["data"].(map[string]any) + audio, _ := data["audio"].(map[string]any) + if audio["redacted"] != true || audio["encoding"] != "hex" || audio["contentType"] != "audio/mpeg" { + t.Fatalf("unexpected redacted audio payload: %+v", audio) + } + if _, ok := audio["sha256"].(string); !ok { + t.Fatalf("expected redacted audio payload to include sha256: %+v", audio) + } +} diff --git a/apps/web/src/pages/admin/SystemSettingsPanel.tsx b/apps/web/src/pages/admin/SystemSettingsPanel.tsx index bbbdc38..cbc04c9 100644 --- a/apps/web/src/pages/admin/SystemSettingsPanel.tsx +++ b/apps/web/src/pages/admin/SystemSettingsPanel.tsx @@ -37,13 +37,13 @@ const providerOptions = [ const defaultScenes = ['upload', 'image_result']; const sceneOptions = [ { value: 'upload', label: '上传', description: 'OpenAPI / 管理端主动上传文件' }, - { value: 'image_result', label: '返图', description: '模型返回 base64 / buffer 图片或视频后的转存' }, + { value: 'image_result', label: '生成媒体结果', description: '模型返回 base64 / buffer 图片、音频或视频后的转存' }, ]; const resultUploadPolicyOptions = [ { value: 'default', label: '默认:仅非链接资源转存', description: 'URL 结果直接保存;base64 / buffer 等结果转存后保存 URL' }, - { value: 'upload_all', label: '全部转存', description: 'URL、base64、buffer 等返图结果都会转存到当前文件渠道' }, - { value: 'upload_none', label: '全部不转存', description: '链接结果直接保存;base64 / buffer 结果写入网关本地静态托管后保存 URL' }, + { value: 'upload_all', label: '全部转存', description: 'URL、base64、buffer 等生成媒体结果都会转存到当前文件渠道' }, + { value: 'upload_none', label: '全部不转存', description: '链接结果直接保存;base64 / buffer 结果写入 24 小时本地静态托管后保存 URL' }, ]; export function SystemSettingsPanel(props: { @@ -146,8 +146,8 @@ export function SystemSettingsPanel(props: {
- 全局返图转存策略 - 对所有返图场景统一生效;渠道只负责上传目标、凭证、重试和轮转。 + 全局生成媒体转存策略 + 对图片、音频和视频生成结果统一生效;网关本地静态资源仅保留 24 小时。