diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1802c..86657a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,3 +120,27 @@ - 后端单测 `36/36` - `bash scripts/check_repo_baseline.sh` - `bash scripts/smoke_fnos_storyforge_lan.sh` + +## 2026-04-04 + +### 平台 Agent 执行回写闭环 + +- 平台 Agent 配置现在不只是“被主 Agent 带进执行链”,还会在主 Agent 完成态后反向记录最近一次执行信息。 +- `platform_agent_profiles` 新增最近执行回写字段,保存: + - 最近 run id + - run 状态 + - 最近使用时间 + - 意图 key + - 使用的 OneLiner 配置版本号 + - 执行摘要 + - 来源页面 +- `GET /v2/platform-agents` 现在会返回 `recent_execution`,平台 Agent 总览卡和详情弹层都会直接显示“最近执行”和“配置 vN”,方便追溯平台配置最近是怎么被主 Agent 用起来的。 +- 这条回写链已经覆盖到主 Agent 完成态读取路径,避免只在治理层能看到版本,执行面却看不到最近一次真实使用记录。 + +### 回归护栏 + +- 后端新增平台 Agent live 路由回写测试,确认: + - 创建并确认一条主 Agent run 之后 + - `GET /v2/platform-agents` 能返回 `recent_execution` + - 最近执行会带上 run id、intent 和 `oneliner_profile_version_no` +- 前端工作台测试新增平台 Agent 最近执行渲染断言,锁住总览卡和详情弹层里的“最近执行”展示。 diff --git a/collector-service/app/database.py b/collector-service/app/database.py index 51cfbfb..794ee6a 100644 --- a/collector-service/app/database.py +++ b/collector-service/app/database.py @@ -364,6 +364,15 @@ class Database: "provider_task_id": "TEXT NOT NULL DEFAULT ''", "result_json": "TEXT NOT NULL DEFAULT '{}'", }, + "platform_agent_profiles": { + "last_run_id": "TEXT NOT NULL DEFAULT ''", + "last_run_status": "TEXT NOT NULL DEFAULT ''", + "last_used_at": "TEXT NOT NULL DEFAULT ''", + "last_intent_key": "TEXT NOT NULL DEFAULT ''", + "last_oneliner_profile_version_no": "INTEGER NOT NULL DEFAULT 0", + "last_execution_summary": "TEXT NOT NULL DEFAULT ''", + "last_source_screen": "TEXT NOT NULL DEFAULT ''", + }, } for table, columns in table_columns.items(): diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 0afab6a..a219a36 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -1225,6 +1225,46 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: readiness_label = "可用" else: readiness_label = "待补全" + recent_execution = None + if row and str(row.get("last_run_id") or "").strip(): + recent_execution = { + "run_id": str(row.get("last_run_id") or "").strip(), + "run_status": str(row.get("last_run_status") or "").strip(), + "used_at": str(row.get("last_used_at") or "").strip(), + "intent_key": str(row.get("last_intent_key") or "").strip(), + "intent_label": INTENT_LABELS.get(str(row.get("last_intent_key") or "").strip() or "custom", "主 Agent 任务"), + "oneliner_profile_version_no": int(row.get("last_oneliner_profile_version_no") or 0), + "summary": str(row.get("last_execution_summary") or "").strip(), + "source_screen": str(row.get("last_source_screen") or "").strip(), + } + if recent_execution is None and str(project_id or "").strip() and str(platform or "").strip(): + latest_run_row = legacy.db.fetch_one( + """ + SELECT * FROM agent_runs + WHERE user_id = ? AND project_id = ? AND platform = ? + ORDER BY updated_at DESC, created_at DESC + LIMIT 1 + """, + (account["id"], project_id, platform), + ) + if latest_run_row: + latest_result = _parse_json(latest_run_row.get("result_json"), {}) + latest_governance = _parse_json(latest_run_row.get("governance_json"), {}) + latest_profile_version = ( + ((latest_result.get("execution_card") or {}).get("oneliner_profile_version")) + or latest_governance.get("oneliner_profile_version") + or {} + ) + recent_execution = { + "run_id": str(latest_run_row.get("id") or "").strip(), + "run_status": str(latest_run_row.get("run_status") or "").strip(), + "used_at": str(latest_run_row.get("finished_at") or latest_run_row.get("updated_at") or "").strip(), + "intent_key": str(latest_run_row.get("intent_key") or "").strip(), + "intent_label": INTENT_LABELS.get(str(latest_run_row.get("intent_key") or "").strip() or "custom", "主 Agent 任务"), + "oneliner_profile_version_no": int(latest_profile_version.get("version_no") or 0), + "summary": str(latest_run_row.get("status_summary") or "").strip(), + "source_screen": str(latest_run_row.get("source_screen") or "").strip(), + } return { "id": row["id"] if row else "", "user_id": account["id"], @@ -1241,6 +1281,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "skill_count": skill_count, "recent_memory": _memory_payload(recent_memory_row) if recent_memory_row else None, "recent_skill": _skill_payload(recent_skill_row) if recent_skill_row else None, + "recent_execution": recent_execution, "readiness_score": readiness_score, "readiness_label": readiness_label, "assistant": assistant, @@ -1275,8 +1316,46 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "readiness_label": str(payload.get("readiness_label") or "").strip(), "recent_memory_title": str(recent_memory.get("title") or "").strip(), "recent_skill_title": str(recent_skill.get("title") or "").strip(), + "recent_execution": payload.get("recent_execution") or {}, } + def _record_platform_agent_execution_feedback( + account_id: str, + *, + project_id: str, + platform: str, + run_id: str, + run_status: str, + intent_key: str, + source_screen: str, + oneliner_profile_version_no: int, + execution_summary: str, + ) -> None: + normalized_platform = _safe_platform(platform, fallback="") if str(platform or "").strip() else "" + if not normalized_platform or not str(project_id or "").strip() or not str(run_id or "").strip(): + return + legacy.db.execute( + """ + UPDATE platform_agent_profiles + SET last_run_id = ?, last_run_status = ?, last_used_at = ?, last_intent_key = ?, + last_oneliner_profile_version_no = ?, last_execution_summary = ?, last_source_screen = ?, updated_at = ? + WHERE user_id = ? AND project_id = ? AND platform = ? + """, + ( + str(run_id).strip(), + str(run_status or "").strip(), + now(), + str(intent_key or "custom").strip() or "custom", + int(oneliner_profile_version_no or 0), + str(execution_summary or "").strip(), + str(source_screen or "").strip(), + now(), + account_id, + project_id, + normalized_platform, + ), + ) + def _memory_payload(row: dict[str, Any]) -> dict[str, Any]: return { "id": row["id"], @@ -3285,6 +3364,17 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: """, (execution_summary, _dump(result_payload), timestamp, timestamp, run_id), ) + _record_platform_agent_execution_feedback( + str(row.get("user_id") or "").strip(), + project_id=str(row.get("project_id") or "").strip(), + platform=str(row.get("platform") or "").strip(), + run_id=run_id, + run_status="done", + intent_key=str(plan.get("intent_key") or row.get("intent_key") or "custom").strip() or "custom", + source_screen=str(row.get("source_screen") or "").strip(), + oneliner_profile_version_no=int(oneliner_profile_version.get("version_no") or 0), + execution_summary=execution_summary, + ) _log_agent_run_event( run_id, event_type="run.done", diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index 1b3f901..9b34dec 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -821,6 +821,55 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertIn("route_checks", self_check_payload) self.assertIn("score", self_check_payload) + run_response = self.client.post( + "/v2/oneliner/runs", + headers=self.ctx["member_headers"], + json={ + "project_id": self.ctx["project_id"], + "source_screen": "playbook", + "source_action_key": "platform-agent-handoff", + "title": "验证平台 Agent 执行回写", + "summary": "确认主 Agent 完成态会回写最近平台执行信息。", + "intent_key": "governance_review", + "platform": "douyin", + "delivery_mode": "confirm_first", + "plan": { + "goal": "验证平台 Agent 执行回写", + "summary": "检查主 Agent 完成态后平台 Agent 是否记录最近执行。", + "steps": ["读取当前主配置", "读取当前平台 Agent", "生成执行结果"], + }, + }, + ) + self.assertEqual(run_response.status_code, 200, run_response.text) + run_payload = run_response.json() + + confirm_response = self.client.post( + f"/v2/oneliner/runs/{run_payload['id']}/confirm", + headers=self.ctx["member_headers"], + json={"note": "执行平台 Agent 回写验证"}, + ) + self.assertEqual(confirm_response.status_code, 200, confirm_response.text) + + detail_response = self.client.get( + f"/v2/oneliner/runs/{run_payload['id']}", + headers=self.ctx["member_headers"], + ) + self.assertEqual(detail_response.status_code, 200, detail_response.text) + detail_payload = detail_response.json() + self.assertEqual(detail_payload["run_status"], "done") + + refreshed_agents = self.client.get( + "/v2/platform-agents", + headers=self.ctx["member_headers"], + params={"project_id": self.ctx["project_id"]}, + ) + self.assertEqual(refreshed_agents.status_code, 200, refreshed_agents.text) + refreshed_douyin = next(item for item in refreshed_agents.json()["items"] if item["platform"] == "douyin") + self.assertIn("recent_execution", refreshed_douyin) + self.assertEqual(refreshed_douyin["recent_execution"]["run_id"], run_payload["id"]) + self.assertEqual(refreshed_douyin["recent_execution"]["intent_key"], "governance_review") + self.assertGreaterEqual(refreshed_douyin["recent_execution"]["oneliner_profile_version_no"], 1) + def test_admin_ops_routes_are_live(self) -> None: now = self.db_module.utc_now() job_id = "job_failed_admin_ops" diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 7797d35..55e3ef8 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -4422,6 +4422,18 @@ function renderPlatformAgentPanel() { ` : ""} ` : ""} + ${item.recent_execution?.run_id ? ` +
+

