diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index 5651cf4..7e64cf6 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -6069,6 +6069,130 @@ function renderPlaybookScreen() {
);
}
+function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, recoverableCount, works, recentDocs }) {
+ const taskCards = [];
+ if (activeTab === "recovery") {
+ const nextRecoverable = failedJobs.find((item) => item.recovery.recoverable) || null;
+ if (nextRecoverable) {
+ taskCards.push(`
+
+
当前要先处理
+
${escapeHtml(`${nextRecoverable.job.title || nextRecoverable.job.id} 仍可恢复,建议优先重开。`)}
+
+ ${escapeHtml(nextRecoverable.recovery.label)}
+ ${actionTag(nextRecoverable.recovery.actionLabel || "立即恢复", "recover-job", `data-job-id="${escapeHtml(nextRecoverable.job.id)}"`)}
+
+
+ `);
+ }
+ taskCards.push(`
+
+
恢复面板
+
${escapeHtml(recoverableCount ? `当前有 ${recoverableCount} 条任务可以直接恢复。` : "当前没有可直接恢复的失败任务。")}
+
+ ${actionTag("批量恢复", "batch-recover-jobs")}
+ ${actionTag("恢复记录", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recovery"`)}
+
+
+ `);
+ } else if (activeTab === "recorder") {
+ const status = appState.liveRecorderStatus || {};
+ const activeCount = safeArray(status.active_recordings).length;
+ taskCards.push(`
+
+
当前要先处理
+
${escapeHtml(status.running ? `Live Recorder 正在运行,当前有 ${activeCount} 路活动录制。` : "先确认 Live Recorder 是否在线,再检查录制源和文件。")}
+
+ ${escapeHtml(status.running ? "运行中" : "待检查")}
+ ${actionTag("录制维护", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recorder"`)}
+
+
+ `);
+ taskCards.push(`
+
+
录制源与文件
+
${escapeHtml(`录制源 ${formatNumber(safeArray(appState.liveRecorderSources).length)} 个 · 文件 ${formatNumber(safeArray(appState.liveRecorderFiles).length)} 个`)}
+
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({
+ sourceScreen: "production",
+ sourceActionKey: "production-mobile-recorder-handoff",
+ intentKey: "production_coordination",
+ title: "继续处理录制维护",
+ goal: "继续处理录制维护",
+ summary: "结合录制维护状态给出下一步动作。",
+ platform: getPreferredPlatform(),
+ platformScope: "single_platform",
+ planSteps: ["读取录制维护状态", "识别当前阻塞项", "生成下一步处理动作"]
+ }))}
+
+
+ `);
+ } else if (activeTab === "outputs") {
+ const topWork = works[0] || null;
+ taskCards.push(`
+
+
当前要先处理
+
${escapeHtml(topWork ? `先看 ${describeVideo(topWork)} 的结果,再决定是否回到复盘。` : "先看最近产物和学习素材,再决定是否继续复盘或返回生产。")}
+
+ ${actionTag("去复盘", "goto-review")}
+ ${actionTag("查看产物", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="outputs"`)}
+
+
+ `);
+ if (recentDocs.length) {
+ taskCards.push(`
+
+
最近学习素材
+
${escapeHtml(brief(recentDocs[0].title || recentDocs[0].style_summary || "最近文档", 72))}
+
+ 学习素材
+ ${escapeHtml(recentDocs[0].source_type || "document")}
+
+
+ `);
+ }
+ } else {
+ const topJob = (activeJobs.length ? activeJobs : []).slice(0, 1)[0] || null;
+ taskCards.push(`
+
+
当前要先处理
+
${escapeHtml(topJob ? `${topJob.title} 还在推进中,建议先看状态再决定是否做 AI 视频或实拍剪辑。` : "先看当前生产队列,再决定是否继续恢复或进入复盘。")}
+
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({
+ sourceScreen: "production",
+ sourceActionKey: "production-mobile-queue-handoff",
+ intentKey: "production_coordination",
+ title: "继续推进生产队列",
+ goal: "继续推进生产队列",
+ summary: "结合当前生产队列给出下一步动作。",
+ platform: getPreferredPlatform(),
+ platformScope: "single_platform",
+ planSteps: ["读取当前生产队列", "识别最该优先推进的项", "生成下一步处理动作"]
+ }))}
+ ${recoverableCount ? actionTag("看失败恢复", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recovery"`) : actionTag("去复盘", "goto-review")}
+
+
+ `);
+ 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")}
+
+
+ `);
+ }
+ }
+ return `
+
+ ${taskCards.slice(0, 2).join("")}
+
+ `;
+}
+
function renderProductionScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
@@ -6163,6 +6287,14 @@ function renderProductionScreen() {
可恢复 ${escapeHtml(formatNumber(recoverableCount))}
产物 ${escapeHtml(formatNumber(works.length))}
+ ${renderProductionMobileTaskDeck({
+ activeTab,
+ activeJobs,
+ failedJobs,
+ recoverableCount,
+ works,
+ recentDocs
+ })}
${renderDetailTabs("productionDetailTab", tabs)}
${activeTab === "queue" ? `
diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css
index 5dda6f7..1184075 100644
--- a/web/storyforge-web-v4/assets/styles.css
+++ b/web/storyforge-web-v4/assets/styles.css
@@ -2359,6 +2359,12 @@ tbody tr:hover {
text-align: center;
}
+ .production-mobile-task-deck {
+ display: grid;
+ gap: 10px;
+ margin-bottom: 14px;
+ }
+
.entity-cell {
align-items: flex-start;
}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index d97b979..c2ced6e 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -207,9 +207,13 @@ test("discovery and production screens expose mobile focus cards with next-step
assert.match(discovery, /导入当前对标/);
assert.match(discovery, /查相似/);
assert.match(production, /mobile-only mobile-flow-focus-card/);
+ assert.match(APP, /function renderProductionMobileTaskDeck\(/);
+ assert.match(APP, /mobile-only production-mobile-task-deck/);
+ assert.match(APP, /当前要先处理/);
assert.match(production, /当前工作流/);
assert.match(production, /批量恢复/);
assert.match(cssMobile, /\.mobile-flow-focus-card/);
+ assert.match(cssMobile, /\.production-mobile-task-deck/);
});
test("mobile heavy screens mark redundant desktop metric blocks for compact hiding", () => {