From bbceada4f119950c0eef7d9b581432d736e7024f Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 31 Mar 2026 04:03:05 +0800 Subject: [PATCH] feat: carry oneliner config version into agent runs --- CHANGELOG.md | 18 ++++- collector-service/app/oneliner_features.py | 77 +++++++++++++++++-- tests/test_main_agent_governance.py | 4 + web/storyforge-web-v4/assets/app.js | 32 ++++++++ .../tests/workbench-pages.test.mjs | 5 ++ 5 files changed, 129 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e4c1e..8464dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,22 @@ - 回滚生成新版本 - 审计记录包含更新与回滚动作 - 前端工作台测试也新增了 `OneLiner 主配置历史` 的回滚与审计入口校验。 +- 主 Agent 配置业务流的这轮修复已经同步到 Gitea,后续可以直接基于当前分支继续收剩余真实能力细节。 + +### OneLiner 配置版本进入执行链 + +- 主 Agent 在创建 run、重试 run 和完成 run 时,都会把当前 `OneLiner 主配置版本` 一起固化进治理快照和结果卡,不再只有治理页知道自己用了哪一版配置。 +- 完成态结果现在会带上 `execution_card.oneliner_profile_version`,前端浮窗、运行卡和结果卡都能统一显示 `配置 vN`,避免进入执行链后丢失配置来源。 +- run 的治理快照也收窄成“当前主配置 + 当前版本”最小运行态,不再把完整版本历史和审计链塞进每次执行记录,避免 `agent_runs.governance_json` 无限制膨胀。 +- Web 回归测试修正了 OneLiner 运行区函数边界,并新增了对执行链配置版本显示的断言;后端治理测试也补上了 run 完成态必须带配置版本的检查。 + +### NAS 联调发布 + +- 最新 Web 已重新发布到 fnOS NAS: - Web: `http://192.168.31.188:19192/` - Collector: `http://192.168.31.188:19193/healthz` -- 主 Agent 配置业务流的这轮修复已经同步到 Gitea,后续可以直接基于当前分支继续收剩余真实能力细节。 +- 当前基线重新验证通过: + - 前端测试 `67/67` + - 后端单测 `36/36` + - `bash scripts/check_repo_baseline.sh` + - `bash scripts/smoke_fnos_storyforge_lan.sh` diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 1c2d401..8e3dbfe 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -1064,6 +1064,26 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: ) return payload + def _oneliner_profile_runtime_snapshot(row: dict[str, Any], *, account: dict[str, Any] | None = None) -> dict[str, Any]: + bundle = _oneliner_profile_bundle(row, account=account) + return { + "id": str(bundle.get("id") or "").strip(), + "assistant_id": str(bundle.get("assistant_id") or "").strip(), + "display_name": str(bundle.get("display_name") or "").strip(), + "long_term_goal": str(bundle.get("long_term_goal") or "").strip(), + "default_platform": str(bundle.get("default_platform") or "").strip(), + "notes": str(bundle.get("notes") or "").strip(), + "summary": str(bundle.get("summary") or "").strip(), + "current_version": { + "id": str((bundle.get("current_version") or {}).get("id") or "").strip(), + "version_no": int((bundle.get("current_version") or {}).get("version_no") or 0), + "title": str((bundle.get("current_version") or {}).get("title") or "").strip(), + "summary": str((bundle.get("current_version") or {}).get("summary") or "").strip(), + "reason": str((bundle.get("current_version") or {}).get("reason") or "").strip(), + "created_at": str((bundle.get("current_version") or {}).get("created_at") or "").strip(), + }, + } + def _profile_payload(row: dict[str, Any], *, account: dict[str, Any] | None = None) -> dict[str, Any]: assistant = None if row.get("assistant_id"): @@ -3164,6 +3184,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: timestamp = now() plan = _parse_json(row.get("plan_json"), {}) + governance = _parse_json(row.get("governance_json"), {}) + oneliner_profile_version = ( + governance.get("oneliner_profile_version") + or (governance.get("oneliner_profile") or {}).get("current_version") + or {} + ) steps = [str(item).strip() for item in list(plan.get("steps") or []) if str(item).strip()] if not steps: steps = ["读取当前项目上下文", "结合治理层生成执行计划", "收口为可执行建议"] @@ -3184,6 +3210,20 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "recommended_action": recommended_action, "result_sections": result_sections, "active_admin_override_notice": _parse_json(row.get("active_admin_override_notice_json"), {}), + "execution_card": { + "intent_key": str(plan.get("intent_key") or row.get("intent_key") or "custom").strip() or "custom", + "intent_label": str(plan.get("intent_label") or "").strip() or "主 Agent 任务", + "delivery_mode": str(plan.get("delivery_mode") or row.get("delivery_mode") or "oneliner").strip() or "oneliner", + "platform": str(plan.get("platform") or row.get("platform") or "").strip(), + "platform_label": str(plan.get("platform_label") or "").strip() or "待判断", + "active_admin_override_notice": _parse_json(row.get("active_admin_override_notice_json"), {}), + "oneliner_profile_version": { + "version_no": int(oneliner_profile_version.get("version_no") or 0), + "title": str(oneliner_profile_version.get("title") or "").strip(), + "summary": str(oneliner_profile_version.get("summary") or "").strip(), + }, + "next_steps": steps, + }, } _log_agent_run_event( run_id, @@ -3527,7 +3567,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: ) if platform else None return { "project": legacy.project_payload(project), - "oneliner_profile": _profile_payload(profile_row, account=account) if profile_row else None, + "oneliner_profile": _oneliner_profile_bundle(profile_row, account=account) if profile_row else None, "platform_agent": _platform_agent_payload(account, platform_profile, platform=platform, project_id=project["id"]) if platform else None, "assistant": legacy.assistant_payload(assistant) if assistant else None, "oneliner_memories": [_memory_payload(row) for row in oneliner_memory_rows], @@ -3561,6 +3601,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: plan: dict[str, Any], ) -> dict[str, Any]: context = _session_context_summary(account, project_id or "", plan.get("platform") or "") + oneliner_profile = context.get("oneliner_profile") or {} platform_agent = context.get("platform_agent") or {} governance = context.get("governance") or {} effective_policy = (governance.get("effective") or {}).get("effective_policy") or {} @@ -3603,6 +3644,9 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: next_steps.append(f"当前会话已叠加 {len(governance_layers)} 层策略,我会先按生效策略执行。") if active_admin_override_notice.get("title"): next_steps.append(f"当前存在管理员覆盖:{active_admin_override_notice.get('title')}。") + oneliner_profile_version = oneliner_profile.get("current_version") or {} + if oneliner_profile_version.get("version_no"): + next_steps.append(f"当前主 Agent 配置版本:v{oneliner_profile_version.get('version_no')}。") summary_lines = [ f"我理解你的目标是:{plan.get('intent_label', '自定义任务')}。", f"建议优先处理的平台:{plan.get('platform_label', '待判断')}。", @@ -3622,6 +3666,8 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: summary_lines.append("当前策略层要求管理员复核,所以我会把高风险动作先压成待确认。") if active_admin_override_notice.get("title"): summary_lines.append(f"管理员覆盖生效中:{active_admin_override_notice.get('title')}。") + if oneliner_profile_version.get("version_no"): + summary_lines.append(f"当前主 Agent 正在按配置版本 v{oneliner_profile_version.get('version_no')} 执行。") secondary_actions: list[dict[str, Any]] = [] if plan.get("platform"): secondary_actions.append( @@ -3786,6 +3832,11 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "primary_action": primary_action or {}, "blocked_reason": blocked_reason, "active_admin_override_notice": active_admin_override_notice, + "oneliner_profile_version": { + "version_no": oneliner_profile_version.get("version_no", 0), + "title": oneliner_profile_version.get("title", ""), + "summary": oneliner_profile_version.get("summary", ""), + }, "evidence": evidence, "next_steps": next_steps, "secondary_actions": secondary_actions, @@ -5035,9 +5086,16 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: subject_project_id=project["id"], platform=normalized_platform, ) + profile_row = _fetch_profile_row(account, project["id"]) or _ensure_oneliner_profile(account, project["id"]) + oneliner_profile_bundle = _oneliner_profile_runtime_snapshot(profile_row, account=account) + governance_snapshot = { + **governance, + "oneliner_profile": oneliner_profile_bundle, + "oneliner_profile_version": oneliner_profile_bundle.get("current_version") or {}, + } plan = _agent_run_plan_payload( request, - governance=governance, + governance=governance_snapshot, platform=normalized_platform, platform_scope=platform_scope, ) @@ -5051,7 +5109,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: _touch_session_for_run(session["id"], platform=normalized_platform, intent_key=plan.get("intent_key", "custom")) run_id = make_id("oline_run") timestamp = now() - active_admin_override_notice = governance.get("active_admin_override_notice") or {} + active_admin_override_notice = governance_snapshot.get("active_admin_override_notice") or {} legacy.db.execute( """ INSERT INTO agent_runs ( @@ -5077,7 +5135,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: _normalize_delivery_mode(request.delivery_mode), _normalize_scheduling_mode(request.scheduling_mode), _dump(plan), - _dump(governance), + _dump(governance_snapshot), "等待你确认执行计划", _dump(active_admin_override_notice), timestamp, @@ -5119,13 +5177,20 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: subject_project_id=project_id, platform=platform, ) + profile_row = _fetch_profile_row(account, project_id) or _ensure_oneliner_profile(account, project_id) + oneliner_profile_bundle = _oneliner_profile_runtime_snapshot(profile_row, account=account) + governance_snapshot = { + **governance, + "oneliner_profile": oneliner_profile_bundle, + "oneliner_profile_version": oneliner_profile_bundle.get("current_version") or {}, + } plan = _parse_json(row.get("plan_json"), {}) session_id = str(row.get("session_id") or "").strip() _touch_session_for_run(session_id, platform=platform, intent_key=str(row.get("intent_key") or "custom").strip() or "custom") next_run_id = make_id("oline_run") timestamp = now() - active_admin_override_notice = governance.get("active_admin_override_notice") or {} + active_admin_override_notice = governance_snapshot.get("active_admin_override_notice") or {} legacy.db.execute( """ INSERT INTO agent_runs ( @@ -5151,7 +5216,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: _normalize_delivery_mode(str(row.get("delivery_mode") or "hybrid")), _normalize_scheduling_mode(str(row.get("scheduling_mode") or "queued")), _dump(plan), - _dump(governance), + _dump(governance_snapshot), "等待你确认执行计划", _dump(active_admin_override_notice), timestamp, diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index 8fee4d3..b150392 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -224,6 +224,9 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertEqual(payload["recommended_preview_action"]["screen"], "discovery") self.assertEqual(payload["governance"]["project_id"], self.ctx["project_id"]) self.assertIn("layers", payload["governance"]) + self.assertIn("oneliner_profile", payload["governance"]) + self.assertIn("oneliner_profile_version", payload["governance"]) + self.assertGreaterEqual(payload["governance"]["oneliner_profile_version"]["version_no"], 1) self.assertEqual(payload["events"][0]["event_type"], "run.created") def test_agent_run_confirm_transitions_to_queue_or_running_and_logs_events(self) -> None: @@ -306,6 +309,7 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertEqual(payload["result"]["result_sections"]["workstream_key"], "discovery") self.assertGreaterEqual(len(payload["result"]["result_sections"]["cards"]), 2) self.assertEqual(payload["result"]["result_sections"]["cards"][0]["title"], "当前焦点") + self.assertGreaterEqual(payload["result"]["execution_card"]["oneliner_profile_version"]["version_no"], 1) event_types = [item["event_type"] for item in payload["events"]] self.assertIn("run.progress", event_types) self.assertIn("run.done", event_types) diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 60ca00a..7fdf0e6 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1130,6 +1130,7 @@ function renderOneLinerRunsHtml() { ? "orange" : ""; const canRetryCurrentRun = ["blocked", "failed", "cancelled"].includes(currentRun.run_status); + const currentRunConfigVersion = currentRun.governance?.oneliner_profile_version || currentRun.governance?.oneliner_profile?.current_version || {}; return `

