feat: version platform agent profiles through main agent runs
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 05:08:15 +08:00
parent 01ce085f6a
commit 895e3f3b13
6 changed files with 554 additions and 3 deletions

View File

@@ -118,6 +118,29 @@
- 当前基线重新验证通过:
- 前端测试 `67/67`
- 后端单测 `36/36`
## 2026-04-04
### 平台 Agent 配置历史与回滚
- `平台 Agent 配置` 现在和 `OneLiner 主配置` 一样,已经支持版本历史、回滚和审计,不再只是直接编辑当前值。
- 后端新增了:
- `GET /v2/platform-agents/{platform}/profile/versions`
- `GET /v2/platform-agents/{platform}/profile/audits`
- `POST /v2/platform-agents/{platform}/profile/rollback`
- `PUT /v2/platform-agents/{platform}/profile` 现在支持记录变更原因,并在保存时自动生成新的版本快照。
- 前端 `平台 Agent 配置` 弹层新增当前版本摘要和变更原因,`平台 Agent 面板 / 详情` 也都新增了 `看配置历史` 入口。
### 平台 Agent 配置进入执行回写
- 主 Agent 在创建 run、重试 run、完成 run 时,都会把当前平台 Agent 配置版本号一起带入执行链。
- 平台 Agent 的 `recent_execution` 现在会显示本轮使用的 `平台 Agent vN`,方便直接判断最近一次执行到底用了哪版平台配置。
- run 完成态结果里的 `execution_card.platform_agent_profile` 也会携带平台 Agent 版本号、标题和摘要,悬浮主 Agent 结果卡能直接回看这轮平台配置来源。
### 回归护栏
- 后端治理测试新增了平台 Agent 配置版本链路覆盖:初始化版本、连续更新、回滚生成新版本、审计记录,以及执行完成后的 `recent_execution.platform_agent_profile_version_no` 回写。
- 前端工作台测试新增了平台 Agent 配置历史入口、历史接口、回滚接口和结果卡版本显示的校验,避免后续再把这条链断开。
- `bash scripts/check_repo_baseline.sh`
- `bash scripts/smoke_fnos_storyforge_lan.sh`

View File

@@ -370,6 +370,7 @@ class Database:
"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_platform_profile_version_no": "INTEGER NOT NULL DEFAULT 0",
"last_execution_summary": "TEXT NOT NULL DEFAULT ''",
"last_source_screen": "TEXT NOT NULL DEFAULT ''",
},

View File

