From b272c5edfd469b40a448803161fbd7d69134f81c Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 4 Apr 2026 08:40:04 +0800 Subject: [PATCH] feat: preserve requested context in main agent results --- CHANGELOG.md | 7 ++ collector-service/app/oneliner_features.py | 84 ++++++++++++++++--- tests/test_main_agent_governance.py | 43 ++++++++++ web/storyforge-web-v4/assets/app.js | 3 + .../tests/workbench-pages.test.mjs | 3 + 5 files changed, 129 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0593494..2eea2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## 2026-04-04 +### 主 Agent 完成态保留精确对象上下文 + +- 主 Agent run 在创建时会把 `target_account_id / tracked_account_id / job_id / review_id / source_id / assistant_id` 这类对象上下文固化进执行计划,不再只记一个泛化的来源页面。 +- 完成态推荐动作现在会优先直接回到具体对象:可以直接打开当前账号、刷新当前跟踪对象、进入任务详情、打开复盘、继续录制维护,或回到刚才编辑的 Agent。 +- 前端推荐动作属性映射补齐了 `account_id / tracked_account_id / assistant_id`,当前运行卡、结果卡、最近动作卡和后续落点入口都能保住真实对象上下文。 +- 治理回归新增了“围绕当前账号继续分析”这条链路,锁住主 Agent 完成态结果必须返回 `select-account` 和真实 `account_id`。 + ### OneLiner 直接执行结果补齐精确落点 - OneLiner 直接执行动作现在统一返回结构化 `recommended_action`,不再只有“执行完成”说明块。 diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 230579d..fb0e4a2 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -3375,12 +3375,43 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: platform_scope: str, ) -> dict[str, Any]: requested_plan = dict(request.plan_request or {}) + requested_payload = dict(request.payload or {}) raw_steps = requested_plan.get("steps") or [] if not isinstance(raw_steps, list): raw_steps = [raw_steps] steps = [str(item).strip() for item in raw_steps if str(item).strip()] if not steps: steps = ["读取当前项目上下文", "结合治理层生成执行计划", "等待用户确认后执行"] + requested_context: dict[str, Any] = {} + for key in ( + "target_account_id", + "targetAccountId", + "tracked_account_id", + "trackedAccountId", + "job_id", + "source_job_id", + "sourceJobId", + "review_id", + "reviewId", + "source_id", + "sourceId", + "assistant_id", + "assistantId", + "platform", + ): + value = requested_payload.get(key) + text = str(value or "").strip() if value is not None else "" + if not text: + continue + normalized_key = { + "targetAccountId": "target_account_id", + "trackedAccountId": "tracked_account_id", + "sourceJobId": "source_job_id", + "reviewId": "review_id", + "sourceId": "source_id", + "assistantId": "assistant_id", + }.get(key, key) + requested_context[normalized_key] = text return { **requested_plan, "goal": str(requested_plan.get("goal") or request.title or "主 Agent 任务").strip() or "主 Agent 任务", @@ -3392,6 +3423,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "source_action_key": str(request.source_action_key or "").strip(), "summary": str(request.summary or requested_plan.get("summary") or "").strip(), "requested_delivery_mode": _normalize_delivery_mode(request.delivery_mode), + "requested_context": requested_context, "active_admin_override_notice": governance.get("active_admin_override_notice") or {}, } @@ -3412,13 +3444,43 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: source_action_key = str(plan.get("source_action_key") or row.get("source_action_key") or "").strip().lower() intent_key = str(plan.get("intent_key") or row.get("intent_key") or "custom").strip().lower() or "custom" - def route(action: str, screen: str, label: str, summary: str) -> dict[str, Any]: - return { + requested_context = plan.get("requested_context") if isinstance(plan.get("requested_context"), dict) else {} + + def route(action: str, screen: str, label: str, summary: str, **extra: Any) -> dict[str, Any]: + payload = { "action": action, "screen": screen, "label": label, "summary": summary, } + for key, value in extra.items(): + text = str(value or "").strip() if value is not None else "" + if not text: + continue + payload[key] = value + return payload + + target_account_id = str(requested_context.get("target_account_id") or "").strip() + tracked_account_id = str(requested_context.get("tracked_account_id") or "").strip() + job_id = str(requested_context.get("job_id") or requested_context.get("source_job_id") or "").strip() + review_id = str(requested_context.get("review_id") or "").strip() + source_id = str(requested_context.get("source_id") or "").strip() + assistant_id = str(requested_context.get("assistant_id") or "").strip() + + if review_id: + return route("open-review-edit", "review", "打开复盘", "继续回到当前复盘对象完善结论和动作。", review_id=review_id, job_id=job_id) + if source_screen == "review" and job_id: + return route("open-review-from-job", "review", "继续写复盘", "继续围绕这条任务生成或完善复盘。", job_id=job_id) + if source_screen in {"discovery", "tracking"} and target_account_id: + return route("select-account", "discovery", "打开当前对象", "继续围绕当前账号查看详情、对标和分析结果。", account_id=target_account_id) + if source_screen == "tracking" and tracked_account_id: + return route("refresh-tracked-account", "tracking", "继续同步当前账号", "继续同步这条已跟踪账号,并回看最近更新。", tracked_account_id=tracked_account_id) + if source_screen == "production" and job_id: + return route("open-job-detail", "production", "看任务详情", "继续回到当前任务,查看执行状态和后续动作。", job_id=job_id) + if source_screen == "production" and source_id: + return route("edit-live-recorder-source", "production", "继续录制维护", "继续查看当前录制源的启停和录制文件。", source_id=source_id) + if source_screen in {"playbook", "agent"} and assistant_id: + return route("open-edit-assistant", "playbook", "继续编辑 Agent", "继续围绕当前 Agent 调整目标、说明和承接能力。", assistant_id=assistant_id) source_routes = { "strategy": route("goto-strategy", "strategy", "回到我的策略", "继续查看当前用户策略与覆盖状态。"), @@ -3442,16 +3504,16 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: return source_routes[source_screen] intent_routes = { - "analyze_account": route("goto-discovery", "discovery", "回到找对标", "继续拆解当前账号和对标对象。"), - "analyze_top_videos": route("goto-discovery", "discovery", "回到找对标", "继续查看高分作品分析结论。"), - "track_account": route("goto-tracking", "tracking", "回到跟踪账号", "继续更新账号跟踪与日报。"), - "ai_video": route("goto-production", "production", "回到生产中心", "继续推进 AI 视频生产任务。"), - "real_cut": route("goto-production", "production", "回到生产中心", "继续推进实拍剪辑任务。"), - "live_recorder": route("goto-production", "production", "回到生产中心", "继续查看录制维护与产物。"), - "review": route("goto-review", "review", "回到发布与复盘", "继续补齐复盘与发布总结。"), - "create_assistant": route("goto-playbook", "playbook", "回到 Agent", "继续创建或调整项目 Agent。"), + "analyze_account": route(target_account_id and "select-account" or "goto-discovery", "discovery", target_account_id and "打开当前对象" or "回到找对标", "继续拆解当前账号和对标对象。", account_id=target_account_id), + "analyze_top_videos": route(target_account_id and "select-account" or "goto-discovery", "discovery", target_account_id and "打开当前对象" or "回到找对标", "继续查看高分作品分析结论。", account_id=target_account_id), + "track_account": route(tracked_account_id and "refresh-tracked-account" or "goto-tracking", "tracking", tracked_account_id and "继续同步当前账号" or "回到跟踪账号", "继续更新账号跟踪与日报。", tracked_account_id=tracked_account_id), + "ai_video": route(job_id and "open-job-detail" or "goto-production", "production", job_id and "看任务详情" or "回到生产中心", "继续推进 AI 视频生产任务。", job_id=job_id), + "real_cut": route(job_id and "open-job-detail" or "goto-production", "production", job_id and "看任务详情" or "回到生产中心", "继续推进实拍剪辑任务。", job_id=job_id), + "live_recorder": route(source_id and "edit-live-recorder-source" or "goto-production", "production", source_id and "继续录制维护" or "回到生产中心", "继续查看录制维护与产物。", source_id=source_id), + "review": route(review_id and "open-review-edit" or job_id and "open-review-from-job" or "goto-review", "review", review_id and "打开复盘" or job_id and "继续写复盘" or "回到发布与复盘", "继续补齐复盘与发布总结。", review_id=review_id, job_id=job_id), + "create_assistant": route(assistant_id and "open-edit-assistant" or "goto-playbook", "playbook", assistant_id and "继续编辑 Agent" or "回到 Agent", "继续创建或调整项目 Agent。", assistant_id=assistant_id), "create_project": route("goto-intake", "projects", "回到我的项目", "继续创建或切换当前项目。"), - "import_homepage": route("goto-discovery", "discovery", "回到找对标", "继续处理主页导入后的账号分析。"), + "import_homepage": route(target_account_id and "select-account" or "goto-discovery", "discovery", target_account_id and "打开当前对象" or "回到找对标", "继续处理主页导入后的账号分析。", account_id=target_account_id), "ops_admin": route("goto-automation", "automation", "回到自动流程", "继续查看系统依赖和治理状态。"), "storage_status": route("goto-automation", "automation", "回到自动流程", "继续查看存储与依赖健康状态。"), } diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index cac8c40..bc3dd02 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -389,6 +389,49 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertIn("run.progress", event_types) self.assertIn("run.done", event_types) + def test_agent_run_result_recommended_action_preserves_requested_object_context(self) -> None: + run_response = self.client.post( + "/v2/oneliner/runs", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "source_screen": "discovery", + "source_action_key": "selected-account-handoff", + "title": "继续分析当前对象", + "summary": "让主 Agent 沿着当前账号继续推进。", + "intent_key": "analyze_account", + "platform": "douyin", + "platform_scope": "single_platform", + "plan_request": { + "goal": "继续分析当前对象", + "steps": ["读取当前账号上下文", "结合当前策略生成下一步建议"], + }, + "payload": { + "target_account_id": "acct_focus_target", + }, + }, + ) + self.assertEqual(run_response.status_code, 200, run_response.text) + run_id = run_response.json()["id"] + + confirm = self.client.post( + f"/v2/oneliner/runs/{run_id}/confirm", + headers=self.ctx["member_headers"], + json={"reason": "继续围绕当前对象推进"}, + ) + self.assertEqual(confirm.status_code, 200, confirm.text) + + detail = self.client.get( + f"/v2/oneliner/runs/{run_id}", + headers=self.ctx["member_headers"], + ) + self.assertEqual(detail.status_code, 200, detail.text) + payload = detail.json() + self.assertEqual(payload["run_status"], "done") + self.assertEqual(payload["result"]["recommended_action"]["action"], "select-account") + self.assertEqual(payload["result"]["recommended_action"]["screen"], "discovery") + self.assertEqual(payload["result"]["recommended_action"]["account_id"], "acct_focus_target") + def test_cancelled_run_can_be_retried_as_a_new_pending_run(self) -> None: create = self.client.post( "/v2/oneliner/runs", diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 3e198e1..7810eb5 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -7903,6 +7903,9 @@ function buildRecommendedActionAttrs(recommendedAction, landing = {}) { job_id: "data-job-id", review_id: "data-review-id", platform: "data-platform", + account_id: "data-account-id", + tracked_account_id: "data-tracked-account-id", + assistant_id: "data-assistant-id", source_id: "data-source-id", file_id: "data-file-id", incident_id: "data-incident-id", diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 1a782af..3348b69 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -762,6 +762,9 @@ test("direct oneliner execution results preserve structured follow-up attrs", () assert.match(helpers, /job_id: "data-job-id"/); assert.match(helpers, /review_id: "data-review-id"/); assert.match(helpers, /platform: "data-platform"/); + assert.match(helpers, /account_id: "data-account-id"/); + assert.match(helpers, /tracked_account_id: "data-tracked-account-id"/); + assert.match(helpers, /assistant_id: "data-assistant-id"/); assert.match(helpers, /source_id: "data-source-id"/); assert.match(execution, /const recommendedAction = payload\.recommended_action/); assert.match(execution, /buildRecommendedActionAttrs\(recommendedAction/);