diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index 7779efa..ecdd53c 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -54,6 +54,7 @@ const appState = {
selectedOnelinerSessionId: "",
onelinerRuns: [],
selectedOnelinerRunId: "",
+ lastCompletedOnelinerRunId: "",
onelinerMessages: [],
onelinerActionRegistry: [],
platformAgents: [],
@@ -975,6 +976,8 @@ function renderOneLinerRunsHtml() {
}
const runEvents = safeArray(currentRun.events).slice(-3);
const planSteps = safeArray(currentRun.plan?.steps).slice(0, 4);
+ const resultPayload = currentRun.result && typeof currentRun.result === "object" ? currentRun.result : null;
+ const hasResultPayload = Boolean(resultPayload && Object.keys(resultPayload).length);
const runStatusLabel = {
needs_confirmation: "待确认",
queued: "排队中",
@@ -1004,6 +1007,7 @@ function renderOneLinerRunsHtml() {
${escapeHtml(runStatusLabel)}
${currentRun.platform_label ? `${escapeHtml(currentRun.platform_label)}` : ""}
${escapeHtml(onelinerIntentLabel(currentRun.intent_key))}
+ ${currentRun.source_screen ? `${escapeHtml(currentRun.source_screen)}` : ""}
${currentRun.active_admin_override_notice?.title ? `
@@ -1012,6 +1016,15 @@ function renderOneLinerRunsHtml() {
${escapeHtml(currentRun.active_admin_override_notice.summary || "当前运行会优先遵循管理员覆盖层。")}
` : ""}
+
${planSteps.map((step, index) => `
@@ -1029,7 +1042,14 @@ function renderOneLinerRunsHtml() {
` : `
${escapeHtml(currentRun.status_summary || "主 Agent 正在推进中")}
`}
+ ${hasResultPayload ? `查看结果` : ""}
+ ${hasResultPayload ? `
+
${runEvents.map((item) => `
@@ -1401,6 +1421,7 @@ async function logoutSession() {
appState.selectedOnelinerSessionId = "";
appState.onelinerRuns = [];
appState.selectedOnelinerRunId = "";
+ appState.lastCompletedOnelinerRunId = "";
appState.onelinerMessages = [];
appState.onelinerActionRegistry = [];
appState.platformAgents = [];
@@ -1465,6 +1486,10 @@ async function hydrateSelectedOneLinerRun() {
? runs.map((item) => (item.id === detail.id ? detail : item))
: [detail, ...runs];
appState.onelinerRuns = nextRuns;
+ if (detail.run_status === "done" && detail.id && appState.lastCompletedOnelinerRunId !== detail.id) {
+ rememberAction("主 Agent 已完成本轮", detail.result?.execution_summary || detail.status_summary || detail.summary || "当前运行已经完成,可以继续执行下一步。", "green", detail);
+ appState.lastCompletedOnelinerRunId = detail.id;
+ }
return detail;
}
@@ -1703,6 +1728,29 @@ function renderOneLinerExecutionPayloadHtml(payload) {
if (!payload || typeof payload !== "object") {
return `
`;
}
+ if (payload.result_kind === "main_agent_plan") {
+ return `
+
+
${escapeHtml(payload.goal || "主 Agent 执行建议")}
+
${escapeHtml(payload.execution_summary || payload.summary_text || "已形成一版可继续执行的主 Agent 建议。")}
+
+ ${payload.platform ? `${escapeHtml(platformLabel(payload.platform))}` : ""}
+ ${escapeHtml(payload.platform_scope === "all_platforms" ? "全平台" : "单平台")}
+ 已收口
+
+
+ ${safeArray(payload.next_steps).length ? `
+
+ ${safeArray(payload.next_steps).slice(0, 4).map((step, index) => `
+
+
下一步 ${escapeHtml(formatNumber(index + 1))}
+
${escapeHtml(step)}
+
+ `).join("")}
+
+ ` : ""}
+ `;
+ }
if (payload.job) {
const job = payload.job || {};
const sourceJob = payload.source_job || {};
@@ -1909,6 +1957,32 @@ async function executeOneLinerAction(executorKey, options = {}) {
return payload;
}
+function openCurrentOneLinerRunResultAction(runId = "") {
+ const currentRun = safeArray(appState.onelinerRuns).find((item) => item.id === runId) || getCurrentOneLinerRun();
+ if (!currentRun?.id) {
+ rememberAction("还没有可查看的结果", "当前主 Agent 任务还没有返回可展示的执行结果。", "orange");
+ renderAll();
+ return;
+ }
+ if (!currentRun.result || !Object.keys(currentRun.result || {}).length) {
+ rememberAction("结果还在生成中", currentRun.status_summary || "当前主 Agent 任务还没有返回执行结果。", "orange", currentRun);
+ renderAll();
+ return;
+ }
+ openActionModal({
+ title: currentRun.title || currentRun.plan?.goal || "主 Agent 执行结果",
+ description: currentRun.result?.execution_summary || currentRun.status_summary || "这是当前主 Agent 任务的执行结果。",
+ hideSubmit: true,
+ fields: [
+ {
+ type: "html",
+ label: "执行结果",
+ html: `
${renderOneLinerExecutionPayloadHtml(currentRun.result)}
`
+ }
+ ]
+ });
+}
+
async function loadPlatformAccount(platform, accountId, requestToken = 0) {
if (!accountId) return;
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
@@ -3992,6 +4066,7 @@ function button(label, action, tone = "secondary", options = {}) {
if (options.disabledReason) classes.push("is-disabled");
const targetAction = options.disabledReason ? "show-disabled-reason" : action;
const title = options.disabledReason || options.title || "";
+ const attrs = options.attrs || "";
return `
`.replace(/\s+/g, " ").trim();
}
@@ -4097,6 +4173,35 @@ function renderIntegrationOverviewPanel(options = {}) {
`;
}
+function buildMainAgentHandoffAttrs({
+ sourceScreen = "",
+ sourceActionKey = "",
+ intentKey = "custom",
+ title = "",
+ goal = "",
+ summary = "",
+ platform = "",
+ platformScope = "single_platform",
+ planSteps = []
+} = {}) {
+ const attrs = [
+ `data-source-screen="${escapeHtml(sourceScreen || appState.screen || "dashboard")}"`,
+ `data-source-action-key="${escapeHtml(sourceActionKey || "main-agent-handoff")}"`,
+ `data-intent-key="${escapeHtml(intentKey || "custom")}"`,
+ `data-title="${escapeHtml(title || goal || "交给主 Agent 处理")}"`,
+ `data-goal="${escapeHtml(goal || title || "交给主 Agent 处理")}"`,
+ `data-summary="${escapeHtml(summary || "")}"`,
+ `data-platform-scope="${escapeHtml(platformScope || "single_platform")}"`
+ ];
+ if (platform) {
+ attrs.push(`data-platform="${escapeHtml(platform)}"`);
+ }
+ if (safeArray(planSteps).length) {
+ attrs.push(`data-plan-steps="${escapeHtml(JSON.stringify(safeArray(planSteps)))}"`);
+ }
+ return attrs.join(" ");
+}
+
function renderEmptyState(title, description) {
return `
${escapeHtml(title)}${escapeHtml(description)}
`;
}
@@ -4997,10 +5102,20 @@ function renderAutomationScreen() {
{ value: "guards", label: "动作防呆" }
];
const activeTab = getActiveDetailTab("automationDetailTab", tabs);
+ const automationHandoffAttrs = buildMainAgentHandoffAttrs({
+ sourceScreen: "automation",
+ sourceActionKey: "automation-main-agent-handoff",
+ intentKey: "ops_admin",
+ title: "继续检查自动流程",
+ goal: "继续检查自动流程",
+ summary: "让主 Agent 结合依赖健康和动作防呆状态,给出下一步处理建议。",
+ platform: getPreferredPlatform(),
+ planSteps: ["读取当前依赖健康", "检查动作防呆和拦截状态", "生成下一步处理建议"]
+ });
return screenShell(
"自动流程",
"自动同步、日报生成和失败补跑先统一看这里。",
- `${button("刷新", "refresh-data")} ${button("OneLiner", "open-oneliner")} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`,
+ `${button("刷新", "refresh-data")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: automationHandoffAttrs })} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`,
`
自动流程
@@ -5143,6 +5258,16 @@ function renderPlaybookScreen() {
const localCatalog = appState.localModelCatalog || {};
const activeAdminOverrideNotice = appState.onelinerGovernanceEffective?.active_admin_override_notice || null;
const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean);
+ const playbookHandoffAttrs = buildMainAgentHandoffAttrs({
+ sourceScreen: "playbook",
+ sourceActionKey: "playbook-main-agent-handoff",
+ intentKey: "custom",
+ title: "继续梳理当前 Agent 工作区",
+ goal: "继续梳理当前 Agent 工作区",
+ summary: "让主 Agent 结合当前 Agent、模型和策略状态,给出下一步执行建议。",
+ platform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform(),
+ planSteps: ["读取当前 Agent 与模型配置", "检查当前策略与平台 Agent 缺口", "生成下一步执行建议"]
+ });
const tabs = [
{ value: "workspace", label: "当前 Agent 工作台" },
{ value: "platform_agents", label: "平台 Agent" },
@@ -5152,7 +5277,7 @@ function renderPlaybookScreen() {
return screenShell(
"Agent",
"这里接真实 Agent 列表,当前已经支持切换和编辑 Agent。",
- `${button("配置 OneLiner", "open-oneliner-profile")} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("生成文案", "open-generate-copy")} ${button("去生产", "goto-production", "primary")}`,
+ `${button("配置 OneLiner", "open-oneliner-profile")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: playbookHandoffAttrs })} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("去生产", "goto-production", "primary")}`,
`
Agent 概览
@@ -5181,7 +5306,7 @@ function renderPlaybookScreen() {
${escapeHtml(appState.onelinerProfile?.display_name || "OneLiner")}
${escapeHtml(appState.onelinerProfile?.default_platform ? platformLabel(appState.onelinerProfile.default_platform) : "未设默认平台")}
- 打开对话
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", playbookHandoffAttrs)}
@@ -5573,10 +5698,20 @@ function renderStrategyScreen() {
const project = getSelectedProject();
const platform = appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform();
const activeAdminOverrideNotice = appState.onelinerGovernanceEffective?.active_admin_override_notice || null;
+ const strategyHandoffAttrs = buildMainAgentHandoffAttrs({
+ sourceScreen: "strategy",
+ sourceActionKey: "strategy-main-agent-handoff",
+ intentKey: "custom",
+ title: "继续调整我的策略",
+ goal: "继续调整我的策略",
+ summary: "让主 Agent 结合当前生效层、个人策略和管理员覆盖,给出下一步治理建议。",
+ platform,
+ planSteps: ["读取当前生效策略", "检查用户层与管理员覆盖差异", "生成下一步治理建议"]
+ });
return screenShell(
"我的策略",
"把你和主 Agent 的对话沉淀成可查看、可回滚、可追溯的个人策略层。",
- `${button("编辑全局策略", "open-user-global-policy")} ${button("编辑当前平台策略", "open-user-platform-policy", "primary")} ${button("打开 OneLiner", "open-oneliner")}`,
+ `${button("编辑全局策略", "open-user-global-policy")} ${button("编辑当前平台策略", "open-user-platform-policy", "primary")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: strategyHandoffAttrs })}`,
`
当前策略工作区
@@ -9369,6 +9504,10 @@ document.addEventListener("click", async (event) => {
}
return;
}
+ if (name === "open-oneliner-run-result") {
+ openCurrentOneLinerRunResultAction(action.dataset.runId || "");
+ return;
+ }
if (name === "confirm-oneliner-run") {
try {
setBusy(true, "正在确认执行计划...");
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index 54f56e6..9212656 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -133,6 +133,9 @@ test("oneliner panel includes a dedicated runtime header for agent runs", () =>
assert.match(source, /data-role="oneliner-runs"/);
assert.match(runtime, /confirm-oneliner-run/);
assert.match(runtime, /cancel-oneliner-run/);
+ assert.match(runtime, /当前计划/);
+ assert.match(runtime, /renderOneLinerExecutionPayloadHtml\(currentRun\.result\)/);
+ assert.match(runtime, /open-oneliner-run-result/);
});
test("oneliner meta and action handlers expose governance entry points", () => {
@@ -147,6 +150,16 @@ test("oneliner meta and action handlers expose governance entry points", () => {
assert.match(actions, /name === "open-system-main-policy"/);
assert.match(actions, /name === "handoff-to-main-agent"/);
assert.match(actions, /name === "confirm-oneliner-run"/);
+ assert.match(actions, /name === "open-oneliner-run-result"/);
+});
+
+test("oneliner runtime remembers completed runs exactly once after hydration", () => {
+ const hydrate = extractBetween(APP, "async function hydrateSelectedOneLinerRun()", "async function loadAgentControlSurfaces(projectId = \"\")");
+ const state = extractBetween(APP, "const appState = {", "};\n\nlet PLATFORM_RUNTIME");
+ assert.match(state, /lastCompletedOnelinerRunId/);
+ assert.match(hydrate, /detail\.run_status === "done"/);
+ assert.match(hydrate, /appState\.lastCompletedOnelinerRunId !== detail\.id/);
+ assert.match(hydrate, /rememberAction\("主 Agent 已完成本轮"/);
});
test("system governance saves refresh control surfaces after persisting", () => {
@@ -199,12 +212,19 @@ test("governance UI exposes admin override target picker and history rollback en
test("user governance UI exposes personal history and rollback entrypoints", () => {
const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
+ const automation = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(playbook, /open-user-global-policy-history/);
assert.match(playbook, /open-user-platform-policy-history/);
+ assert.match(playbook, /handoff-to-main-agent/);
+ assert.match(playbook, /playbook-main-agent-handoff/);
assert.match(strategy, /active_admin_override_notice/);
assert.match(strategy, /管理员覆盖生效中/);
+ assert.match(strategy, /handoff-to-main-agent/);
+ assert.match(strategy, /strategy-main-agent-handoff/);
+ assert.match(automation, /handoff-to-main-agent/);
+ assert.match(automation, /automation-main-agent-handoff/);
assert.match(actions, /name === "open-user-global-policy-history"/);
assert.match(actions, /name === "open-user-platform-policy-history"/);