From 294846e6032d14d7aecf2aef6072f3ae526eb881 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 4 Apr 2026 07:56:20 +0800 Subject: [PATCH] feat: complete main agent message config tracing --- CHANGELOG.md | 6 ++ collector-service/app/oneliner_features.py | 30 +++++++-- tests/test_main_agent_governance.py | 64 +++++++++++++++++++ web/storyforge-web-v4/assets/app.js | 46 +++++++------ .../tests/workbench-pages.test.mjs | 4 ++ 5 files changed, 126 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74484ed..8aaf15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## 2026-04-04 +### 主 Agent 消息卡补齐配置追溯与主动作执行上下文 + +- OneLiner 助手消息卡里的 `主配置历史 / 平台配置历史` 现在终于拿到真实 `version_id`,不再出现“入口在,但打开后只能停在列表顶部”的半截体验。 +- 助手消息卡里的主动作也改成了和次级动作一致的执行标签:会把 `session_id / platform / executor_key / payload` 一起带上,后续再从消息卡直接执行时,不会丢掉真实上下文。 +- 后端回归新增了消息卡 `execution_card` 配置追溯断言,前端回归也锁住了主动作统一走 `actionTag + buildOnelinerActionAttrs`,避免后续又退回到只剩一个裸 `data-action`。 + ### 主 Agent 结果卡支持直达配置版本 - 主 Agent 当前运行卡、执行结果卡、平台 Agent 最近执行卡,现在不只显示 `配置 vN / 平台 Agent vN`,而且可以直接点进去打开对应的历史弹层。 diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index ac806b6..3e3a728 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -4052,6 +4052,8 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: 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 {} + platform_agent_assistant = platform_agent.get("assistant") or {} + context_assistant = context.get("assistant") or {} governance = context.get("governance") or {} effective_policy = (governance.get("effective") or {}).get("effective_policy") or {} governance_layers = (governance.get("effective") or {}).get("layers") or [] @@ -4085,8 +4087,8 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: next_steps = [] if primary_action: next_steps.append(f"优先执行「{primary_action.get('label', primary_action.get('key', '下一步'))}」。") - if platform_agent.get("assistant", {}).get("name"): - next_steps.append(f"默认调度 {platform_agent['assistant']['name']} 作为执行 Agent。") + if platform_agent_assistant.get("name"): + next_steps.append(f"默认调度 {platform_agent_assistant.get('name')} 作为执行 Agent。") if evidence: next_steps.append("我会优先参考该平台 Agent 最近沉淀的方法与技能。") if governance_layers: @@ -4106,7 +4108,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: if plan.get("intent_key") == "ops_admin" and account.get("role") != "super_admin": summary_lines.append("当前账号不是平台最高权限用户,所以我不会放出运维 Agent 入口。") if context.get("platform_agent"): - summary_lines.append(f"当前 {context['platform_agent']['platform_label']} Agent 已绑定:{context['platform_agent'].get('assistant', {}).get('name') or '未绑定执行 Agent'}。") + summary_lines.append(f"当前 {context['platform_agent']['platform_label']} Agent 已绑定:{platform_agent_assistant.get('name') or '未绑定执行 Agent'}。") if platform_agent.get("recent_memory"): summary_lines.append(f"最近有效经验:{platform_agent['recent_memory'].get('title') or '一条平台记忆'}。") if platform_agent.get("recent_skill"): @@ -4204,7 +4206,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: }, } ) - if context.get("assistant"): + if context_assistant: secondary_actions.append( { "key": "run-oneliner-action", @@ -4261,6 +4263,11 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "platform": "", } ) + decorated_primary_action = _decorate_oneliner_action( + account, + project_id=project_id or "", + action=primary_action or {}, + ) if primary_action else {} secondary_actions = [ _decorate_oneliner_action(account, project_id=project_id or "", action=item) for item in secondary_actions @@ -4275,17 +4282,28 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "platform": plan.get("platform", ""), "platform_label": plan.get("platform_label", "待判断"), "platform_agent_name": platform_agent.get("name") or "", - "assistant_name": platform_agent.get("assistant", {}).get("name") or context.get("assistant", {}).get("name") or "", + "assistant_name": platform_agent_assistant.get("name") or context_assistant.get("name") or "", "readiness_label": platform_agent.get("readiness_label") or "", "readiness_score": platform_agent.get("readiness_score") or 0, - "primary_action": primary_action or {}, + "primary_action": decorated_primary_action, "blocked_reason": blocked_reason, "active_admin_override_notice": active_admin_override_notice, "oneliner_profile_version": { + "version_id": oneliner_profile_version.get("id", ""), "version_no": oneliner_profile_version.get("version_no", 0), "title": oneliner_profile_version.get("title", ""), "summary": oneliner_profile_version.get("summary", ""), }, + "platform_agent_profile": { + "platform": platform_agent.get("platform") or plan.get("platform", ""), + "platform_label": platform_agent.get("platform_label") or plan.get("platform_label", ""), + "name": platform_agent.get("name", ""), + "assistant_name": platform_agent_assistant.get("name") or context_assistant.get("name") or "", + "version_id": ((platform_agent.get("current_version") or {}).get("id") or ""), + "version_no": ((platform_agent.get("current_version") or {}).get("version_no") or 0), + "readiness_label": platform_agent.get("readiness_label") or "", + "readiness_score": platform_agent.get("readiness_score") or 0, + }, "evidence": evidence, "next_steps": next_steps, "secondary_actions": secondary_actions, diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index 7752f74..33bb7e2 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -1401,3 +1401,67 @@ class MainAgentGovernanceTests(unittest.TestCase): action_keys = [item["action_key"] for item in audits_payload["items"]] self.assertIn("update-oneliner-profile", action_keys) self.assertIn("rollback-oneliner-profile", action_keys) + + def test_oneliner_message_execution_card_tracks_config_versions(self) -> None: + profile_response = self.client.put( + "/v2/oneliner/profile", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "display_name": "增长总控 OneLiner", + "assistant_id": "", + "default_platform": "douyin", + "long_term_goal": "优先分析当前平台账号并收口到下一步动作", + "notes": "验证消息卡里的配置追溯链", + "config": {"analysis_mode": "fast"}, + "reason": "给消息卡提供明确的主配置版本", + }, + ) + self.assertEqual(profile_response.status_code, 200, profile_response.text) + current_profile_version = profile_response.json()["current_version"] + + platform_response = self.client.put( + "/v2/platform-agents/douyin/profile", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "name": "抖音增长 Agent", + "mission": "优先分析当前账号和高分作品", + "notes": "验证消息卡里的平台配置追溯链", + "status": "active", + "config": {"focus": "analysis"}, + }, + ) + self.assertEqual(platform_response.status_code, 200, platform_response.text) + current_platform_version = platform_response.json()["current_version"] + + session_response = self.client.post( + "/v2/oneliner/sessions", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "preferred_platform": "douyin", + "title": "消息卡配置追溯", + }, + ) + self.assertEqual(session_response.status_code, 200, session_response.text) + session_payload = session_response.json() + + message_response = self.client.post( + f"/v2/oneliner/sessions/{session_payload['id']}/messages", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "platform": "douyin", + "content": "帮我创建 Agent", + }, + ) + self.assertEqual(message_response.status_code, 200, message_response.text) + payload = message_response.json() + execution_card = (((payload.get("assistant_message") or {}).get("result")) or {}).get("execution_card") or {} + self.assertEqual((execution_card.get("primary_action") or {}).get("key"), "open-create-assistant") + self.assertEqual((execution_card.get("oneliner_profile_version") or {}).get("version_id"), current_profile_version["id"]) + self.assertEqual((execution_card.get("oneliner_profile_version") or {}).get("version_no"), current_profile_version["version_no"]) + self.assertEqual((execution_card.get("platform_agent_profile") or {}).get("platform"), "douyin") + self.assertEqual((execution_card.get("platform_agent_profile") or {}).get("version_id"), current_platform_version["id"]) + self.assertEqual((execution_card.get("platform_agent_profile") or {}).get("version_no"), current_platform_version["version_no"]) diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 09d62eb..4842f8a 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1346,8 +1346,28 @@ function renderOneLinerMessagesHtml() { const executionCard = result.execution_card || {}; const activeAdminOverrideNotice = executionCard.active_admin_override_notice || null; const profileVersion = executionCard.oneliner_profile_version || {}; + const platformAgentProfile = executionCard.platform_agent_profile || {}; const actions = safeArray(plan.suggested_actions); + const primaryAction = executionCard.primary_action || {}; const secondaryActions = safeArray(executionCard.secondary_actions); + const buildOnelinerActionAttrs = (item) => { + const attrs = [ + item.executor_key ? `data-executor-key="${escapeHtml(item.executor_key)}"` : "", + item.platform ? `data-platform="${escapeHtml(item.platform)}"` : "", + message.session_id ? `data-session-id="${escapeHtml(message.session_id)}"` : "", + ]; + Object.entries(item.payload || {}).forEach(([payloadKey, payloadValue]) => { + const attrKey = String(payloadKey || "") + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .replace(/_/g, "-") + .toLowerCase(); + const serialized = typeof payloadValue === "string" + ? payloadValue + : JSON.stringify(payloadValue); + attrs.push(`data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`); + }); + return attrs.filter(Boolean).join(" "); + }; return `
@@ -1375,9 +1395,14 @@ function renderOneLinerMessagesHtml() { ${executionCard.assistant_name ? `${escapeHtml(executionCard.assistant_name)}` : ""} ${profileVersion.version_no ? `配置 v${escapeHtml(formatNumber(profileVersion.version_no || 0))}` : ""} ${profileVersion.version_no ? `看主配置历史` : ""} - ${executionCard.platform && executionCard.platform_agent_profile?.version_no ? `看平台配置历史` : ""} + ${executionCard.platform && platformAgentProfile.version_no ? `看平台配置历史` : ""} ${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 || "执行下一步")}` : ""} + ${primaryAction.key ? actionTag( + primaryAction.label || "执行下一步", + primaryAction.key || "", + buildOnelinerActionAttrs(primaryAction), + { disabledReason: primaryAction.disabled_reason || "" } + ) : ""}
${profileVersion.version_no ? `
${escapeHtml(profileVersion.summary || profileVersion.title || `当前按 OneLiner 配置 v${formatNumber(profileVersion.version_no || 0)} 执行。`)}
@@ -1412,22 +1437,7 @@ function renderOneLinerMessagesHtml() { ${secondaryActions.map((item) => actionTag( item.label || item.key || "执行", item.key || "", - [ - item.executor_key ? `data-executor-key="${escapeHtml(item.executor_key)}"` : "", - item.platform ? `data-platform="${escapeHtml(item.platform)}"` : "", - message.session_id ? `data-session-id="${escapeHtml(message.session_id)}"` : "", - ...Object.entries(item.payload || {}).map(([payloadKey, payloadValue]) => { - const attrKey = String(payloadKey || "") - .replace(/([a-z0-9])([A-Z])/g, "$1-$2") - .replace(/_/g, "-") - .toLowerCase(); - const serialized = typeof payloadValue === "string" - ? payloadValue - : JSON.stringify(payloadValue); - return `data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`; - }) - ].filter(Boolean).join(" ") - , + buildOnelinerActionAttrs(item), { disabledReason: item.disabled_reason || "" } )).join("")}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index d55b152..2a0aa23 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -961,9 +961,13 @@ test("main agent execution cards can jump to oneliner and platform profile histo const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderAutoConnectingScreen(screenTitle, nextStepText)"); const runtime = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()"); const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)"); + assert.match(messages, /const buildOnelinerActionAttrs = \(item\) =>/); assert.match(messages, /data-action="open-oneliner-profile-history"/); assert.match(messages, /data-action="open-platform-agent-profile-history"/); assert.match(messages, /data-version-id="\$\{escapeHtml\(profileVersion\.version_id \|\| ""\)\}"/); + assert.match(messages, /data-version-id="\$\{escapeHtml\(platformAgentProfile\.version_id \|\| ""\)\}"/); + assert.match(messages, /actionTag\(\s*primaryAction\.label \|\| "执行下一步"/); + assert.match(messages, /buildOnelinerActionAttrs\(primaryAction\)/); assert.match(runtime, /data-version-id="\$\{escapeHtml\(currentRunConfigVersion\.version_id \|\| ""\)\}"/); assert.match(execution, /data-version-id="\$\{escapeHtml\(configVersion\.version_id \|\| ""\)\}"/); assert.match(execution, /data-version-id="\$\{escapeHtml\(platformAgentProfile\.version_id \|\| ""\)\}"/);