近期运行概况

@@ -1172,6 +1173,7 @@ function renderOneLinerRunsHtml() { 当前运行 ${escapeHtml(runStatusLabel)} ${escapeHtml(currentRun.platform_scope === "all_platforms" ? "全平台" : "单平台")} + ${currentRunConfigVersion.version_no ? `${escapeHtml(`配置 v${formatNumber(currentRunConfigVersion.version_no || 0)}`)}` : ""} ${escapeHtml(recommendedAction?.label || "继续这个任务")}
@@ -1199,6 +1201,7 @@ function renderOneLinerRunsHtml() { ${escapeHtml(runStatusLabel)} ${currentRun.platform_label ? `${escapeHtml(currentRun.platform_label)}` : ""} ${escapeHtml(onelinerIntentLabel(currentRun.intent_key))} + ${currentRunConfigVersion.version_no ? `配置 v${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))}` : ""} ${currentRun.source_screen ? `${escapeHtml(currentRun.source_screen)}` : ""}
@@ -1215,9 +1218,20 @@ function renderOneLinerRunsHtml() { ${escapeHtml(currentRun.source_action_key || "manual-handoff")} ${escapeHtml(currentRun.platform_scope === "all_platforms" ? "全平台" : "单平台")} ${currentRun.delivery_mode ? `${escapeHtml(currentRun.delivery_mode)}` : ""} + ${currentRunConfigVersion.version_no ? `配置版本 ${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))}` : ""} ${previewAction?.action ? `${escapeHtml(`预计落点 · ${previewAction.label || "对应页面"}`)}` : ""} + ${currentRunConfigVersion.version_no ? ` +
+

本轮主配置版本

+

${escapeHtml(currentRunConfigVersion.summary || currentRunConfigVersion.title || `OneLiner 配置 v${formatNumber(currentRunConfigVersion.version_no || 0)}`)}

+
+ 配置 v${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))} + 查看配置历史 +
+
+ ` : ""} ${!hasResultPayload && previewAction?.summary ? `
${escapeHtml(previewAction.summary)}
` : ""} ${planSteps.length ? `
@@ -1304,6 +1318,7 @@ function renderOneLinerMessagesHtml() { const plan = message.plan || {}; const executionCard = result.execution_card || {}; const activeAdminOverrideNotice = executionCard.active_admin_override_notice || null; + const profileVersion = executionCard.oneliner_profile_version || {}; const actions = safeArray(plan.suggested_actions); const secondaryActions = safeArray(executionCard.secondary_actions); return ` @@ -1331,9 +1346,13 @@ function renderOneLinerMessagesHtml() { ${executionCard.platform_label ? `${escapeHtml(executionCard.platform_label)}` : ""} ${executionCard.platform_agent_name ? `${escapeHtml(executionCard.platform_agent_name)}` : ""} ${executionCard.assistant_name ? `${escapeHtml(executionCard.assistant_name)}` : ""} + ${profileVersion.version_no ? `配置 v${escapeHtml(formatNumber(profileVersion.version_no || 0))}` : ""} ${executionCard.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(executionCard.readiness_label)} ${escapeHtml(formatNumber(executionCard.readiness_score || 0))}` : ""} ${executionCard.primary_action?.key ? `${escapeHtml(executionCard.primary_action.label || "执行下一步")}` : ""}
+ ${profileVersion.version_no ? ` +
${escapeHtml(profileVersion.summary || profileVersion.title || `当前按 OneLiner 配置 v${formatNumber(profileVersion.version_no || 0)} 执行。`)}
+ ` : ""} ${activeAdminOverrideNotice?.title ? `

管理员覆盖生效中

@@ -1425,6 +1444,7 @@ function renderOneLinerUi() { ${escapeHtml(profile?.display_name || "OneLiner")} ${escapeHtml(getSelectedProject()?.name || "未选项目")} ${escapeHtml(profile?.default_platform ? platformLabel(profile.default_platform) : "未设默认平台")} + ${profile?.current_version?.version_no ? `配置 v${escapeHtml(formatNumber(profile.current_version.version_no || 0))}` : ""} ${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent
${escapeHtml(profile?.long_term_goal || "当前没有设置长期目标。你可以先在这里说目标,后续再逐步产品化。")}
@@ -1948,6 +1968,7 @@ function renderOneLinerExecutionPayloadHtml(payload) { ? payload.result_sections : {}; const resultCards = safeArray(resultSections.cards).slice(0, 4); + const configVersion = payload.execution_card?.oneliner_profile_version || payload.context?.oneliner_profile?.current_version || {}; const landingAttrs = buildMainAgentLandingAttrs({ runId: landingRunId, screen: landingScreen, @@ -1961,10 +1982,21 @@ function renderOneLinerExecutionPayloadHtml(payload) {
${payload.platform ? `${escapeHtml(platformLabel(payload.platform))}` : ""} ${escapeHtml(payload.platform_scope === "all_platforms" ? "全平台" : "单平台")} + ${configVersion.version_no ? `配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))}` : ""} 已收口 ${payload.recommended_action?.action ? `${escapeHtml(payload.recommended_action.label || "回到对应页面")}` : ""}
+ ${configVersion.version_no ? ` +
+

本轮使用的主配置

+

${escapeHtml(configVersion.summary || configVersion.title || `OneLiner 主配置版本 v${formatNumber(configVersion.version_no || 0)}`)}

+
+ 配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))} + 查看配置历史 +
+
+ ` : ""} ${resultCards.length ? `

${escapeHtml(resultSections.workstream_label || "执行结果分组")}

diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 873d8bc..d607509 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -572,10 +572,15 @@ 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()"); const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); + const runs = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()"); assert.match(meta, /open-user-global-policy/); + assert.match(meta, /配置 v/); assert.match(meta, /renderOneLinerRunsHtml\(\)/); assert.match(meta, /policyScopeTagLabel/); assert.match(messages, /active_admin_override_notice/); + assert.match(messages, /oneliner_profile_version/); + assert.match(runs, /oneliner_profile_version/); + assert.match(runs, /open-oneliner-profile-history/); assert.match(actions, /name === "open-user-global-policy"/); assert.match(actions, /name === "open-system-main-policy"/); assert.match(actions, /name === "handoff-to-main-agent"/);