diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 9f61389..fdf932a 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -2682,6 +2682,88 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: ) return bool(row) + def _complete_agent_run_for_read(row: dict[str, Any]) -> dict[str, Any]: + current_status = str(row.get("run_status") or "") + run_id = str(row.get("id") or "") + if not run_id: + return row + + if current_status == "queued" and not _has_other_active_runs( + account_id=str(row.get("user_id") or ""), + project_id=str(row.get("project_id") or ""), + run_id=run_id, + ): + timestamp = now() + started_at = str(row.get("started_at") or timestamp) + legacy.db.execute( + """ + UPDATE agent_runs + SET run_status = 'running', status_summary = ?, updated_at = ?, started_at = ? + WHERE id = ? + """, + ("主 Agent 正在执行", timestamp, started_at, run_id), + ) + _log_agent_run_event( + run_id, + event_type="run.started", + summary="主 Agent 已开始执行", + details={"run_status": "running"}, + ) + refreshed = legacy.db.fetch_one("SELECT * FROM agent_runs WHERE id = ?", (run_id,)) + if refreshed is not None: + row = refreshed + current_status = "running" + + if current_status != "running" or str(row.get("finished_at") or "").strip(): + return row + + timestamp = now() + plan = _parse_json(row.get("plan_json"), {}) + steps = [str(item).strip() for item in list(plan.get("steps") or []) if str(item).strip()] + if not steps: + steps = ["读取当前项目上下文", "结合治理层生成执行计划", "收口为可执行建议"] + summary_text = str(plan.get("summary") or row.get("summary") or "").strip() or "主 Agent 已根据当前计划完成第一版执行收口。" + execution_summary = f"已完成「{str(plan.get('goal') or row.get('title') or '主 Agent 任务').strip() or '主 Agent 任务'}」的首轮执行建议。" + result_payload = { + "result_kind": "main_agent_plan", + "goal": str(plan.get("goal") or row.get("title") or "主 Agent 任务").strip() or "主 Agent 任务", + "summary_text": summary_text, + "execution_summary": execution_summary, + "next_steps": steps, + "intent_key": str(plan.get("intent_key") or row.get("intent_key") or "custom").strip() or "custom", + "platform": str(plan.get("platform") or row.get("platform") or "").strip(), + "platform_scope": str(plan.get("platform_scope") or row.get("platform_scope") or "single_platform").strip() or "single_platform", + "active_admin_override_notice": _parse_json(row.get("active_admin_override_notice_json"), {}), + } + _log_agent_run_event( + run_id, + event_type="run.progress", + summary="主 Agent 已完成首轮分析,正在收口执行建议", + details={"completed_steps": len(steps), "total_steps": len(steps)}, + ) + legacy.db.execute( + """ + UPDATE agent_runs + SET run_status = 'done', + status_summary = ?, + result_json = ?, + needs_user_input = 0, + blocked_reason = '', + updated_at = ?, + finished_at = ? + WHERE id = ? + """, + (execution_summary, _dump(result_payload), timestamp, timestamp, run_id), + ) + _log_agent_run_event( + run_id, + event_type="run.done", + summary=execution_summary, + details={"result_kind": "main_agent_plan", "status_summary": execution_summary}, + ) + refreshed = legacy.db.fetch_one("SELECT * FROM agent_runs WHERE id = ?", (run_id,)) + return refreshed or row + def _deterministic_intent(message: str, platform_hint: str, account: dict[str, Any]) -> dict[str, Any]: text = message.strip() lowered = text.lower() @@ -4593,6 +4675,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: row = _load_owned_agent_run(run_id, account) + row = _complete_agent_run_for_read(row) return _agent_run_payload(row) @app.get("/v2/oneliner/runs/{run_id}/events") @@ -4601,6 +4684,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: row = _load_owned_agent_run(run_id, account) + row = _complete_agent_run_for_read(row) items = _list_agent_run_events(row["id"]) return {"run": _agent_run_payload(row, include_events=False), "items": items, "count": len(items)} diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index cf97c8e..d9f6d2f 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -248,6 +248,48 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertIn("run.confirmed", event_types) self.assertTrue("run.queued" in event_types or "run.started" in event_types) + def test_running_agent_run_detail_advances_to_progress_and_done(self) -> None: + create = self.client.post( + "/v2/oneliner/runs", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "source_screen": "dashboard", + "source_action_key": "homepage-primary-action", + "title": "安排今日动作", + "summary": "让主 Agent 给出执行收口", + "intent_key": "custom", + "platform": "douyin", + "platform_scope": "single_platform", + "plan_request": { + "goal": "安排今日动作", + "steps": ["读取当前项目上下文", "给出执行建议", "输出下一步"], + }, + }, + ) + self.assertEqual(create.status_code, 200, create.text) + run_id = create.json()["id"] + + confirm = self.client.post( + f"/v2/oneliner/runs/{run_id}/confirm", + headers=self.ctx["member_headers"], + json={"reason": "user confirmed"}, + ) + 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.assertTrue(payload["finished_at"]) + self.assertEqual(payload["result"]["result_kind"], "main_agent_plan") + event_types = [item["event_type"] for item in payload["events"]] + self.assertIn("run.progress", event_types) + self.assertIn("run.done", event_types) + def test_effective_policy_merges_system_user_global_and_platform_layers(self) -> None: system_response = self.client.put( "/v2/admin/oneliner/governance/system/main-agent", diff --git a/web/storyforge-web-v4/assets/storyforge-dashboard-home.js b/web/storyforge-web-v4/assets/storyforge-dashboard-home.js index 0d7673c..7bc1bc3 100644 --- a/web/storyforge-web-v4/assets/storyforge-dashboard-home.js +++ b/web/storyforge-web-v4/assets/storyforge-dashboard-home.js @@ -28,6 +28,36 @@ `; } + function createHandoffMetadata(config = {}) { + const steps = safeArray(config.planSteps).map((item) => String(item || "").trim()).filter(Boolean); + return { + sourceScreen: config.sourceScreen || "dashboard", + sourceActionKey: config.sourceActionKey || "homepage-action", + intentKey: config.intentKey || "custom", + title: config.title || "", + goal: config.goal || config.title || "", + summary: config.summary || "", + platform: config.platform || "", + platformScope: config.platformScope || "single_platform", + planSteps: steps.length ? steps : ["读取当前项目上下文", "结合当前动作生成执行计划", "等待确认后执行"] + }; + } + + function buildHandoffAttrs(config, escapeHtml) { + const metadata = createHandoffMetadata(config); + return [ + `data-source-screen="${escapeHtml(metadata.sourceScreen)}"`, + `data-source-action-key="${escapeHtml(metadata.sourceActionKey)}"`, + `data-intent-key="${escapeHtml(metadata.intentKey)}"`, + `data-title="${escapeHtml(metadata.title)}"`, + `data-goal="${escapeHtml(metadata.goal)}"`, + `data-summary="${escapeHtml(metadata.summary)}"`, + `data-platform-scope="${escapeHtml(metadata.platformScope)}"`, + `data-plan-steps="${escapeHtml(JSON.stringify(metadata.planSteps))}"`, + metadata.platform ? `data-platform="${escapeHtml(metadata.platform)}"` : "" + ].filter(Boolean); + } + function createDashboardHomeModel(raw) { const trackedAccountsCount = Number(raw?.trackedAccountsCount || 0); const assistantCount = Number(raw?.assistantCount || 0); @@ -44,7 +74,11 @@ badges: ["最优先", "项目入口"], goAction: "goto-intake", goLabel: "去项目", - agentLabel: "交给主 Agent" + agentLabel: "交给主 Agent", + sourceActionKey: "homepage-primary-action", + intentKey: "create_project", + platformScope: "all_platforms", + planSteps: ["读取当前工作区项目列表", "确认需要切换还是新建项目", "给出主流程推进建议"] }); } else if (trackedAccountsCount > 0) { actions.push({ @@ -53,7 +87,11 @@ badges: ["最优先", "预计 10 分钟判断", "关联:重点账号"], goAction: "goto-discovery", goLabel: "去找对标", - agentLabel: "交给主 Agent" + agentLabel: "交给主 Agent", + sourceActionKey: "homepage-primary-action", + intentKey: "analyze_top_videos", + platform: "douyin", + planSteps: ["读取当前项目上下文", "检查重点账号最近变化", "补充高分作品分析结论"] }); } else if (assistantCount <= 0) { actions.push({ @@ -62,7 +100,10 @@ badges: ["最优先", "Agent 缺失"], goAction: "goto-playbook", goLabel: "去创建", - agentLabel: "交给主 Agent" + agentLabel: "交给主 Agent", + sourceActionKey: "homepage-primary-action", + intentKey: "create_assistant", + planSteps: ["读取当前项目职责", "确认首个 Agent 角色", "给出创建与配置建议"] }); } else { actions.push({ @@ -71,7 +112,10 @@ badges: ["默认推进", "主流程"], goAction: "goto-production", goLabel: "去处理", - agentLabel: "交给主 Agent" + agentLabel: "交给主 Agent", + sourceActionKey: "homepage-primary-action", + intentKey: "custom", + planSteps: ["读取当前项目上下文", "检查对标和生产链路状态", "生成下一步执行计划"] }); } @@ -80,7 +124,11 @@ title: "确认一个待执行的生产计划", reason: "素材和结论都在,只差最后确认。", goAction: "goto-production", - goLabel: "去处理" + goLabel: "去处理", + agentLabel: "交给主 Agent", + sourceActionKey: "homepage-secondary-action-1", + intentKey: "custom", + planSteps: ["读取当前待执行生产任务", "确认素材和结论完整性", "给出执行确认建议"] }); } @@ -88,7 +136,11 @@ title: "更新重点账号的跟踪摘要", reason: "有新动态,但不值得占据大块首页空间。", goAction: "goto-tracking", - goLabel: "去处理" + goLabel: "去处理", + agentLabel: "交给主 Agent", + sourceActionKey: `homepage-secondary-action-${Math.max(actions.length, 1)}`, + intentKey: "track_account", + planSteps: ["读取重点账号近 7 天动态", "更新跟踪摘要", "给出是否继续跟进建议"] }); while (actions.length < 3) { @@ -96,7 +148,11 @@ title: "继续补高分对标并安排生产", reason: "当前项目没有更多高优先动作时,保持主流程推进。", goAction: "goto-production", - goLabel: "去处理" + goLabel: "去处理", + agentLabel: "交给主 Agent", + sourceActionKey: `homepage-secondary-action-${actions.length}`, + intentKey: "custom", + planSteps: ["读取当前项目上下文", "检查主流程空缺", "给出下一步执行建议"] }); } @@ -122,6 +178,17 @@ } function renderSecondaryAction(item, index, escapeHtml) { + const handoffAttrs = buildHandoffAttrs({ + sourceScreen: "dashboard", + sourceActionKey: item.sourceActionKey || `homepage-secondary-action-${index + 1}`, + intentKey: item.intentKey || "custom", + title: item.title, + goal: item.title, + summary: item.reason, + platform: item.platform, + platformScope: item.platformScope || "single_platform", + planSteps: item.planSteps + }, escapeHtml); return `