feat: add main agent runtime flow v1

This commit is contained in:
kris
2026-03-29 18:25:39 +08:00
parent ccbe6ca565
commit 30e37e5ce1
6 changed files with 845 additions and 5 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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", () => {