feat: advance main agent runs from homepage handoff
This commit is contained in:
@@ -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)}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 `
|
||||
<div class="dashboard-action-secondary">
|
||||
<div class="dashboard-action-index">${index + 2}</div>
|
||||
@@ -141,6 +208,12 @@
|
||||
action: item.goAction || "goto-production",
|
||||
tone: "secondary"
|
||||
}, escapeHtml)}
|
||||
${renderActionButton({
|
||||
label: item.agentLabel || "交给主 Agent",
|
||||
action: "handoff-to-main-agent",
|
||||
tone: "secondary",
|
||||
attrs: handoffAttrs
|
||||
}, escapeHtml)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -152,6 +225,17 @@
|
||||
const contextLinks = safeArray(model?.contextLinks);
|
||||
const primaryAction = model?.primaryAction || {};
|
||||
const secondaryActions = safeArray(model?.secondaryActions);
|
||||
const primaryHandoffAttrs = buildHandoffAttrs({
|
||||
sourceScreen: "dashboard",
|
||||
sourceActionKey: primaryAction.sourceActionKey || "homepage-primary-action",
|
||||
intentKey: primaryAction.intentKey || "custom",
|
||||
title: primaryAction.title,
|
||||
goal: primaryAction.title,
|
||||
summary: primaryAction.reason,
|
||||
platform: primaryAction.platform,
|
||||
platformScope: primaryAction.platformScope || "single_platform",
|
||||
planSteps: primaryAction.planSteps
|
||||
}, escapeHtml);
|
||||
|
||||
return `
|
||||
<div class="dashboard-home">
|
||||
@@ -214,8 +298,9 @@
|
||||
}, escapeHtml)}
|
||||
${renderActionButton({
|
||||
label: primaryAction.agentLabel || "交给主 Agent",
|
||||
action: "open-oneliner",
|
||||
tone: "primary"
|
||||
action: "handoff-to-main-agent",
|
||||
tone: "primary",
|
||||
attrs: primaryHandoffAttrs
|
||||
}, escapeHtml)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,6 +57,7 @@ test("homepage v6 puts actions before overview and uses 1-primary-2-secondary st
|
||||
assert.match(html, /先补抖音重点对标的高分作品分析/);
|
||||
assert.match(html, /确认一个待执行的生产计划/);
|
||||
assert.match(html, /更新重点账号的跟踪摘要/);
|
||||
assert.match(html, /data-action="handoff-to-main-agent"/);
|
||||
});
|
||||
|
||||
test("homepage model builds one primary action, two secondary actions, and a rule fallback label", () => {
|
||||
@@ -98,3 +99,11 @@ test("homepage overview uses tab buttons and does not render legacy repeated sec
|
||||
assert.ok(!html.includes("当前项目推进详情"));
|
||||
assert.ok(!html.includes("重点账号 / 对标</h3><div class=\"panel-subtitle\">右栏保留"));
|
||||
});
|
||||
|
||||
test("homepage handoff buttons carry runtime plan metadata instead of only opening chat", () => {
|
||||
const source = fs.readFileSync(path.join(ROOT, "assets/storyforge-dashboard-home.js"), "utf8");
|
||||
assert.match(source, /handoff-to-main-agent/);
|
||||
assert.match(source, /data-source-screen=/);
|
||||
assert.match(source, /data-source-action-key=/);
|
||||
assert.match(source, /data-plan-steps=/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user