feat: preserve requested context in main agent results
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-04-04 08:40:04 +08:00
parent 3f93d5c088
commit b272c5edfd
5 changed files with 129 additions and 11 deletions

View File

@@ -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`,不再只有“执行完成”说明块。

View File

@@ -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", "回到自动流程", "继续查看存储与依赖健康状态。"),
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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/);