diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 7884547..1e0ee02 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -2075,6 +2075,78 @@ function openCurrentOneLinerRunResultAction(runId = "") { }); } +function openConfirmOneLinerRunAction(runId = "") { + const run = safeArray(appState.onelinerRuns).find((item) => item.id === runId) || getCurrentOneLinerRun(); + if (!run?.id) { + rememberAction("还没有可确认的任务", "当前没有主 Agent 待确认任务。", "orange"); + renderAll(); + return; + } + const planSteps = safeArray(run.plan?.steps).slice(0, 5); + const previewAction = run.recommended_preview_action || null; + const tags = [ + run.platform_label ? `${escapeHtml(run.platform_label)}` : "", + `${escapeHtml(run.platform_scope === "all_platforms" ? "全平台" : "单平台")}`, + `${escapeHtml(onelinerIntentLabel(run.intent_key))}`, + run.source_screen ? `${escapeHtml(run.source_screen)}` : "" + ].filter(Boolean).join(""); + const planHtml = ` +
+

当前计划

+

${escapeHtml(run.plan?.summary || run.summary || "主 Agent 会先按这张确认卡理解目标,再继续执行。")}

+
${tags}
+ ${planSteps.length ? ` +
+ ${planSteps.map((step, index) => ` +
+

步骤 ${escapeHtml(formatNumber(index + 1))}

+

${escapeHtml(step)}

+
+ `).join("")} +
+ ` : ""} + ${previewAction ? ` +
+

预计落点

+

${escapeHtml(previewAction.summary || "执行后会回到更合适的业务页面继续推进。")}

+
+ ${escapeHtml(previewAction.label || "回到对应页面")} + ${previewAction.screen ? `${escapeHtml(previewAction.screen)}` : ""} +
+
+ ` : ""} +
+ `; + openActionModal({ + title: "确认主 Agent 执行计划", + description: "确认后,主 Agent 会按当前计划进入执行流;你也可以先补一句执行说明。", + submitLabel: "确认执行", + fields: [ + { + name: "plan", + type: "html", + label: "执行计划", + html: `
${planHtml}
` + }, + { + name: "reason", + type: "textarea", + label: "补充说明", + rows: 3, + placeholder: "可选:比如优先做抖音,或者先给我一版更保守的执行建议。", + value: "" + } + ], + onSubmit: async (values) => { + const payload = await confirmOneLinerRun(run.id, values.reason || "user confirmed"); + return { + keepOpen: false, + payload + }; + } + }); +} + async function loadPlatformAccount(platform, accountId, requestToken = 0) { if (!accountId) return; const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); @@ -9784,14 +9856,7 @@ document.addEventListener("click", async (event) => { return; } if (name === "confirm-oneliner-run") { - try { - setBusy(true, "正在确认执行计划..."); - await confirmOneLinerRun(action.dataset.runId || "", "user confirmed"); - } catch (error) { - presentActionFailure(error, "主 Agent 确认失败"); - } finally { - setBusy(false, ""); - } + openConfirmOneLinerRunAction(action.dataset.runId || ""); return; } if (name === "cancel-oneliner-run") { diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 7804866..f22c423 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -140,6 +140,19 @@ test("oneliner panel includes a dedicated runtime header for agent runs", () => assert.match(runtime, /recommended_action/); }); +test("confirm-oneliner-run opens a dedicated confirmation sheet before execution", () => { + const confirmSheet = extractBetween(APP, "function openConfirmOneLinerRunAction(runId = \"\")", "async function loadPlatformAccount("); + const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); + + assert.match(confirmSheet, /确认主 Agent 执行计划/); + assert.match(confirmSheet, /当前计划/); + assert.match(confirmSheet, /预计落点/); + assert.match(confirmSheet, /submitLabel: "确认执行"/); + assert.match(confirmSheet, /reason/); + assert.match(confirmSheet, /confirmOneLinerRun\(run\.id,\s*values\.reason/); + assert.match(actions, /openConfirmOneLinerRunAction\(action\.dataset\.runId \|\| ""\)/); +}); + test("oneliner meta and action handlers expose governance entry points", () => { const meta = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()"); const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderOneLinerUi()");