feat: add main agent runtime flow v1
This commit is contained in:
@@ -158,6 +158,30 @@ class AgentPolicyRollbackRequest(BaseModel):
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class AgentRunCreateRequest(BaseModel):
|
||||
project_id: str = ""
|
||||
session_id: str = ""
|
||||
source_screen: str = "dashboard"
|
||||
source_action_key: str = ""
|
||||
title: str = ""
|
||||
summary: str = ""
|
||||
intent_key: str = "custom"
|
||||
platform: str = ""
|
||||
platform_scope: str = "single_platform"
|
||||
delivery_mode: str = "hybrid"
|
||||
scheduling_mode: str = "queued"
|
||||
plan_request: dict[str, Any] = Field(default_factory=dict)
|
||||
payload: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AgentRunConfirmRequest(BaseModel):
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class AgentRunCancelRequest(BaseModel):
|
||||
reason: str = ""
|
||||
|
||||
|
||||
INTENT_ACTIONS: dict[str, list[dict[str, Any]]] = {
|
||||
"create_project": [{"key": "goto-intake", "label": "去我的项目", "kind": "navigate"}],
|
||||
"create_assistant": [{"key": "open-create-assistant", "label": "创建 Agent", "kind": "ui_action"}],
|
||||
@@ -541,6 +565,48 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
session_id TEXT NOT NULL DEFAULT '',
|
||||
source_screen TEXT NOT NULL DEFAULT '',
|
||||
source_action_key TEXT NOT NULL DEFAULT '',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
intent_key TEXT NOT NULL DEFAULT 'custom',
|
||||
platform TEXT NOT NULL DEFAULT '',
|
||||
platform_scope TEXT NOT NULL DEFAULT 'single_platform',
|
||||
delivery_mode TEXT NOT NULL DEFAULT 'hybrid',
|
||||
run_status TEXT NOT NULL DEFAULT 'needs_confirmation',
|
||||
scheduling_mode TEXT NOT NULL DEFAULT 'queued',
|
||||
active_executor_key TEXT NOT NULL DEFAULT 'main_agent',
|
||||
plan_json TEXT NOT NULL DEFAULT '{}',
|
||||
governance_json TEXT NOT NULL DEFAULT '{}',
|
||||
result_json TEXT NOT NULL DEFAULT '{}',
|
||||
status_summary TEXT NOT NULL DEFAULT '',
|
||||
needs_user_input INTEGER NOT NULL DEFAULT 1,
|
||||
blocked_reason TEXT NOT NULL DEFAULT '',
|
||||
active_admin_override_notice_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL DEFAULT '',
|
||||
finished_at TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY(user_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY(session_id) REFERENCES oneliner_sessions(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_run_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
details_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_ops_incidents (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_user_id TEXT NOT NULL DEFAULT '',
|
||||
@@ -955,6 +1021,61 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
||||
"created_at": row["created_at"],
|
||||
}
|
||||
|
||||
def _agent_run_event_payload(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"id": row["id"],
|
||||
"run_id": row.get("run_id", ""),
|
||||
"event_type": row.get("event_type", ""),
|
||||
"summary": row.get("summary", ""),
|
||||
"details": _parse_json(row.get("details_json"), {}),
|
||||
"created_at": row.get("created_at", ""),
|
||||
}
|
||||
|
||||
def _list_agent_run_events(run_id: str) -> list[dict[str, Any]]:
|
||||
rows = legacy.db.fetch_all(
|
||||
"""
|
||||
SELECT * FROM agent_run_events
|
||||
WHERE run_id = ?
|
||||
ORDER BY created_at ASC
|
||||
""",
|
||||
(run_id,),
|
||||
)
|
||||
return [_agent_run_event_payload(row) for row in rows]
|
||||
|
||||
def _agent_run_payload(row: dict[str, Any], *, include_events: bool = True) -> dict[str, Any]:
|
||||
payload = {
|
||||
"id": row["id"],
|
||||
"user_id": row.get("user_id", ""),
|
||||
"project_id": row.get("project_id", ""),
|
||||
"session_id": row.get("session_id", ""),
|
||||
"source_screen": row.get("source_screen", ""),
|
||||
"source_action_key": row.get("source_action_key", ""),
|
||||
"title": row.get("title", ""),
|
||||
"summary": row.get("summary", ""),
|
||||
"intent_key": row.get("intent_key", "custom"),
|
||||
"platform": row.get("platform", ""),
|
||||
"platform_label": legacy.platform_label(row.get("platform", "")) if row.get("platform") else "",
|
||||
"platform_scope": row.get("platform_scope", "single_platform"),
|
||||
"delivery_mode": row.get("delivery_mode", "hybrid"),
|
||||
"run_status": row.get("run_status", "needs_confirmation"),
|
||||
"scheduling_mode": row.get("scheduling_mode", "queued"),
|
||||
"active_executor_key": row.get("active_executor_key", "main_agent"),
|
||||
"plan": _parse_json(row.get("plan_json"), {}),
|
||||
"governance": _parse_json(row.get("governance_json"), {}),
|
||||
"result": _parse_json(row.get("result_json"), {}),
|
||||
"status_summary": row.get("status_summary", ""),
|
||||
"needs_user_input": bool(row.get("needs_user_input")),
|
||||
"blocked_reason": row.get("blocked_reason", ""),
|
||||
"active_admin_override_notice": _parse_json(row.get("active_admin_override_notice_json"), {}),
|
||||
"created_at": row.get("created_at", ""),
|
||||
"updated_at": row.get("updated_at", ""),
|
||||
"started_at": row.get("started_at", ""),
|
||||
"finished_at": row.get("finished_at", ""),
|
||||
}
|
||||
if include_events:
|
||||
payload["events"] = _list_agent_run_events(row["id"])
|
||||
return payload
|
||||
|
||||
def _incident_payload(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"id": row["id"],
|
||||
@@ -2410,6 +2531,157 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
||||
raise HTTPException(status_code=404, detail="OneLiner session not found")
|
||||
return row
|
||||
|
||||
def _load_owned_agent_run(run_id: str, account: dict[str, Any]) -> dict[str, Any]:
|
||||
row = legacy.db.fetch_one(
|
||||
"SELECT * FROM agent_runs WHERE id = ? AND user_id = ?",
|
||||
(run_id, account["id"]),
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="OneLiner run not found")
|
||||
return row
|
||||
|
||||
def _normalize_platform_scope(value: str | None) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if normalized == "all_platforms":
|
||||
return "all_platforms"
|
||||
return "single_platform"
|
||||
|
||||
def _normalize_delivery_mode(value: str | None) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if normalized in {"ui", "oneliner", "hybrid"}:
|
||||
return normalized
|
||||
return "hybrid"
|
||||
|
||||
def _normalize_scheduling_mode(value: str | None) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if normalized == "parallel":
|
||||
return "parallel"
|
||||
return "queued"
|
||||
|
||||
def _ensure_run_session(
|
||||
account: dict[str, Any],
|
||||
*,
|
||||
project_id: str,
|
||||
requested_session_id: str,
|
||||
title: str,
|
||||
preferred_platform: str,
|
||||
) -> dict[str, Any]:
|
||||
if requested_session_id:
|
||||
return _load_owned_session(requested_session_id, account)
|
||||
latest_row = legacy.db.fetch_one(
|
||||
"""
|
||||
SELECT * FROM oneliner_sessions
|
||||
WHERE user_id = ? AND project_id = ?
|
||||
ORDER BY updated_at DESC, created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(account["id"], project_id),
|
||||
)
|
||||
if latest_row:
|
||||
return latest_row
|
||||
profile = _fetch_profile_row(account, project_id)
|
||||
session_id = make_id("oline")
|
||||
timestamp = now()
|
||||
legacy.db.execute(
|
||||
"""
|
||||
INSERT INTO oneliner_sessions (
|
||||
id, user_id, project_id, profile_id, title, status, preferred_platform,
|
||||
last_platform, last_intent_key, last_message_at, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, 'active', ?, '', '', ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
session_id,
|
||||
account["id"],
|
||||
project_id,
|
||||
(profile or {}).get("id"),
|
||||
title.strip() or "新的 OneLiner 会话",
|
||||
preferred_platform,
|
||||
timestamp,
|
||||
timestamp,
|
||||
timestamp,
|
||||
),
|
||||
)
|
||||
created = legacy.db.fetch_one("SELECT * FROM oneliner_sessions WHERE id = ?", (session_id,))
|
||||
assert created is not None
|
||||
return created
|
||||
|
||||
def _touch_session_for_run(session_id: str, *, platform: str, intent_key: str) -> None:
|
||||
timestamp = now()
|
||||
legacy.db.execute(
|
||||
"""
|
||||
UPDATE oneliner_sessions
|
||||
SET last_platform = ?, last_intent_key = ?, last_message_at = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(platform, intent_key, timestamp, timestamp, session_id),
|
||||
)
|
||||
|
||||
def _log_agent_run_event(
|
||||
run_id: str,
|
||||
*,
|
||||
event_type: str,
|
||||
summary: str,
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
event_id = make_id("run_evt")
|
||||
legacy.db.execute(
|
||||
"""
|
||||
INSERT INTO agent_run_events (id, run_id, event_type, summary, details_json, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
event_id,
|
||||
run_id,
|
||||
event_type,
|
||||
summary,
|
||||
_dump(details or {}),
|
||||
now(),
|
||||
),
|
||||
)
|
||||
row = legacy.db.fetch_one("SELECT * FROM agent_run_events WHERE id = ?", (event_id,))
|
||||
assert row is not None
|
||||
return _agent_run_event_payload(row)
|
||||
|
||||
def _agent_run_plan_payload(
|
||||
request: AgentRunCreateRequest,
|
||||
*,
|
||||
governance: dict[str, Any],
|
||||
platform: str,
|
||||
platform_scope: str,
|
||||
) -> dict[str, Any]:
|
||||
requested_plan = dict(request.plan_request or {})
|
||||
raw_steps = requested_plan.get("steps") or []
|
||||
if not isinstance(raw_steps, list):
|
||||
raw_steps = [raw_steps]
|
||||
steps = [str(item).strip() for item in raw_steps if str(item).strip()]
|
||||
if not steps:
|
||||
steps = ["读取当前项目上下文", "结合治理层生成执行计划", "等待用户确认后执行"]
|
||||
return {
|
||||
**requested_plan,
|
||||
"goal": str(requested_plan.get("goal") or request.title or "主 Agent 任务").strip() or "主 Agent 任务",
|
||||
"steps": steps,
|
||||
"intent_key": str(request.intent_key or "custom").strip() or "custom",
|
||||
"platform": platform,
|
||||
"platform_scope": platform_scope,
|
||||
"source_screen": str(request.source_screen or "").strip(),
|
||||
"source_action_key": str(request.source_action_key or "").strip(),
|
||||
"summary": str(request.summary or requested_plan.get("summary") or "").strip(),
|
||||
"requested_delivery_mode": _normalize_delivery_mode(request.delivery_mode),
|
||||
"active_admin_override_notice": governance.get("active_admin_override_notice") or {},
|
||||
}
|
||||
|
||||
def _has_other_active_runs(*, account_id: str, project_id: str, run_id: str) -> bool:
|
||||
row = legacy.db.fetch_one(
|
||||
"""
|
||||
SELECT id FROM agent_runs
|
||||
WHERE user_id = ? AND project_id = ? AND id != ? AND run_status IN ('queued', 'running', 'blocked')
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(account_id, project_id, run_id),
|
||||
)
|
||||
return bool(row)
|
||||
|
||||
def _deterministic_intent(message: str, platform_hint: str, account: dict[str, Any]) -> dict[str, Any]:
|
||||
text = message.strip()
|
||||
lowered = text.lower()
|
||||
@@ -4207,6 +4479,206 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
|
||||
**result,
|
||||
}
|
||||
|
||||
@app.post("/v2/oneliner/runs")
|
||||
def create_oneliner_run(
|
||||
request: AgentRunCreateRequest,
|
||||
account: dict[str, Any] = Depends(legacy.require_approved),
|
||||
) -> dict[str, Any]:
|
||||
project = _resolve_project(account, request.project_id or None)
|
||||
normalized_platform = _safe_platform(request.platform, fallback="") if str(request.platform or "").strip() else ""
|
||||
platform_scope = _normalize_platform_scope(request.platform_scope)
|
||||
governance = _effective_policy_payload(
|
||||
subject_account=account,
|
||||
subject_project_id=project["id"],
|
||||
platform=normalized_platform,
|
||||
)
|
||||
plan = _agent_run_plan_payload(
|
||||
request,
|
||||
governance=governance,
|
||||
platform=normalized_platform,
|
||||
platform_scope=platform_scope,
|
||||
)
|
||||
session = _ensure_run_session(
|
||||
account,
|
||||
project_id=project["id"],
|
||||
requested_session_id=request.session_id,
|
||||
title=request.title.strip() or plan["goal"],
|
||||
preferred_platform=normalized_platform or "douyin",
|
||||
)
|
||||
_touch_session_for_run(session["id"], platform=normalized_platform, intent_key=plan.get("intent_key", "custom"))
|
||||
run_id = make_id("oline_run")
|
||||
timestamp = now()
|
||||
active_admin_override_notice = governance.get("active_admin_override_notice") or {}
|
||||
legacy.db.execute(
|
||||
"""
|
||||
INSERT INTO agent_runs (
|
||||
id, user_id, project_id, session_id, source_screen, source_action_key, title, summary,
|
||||
intent_key, platform, platform_scope, delivery_mode, run_status, scheduling_mode,
|
||||
active_executor_key, plan_json, governance_json, result_json, status_summary,
|
||||
needs_user_input, blocked_reason, active_admin_override_notice_json,
|
||||
created_at, updated_at, started_at, finished_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'needs_confirmation', ?, 'main_agent', ?, ?, '{}', ?, 1, '', ?, ?, ?, '', '')
|
||||
""",
|
||||
(
|
||||
run_id,
|
||||
account["id"],
|
||||
project["id"],
|
||||
session["id"],
|
||||
str(request.source_screen or "").strip(),
|
||||
str(request.source_action_key or "").strip(),
|
||||
request.title.strip() or plan["goal"],
|
||||
request.summary.strip(),
|
||||
str(request.intent_key or "custom").strip() or "custom",
|
||||
normalized_platform,
|
||||
platform_scope,
|
||||
_normalize_delivery_mode(request.delivery_mode),
|
||||
_normalize_scheduling_mode(request.scheduling_mode),
|
||||
_dump(plan),
|
||||
_dump(governance),
|
||||
"等待你确认执行计划",
|
||||
_dump(active_admin_override_notice),
|
||||
timestamp,
|
||||
timestamp,
|
||||
),
|
||||
)
|
||||
_log_agent_run_event(
|
||||
run_id,
|
||||
event_type="run.created",
|
||||
summary=f"已创建待确认任务:{request.title.strip() or plan['goal']}",
|
||||
details={
|
||||
"run_status": "needs_confirmation",
|
||||
"source_screen": str(request.source_screen or "").strip(),
|
||||
"source_action_key": str(request.source_action_key or "").strip(),
|
||||
"platform": normalized_platform,
|
||||
"platform_scope": platform_scope,
|
||||
},
|
||||
)
|
||||
row = legacy.db.fetch_one("SELECT * FROM agent_runs WHERE id = ?", (run_id,))
|
||||
assert row is not None
|
||||
return _agent_run_payload(row)
|
||||
|
||||
@app.get("/v2/oneliner/runs")
|
||||
def list_oneliner_runs(
|
||||
project_id: str | None = Query(default=None),
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
account: dict[str, Any] = Depends(legacy.require_approved),
|
||||
) -> dict[str, Any]:
|
||||
project = _resolve_project_for_read(account, project_id or None) if (project_id or "").strip() else _resolve_project_for_read(account, None)
|
||||
if project:
|
||||
rows = legacy.db.fetch_all(
|
||||
"""
|
||||
SELECT * FROM agent_runs
|
||||
WHERE user_id = ? AND project_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(account["id"], project["id"], limit),
|
||||
)
|
||||
else:
|
||||
rows = legacy.db.fetch_all(
|
||||
"""
|
||||
SELECT * FROM agent_runs
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(account["id"], limit),
|
||||
)
|
||||
items = [_agent_run_payload(row, include_events=False) for row in rows]
|
||||
return {"items": items, "count": len(items)}
|
||||
|
||||
@app.get("/v2/oneliner/runs/{run_id}")
|
||||
def get_oneliner_run(
|
||||
run_id: str,
|
||||
account: dict[str, Any] = Depends(legacy.require_approved),
|
||||
) -> dict[str, Any]:
|
||||
row = _load_owned_agent_run(run_id, account)
|
||||
return _agent_run_payload(row)
|
||||
|
||||
@app.get("/v2/oneliner/runs/{run_id}/events")
|
||||
def list_oneliner_run_events(
|
||||
run_id: str,
|
||||
account: dict[str, Any] = Depends(legacy.require_approved),
|
||||
) -> dict[str, Any]:
|
||||
row = _load_owned_agent_run(run_id, account)
|
||||
items = _list_agent_run_events(row["id"])
|
||||
return {"run": _agent_run_payload(row, include_events=False), "items": items, "count": len(items)}
|
||||
|
||||
@app.post("/v2/oneliner/runs/{run_id}/confirm")
|
||||
def confirm_oneliner_run(
|
||||
run_id: str,
|
||||
request: AgentRunConfirmRequest,
|
||||
account: dict[str, Any] = Depends(legacy.require_approved),
|
||||
) -> dict[str, Any]:
|
||||
row = _load_owned_agent_run(run_id, account)
|
||||
current_status = str(row.get("run_status") or "needs_confirmation")
|
||||
if current_status not in {"needs_confirmation", "queued", "running"}:
|
||||
raise HTTPException(status_code=409, detail="Run can no longer be confirmed")
|
||||
if current_status == "needs_confirmation":
|
||||
_log_agent_run_event(
|
||||
run_id,
|
||||
event_type="run.confirmed",
|
||||
summary=request.reason.strip() or "用户已确认执行计划",
|
||||
details={"reason": request.reason.strip()},
|
||||
)
|
||||
next_status = "queued" if _has_other_active_runs(account_id=account["id"], project_id=row.get("project_id", ""), run_id=run_id) else "running"
|
||||
timestamp = now()
|
||||
started_at = row.get("started_at", "")
|
||||
event_type = "run.queued"
|
||||
event_summary = "已进入主 Agent 等待队列"
|
||||
status_summary = "等待主 Agent 执行"
|
||||
if next_status == "running":
|
||||
started_at = timestamp
|
||||
event_type = "run.started"
|
||||
event_summary = "主 Agent 已开始执行"
|
||||
status_summary = "主 Agent 正在执行"
|
||||
legacy.db.execute(
|
||||
"""
|
||||
UPDATE agent_runs
|
||||
SET run_status = ?, status_summary = ?, needs_user_input = 0, updated_at = ?, started_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(next_status, status_summary, timestamp, started_at, run_id),
|
||||
)
|
||||
_log_agent_run_event(
|
||||
run_id,
|
||||
event_type=event_type,
|
||||
summary=event_summary,
|
||||
details={"run_status": next_status},
|
||||
)
|
||||
updated = legacy.db.fetch_one("SELECT * FROM agent_runs WHERE id = ?", (run_id,))
|
||||
assert updated is not None
|
||||
return _agent_run_payload(updated)
|
||||
|
||||
@app.post("/v2/oneliner/runs/{run_id}/cancel")
|
||||
def cancel_oneliner_run(
|
||||
run_id: str,
|
||||
request: AgentRunCancelRequest,
|
||||
account: dict[str, Any] = Depends(legacy.require_approved),
|
||||
) -> dict[str, Any]:
|
||||
row = _load_owned_agent_run(run_id, account)
|
||||
current_status = str(row.get("run_status") or "")
|
||||
if current_status not in {"needs_confirmation", "queued"}:
|
||||
raise HTTPException(status_code=409, detail="Run can no longer be cancelled")
|
||||
timestamp = now()
|
||||
legacy.db.execute(
|
||||
"""
|
||||
UPDATE agent_runs
|
||||
SET run_status = 'cancelled', status_summary = ?, needs_user_input = 0, updated_at = ?, finished_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(request.reason.strip() or "任务已取消", timestamp, timestamp, run_id),
|
||||
)
|
||||
_log_agent_run_event(
|
||||
run_id,
|
||||
event_type="run.cancelled",
|
||||
summary=request.reason.strip() or "用户取消了当前任务",
|
||||
details={"from_status": current_status},
|
||||
)
|
||||
updated = legacy.db.fetch_one("SELECT * FROM agent_runs WHERE id = ?", (run_id,))
|
||||
assert updated is not None
|
||||
return _agent_run_payload(updated)
|
||||
|
||||
@app.get("/v2/oneliner/profile")
|
||||
def get_oneliner_profile(
|
||||
project_id: str | None = Query(default=None),
|
||||
|
||||
@@ -71,7 +71,13 @@ need_cmd() {
|
||||
need_cmd python3
|
||||
need_cmd security
|
||||
need_cmd rsync
|
||||
need_cmd docker
|
||||
|
||||
if [ "$DEPLOY_MODE" = "prebuilt_local" ]; then
|
||||
if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then
|
||||
echo "[deploy] local Docker unavailable, fallback to remote_build" >&2
|
||||
DEPLOY_MODE="remote_build"
|
||||
fi
|
||||
fi
|
||||
|
||||
shell_quote() {
|
||||
python3 - "$1" <<'PY'
|
||||
|
||||
@@ -49,6 +49,8 @@ class MainAgentGovernanceTests(unittest.TestCase):
|
||||
|
||||
def _clear_tables(self) -> None:
|
||||
tables = [
|
||||
"agent_run_events",
|
||||
"agent_runs",
|
||||
"agent_policy_audit_logs",
|
||||
"agent_policy_effectivity",
|
||||
"agent_policy_versions",
|
||||
@@ -180,6 +182,72 @@ class MainAgentGovernanceTests(unittest.TestCase):
|
||||
"member_headers": {"Authorization": f"Bearer {member_token}"},
|
||||
}
|
||||
|
||||
def test_agent_run_creation_snapshots_governance_and_needs_confirmation(self) -> None:
|
||||
response = 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": "track_account",
|
||||
"platform": "douyin",
|
||||
"platform_scope": "single_platform",
|
||||
"plan_request": {
|
||||
"goal": "跟进重点账号",
|
||||
"steps": ["读取当前项目上下文", "检查重点账号变化", "决定下一步"],
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["run_status"], "needs_confirmation")
|
||||
self.assertEqual(payload["source_screen"], "dashboard")
|
||||
self.assertEqual(payload["platform"], "douyin")
|
||||
self.assertEqual(payload["platform_scope"], "single_platform")
|
||||
self.assertEqual(payload["session_id"][:5], "oline")
|
||||
self.assertEqual(payload["plan"]["goal"], "跟进重点账号")
|
||||
self.assertEqual(payload["governance"]["project_id"], self.ctx["project_id"])
|
||||
self.assertIn("layers", payload["governance"])
|
||||
self.assertEqual(payload["events"][0]["event_type"], "run.created")
|
||||
|
||||
def test_agent_run_confirm_transitions_to_queue_or_running_and_logs_events(self) -> None:
|
||||
create = self.client.post(
|
||||
"/v2/oneliner/runs",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"project_id": self.ctx["project_id"],
|
||||
"source_screen": "strategy",
|
||||
"source_action_key": "handoff-to-main-agent",
|
||||
"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)
|
||||
payload = confirm.json()
|
||||
self.assertIn(payload["run_status"], {"queued", "running"})
|
||||
event_types = [item["event_type"] for item in payload["events"]]
|
||||
self.assertIn("run.created", event_types)
|
||||
self.assertIn("run.confirmed", event_types)
|
||||
self.assertTrue("run.queued" in event_types or "run.started" in 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",
|
||||
|
||||
@@ -52,6 +52,8 @@ const appState = {
|
||||
onelinerProfile: null,
|
||||
onelinerSessions: [],
|
||||
selectedOnelinerSessionId: "",
|
||||
onelinerRuns: [],
|
||||
selectedOnelinerRunId: "",
|
||||
onelinerMessages: [],
|
||||
onelinerActionRegistry: [],
|
||||
platformAgents: [],
|
||||
@@ -209,6 +211,15 @@ function safeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function parseJsonSafe(value, fallback) {
|
||||
if (typeof value !== "string" || !value.trim()) return fallback;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimePlatformValues() {
|
||||
return PLATFORM_RUNTIME.getRuntimePlatformValues();
|
||||
}
|
||||
@@ -918,6 +929,7 @@ function ensureOneLinerUi() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="oneliner-meta" data-role="oneliner-meta"></div>
|
||||
<div class="oneliner-runs" data-role="oneliner-runs"></div>
|
||||
<div class="oneliner-sessions" data-role="oneliner-sessions"></div>
|
||||
<div class="oneliner-messages" data-role="oneliner-messages"></div>
|
||||
<form class="oneliner-composer" data-role="oneliner-form">
|
||||
@@ -950,6 +962,97 @@ function renderOneLinerSessionTabs() {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOneLinerRunsHtml() {
|
||||
const runs = safeArray(appState.onelinerRuns);
|
||||
const currentRun = getCurrentOneLinerRun();
|
||||
if (!runs.length || !currentRun) {
|
||||
return `
|
||||
<div class="task-item compact">
|
||||
<h4>还没有主 Agent 运行任务</h4>
|
||||
<p>你在首页、策略页或 Agent 页点击“交给主 Agent”后,这里会先出现待确认执行卡。</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const runEvents = safeArray(currentRun.events).slice(-3);
|
||||
const planSteps = safeArray(currentRun.plan?.steps).slice(0, 4);
|
||||
const runStatusLabel = {
|
||||
needs_confirmation: "待确认",
|
||||
queued: "排队中",
|
||||
running: "执行中",
|
||||
blocked: "已阻塞",
|
||||
done: "已完成",
|
||||
failed: "已失败",
|
||||
cancelled: "已取消"
|
||||
}[currentRun.run_status] || currentRun.run_status || "运行中";
|
||||
const statusTone = currentRun.run_status === "needs_confirmation"
|
||||
? "blue"
|
||||
: currentRun.run_status === "running"
|
||||
? "green"
|
||||
: currentRun.run_status === "queued"
|
||||
? "orange"
|
||||
: currentRun.run_status === "failed"
|
||||
? "orange"
|
||||
: "";
|
||||
return `
|
||||
<div class="task-item compact oneliner-run-card">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h4>${escapeHtml(currentRun.title || currentRun.plan?.goal || "主 Agent 任务")}</h4>
|
||||
<div class="panel-subtitle">${escapeHtml(currentRun.summary || currentRun.status_summary || "主 Agent 会先给你一张确认卡,再继续执行。")}</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${statusTone}">${escapeHtml(runStatusLabel)}</span>
|
||||
${currentRun.platform_label ? `<span class="tag">${escapeHtml(currentRun.platform_label)}</span>` : ""}
|
||||
<span class="tag">${escapeHtml(onelinerIntentLabel(currentRun.intent_key))}</span>
|
||||
</div>
|
||||
</div>
|
||||
${currentRun.active_admin_override_notice?.title ? `
|
||||
<div class="task-item compact" style="margin-top:10px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
|
||||
<h4>管理员覆盖生效中</h4>
|
||||
<p>${escapeHtml(currentRun.active_admin_override_notice.summary || "当前运行会优先遵循管理员覆盖层。")}</p>
|
||||
</div>
|
||||
` : ""}
|
||||
${planSteps.length ? `
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${planSteps.map((step, index) => `
|
||||
<div class="task-item compact">
|
||||
<h4>步骤 ${escapeHtml(formatNumber(index + 1))}</h4>
|
||||
<p>${escapeHtml(step)}</p>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
<div class="task-meta" style="margin-top:10px;">
|
||||
${currentRun.run_status === "needs_confirmation" ? `
|
||||
<span class="tag clickable-tag" data-action="confirm-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">确认执行</span>
|
||||
<span class="tag clickable-tag" data-action="cancel-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">取消本轮</span>
|
||||
` : `
|
||||
<span class="tag ${statusTone}">${escapeHtml(currentRun.status_summary || "主 Agent 正在推进中")}</span>
|
||||
`}
|
||||
</div>
|
||||
${runEvents.length ? `
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${runEvents.map((item) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(item.event_type || "run.progress")}</h4>
|
||||
<p>${escapeHtml(item.summary || "运行状态已更新。")}</p>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
${runs.length > 1 ? `
|
||||
<div class="chip-row">
|
||||
${runs.slice(0, 6).map((item) => `
|
||||
<span class="chip clickable-tag ${item.id === currentRun.id ? "active" : ""}" data-action="select-oneliner-run" data-run-id="${escapeHtml(item.id)}">
|
||||
${escapeHtml(brief(item.title || item.plan?.goal || "主 Agent 任务", 14))}
|
||||
</span>
|
||||
`).join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOneLinerMessagesHtml() {
|
||||
const messages = safeArray(appState.onelinerMessages);
|
||||
if (!messages.length) {
|
||||
@@ -1058,6 +1161,7 @@ function renderOneLinerUi() {
|
||||
ensureOneLinerUi();
|
||||
const fab = document.querySelector(".oneliner-fab");
|
||||
const meta = document.querySelector('[data-role="oneliner-meta"]');
|
||||
const runs = document.querySelector('[data-role="oneliner-runs"]');
|
||||
const sessions = document.querySelector('[data-role="oneliner-sessions"]');
|
||||
const messages = document.querySelector('[data-role="oneliner-messages"]');
|
||||
const status = document.querySelector('[data-role="oneliner-status"]');
|
||||
@@ -1067,8 +1171,18 @@ function renderOneLinerUi() {
|
||||
const activeAdminOverrideNotice = effective?.active_admin_override_notice || null;
|
||||
const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || "");
|
||||
const layers = safeArray(effective?.layers);
|
||||
const currentRun = getCurrentOneLinerRun();
|
||||
const activeRuns = safeArray(appState.onelinerRuns).filter((item) => !["done", "failed", "cancelled"].includes(item.run_status));
|
||||
if (fab) {
|
||||
fab.hidden = !appState.session;
|
||||
const mark = fab.querySelector(".oneliner-fab-mark");
|
||||
const text = fab.querySelector(".oneliner-fab-text");
|
||||
if (mark) mark.textContent = String(activeRuns.length || 1);
|
||||
if (text) {
|
||||
text.textContent = currentRun
|
||||
? `OneLiner · ${currentRun.run_status === "needs_confirmation" ? "待确认" : currentRun.run_status === "running" ? "执行中" : currentRun.run_status === "queued" ? "排队中" : "工作中"}`
|
||||
: "OneLiner";
|
||||
}
|
||||
}
|
||||
if (meta) {
|
||||
meta.innerHTML = `
|
||||
@@ -1096,6 +1210,7 @@ function renderOneLinerUi() {
|
||||
` : ""}
|
||||
`;
|
||||
}
|
||||
if (runs) runs.innerHTML = renderOneLinerRunsHtml();
|
||||
if (sessions) sessions.innerHTML = renderOneLinerSessionTabs();
|
||||
if (messages) {
|
||||
messages.innerHTML = renderOneLinerMessagesHtml();
|
||||
@@ -1284,6 +1399,8 @@ async function logoutSession() {
|
||||
appState.onelinerProfile = null;
|
||||
appState.onelinerSessions = [];
|
||||
appState.selectedOnelinerSessionId = "";
|
||||
appState.onelinerRuns = [];
|
||||
appState.selectedOnelinerRunId = "";
|
||||
appState.onelinerMessages = [];
|
||||
appState.onelinerActionRegistry = [];
|
||||
appState.platformAgents = [];
|
||||
@@ -1336,11 +1453,27 @@ async function loadStorageStatus(projectId = "") {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function hydrateSelectedOneLinerRun() {
|
||||
const runId = appState.selectedOnelinerRunId || "";
|
||||
if (!runId || !backendSupports("/v2/oneliner/runs/{run_id}")) {
|
||||
return null;
|
||||
}
|
||||
const detail = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}`).catch(() => null);
|
||||
if (!detail?.id) return null;
|
||||
const runs = safeArray(appState.onelinerRuns);
|
||||
const nextRuns = runs.some((item) => item.id === detail.id)
|
||||
? runs.map((item) => (item.id === detail.id ? detail : item))
|
||||
: [detail, ...runs];
|
||||
appState.onelinerRuns = nextRuns;
|
||||
return detail;
|
||||
}
|
||||
|
||||
async function loadAgentControlSurfaces(projectId = "") {
|
||||
const normalizedProjectId = projectId || getOneLinerProjectId();
|
||||
const governancePlatform = normalizePlatformValue(getPreferredPlatform(), "douyin");
|
||||
const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile");
|
||||
const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions");
|
||||
const supportsOneLinerRuns = backendSupports("/v2/oneliner/runs");
|
||||
const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry");
|
||||
const supportsPlatformAgents = backendSupports("/v2/platform-agents");
|
||||
const supportsGovernanceEffective = backendSupports("/v2/oneliner/governance/effective");
|
||||
@@ -1357,13 +1490,16 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
const supportsTenantQuota = backendSupports("/v2/tenant/quota");
|
||||
const supportsTenantUsage = backendSupports("/v2/tenant/usage");
|
||||
|
||||
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, userPolicyAuditsPayload, adminSystemMainPolicy, adminSystemPlatformPolicies, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
|
||||
const [profile, sessionsPayload, runsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, userPolicyAuditsPayload, adminSystemMainPolicy, adminSystemPlatformPolicies, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
|
||||
supportsOneLinerProfile
|
||||
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
supportsOneLinerSessions
|
||||
? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
|
||||
: Promise.resolve({ items: [] }),
|
||||
supportsOneLinerRuns
|
||||
? storyforgeFetch(`/v2/oneliner/runs?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
|
||||
: Promise.resolve({ items: [] }),
|
||||
supportsActionRegistry
|
||||
? storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
|
||||
: Promise.resolve({ items: [] }),
|
||||
@@ -1409,10 +1545,15 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
|
||||
appState.onelinerProfile = profile;
|
||||
appState.onelinerSessions = safeArray(sessionsPayload?.items || sessionsPayload);
|
||||
appState.onelinerRuns = safeArray(runsPayload?.items || runsPayload);
|
||||
appState.onelinerActionRegistry = safeArray(actionRegistryPayload?.items || actionRegistryPayload);
|
||||
if (!appState.selectedOnelinerSessionId || !safeArray(appState.onelinerSessions).some((item) => item.id === appState.selectedOnelinerSessionId)) {
|
||||
appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || "";
|
||||
}
|
||||
appState.selectedOnelinerRunId = choosePreferredOneLinerRunId(appState.onelinerRuns, appState.selectedOnelinerRunId || "");
|
||||
if (appState.selectedOnelinerRunId) {
|
||||
await hydrateSelectedOneLinerRun();
|
||||
}
|
||||
appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload);
|
||||
appState.onelinerGovernanceEffective = governanceEffective;
|
||||
appState.userGlobalPolicy = userGlobalPolicy;
|
||||
@@ -1514,6 +1655,50 @@ async function submitOneLinerMessage(content) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function createOneLinerRun(runRequest) {
|
||||
if (!backendSupports("/v2/oneliner/runs")) {
|
||||
throw new Error("当前后端还没有接入主 Agent 运行层。");
|
||||
}
|
||||
const projectId = getOneLinerProjectId();
|
||||
const payload = await storyforgeFetch("/v2/oneliner/runs", {
|
||||
method: "POST",
|
||||
body: {
|
||||
project_id: projectId,
|
||||
platform: getPreferredPlatform(),
|
||||
platform_scope: "single_platform",
|
||||
delivery_mode: "hybrid",
|
||||
scheduling_mode: "queued",
|
||||
...runRequest
|
||||
}
|
||||
});
|
||||
await loadAgentControlSurfaces(projectId);
|
||||
appState.selectedOnelinerRunId = payload?.id || choosePreferredOneLinerRunId(appState.onelinerRuns, "");
|
||||
rememberAction("主 Agent 已接单", payload?.title || payload?.plan?.goal || "已创建待确认任务。", "blue", payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function confirmOneLinerRun(runId, reason = "") {
|
||||
const payload = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}/confirm`, {
|
||||
method: "POST",
|
||||
body: { reason }
|
||||
});
|
||||
await loadAgentControlSurfaces(getOneLinerProjectId());
|
||||
appState.selectedOnelinerRunId = payload?.id || runId;
|
||||
rememberAction("主 Agent 已确认执行", payload?.status_summary || "当前任务已进入执行流。", "green", payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function cancelOneLinerRun(runId, reason = "") {
|
||||
const payload = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}/cancel`, {
|
||||
method: "POST",
|
||||
body: { reason }
|
||||
});
|
||||
await loadAgentControlSurfaces(getOneLinerProjectId());
|
||||
appState.selectedOnelinerRunId = choosePreferredOneLinerRunId(appState.onelinerRuns, "");
|
||||
rememberAction("主 Agent 任务已取消", payload?.status_summary || "当前任务已取消。", "orange", payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderOneLinerExecutionPayloadHtml(payload) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return `<div class="task-item compact"><h4>没有返回执行结果</h4><p>当前执行器没有附带额外数据。</p></div>`;
|
||||
@@ -2105,6 +2290,20 @@ function getCurrentOneLinerSession() {
|
||||
return sessions.find((item) => item.id === appState.selectedOnelinerSessionId) || sessions[0] || null;
|
||||
}
|
||||
|
||||
function choosePreferredOneLinerRunId(items, currentId = "") {
|
||||
const runs = safeArray(items);
|
||||
if (currentId && runs.some((item) => item.id === currentId)) {
|
||||
return currentId;
|
||||
}
|
||||
return runs.find((item) => item.run_status === "needs_confirmation")?.id || runs[0]?.id || "";
|
||||
}
|
||||
|
||||
function getCurrentOneLinerRun() {
|
||||
const runs = safeArray(appState.onelinerRuns);
|
||||
const preferredId = choosePreferredOneLinerRunId(runs, appState.selectedOnelinerRunId || "");
|
||||
return runs.find((item) => item.id === preferredId) || null;
|
||||
}
|
||||
|
||||
function onelinerIntentLabel(value) {
|
||||
return ONELINER_INTENT_LABELS[value] || value || "自定义任务";
|
||||
}
|
||||
@@ -3381,7 +3580,16 @@ function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction
|
||||
${resolvedActions.length ? `
|
||||
<div class="task-meta" style="margin-top:10px;">
|
||||
${resolvedActions.map((item) => `
|
||||
<span class="tag clickable-tag" data-action="${escapeHtml(item.action || "")}" ${item.platform ? `data-platform="${escapeHtml(item.platform)}"` : ""}>
|
||||
<span class="tag clickable-tag" data-action="${escapeHtml(item.action || "")}"
|
||||
${item.platform ? `data-platform="${escapeHtml(item.platform)}"` : ""}
|
||||
${item.sourceActionKey ? `data-source-action-key="${escapeHtml(item.sourceActionKey)}"` : ""}
|
||||
${item.sourceScreen ? `data-source-screen="${escapeHtml(item.sourceScreen)}"` : ""}
|
||||
${item.intentKey ? `data-intent-key="${escapeHtml(item.intentKey)}"` : ""}
|
||||
${item.title ? `data-title="${escapeHtml(item.title)}"` : ""}
|
||||
${item.goal ? `data-goal="${escapeHtml(item.goal)}"` : ""}
|
||||
${item.summary ? `data-summary="${escapeHtml(item.summary)}"` : ""}
|
||||
${item.platformScope ? `data-platform-scope="${escapeHtml(item.platformScope)}"` : ""}
|
||||
${item.planSteps ? `data-plan-steps="${escapeHtml(JSON.stringify(item.planSteps))}"` : ""}>
|
||||
${escapeHtml(item.label || "查看")}
|
||||
</span>
|
||||
`).join("")}
|
||||
@@ -5402,7 +5610,18 @@ function renderStrategyScreen() {
|
||||
actions: [
|
||||
{ action: "open-user-global-policy", label: "编辑我的全局策略" },
|
||||
{ action: "open-user-platform-policy", label: "编辑当前平台策略", platform },
|
||||
{ action: "open-oneliner", label: "交给 OneLiner 调整" }
|
||||
{
|
||||
action: "handoff-to-main-agent",
|
||||
label: "交给主 Agent 调整",
|
||||
platform,
|
||||
sourceScreen: "strategy",
|
||||
sourceActionKey: "governance-summary-handoff",
|
||||
intentKey: "custom",
|
||||
title: "调整当前策略",
|
||||
goal: "调整当前策略",
|
||||
summary: "先由主 Agent 读取当前治理层,再给一版确认卡。",
|
||||
planSteps: ["读取当前生效策略", "结合管理员覆盖与个人策略生成方案", "等待用户确认后执行"]
|
||||
}
|
||||
]
|
||||
})}
|
||||
</div>
|
||||
@@ -7515,7 +7734,7 @@ async function openPlatformAgentDetailAction(platform) {
|
||||
<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-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="open-oneliner">让 OneLiner 调度</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>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -9122,6 +9341,61 @@ document.addEventListener("click", async (event) => {
|
||||
await openAdminOverrideHistoryAction();
|
||||
return;
|
||||
}
|
||||
if (name === "handoff-to-main-agent") {
|
||||
try {
|
||||
setBusy(true, "正在为主 Agent 创建执行计划...");
|
||||
const payload = await createOneLinerRun({
|
||||
source_screen: action.dataset.sourceScreen || appState.screen || "dashboard",
|
||||
source_action_key: action.dataset.sourceActionKey || name,
|
||||
title: action.dataset.title || action.textContent?.trim() || "交给主 Agent 处理",
|
||||
summary: action.dataset.summary || "",
|
||||
intent_key: action.dataset.intentKey || "custom",
|
||||
platform: action.dataset.platform || getPreferredPlatform(),
|
||||
platform_scope: action.dataset.platformScope || "single_platform",
|
||||
plan_request: {
|
||||
goal: action.dataset.goal || action.dataset.title || action.textContent?.trim() || "交给主 Agent 处理",
|
||||
steps: parseJsonSafe(action.dataset.planSteps, []),
|
||||
summary: action.dataset.summary || ""
|
||||
}
|
||||
});
|
||||
appState.selectedOnelinerRunId = payload?.id || "";
|
||||
openOneLinerPanel();
|
||||
renderAll();
|
||||
} catch (error) {
|
||||
presentActionFailure(error, "主 Agent 接单失败");
|
||||
openOneLinerPanel();
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (name === "confirm-oneliner-run") {
|
||||
try {
|
||||
setBusy(true, "正在确认执行计划...");
|
||||
await confirmOneLinerRun(action.dataset.runId || "", "user confirmed");
|
||||
} catch (error) {
|
||||
presentActionFailure(error, "主 Agent 确认失败");
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (name === "cancel-oneliner-run") {
|
||||
try {
|
||||
setBusy(true, "正在取消当前任务...");
|
||||
await cancelOneLinerRun(action.dataset.runId || "", "user cancelled");
|
||||
} catch (error) {
|
||||
presentActionFailure(error, "主 Agent 取消失败");
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (name === "select-oneliner-run") {
|
||||
appState.selectedOnelinerRunId = action.dataset.runId || "";
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
if (name === "select-oneliner-session") {
|
||||
appState.selectedOnelinerSessionId = action.dataset.sessionId || "";
|
||||
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
|
||||
|
||||
@@ -1130,11 +1130,17 @@ select {
|
||||
}
|
||||
|
||||
.oneliner-meta,
|
||||
.oneliner-runs,
|
||||
.oneliner-sessions {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.oneliner-run-card {
|
||||
border-color: rgba(79, 143, 238, 0.16);
|
||||
background: linear-gradient(180deg, rgba(248, 252, 255, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);
|
||||
}
|
||||
|
||||
.oneliner-messages {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
|
||||
@@ -113,6 +113,9 @@ test("oneliner submit failures stay inside the app instead of using a browser al
|
||||
test("agent control surfaces load governance endpoints for user and admin summaries", () => {
|
||||
const source = extractBetween(APP, "async function loadAgentControlSurfaces(projectId = \"\")", "async function loadOneLinerMessages(sessionId)");
|
||||
assert.match(source, /\/v2\/oneliner\/governance\/effective/);
|
||||
assert.match(source, /\/v2\/oneliner\/runs/);
|
||||
assert.match(APP, /function hydrateSelectedOneLinerRun\(\)/);
|
||||
assert.match(APP, /\/v2\/oneliner\/runs\/\$\{encodeURIComponent\(runId\)\}/);
|
||||
assert.match(source, /\/v2\/oneliner\/governance\/user\/global/);
|
||||
assert.match(source, /\/v2\/oneliner\/governance\/user\/platforms\/\$\{encodeURIComponent\(governancePlatform\)\}/);
|
||||
assert.match(source, /\/v2\/admin\/oneliner\/governance\/system\/main-agent/);
|
||||
@@ -124,15 +127,26 @@ test("agent control surfaces load governance endpoints for user and admin summar
|
||||
assert.match(source, /const targetProjectId = requestedProjectId === ""/);
|
||||
});
|
||||
|
||||
test("oneliner panel includes a dedicated runtime header for agent runs", () => {
|
||||
const source = extractBetween(APP, "function ensureOneLinerUi()", "function renderOneLinerSessionTabs()");
|
||||
const runtime = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()");
|
||||
assert.match(source, /data-role="oneliner-runs"/);
|
||||
assert.match(runtime, /confirm-oneliner-run/);
|
||||
assert.match(runtime, /cancel-oneliner-run/);
|
||||
});
|
||||
|
||||
test("oneliner meta and action handlers expose governance entry points", () => {
|
||||
const meta = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()");
|
||||
const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderOneLinerUi()");
|
||||
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||
assert.match(meta, /open-user-global-policy/);
|
||||
assert.match(meta, /renderOneLinerRunsHtml\(\)/);
|
||||
assert.match(meta, /policyScopeTagLabel/);
|
||||
assert.match(messages, /active_admin_override_notice/);
|
||||
assert.match(actions, /name === "open-user-global-policy"/);
|
||||
assert.match(actions, /name === "open-system-main-policy"/);
|
||||
assert.match(actions, /name === "handoff-to-main-agent"/);
|
||||
assert.match(actions, /name === "confirm-oneliner-run"/);
|
||||
});
|
||||
|
||||
test("system governance saves refresh control surfaces after persisting", () => {
|
||||
|
||||
Reference in New Issue
Block a user