From 56255688c1708f2be19681aab58e474dad40cd50 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 23 Mar 2026 11:53:21 +0800 Subject: [PATCH] feat: expand nas storage workspace panel --- web/storyforge-web-v4/README.md | 10 +- web/storyforge-web-v4/assets/app.js | 225 +++++++++++++++++++++++++--- 2 files changed, 215 insertions(+), 20 deletions(-) diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md index 0323e58..299a0d0 100644 --- a/web/storyforge-web-v4/README.md +++ b/web/storyforge-web-v4/README.md @@ -101,8 +101,7 @@ python3 -m http.server 3918 ## 后续建议 -- 继续补多平台真实接入,而不只是一套 Douyin 工作流 -- `xiaohongshu`、`bilibili`、`kuaishou`、`wechat_video` 先保持 `待接入工作台` 占位态 +- 继续补多平台各自更深的专属采集与解析能力,而不只是一套统一抽象层 - 把对标导入后的 Agent 绑定和知识库入库反馈做得更完整 - 把跟踪日报从 Douyin 扩到多平台统一模型,并接入真正的定时调度 - 把全局搜索和页内搜索合并成统一搜索体验 @@ -111,6 +110,13 @@ python3 -m http.server 3918 - 现在的推荐策略是: - 数据库继续留本机 - `jobs / downloads` 这类大文件缓存优先放 NAS + - 下载产物和分析产物通过 `/v2/storage/artifacts/{file_id}/content` 走租户鉴权代理访问 - 如果后续出现速度或稳定性问题,再切到 OSS +- `项目总台` 里的“存储状态”面板现在已经会显示: + - NAS / 本机策略 + - 项目与账号占用 + - 最近分析产物 + - 最近执行缓存 + - 最近录像文件 - 不要把这套页面重新塞回 `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 9ca7d55..6df526a 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1385,20 +1385,141 @@ function renderLiveRecorderSummaryHtml() { `; } +function getStorageItemPath(item) { + return ( + item?.artifacts?.source_path || + item?.artifacts?.uploaded_path || + item?.artifacts?.output_path || + item?.artifacts?.file_path || + item?.result?.source_path || + item?.result?.output_path || + item?.result?.file_path || + item?.result?.path || + item?.save_path || + item?.path || + item?.relative_path || + item?.content_url || + item?.job_id || + item?.id || + "-" + ); +} + +function renderStorageJobCards(items, emptyTitle, emptyText) { + return safeArray(items).slice(0, 4).map((item) => ` +
+

${escapeHtml(item.title || item.name || item.job_id || "任务")}

+

${escapeHtml(brief(getStorageItemPath(item), 140))}

+
+ ${escapeHtml(item.status || "-")} + ${item.project_name ? `${escapeHtml(item.project_name)}` : ""} + ${item.line_type || item.source_type ? `${escapeHtml(item.line_type || item.source_type)}` : ""} + ${item.id ? `看详情` : ""} +
+
+ `).join("") || `

${escapeHtml(emptyTitle)}

${escapeHtml(emptyText)}

`; +} + +function renderStorageFileCards(items, emptyTitle, emptyText) { + return safeArray(items).slice(0, 4).map((item) => ` +
+

${escapeHtml(item.title || item.name || item.relative_path || "文件")}

+

${escapeHtml(brief(item.relative_path || item.name || item.content_url || "-", 140))}

+
+ ${(item.updated_at || item.mtime) ? `${escapeHtml(formatDateTime(item.updated_at || item.mtime))}` : ""} + ${(item.size_bytes || item.size) ? `${escapeHtml(formatBytes(item.size_bytes || item.size))}` : ""} + ${item.id ? `打开文件` : ""} +
+
+ `).join("") || `

${escapeHtml(emptyTitle)}

${escapeHtml(emptyText)}

