diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md index 1e4b06b..0323e58 100644 --- a/web/storyforge-web-v4/README.md +++ b/web/storyforge-web-v4/README.md @@ -72,6 +72,10 @@ - 录制源按当前账号和项目归属保存 - 录像文件只通过当前租户的后端代理访问 - 前端不再直接暴露 NAS 全局配置和下载根地址 +- 存储状态面板已接上: + - 当前项目和当前账号的缓存占用 + - 数据库本机 / 分析缓存 NAS / 下载缓存 NAS 的目录策略 + - 最近写入 NAS 的缓存样本路径 - 会先识别后端是否具备 `tracking / reviews / integrations` 路由,再决定是否请求,避免不同版本 live collector 刷 404 - 依赖不可达时,自动拦住 AI 视频 / 实拍剪辑动作并展示原因 - 使用 Agent 生成文案 @@ -104,5 +108,9 @@ python3 -m http.server 3918 - 把全局搜索和页内搜索合并成统一搜索体验 - 为 `生产中心 / 发布与复盘` 接入更完整的成片预览与封面对象 - 如果后续要开放外网多租户录像访问,继续沿用 collector 的鉴权代理,不要把 NAS 下载目录直接暴露给浏览器 +- 现在的推荐策略是: + - 数据库继续留本机 + - `jobs / downloads` 这类大文件缓存优先放 NAS + - 如果后续出现速度或稳定性问题,再切到 OSS - 不要把这套页面重新塞回 `scripts/douyin-browser-capture/control_panel.mjs` - 抖音采集控制台仍作为独立工具存在,这里才是正式业务应用壳 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index df59abb..9ca7d55 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -27,6 +27,7 @@ const appState = { liveRecorderSources: [], liveRecorderStatus: null, liveRecorderFiles: [], + storageStatus: null, integrationHealth: null, localModelCatalog: null, backendCapabilities: null, @@ -288,6 +289,20 @@ function formatNumber(value) { return String(Math.round(num * 10) / 10); } +function formatBytes(value) { + const num = Number(value || 0); + if (!Number.isFinite(num) || num <= 0) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let size = num; + let idx = 0; + while (size >= 1024 && idx < units.length - 1) { + size /= 1024; + idx += 1; + } + const fixed = size >= 10 || idx === 0 ? size.toFixed(0) : size.toFixed(1); + return `${fixed}${units[idx]}`; +} + function formatDateTime(value) { if (!value) return "-"; const date = new Date(value); @@ -764,6 +779,7 @@ async function logoutSession() { appState.trackingDigest = null; appState.reviews = []; appState.integrationHealth = null; + appState.storageStatus = null; appState.backendCapabilities = null; appState.lastAction = null; appState.lastGeneratedCopy = null; @@ -784,6 +800,17 @@ async function loadKnowledgeDocuments(knowledgeBases) { return groups.flat().slice(0, 12); } +async function loadStorageStatus(projectId = "") { + if (!backendSupports("/v2/storage/status")) { + appState.storageStatus = null; + return null; + } + const suffix = projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""; + const payload = await storyforgeFetch(`/v2/storage/status${suffix}`).catch(() => null); + appState.storageStatus = payload; + return payload; +} + async function loadPlatformAccount(platform, accountId) { if (!accountId) return; const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); @@ -852,6 +879,7 @@ async function bootstrap() { const supportsReviews = backendSupports("/v2/reviews"); const supportsIntegrationHealth = backendSupports("/v2/integrations/health"); const supportsLocalModels = backendSupports("/v2/integrations/local-models"); + const supportsStorageStatus = backendSupports("/v2/storage/status"); const supportsLiveRecorderSources = backendSupports("/v2/live-recorder/sources"); const supportsLiveRecorderStatus = backendSupports("/v2/live-recorder/status"); const supportsLiveRecorderFiles = backendSupports("/v2/live-recorder/files"); @@ -896,6 +924,11 @@ async function bootstrap() { appState.localModelCatalog = localModelCatalog; appState.documents = await loadKnowledgeDocuments(dashboard.knowledge_bases); appState.selectedProjectId = appState.selectedProjectId || dashboard.projects?.[0]?.id || ""; + if (supportsStorageStatus) { + await loadStorageStatus(appState.selectedProjectId || ""); + } else { + appState.storageStatus = null; + } const selectedAssistantExists = safeArray(dashboard.assistants).some((item) => item.id === appState.selectedAssistantId); appState.selectedAssistantId = selectedAssistantExists ? appState.selectedAssistantId : (dashboard.assistants?.[0]?.id || ""); const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId); @@ -1352,6 +1385,71 @@ function renderLiveRecorderSummaryHtml() { `; } +function renderStorageStatusPanel() { + const storage = appState.storageStatus; + if (!storage) { + return ` +
等 live collector 更新后,这里会显示 NAS 缓存、当前项目占用和目录策略。
${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")}
+${escapeHtml(usage.project_downloads?.path || strategy.downloads?.path || "-")}
+${escapeHtml(item.paths?.[0]?.path || item.job_id)}
+ +上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。