feat: surface recent platform agent execution feedback
This commit is contained in:
24
CHANGELOG.md
24
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 最近执行渲染断言,锁住总览卡和详情弹层里的“最近执行”展示。
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4422,6 +4422,18 @@ function renderPlatformAgentPanel() {
|
||||
` : ""}
|
||||
</div>
|
||||
` : ""}
|
||||
${item.recent_execution?.run_id ? `
|
||||
<div class="task-item compact" style="margin-top:10px;">
|
||||
<h4>最近执行</h4>
|
||||
<p>${escapeHtml(item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(item.recent_execution.intent_label || "主 Agent 任务")}</span>
|
||||
<span class="tag">${escapeHtml(item.recent_execution.run_status || "done")}</span>
|
||||
${item.recent_execution.oneliner_profile_version_no ? `<span class="tag">配置 v${escapeHtml(formatNumber(item.recent_execution.oneliner_profile_version_no))}</span>` : ""}
|
||||
${item.recent_execution.source_screen ? `<span class="tag">${escapeHtml(screenLabel(item.recent_execution.source_screen) || item.recent_execution.source_screen)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
<div class="task-meta" style="margin-top:10px;">
|
||||
<span class="tag clickable-tag" data-action="open-platform-agent-detail" data-platform="${escapeHtml(item.platform)}">查看详情</span>
|
||||
<span class="tag clickable-tag" data-action="open-platform-agent-profile" data-platform="${escapeHtml(item.platform)}">配置</span>
|
||||
@@ -9342,6 +9354,18 @@ async function openPlatformAgentDetailAction(platform) {
|
||||
<span class="tag">${escapeHtml(profile.assistant?.name || "未绑执行 Agent")}</span>
|
||||
</div>
|
||||
</div>
|
||||
${profile.recent_execution?.run_id ? `
|
||||
<div class="task-item compact" style="margin-top:12px;">
|
||||
<h4>最近执行</h4>
|
||||
<p>${escapeHtml(profile.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(profile.recent_execution.intent_label || "主 Agent 任务")}</span>
|
||||
<span class="tag">${escapeHtml(profile.recent_execution.run_status || "done")}</span>
|
||||
${profile.recent_execution.oneliner_profile_version_no ? `<span class="tag">配置 v${escapeHtml(formatNumber(profile.recent_execution.oneliner_profile_version_no))}</span>` : ""}
|
||||
${profile.recent_execution.source_screen ? `<span class="tag">${escapeHtml(screenLabel(profile.recent_execution.source_screen) || profile.recent_execution.source_screen)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
<div class="two-col" style="margin-top:12px;">
|
||||
<div>
|
||||
<div class="panel-subtitle">最近记忆</div>
|
||||
|
||||
@@ -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()");
|
||||
|
||||
Reference in New Issue
Block a user