最近执行

+

${escapeHtml(item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}

+
+ ${escapeHtml(item.recent_execution.intent_label || "主 Agent 任务")} + ${escapeHtml(item.recent_execution.run_status || "done")} + ${item.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(item.recent_execution.oneliner_profile_version_no))}` : ""} + ${item.recent_execution.source_screen ? `${escapeHtml(screenLabel(item.recent_execution.source_screen) || item.recent_execution.source_screen)}` : ""} +
+
+ ` : ""}
查看详情 配置 @@ -9342,6 +9354,18 @@ async function openPlatformAgentDetailAction(platform) { ${escapeHtml(profile.assistant?.name || "未绑执行 Agent")}
+ ${profile.recent_execution?.run_id ? ` +
+

最近执行

+

${escapeHtml(profile.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}

+
+ ${escapeHtml(profile.recent_execution.intent_label || "主 Agent 任务")} + ${escapeHtml(profile.recent_execution.run_status || "done")} + ${profile.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(profile.recent_execution.oneliner_profile_version_no))}` : ""} + ${profile.recent_execution.source_screen ? `${escapeHtml(screenLabel(profile.recent_execution.source_screen) || profile.recent_execution.source_screen)}` : ""} +
+
+ ` : ""}
最近记忆
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 344ae2d..f446d51 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -271,6 +271,17 @@ test("governance and quota panels use real empty-state language instead of backe assert.match(platformAgents, /open-platform-agent-profile/); }); +test("platform agent surfaces recent execution feedback from main agent runs", () => { + const platformAgents = extractBetween(APP, "function renderPlatformAgentPanel()", "function renderAdminOpsPanel()"); + const detail = extractBetween(APP, "async function openPlatformAgentDetailAction(platform)", "function openPlatformSkillReviewAction(platform, skillId, accepted)"); + + assert.match(platformAgents, /recent_execution/); + assert.match(platformAgents, /最近执行/); + assert.match(platformAgents, /配置 v/); + assert.match(detail, /最近执行/); + assert.match(detail, /recent_execution/); +}); + test("quota and review screens foreground live next-step guidance", () => { const tenantQuota = extractBetween(APP, "function renderTenantQuotaPanel()", "function policyScopeTagLabel("); const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");