`; +} + function renderStorageStatusPanel() { const storage = appState.storageStatus; + const dashboardJobs = safeArray(appState.dashboard?.recent_jobs); + const currentProject = getSelectedProject(); + const currentAccount = getSelectedAccount(); + const liveRecorderSources = safeArray(appState.liveRecorderSources); + const liveRecorderFiles = safeArray(appState.liveRecorderFiles); + const fallbackRecentJobs = dashboardJobs.slice(0, 4); + const recentJobs = safeArray(storage?.tenant_usage?.recent_jobs).length ? safeArray(storage?.tenant_usage?.recent_jobs) : fallbackRecentJobs; + const recentFiles = [ + ...safeArray(storage?.tenant_usage?.recent_download_artifacts), + ...safeArray(storage?.tenant_usage?.recent_job_artifacts), + ...safeArray(storage?.recent_files || storage?.recent_artifacts || storage?.tenant_usage?.recent_files || storage?.tenant_usage?.recent_artifacts) + ]; if (!storage) { + const projectJobCount = appState.selectedProjectId ? dashboardJobs.filter((item) => item.project_id === appState.selectedProjectId).length : dashboardJobs.length; + const accountJobCount = appState.selectedAccountId ? dashboardJobs.filter((item) => item.account_id === appState.selectedAccountId).length : dashboardJobs.length; return `
-

存储状态

当前后端暂未接入存储状态接口
-

未拉取

等 live collector 更新后,这里会显示 NAS 缓存、当前项目占用和目录策略。

+
+
+

存储状态

+
后端暂未提供 /v2/storage/status,先用任务和录像文件做本地观察
+
+ 降级视图 +
+
+

未拉取到 NAS 策略

+

后端补上 storage/status 后,这里会自动显示账号 / 项目 / 任务分层、容量和最近写入路径。

+
+ 最近任务 ${escapeHtml(formatNumber(projectJobCount))} + 录制源 ${escapeHtml(formatNumber(liveRecorderSources.length))} + 录像文件 ${escapeHtml(formatNumber(liveRecorderFiles.length))} +
+
+
+
+ 当前项目 + ${escapeHtml(currentProject?.name || appState.selectedProjectId || "未选择")} + 任务 ${escapeHtml(formatNumber(projectJobCount))} +
+
+ 当前账号 + ${escapeHtml(currentAccount ? getAccountName(currentAccount) : "未选择")} + 任务 ${escapeHtml(formatNumber(accountJobCount))} +
+
+ 录像源 + ${escapeHtml(formatNumber(liveRecorderSources.length))} + 仅当前租户可见 +
+
+ 最近文件 + ${escapeHtml(formatNumber(liveRecorderFiles.length))} + 可直接打开 +
+
+
+
+

最近任务

+

优先展示 dashboard.recent_jobs,方便在没有 storage/status 时也能继续追踪产物。

+
+ ${renderStorageJobCards( + fallbackRecentJobs, + "还没有任务样本", + "等你完成一次分析、下载或剪辑后,这里就会出现最近的任务路径和详情入口。" + )} +
+

最近录像文件

+

录像文件沿用 live-recorder 的当前租户视图,支持直接打开查看。

+
+ ${renderStorageFileCards( + liveRecorderFiles, + "还没有录像文件", + "录制完成后,这里会直接暴露当前租户的最近文件入口。" + )} +
`; } const strategy = storage.strategy || {}; const disk = storage.disk || {}; const usage = storage.tenant_usage || {}; - const recentJobs = safeArray(usage.recent_jobs); + const strategyMode = (strategy.jobs?.mode || "local").toUpperCase(); + const projectName = currentProject?.name || appState.selectedProjectId || "未选择"; + const accountName = currentAccount ? getAccountName(currentAccount) : "未选择"; const strategyTags = [ `数据库 ${strategy.database?.mode || "local"}`, `分析缓存 ${strategy.jobs?.mode || "local"}`, @@ -1409,11 +1530,22 @@ function renderStorageStatusPanel() { { label: "当前项目缓存", value: formatBytes(usage.project_jobs?.bytes), sub: `文件 ${formatNumber(usage.project_jobs?.file_count)}` }, { label: "当前项目下载", value: formatBytes(usage.project_downloads?.bytes), sub: `文件 ${formatNumber(usage.project_downloads?.file_count)}` }, { label: "当前账号缓存", value: formatBytes(usage.account_jobs?.bytes), sub: `文件 ${formatNumber(usage.account_jobs?.file_count)}` }, + { label: "当前账号下载", value: formatBytes(usage.account_downloads?.bytes), sub: `文件 ${formatNumber(usage.account_downloads?.file_count)}` }, { label: "NAS 剩余", value: formatBytes(disk.jobs?.free_bytes), sub: `总量 ${formatBytes(disk.jobs?.total_bytes)}` } ]; return `
-

存储状态

数据库留本机,大文件缓存优先走 NAS
${escapeHtml((strategy.jobs?.mode || "local").toUpperCase())}
+
+
+

