diff --git a/CHANGELOG.md b/CHANGELOG.md index 900df2c..0c4a8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -689,3 +689,4 @@ - 首页 `1 主 2 次` 动作里把 `视频录制` 抬成了高频次级动作,当前项目有生产任务时能更快进入录制维护入口。 - `AI 视频` 表单开始直接显示“当前项目最近使用的视频引擎”,像 `Seedance 2.0 · seedance-2.0-pro` 这类信息会在打开表单时直接可见,并保留跳到火山配置状态的入口。 - `生产中心` 现在把 `导入主页 / 导入作品 / 导入文本 / 上传视频` 这批接入入口统一接进了顶部动作区和生产队列工作流卡,不用离开生产中心也能开始接入素材。 +- `生产中心` 的 `生产队列` 首屏现在会直接显示 `接入与录制` 概况:最近内容源同步条数、录制源数量、录制服务状态,以及一组直达 `导入主页 / 导入作品 / 视频录制` 的动作。 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 51358d9..888c18c 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -7559,7 +7559,7 @@ function renderPlaybookScreen() { ); } -function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, recoverableCount, works, recentDocs }) { +function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, recoverableCount, works, recentDocs, contentSyncCount = 0, recorderSourceCount = 0, recorderRunning = false }) { const taskCards = []; if (activeTab === "recovery") { const nextRecoverable = failedJobs.find((item) => item.recovery.recoverable) || null; @@ -7663,18 +7663,17 @@ function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, rec `); - if (topJob) { - taskCards.push(` -
-

${escapeHtml(topJob.title)}

-

${escapeHtml(brief(topJob.style_summary || topJob.transcript_text || topJob.error || "暂无摘要", 84))}

-
- ${escapeHtml(topJob.status)} - ${escapeHtml(topJob.line_type || "analysis")} -
+ taskCards.push(` +
+

接入与录制

+

${escapeHtml(`最近内容源同步 ${formatNumber(contentSyncCount)} 条 · 录制源 ${formatNumber(recorderSourceCount)} 个${recorderRunning ? ",录制服务正在运行。" : ",录制服务待确认。"} `)}

+
+ ${actionTag("导入主页", "open-import-homepage")} + ${actionTag("导入作品", "open-import-video-link")} + ${actionTag("视频录制", "focus-live-recorder-maintenance")}
- `); - } +
+ `); } return `
@@ -7696,6 +7695,9 @@ function renderProductionScreen() { const recoverableCount = failedJobs.filter((item) => item.recovery.recoverable).length; const recentDocs = appState.documents.slice(0, 3); const works = getProductionWorks(6); + const contentSyncCount = jobs.filter((item) => item.line_type === "content_source_sync").length; + const recorderSourceCount = safeArray(appState.liveRecorderSources).length; + const recorderRunning = Boolean(appState.liveRecorderStatus?.running); const isMobileUi = isMobileViewport(); const productionHandoffAttrs = buildMainAgentHandoffAttrs({ sourceScreen: "production", @@ -7735,12 +7737,13 @@ function renderProductionScreen() {

分析任务

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} 条

实拍剪辑

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} 条

AI 视频

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} 条

-

内容源同步

最近 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "content_source_sync").length))} 条

+

接入与录制

同步 ${escapeHtml(formatNumber(contentSyncCount))} 条 · 录制源 ${escapeHtml(formatNumber(recorderSourceCount))} 个

分析 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} 实拍 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} AI 视频 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} + 录制 ${escapeHtml(formatNumber(recorderSourceCount))}
${renderQuotaBlockingNotice()} @@ -7788,7 +7791,10 @@ function renderProductionScreen() { failedJobs, recoverableCount, works, - recentDocs + recentDocs, + contentSyncCount, + recorderSourceCount, + recorderRunning })} ${renderDetailTabs("productionDetailTab", tabs)} ${activeTab === "queue" ? ` diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 3bea86c..5f9c1c7 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -496,12 +496,19 @@ test("mobile discovery and production simplify duplicated top-level actions", () test("production queue promotes intake entrypoints so import flows are reachable without leaving production", () => { const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()"); + const mobileDeck = extractBetween(APP, "function renderProductionMobileTaskDeck(", "function renderProductionScreen()"); assert.match(production, /const intakeEntryActionsHtml =/); assert.match(production, /

接入素材<\/h4>/); assert.match(production, /把主页、作品链接、文本素材或本地视频先接进项目/); assert.match(production, /先从导入主页、作品、文本或上传视频开始接入素材/); + assert.match(production, /

接入与录制<\/h4>/); + assert.match(production, /同步 .* 条 · 录制源 .* 个/); + assert.match(production, /录制 \${escapeHtml\(formatNumber\(recorderSourceCount\)\)}/); assert.match(production, /actionTag\("导入作品", "open-import-video-link"\)/); assert.match(production, /actionTag\("上传视频", "open-upload-video"\)/); + assert.match(mobileDeck, /

接入与录制<\/h4>/); + assert.match(mobileDeck, /actionTag\("导入主页", "open-import-homepage"\)/); + assert.match(mobileDeck, /actionTag\("视频录制", "focus-live-recorder-maintenance"\)/); }); test("discovery page promotes selected-account actions into direct execute flows", () => {