@@ -49,6 +49,13 @@ class PlatformAgentProfileRequest(BaseModel):
notes: str = ""
status: str = "active"
config: dict[str, Any] = Field(default_factory=dict)
reason: str = ""
class PlatformAgentProfileRollbackRequest(BaseModel):
project_id: str = ""
version_id: str
reason: str = ""
class AgentMemoryUpsertRequest(BaseModel):
@@ -510,6 +517,45 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
FOREIGN KEY(assistant_id) REFERENCES assistants(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS platform_agent_profile_versions (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
user_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
platform TEXT NOT NULL DEFAULT '',
assistant_id TEXT NOT NULL DEFAULT '',
version_no INTEGER NOT NULL DEFAULT 1,
name TEXT NOT NULL DEFAULT '',
mission TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
config_json TEXT NOT NULL DEFAULT '{}',
summary TEXT NOT NULL DEFAULT '',
reason TEXT NOT NULL DEFAULT '',
source_type TEXT NOT NULL DEFAULT 'user_update',
rollback_from_version_id TEXT NOT NULL DEFAULT '',
actor_user_id TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
UNIQUE(profile_id, version_no),
FOREIGN KEY(profile_id) REFERENCES platform_agent_profiles(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES accounts(id) ON DELETE CASCADE,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE SET NULL,
FOREIGN KEY(assistant_id) REFERENCES assistants(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS platform_agent_profile_audit_logs (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
version_id TEXT NOT NULL DEFAULT '',
actor_user_id TEXT NOT NULL DEFAULT '',
action_key TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
details_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
FOREIGN KEY(profile_id) REFERENCES platform_agent_profiles(id) ON DELETE CASCADE,
FOREIGN KEY(version_id) REFERENCES platform_agent_profile_versions(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS agent_memories (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
@@ -1064,6 +1110,217 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
)
return payload
def _summarize_platform_agent_profile(row: dict[str, Any] | None) -> str:
data = row or {}
def _clip(value: str, limit: int) -> str:
text = str(value or "").strip()
if len(text) <= limit:
return text
return f"{text[: max(limit - 1, 0)].rstrip()}"
parts: list[str] = []
name = str(data.get("name") or "").strip()
if name:
parts.append(name)
mission = str(data.get("mission") or "").strip()
if mission:
parts.append(_clip(mission, 36))
notes = str(data.get("notes") or "").strip()
if notes:
parts.append(f"备注 {_clip(notes, 32)}")
return "".join(parts[:4])
def _platform_agent_profile_version_payload(row: dict[str, Any] | None) -> dict[str, Any] | None:
if not row:
return None
assistant = None
if row.get("assistant_id"):
assistant_row = legacy.db.fetch_one("SELECT * FROM assistants WHERE id = ?", (row["assistant_id"],))
if assistant_row:
assistant = legacy.assistant_payload(assistant_row)
return {
"id": row["id"],
"profile_id": row.get("profile_id", ""),
"user_id": row.get("user_id", ""),
"project_id": row.get("project_id", ""),
"platform": row.get("platform", ""),
"assistant_id": row.get("assistant_id", ""),
"version_no": int(row.get("version_no") or 0),
"name": row.get("name", ""),
"mission": row.get("mission", ""),
"notes": row.get("notes", ""),
"status": row.get("status", "active"),
"config": _parse_json(row.get("config_json"), {}),
"summary": row.get("summary", ""),
"reason": row.get("reason", ""),
"source_type": row.get("source_type", ""),
"rollback_from_version_id": row.get("rollback_from_version_id", ""),
"actor_user_id": row.get("actor_user_id", ""),
"assistant": assistant,
"created_at": row.get("created_at", ""),
}
def _platform_agent_profile_audit_payload(row: dict[str, Any] | None) -> dict[str, Any] | None:
if not row:
return None
version_row = legacy.db.fetch_one("SELECT * FROM platform_agent_profile_versions WHERE id = ?", (row.get("version_id", ""),)) if row.get("version_id") else None
return {
"id": row["id"],
"profile_id": row.get("profile_id", ""),
"version_id": row.get("version_id", ""),
"actor_user_id": row.get("actor_user_id", ""),
"action_key": row.get("action_key", ""),
"summary": row.get("summary", ""),
"details": _parse_json(row.get("details_json"), {}),
"version": _platform_agent_profile_version_payload(version_row),
"created_at": row.get("created_at", ""),
}
def _list_platform_agent_profile_versions(profile_row: dict[str, Any] | None) -> list[dict[str, Any]]:
if not profile_row:
return []
rows = legacy.db.fetch_all(
"""
SELECT * FROM platform_agent_profile_versions
WHERE profile_id = ?
ORDER BY version_no DESC, created_at DESC
""",
(profile_row["id"],),
)
return [_platform_agent_profile_version_payload(row) for row in rows if row]
def _list_platform_agent_profile_audits(profile_row: dict[str, Any] | None, *, limit: int = 12) -> list[dict[str, Any]]:
if not profile_row:
return []
rows = legacy.db.fetch_all(
"""
SELECT * FROM platform_agent_profile_audit_logs
WHERE profile_id = ?
ORDER BY created_at DESC
LIMIT ?
""",
(profile_row["id"], limit),
)
return [_platform_agent_profile_audit_payload(row) for row in rows if row]
def _current_platform_agent_profile_version_row(profile_row: dict[str, Any] | None) -> dict[str, Any] | None:
if not profile_row:
return None
return legacy.db.fetch_one(
"""
SELECT * FROM platform_agent_profile_versions
WHERE profile_id = ?
ORDER BY version_no DESC, created_at DESC
LIMIT 1
""",
(profile_row["id"],),
)
def _log_platform_agent_profile_audit(
*,
profile_id: str,
version_id: str,
actor_user_id: str,
action_key: str,
summary: str,
details: dict[str, Any] | None = None,
) -> dict[str, Any]:
audit_id = make_id("plat_agent_audit")
legacy.db.execute(
"""
INSERT INTO platform_agent_profile_audit_logs (
id, profile_id, version_id, actor_user_id, action_key, summary, details_json, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
audit_id,
profile_id,
version_id,
actor_user_id,
action_key,
summary,
_dump(details or {}),
now(),
),
)
row = legacy.db.fetch_one("SELECT * FROM platform_agent_profile_audit_logs WHERE id = ?", (audit_id,))
assert row is not None
return _platform_agent_profile_audit_payload(row)
def _create_platform_agent_profile_version(
profile_row: dict[str, Any],
*,
actor_user_id: str,
source_type: str,
reason: str,
rollback_from_version_id: str = "",
) -> dict[str, Any]:
current = legacy.db.fetch_one(
"SELECT COALESCE(MAX(version_no), 0) AS max_version FROM platform_agent_profile_versions WHERE profile_id = ?",
(profile_row["id"],),
)
version_no = int((current or {}).get("max_version") or 0) + 1
version_id = make_id("plat_agent_ver")
summary = _summarize_platform_agent_profile(profile_row)
version_params = (
version_id,
profile_row["id"],
profile_row.get("user_id", ""),
profile_row.get("project_id", ""),
profile_row.get("platform", ""),
profile_row.get("assistant_id", ""),
version_no,
profile_row.get("name", ""),
profile_row.get("mission", ""),
profile_row.get("notes", ""),
profile_row.get("status", "active"),
profile_row.get("config_json", "{}"),
summary,
reason.strip(),
source_type,
rollback_from_version_id,
actor_user_id,
now(),
)
insert_sql = """
INSERT INTO platform_agent_profile_versions (
id, profile_id, user_id, project_id, platform, assistant_id, version_no,
name, mission, notes, status, config_json, summary, reason,
source_type, rollback_from_version_id, actor_user_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
if profile_row.get("assistant_id"):
legacy.db.execute(insert_sql, version_params)
else:
with legacy.db.session() as conn:
conn.execute("PRAGMA foreign_keys=OFF")
conn.execute(insert_sql, version_params)
conn.execute("PRAGMA foreign_keys=ON")
row = legacy.db.fetch_one("SELECT * FROM platform_agent_profile_versions WHERE id = ?", (version_id,))
assert row is not None
return _platform_agent_profile_version_payload(row)
def _ensure_platform_agent_profile_version_seed(profile_row: dict[str, Any], *, actor_user_id: str = "") -> dict[str, Any]:
current_version = _current_platform_agent_profile_version_row(profile_row)
if current_version:
payload = _platform_agent_profile_version_payload(current_version)
assert payload is not None
return payload
payload = _create_platform_agent_profile_version(
profile_row,
actor_user_id=actor_user_id or profile_row.get("user_id", ""),
source_type="system_seed",
reason="初始化平台 Agent 配置",
)
_log_platform_agent_profile_audit(
profile_id=profile_row["id"],
version_id=payload["id"],
actor_user_id=actor_user_id or profile_row.get("user_id", ""),
action_key="seed-platform-agent-profile",
summary="初始化平台 Agent 配置版本",
details={"project_id": profile_row.get("project_id", ""), "platform": profile_row.get("platform", "")},
)
return payload
def _oneliner_profile_runtime_snapshot(row: dict[str, Any], *, account: dict[str, Any] | None = None) -> dict[str, Any]:
bundle = _oneliner_profile_bundle(row, account=account)
return {
@@ -1226,6 +1483,10 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
else:
readiness_label = "待补全"
recent_execution = None
current_version = None
if row:
_ensure_platform_agent_profile_version_seed(row, actor_user_id=account.get("id", ""))
current_version = _platform_agent_profile_version_payload(_current_platform_agent_profile_version_row(row))
if row and str(row.get("last_run_id") or "").strip():
recent_execution = {
"run_id": str(row.get("last_run_id") or "").strip(),
@@ -1234,6 +1495,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
"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),
"platform_agent_profile_version_no": int(row.get("last_platform_profile_version_no") or 0),
"summary": str(row.get("last_execution_summary") or "").strip(),
"source_screen": str(row.get("last_source_screen") or "").strip(),
}
@@ -1262,6 +1524,11 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
"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),
"platform_agent_profile_version_no": int(
(((latest_result.get("execution_card") or {}).get("platform_agent_profile") or {}).get("version_no"))
or (((latest_governance.get("platform_agent_profile") or {}).get("current_version") or {}).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(),
}
@@ -1285,6 +1552,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
"readiness_score": readiness_score,
"readiness_label": readiness_label,
"assistant": assistant,
"current_version": current_version,
"created_at": (row or {}).get("created_at", ""),
"updated_at": (row or {}).get("updated_at", ""),
}
@@ -1317,6 +1585,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
"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 {},
"current_version": payload.get("current_version") or {},
}
def _record_platform_agent_execution_feedback(
@@ -1329,6 +1598,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
intent_key: str,
source_screen: str,
oneliner_profile_version_no: int,
platform_agent_profile_version_no: int,
execution_summary: str,
) -> None:
normalized_platform = _safe_platform(platform, fallback="") if str(platform or "").strip() else ""
@@ -1338,7 +1608,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
"""
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 = ?
last_oneliner_profile_version_no = ?, last_platform_profile_version_no = ?, last_execution_summary = ?, last_source_screen = ?, updated_at = ?
WHERE user_id = ? AND project_id = ? AND platform = ?
""",
(
@@ -1347,6 +1617,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
now(),
str(intent_key or "custom").strip() or "custom",
int(oneliner_profile_version_no or 0),
int(platform_agent_profile_version_no or 0),
str(execution_summary or "").strip(),
str(source_screen or "").strip(),
now(),
@@ -3336,6 +3607,9 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
"platform_label": str(platform_agent_profile.get("platform_label") or "").strip(),
"name": str(platform_agent_profile.get("name") or "").strip(),
"assistant_name": str(platform_agent_profile.get("assistant_name") or "").strip(),
"version_no": int(((platform_agent_profile.get("current_version") or {}).get("version_no") or 0)),
"version_title": str(((platform_agent_profile.get("current_version") or {}).get("title") or "").strip()),
"version_summary": str(((platform_agent_profile.get("current_version") or {}).get("summary") or "").strip()),
"mission": str(platform_agent_profile.get("mission") or "").strip(),
"status": str(platform_agent_profile.get("status") or "").strip(),
"readiness_label": str(platform_agent_profile.get("readiness_label") or "").strip(),
@@ -3373,6 +3647,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
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),
platform_agent_profile_version_no=int(((platform_agent_profile.get("current_version") or {}).get("version_no") or 0)),
execution_summary=execution_summary,
)
_log_agent_run_event(
@@ -4058,6 +4333,21 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
conn.execute(insert_sql, insert_params)
conn.execute("PRAGMA foreign_keys=ON")
row = legacy.db.fetch_one("SELECT * FROM platform_agent_profiles WHERE id = ?", (profile_id,))
assert row is not None
version = _create_platform_agent_profile_version(
row,
actor_user_id=account["id"],
source_type="user_update",
reason=request.reason.strip() or f"更新 {legacy.platform_label(platform)} Agent 配置",
)
_log_platform_agent_profile_audit(
profile_id=row["id"],
version_id=version["id"],
actor_user_id=account["id"],
action_key="update-platform-agent-profile",
summary=f"已更新 {legacy.platform_label(platform)} Agent 配置",
details={"project_id": project["id"], "platform": platform, "assistant_id": row.get("assistant_id", "")},
)
return _platform_agent_payload(account, row, platform=platform, project_id=project["id"])
def _upsert_memory(
@@ -5832,6 +6122,110 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
normalized_platform = _safe_platform(platform)
return _upsert_platform_profile(account, normalized_platform, request)
@app.get("/v2/platform-agents/{platform}/profile/versions")
def list_platform_agent_profile_versions(
platform: str,
project_id: str | None = Query(default=None),
account: dict[str, Any] = Depends(legacy.require_approved),
) -> dict[str, Any]:
project = _resolve_project(account, project_id or None)
normalized_platform = _safe_platform(platform)
row = legacy.db.fetch_one(
"SELECT * FROM platform_agent_profiles WHERE user_id = ? AND project_id = ? AND platform = ?",
(account["id"], project["id"], normalized_platform),
)
if not row:
return {"items": [], "count": 0, "current_version": None}
_ensure_platform_agent_profile_version_seed(row, actor_user_id=account["id"])
items = _list_platform_agent_profile_versions(row)
return {"items": items, "count": len(items), "current_version": items[0] if items else None}
@app.get("/v2/platform-agents/{platform}/profile/audits")
def list_platform_agent_profile_audits(
platform: str,
project_id: str | None = Query(default=None),
limit: int = Query(default=12, ge=1, le=50),
account: dict[str, Any] = Depends(legacy.require_approved),
) -> dict[str, Any]:
project = _resolve_project(account, project_id or None)
normalized_platform = _safe_platform(platform)
row = legacy.db.fetch_one(
"SELECT * FROM platform_agent_profiles WHERE user_id = ? AND project_id = ? AND platform = ?",
(account["id"], project["id"], normalized_platform),
)
if not row:
return {"items": [], "count": 0}
_ensure_platform_agent_profile_version_seed(row, actor_user_id=account["id"])
items = _list_platform_agent_profile_audits(row, limit=limit)
return {"items": items, "count": len(items)}
@app.post("/v2/platform-agents/{platform}/profile/rollback")
def rollback_platform_agent_profile(
platform: str,
request: PlatformAgentProfileRollbackRequest,
account: dict[str, Any] = Depends(legacy.require_approved),
) -> dict[str, Any]:
project = _resolve_project(account, request.project_id or None)
normalized_platform = _safe_platform(platform)
row = legacy.db.fetch_one(
"SELECT * FROM platform_agent_profiles WHERE user_id = ? AND project_id = ? AND platform = ?",
(account["id"], project["id"], normalized_platform),
)
if not row:
raise HTTPException(status_code=404, detail="Platform agent profile not found")
_ensure_platform_agent_profile_version_seed(row, actor_user_id=account["id"])
target_version = legacy.db.fetch_one(
"""
SELECT * FROM platform_agent_profile_versions
WHERE id = ? AND profile_id = ?
""",
(request.version_id, row["id"]),
)
if not target_version:
raise HTTPException(status_code=404, detail="Platform agent profile version not found")
assistant = _resolve_assistant(account, target_version.get("assistant_id", ""), project["id"])
timestamp = now()
update_sql = """
UPDATE platform_agent_profiles
SET assistant_id = ?, name = ?, mission = ?, notes = ?, status = ?, config_json = ?, updated_at = ?
WHERE id = ?
"""
update_params = (
(assistant or {}).get("id", ""),
target_version.get("name", f"{legacy.platform_label(normalized_platform)} Agent"),
target_version.get("mission", ""),
target_version.get("notes", ""),
target_version.get("status", "active"),
target_version.get("config_json", "{}"),
timestamp,
row["id"],
)
if (assistant or {}).get("id", ""):
legacy.db.execute(update_sql, update_params)
else:
with legacy.db.session() as conn:
conn.execute("PRAGMA foreign_keys=OFF")
conn.execute(update_sql, update_params)
conn.execute("PRAGMA foreign_keys=ON")
updated_row = legacy.db.fetch_one("SELECT * FROM platform_agent_profiles WHERE id = ?", (row["id"],))
assert updated_row is not None
version = _create_platform_agent_profile_version(
updated_row,
actor_user_id=account["id"],
source_type="user_rollback",
reason=request.reason.strip() or f"回滚到版本 {target_version.get('version_no') or request.version_id}",
rollback_from_version_id=target_version["id"],
)
_log_platform_agent_profile_audit(
profile_id=updated_row["id"],
version_id=version["id"],
actor_user_id=account["id"],
action_key="rollback-platform-agent-profile",
summary=f"已回滚 {legacy.platform_label(normalized_platform)} Agent 配置到版本 {target_version.get('version_no') or request.version_id}",
details={"project_id": project["id"], "platform": normalized_platform, "rollback_to_version_id": target_version["id"]},
)
return _platform_agent_payload(account, updated_row, platform=normalized_platform, project_id=project["id"])
@app.get("/v2/platform-agents/{platform}/memories")
def list_platform_memories(
platform: str,

View File

@@ -721,6 +721,16 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(profile_payload["platform"], "douyin")
self.assertEqual(profile_payload["name"], "抖音增长 Agent")
self.assertEqual(profile_payload["config"]["focus"], "conversion")
self.assertEqual(profile_payload["current_version"]["version_no"], 1)
first_profile_version_id = profile_payload["current_version"]["id"]
profile_versions = self.client.get(
"/v2/platform-agents/douyin/profile/versions",
headers=self.ctx["member_headers"],
params={"project_id": self.ctx["project_id"]},
)
self.assertEqual(profile_versions.status_code, 200, profile_versions.text)
self.assertEqual(profile_versions.json()["count"], 1)
memory_response = self.client.post(
"/v2/platform-agents/douyin/memories",
@@ -790,6 +800,44 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(review_skill.status_code, 200, review_skill.text)
self.assertEqual(review_skill.json()["status"], "validated")
update_profile = self.client.put(
"/v2/platform-agents/douyin/profile",
headers=self.ctx["member_headers"],
json={
"project_id": self.ctx["project_id"],
"name": "抖音增长 Agent",
"mission": "改成优先对标高互动账号。",
"notes": "先压缩近期重点方向。",
"status": "active",
"config": {"focus": "engagement"},
"reason": "调整当前抖音平台策略",
},
)
self.assertEqual(update_profile.status_code, 200, update_profile.text)
self.assertEqual(update_profile.json()["current_version"]["version_no"], 2)
rollback_profile = self.client.post(
"/v2/platform-agents/douyin/profile/rollback",
headers=self.ctx["member_headers"],
json={
"project_id": self.ctx["project_id"],
"version_id": first_profile_version_id,
"reason": "恢复到第一版平台 Agent 配置",
},
)
self.assertEqual(rollback_profile.status_code, 200, rollback_profile.text)
rollback_profile_payload = rollback_profile.json()
self.assertEqual(rollback_profile_payload["current_version"]["rollback_from_version_id"], first_profile_version_id)
self.assertEqual(rollback_profile_payload["config"]["focus"], "conversion")
profile_audits = self.client.get(
"/v2/platform-agents/douyin/profile/audits",
headers=self.ctx["member_headers"],
params={"project_id": self.ctx["project_id"]},
)
self.assertEqual(profile_audits.status_code, 200, profile_audits.text)
self.assertGreaterEqual(profile_audits.json()["count"], 3)
versions = self.client.get(
f"/v2/platform-agents/douyin/skills/{skill_payload['id']}/versions",
headers=self.ctx["member_headers"],
@@ -857,6 +905,10 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(detail_response.status_code, 200, detail_response.text)
detail_payload = detail_response.json()
self.assertEqual(detail_payload["run_status"], "done")
self.assertEqual(
(((detail_payload.get("result") or {}).get("execution_card") or {}).get("platform_agent_profile") or {}).get("version_no"),
rollback_profile_payload["current_version"]["version_no"],
)
refreshed_agents = self.client.get(
"/v2/platform-agents",
@@ -866,9 +918,14 @@ class MainAgentGovernanceTests(unittest.TestCase):
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["current_version"]["version_no"], rollback_profile_payload["current_version"]["version_no"])
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)
self.assertEqual(
refreshed_douyin["recent_execution"]["platform_agent_profile_version_no"],
rollback_profile_payload["current_version"]["version_no"],
)
def test_admin_ops_routes_are_live(self) -> None:
now = self.db_module.utc_now()

View File

@@ -2019,6 +2019,7 @@ function renderOneLinerExecutionPayloadHtml(payload) {
<span class="tag blue">${escapeHtml(platformAgentProfile.platform_label || platformLabel(platformAgentProfile.platform))}</span>
${platformAgentProfile.name ? `<span class="tag">${escapeHtml(platformAgentProfile.name)}</span>` : ""}
${platformAgentProfile.assistant_name ? `<span class="tag green">${escapeHtml(platformAgentProfile.assistant_name)}</span>` : ""}
${platformAgentProfile.version_no ? `<span class="tag">${escapeHtml(platformLabel(platformAgentProfile.platform || payload.platform || ""))} Agent v${escapeHtml(formatNumber(platformAgentProfile.version_no || 0))}</span>` : ""}
${platformAgentProfile.readiness_label ? `<span class="tag ${platformAgentProfile.readiness_score >= 75 ? "green" : platformAgentProfile.readiness_score >= 50 ? "blue" : "orange"}">${escapeHtml(platformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(platformAgentProfile.readiness_score || 0))}</span>` : ""}
</div>
</div>
@@ -4458,6 +4459,7 @@ function renderPlatformAgentPanel() {
<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.platform_agent_profile_version_no ? `<span class="tag">${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent v${escapeHtml(formatNumber(item.recent_execution.platform_agent_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 class="task-meta" style="margin-top:8px;">
@@ -4469,6 +4471,7 @@ function renderPlatformAgentPanel() {
<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>
<span class="tag clickable-tag" data-action="open-platform-agent-profile-history" data-platform="${escapeHtml(item.platform)}">看配置历史</span>
<span class="tag clickable-tag" data-action="open-platform-agent-memory" data-platform="${escapeHtml(item.platform)}">补记忆</span>
<span class="tag clickable-tag" data-action="open-platform-agent-skill" data-platform="${escapeHtml(item.platform)}">补技能</span>
</div>
@@ -9239,11 +9242,13 @@ function openPlatformAgentProfileAction(platform) {
description: "给这个平台绑定自己的执行 Agent并补充任务目标和方法论定位。",
submitLabel: "保存平台 Agent",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(current, `当前 ${platformLabel(platform)} Agent 还没有历史版本。`) },
{ name: "assistantId", label: "绑定执行 Agent", type: "select", value: current.assistant_id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "name", label: "名称", value: current.name || `${platformLabel(platform)} Agent`, placeholder: "例如:快手增长 Agent" },
{ name: "mission", label: "任务目标", type: "textarea", rows: 4, value: current.mission || "", placeholder: "例如:沉淀快手平台的开场结构、停留逻辑和转化方法论" },
{ name: "notes", label: "补充说明", type: "textarea", rows: 4, value: current.notes || "", placeholder: "例如:优先观察短句节奏、直播切片和成交句式" },
{ name: "status", label: "状态", type: "select", value: current.status || "active", options: [{ value: "active", label: "启用" }, { value: "draft", label: "草稿" }, { value: "paused", label: "暂停" }] }
{ name: "status", label: "状态", type: "select", value: current.status || "active", options: [{ value: "active", label: "启用" }, { value: "draft", label: "草稿" }, { value: "paused", label: "暂停" }] },
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:调整当前平台 Agent 的拆解重点和执行方向" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/profile`, {
@@ -9255,6 +9260,7 @@ function openPlatformAgentProfileAction(platform) {
mission: values.mission || "",
notes: values.notes || "",
status: values.status || "active",
reason: values.reason || "",
config: {
self_optimize: true,
tenant_scoped_memory: true,
@@ -9263,7 +9269,44 @@ function openPlatformAgentProfileAction(platform) {
}
});
appState.platformAgents = safeArray(appState.platformAgents).filter((item) => item.platform !== platform).concat(saved).sort((a, b) => String(a.platform).localeCompare(String(b.platform)));
rememberAction("平台 Agent 已保存", `已更新 ${platformLabel(platform)} Agent。`, "green", saved);
rememberAction("平台 Agent 已保存", `已更新 ${platformLabel(platform)} Agent,当前版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
async function openPlatformAgentProfileHistoryAction(platform) {
const project = requireSelectedProject();
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
const history = await loadPolicyVersions(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/profile/versions?project_id=${encodeURIComponent(project.id)}`);
const audits = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/profile/audits?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] }));
const current = safeArray(appState.platformAgents).find((item) => item.platform === normalizedPlatform) || {};
const selectedVersionId = history.items[0]?.id || "";
openActionModal({
title: `${platformLabel(normalizedPlatform)} Agent 配置历史`,
description: "查看平台 Agent 配置版本,并从历史里选择一个版本回滚。",
submitLabel: "回滚到所选版本",
hideSubmit: !selectedVersionId,
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(current, `当前 ${platformLabel(normalizedPlatform)} Agent 还没有历史版本。`) },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, `当前 ${platformLabel(normalizedPlatform)} Agent 还没有历史版本。`) },
{ type: "html", label: "审计记录", html: renderPolicyAuditsHtml(safeArray(audits.items || audits), "当前还没有审计记录。") },
...(selectedVersionId ? [
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版平台 Agent 方法论" }
] : [])
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/profile/rollback`, {
method: "POST",
body: {
project_id: project.id,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.platformAgents = safeArray(appState.platformAgents).filter((item) => item.platform !== normalizedPlatform).concat(saved).sort((a, b) => String(a.platform).localeCompare(String(b.platform)));
rememberAction(`${platformLabel(normalizedPlatform)} Agent 已回滚`, `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
@@ -9394,6 +9437,7 @@ async function openPlatformAgentDetailAction(platform) {
<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.platform_agent_profile_version_no ? `<span class="tag">${escapeHtml(platformLabel(normalizedPlatform))} Agent v${escapeHtml(formatNumber(profile.recent_execution.platform_agent_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 class="task-meta" style="margin-top:8px;">
@@ -9446,6 +9490,7 @@ async function openPlatformAgentDetailAction(platform) {
<div class="task-meta" style="margin-top:12px;">
<span class="tag clickable-tag" data-action="run-oneliner-action" data-executor-key="platform-self-check" data-platform="${escapeHtml(normalizedPlatform)}">运行平台自检</span>
<span class="tag clickable-tag" data-action="open-platform-agent-profile" data-platform="${escapeHtml(normalizedPlatform)}">编辑配置</span>
<span class="tag clickable-tag" data-action="open-platform-agent-profile-history" data-platform="${escapeHtml(normalizedPlatform)}">看配置历史</span>
<span class="tag clickable-tag" data-action="open-platform-agent-memory" data-platform="${escapeHtml(normalizedPlatform)}">继续补记忆</span>
<span class="tag clickable-tag" data-action="open-platform-agent-skill" data-platform="${escapeHtml(normalizedPlatform)}">继续补技能</span>
<span class="tag clickable-tag" data-action="handoff-to-main-agent" data-platform="${escapeHtml(normalizedPlatform)}" data-source-screen="playbook" data-source-action-key="platform-agent-handoff" data-intent-key="custom" data-title="继续完善平台 Agent" data-goal="继续完善平台 Agent" data-summary="让主 Agent 结合当前平台记忆和技能,给出下一步执行计划。" data-plan-steps="${escapeHtml(JSON.stringify(["读取当前平台 Agent 配置", "检查记忆与技能缺口", "生成下一步执行计划"]))}">交给主 Agent 继续</span>
@@ -11172,6 +11217,10 @@ document.addEventListener("click", async (event) => {
openPlatformAgentProfileAction(action.dataset.platform || "");
return;
}
if (name === "open-platform-agent-profile-history") {
await openPlatformAgentProfileHistoryAction(action.dataset.platform || "");
return;
}
if (name === "open-platform-agent-detail") {
await openPlatformAgentDetailAction(action.dataset.platform || "");
return;

View File

@@ -749,6 +749,33 @@ test("main agent result rendering offers a direct route back into the recommende
assert.match(lastAction, /recommended_action/);
});
test("platform agent profiles expose history, rollback, and execution version context", () => {
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
const profileEditor = extractBetween(APP, "function openPlatformAgentProfileAction(platform)", "async function openPlatformAgentProfileHistoryAction(platform)");
const profileHistory = extractBetween(APP, "async function openPlatformAgentProfileHistoryAction(platform)", "function openPlatformAgentMemoryAction(platform)");
const panel = extractBetween(APP, "function renderPlatformAgentPanel()", "function renderAdminOpsPanel()");
const detail = extractBetween(APP, "async function openPlatformAgentDetailAction(platform)", "function openPlatformSkillReviewAction(platform, skillId, accepted)");
const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)");
assert.match(profileEditor, /renderPolicyVersionSummary\(current,/);
assert.match(profileEditor, /name: "reason"/);
assert.match(profileEditor, /saved\.current_version\?\.version_no/);
assert.match(profileHistory, /\/v2\/platform-agents\/\$\{encodeURIComponent\(normalizedPlatform\)\}\/profile\/versions/);
assert.match(profileHistory, /\/v2\/platform-agents\/\$\{encodeURIComponent\(normalizedPlatform\)\}\/profile\/audits/);
assert.match(profileHistory, /\/v2\/platform-agents\/\$\{encodeURIComponent\(normalizedPlatform\)\}\/profile\/rollback/);
assert.match(profileHistory, /renderPolicyVersionsHtml/);
assert.match(profileHistory, /renderPolicyAuditsHtml/);
assert.match(panel, /open-platform-agent-profile-history/);
assert.match(detail, /open-platform-agent-profile-history/);
assert.match(panel, /platform_agent_profile_version_no/);
assert.match(detail, /platform_agent_profile_version_no/);
assert.match(execution, /platformAgentProfile\.version_no/);
assert.match(actions, /name === "open-platform-agent-profile-history"/);
assert.match(actions, /openPlatformAgentProfileHistoryAction/);
});
test("main agent route actions keep landing context and destination screens render a notice", () => {
const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");