存储状态

+
数据库留本机,大文件缓存优先走 NAS
+
+ ${escapeHtml(strategyMode)} +
+
+

当前观察范围

+

${escapeHtml(`项目 ${projectName} · 账号 ${accountName} · 最近任务 ${recentJobs.length} 条 · 最近录像 ${liveRecorderFiles.length} 个`)}

+
${strategyTags.map((item) => `${escapeHtml(item)}`).join("")}
@@ -1426,25 +1558,60 @@ function renderStorageStatusPanel() {
`).join("")} +
+
+

目录策略

+

${escapeHtml([ + `数据库 ${strategy.database?.path || "本机"}`, + `任务缓存 ${usage.project_jobs?.path || strategy.jobs?.path || "-"}`, + `下载缓存 ${usage.project_downloads?.path || strategy.downloads?.path || "-"}`, + `录制缓存 ${strategy.live_recorder?.path || strategy.live_recorder?.base_path || "-"}` + ].join(" · "))}

+
+ 项目层 ${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")} + 账号层 ${escapeHtml(usage.account_jobs?.path || "-")} +
+
+
+

产物入口

+

最近任务、分析产物和录像文件都能直接点开,便于从 NAS 面板跳回详情或原文件。

+
+ 任务 ${escapeHtml(formatNumber(recentJobs.length))} + 产物 ${escapeHtml(formatNumber(recentFiles.length))} + 录像 ${escapeHtml(formatNumber(liveRecorderFiles.length))} +
+
+
-

当前项目缓存根目录

-

${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")}

+

最近任务样本

+

默认取 storage.status 里的 recent_jobs;如果后端没给,会退回到 dashboard.recent_jobs。

-
-

下载缓存根目录

-

${escapeHtml(usage.project_downloads?.path || strategy.downloads?.path || "-")}

-
- ${recentJobs.slice(0, 3).map((item) => ` + ${renderStorageJobCards( + recentJobs, + "还没有缓存样本", + "上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。" + )} + ${recentFiles.length ? `
-

${escapeHtml(item.title || item.job_id)}

-

${escapeHtml(item.paths?.[0]?.path || item.job_id)}

-
- ${escapeHtml(item.status || "-")} - ${escapeHtml(formatDateTime(item.updated_at))} -
+

最近产物文件

+

后端如果提供产物文件索引,这里会优先直接露出最近写入的文件入口。

- `).join("") || `

还没有缓存样本

上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。

`} + ${renderStorageFileCards( + recentFiles, + "还没有产物文件", + "当前 storage/status 没有返回可直接打开的产物文件。" + )} + ` : ""} +
+

最近录像文件

+

如果 live-recorder 已接入,这里会继续显示当前租户的录像文件入口。

+
+ ${renderStorageFileCards( + liveRecorderFiles, + "还没有录像文件", + "录制完成后的文件会出现在当前租户的录像列表里。" + )}
`; @@ -3580,6 +3747,24 @@ async function openLiveRecorderFileAction(fileId) { window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000); } +async function openStorageArtifactAction(fileId) { + const usage = appState.storageStatus?.tenant_usage || {}; + const candidates = [ + ...safeArray(usage.recent_download_artifacts), + ...safeArray(usage.recent_job_artifacts), + ...safeArray(appState.storageStatus?.recent_files), + ...safeArray(appState.storageStatus?.recent_artifacts) + ]; + const target = candidates.find((item) => item.id === fileId); + if (!target?.content_url) { + throw new Error("当前产物不存在,可能已经被清理"); + } + const blob = await storyforgeFetchBlob(target.content_url); + const blobUrl = URL.createObjectURL(blob); + window.open(blobUrl, "_blank", "noopener,noreferrer"); + window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000); +} + function openReviewAction(defaults = {}) { const project = requireSelectedProject(); const assistants = getAssistantOptions(project.id); @@ -3704,6 +3889,10 @@ document.addEventListener("click", async (event) => { await openLiveRecorderFileAction(action.dataset.fileId || ""); return; } + if (name === "open-storage-artifact") { + await openStorageArtifactAction(action.dataset.fileId || ""); + return; + } if (name === "mark-tracking-read") { await markTrackingDigestRead(); rememberAction("日报已标记", "当前跟踪摘要已更新为已读,下次会从新的时间点继续汇总。", "green");