feat(goal-run): add multi-checkpoint goal orchestration with H5 and admin surfaces.

Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval
UX in chat, and add admin inspection routes with local verify scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 17:03:16 +08:00
parent 43bc8bbc2b
commit 666db0b939
47 changed files with 4417 additions and 4 deletions
+2
View File
@@ -16,6 +16,7 @@ import { WechatPage } from './pages/admin/WechatPage';
import { SystemPolicyPage } from './pages/admin/SystemPolicyPage';
import { OrchestratorPage } from './pages/admin/OrchestratorPage';
import { LlmProvidersPage } from './pages/admin/LlmProvidersPage';
import { GoalRunPage } from './pages/admin/GoalRunPage';
export function App() {
return (
@@ -52,6 +53,7 @@ export function App() {
<Route path="policy" element={<SystemPolicyPage />} />
<Route path="orchestrator" element={<OrchestratorPage />} />
<Route path="llm" element={<LlmProvidersPage />} />
<Route path="goal-runs" element={<GoalRunPage />} />
<Route path="*" element={<Navigate to="/admin" replace />} />
</Route>
+123
View File
@@ -893,3 +893,126 @@ export async function patchMemoryV2Config(patch: Record<string, unknown>) {
export async function fetchMemoryV2Runtime() {
return adminFetch<MemoryV2RuntimeState>('/admin-api/memory-v2/runtime');
}
export type GoalRunRuntimeState = {
enabled: boolean;
canaryUserIds: string[];
canaryMode: boolean;
};
export type GoalRunSummaryResponse = {
counts: Record<string, number>;
runtime: GoalRunRuntimeState;
};
export type GoalRunListItem = {
id: string;
userId: string;
username: string | null;
title: string;
intentSummary: string;
status: string;
priority: number;
sourceChannel: string;
sourceSessionId: string | null;
currentCheckpointId: string | null;
checkpointCount: number;
activeAgentRunCount: number;
createdAt: number;
updatedAt: number;
completedAt: number | null;
};
export type GoalRunCheckpointRow = {
id: string;
goalRunId: string;
sequence: number;
title: string;
description: string | null;
status: string;
agentRunId: string | null;
outputSummary: string | null;
userFeedback: string | null;
approvedAt: number | null;
createdAt: number;
updatedAt: number;
startedAt: number | null;
completedAt: number | null;
};
export type GoalRunAgentRunRow = {
id: string;
status: string;
requestId: string;
goalCheckpointId: string | null;
createdAt: number;
updatedAt: number;
completedAt: number | null;
};
export type GoalRunDetail = {
id: string;
userId: string;
username: string | null;
title: string;
intentSummary: string;
status: string;
priority: number;
sourceChannel: string;
sourceSessionId: string | null;
sourceMessageId: string | null;
currentCheckpointId: string | null;
context: unknown;
memorySnapshot: unknown;
createdAt: number;
updatedAt: number;
completedAt: number | null;
checkpoints: GoalRunCheckpointRow[];
agentRuns: GoalRunAgentRunRow[];
canaryEnabled: boolean;
};
export async function fetchGoalRunSummary() {
return adminFetch<GoalRunSummaryResponse>('/admin-api/goal-runs/summary');
}
export async function fetchGoalRuns(params?: {
status?: string;
userId?: string;
limit?: number;
offset?: number;
}) {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.userId) query.set('userId', params.userId);
if (params?.limit != null) query.set('limit', String(params.limit));
if (params?.offset != null) query.set('offset', String(params.offset));
const suffix = query.toString() ? `?${query.toString()}` : '';
return adminFetch<{ items: GoalRunListItem[]; status: string | null; limit: number; offset: number }>(
`/admin-api/goal-runs${suffix}`,
);
}
export async function fetchGoalRunDetail(goalRunId: string) {
return adminFetch<{ goal: GoalRunDetail }>(`/admin-api/goal-runs/${encodeURIComponent(goalRunId)}`);
}
export async function approveGoalRunCheckpoint(
goalRunId: string,
checkpointId: string,
feedback?: string | null,
) {
return adminFetch<{ ok: true; goal: GoalRunDetail }>(
`/admin-api/goal-runs/${encodeURIComponent(goalRunId)}/checkpoints/${encodeURIComponent(checkpointId)}/approve`,
{
method: 'POST',
body: JSON.stringify({ feedback: feedback ?? null }),
},
);
}
export async function cancelGoalRun(goalRunId: string) {
return adminFetch<{ ok: true; goal: GoalRunDetail }>(
`/admin-api/goal-runs/${encodeURIComponent(goalRunId)}/cancel`,
{ method: 'POST', body: JSON.stringify({}) },
);
}
+1
View File
@@ -9,6 +9,7 @@ const links = [
{ to: '/admin/policy', label: '策略中心' },
{ to: '/admin/orchestrator', label: '任务编排' },
{ to: '/admin/llm', label: '统一大模型' },
{ to: '/admin/goal-runs', label: 'Goal Run' },
];
export function AdminLayout() {
+390
View File
@@ -0,0 +1,390 @@
import { useCallback, useEffect, useState } from 'react';
import {
approveGoalRunCheckpoint,
cancelGoalRun,
fetchGoalRunDetail,
fetchGoalRunSummary,
fetchGoalRuns,
type GoalRunCheckpointRow,
type GoalRunDetail,
type GoalRunListItem,
type GoalRunSummaryResponse,
} from '../../api/admin';
const STATUS_OPTIONS = [
{ value: '', label: '全部状态' },
{ value: 'active', label: 'active' },
{ value: 'awaiting_user', label: 'awaiting_user' },
{ value: 'paused', label: 'paused' },
{ value: 'completed', label: 'completed' },
{ value: 'failed', label: 'failed' },
{ value: 'cancelled', label: 'cancelled' },
];
function formatTime(value: number | null | undefined) {
if (!value) return '—';
return new Date(value).toLocaleString('zh-CN', { hour12: false });
}
function statusBadge(status: string) {
const colors: Record<string, string> = {
active: '#1f6feb',
awaiting_user: '#b78103',
paused: '#68716c',
completed: '#1a7f37',
failed: '#cf222e',
cancelled: '#68716c',
running: '#1f6feb',
pending: '#68716c',
approved: '#1a7f37',
};
return (
<span
style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: 999,
fontSize: 12,
background: `${colors[status] ?? '#68716c'}14`,
color: colors[status] ?? '#68716c',
}}
>
{status}
</span>
);
}
function CheckpointList({
checkpoints,
onApprove,
actionBusy,
}: {
checkpoints: GoalRunCheckpointRow[];
onApprove: (checkpointId: string) => void;
actionBusy: string | null;
}) {
if (!checkpoints.length) return <p style={{ color: '#68716c', margin: 0 }}></p>;
return (
<div style={{ display: 'grid', gap: 10 }}>
{checkpoints.map((checkpoint) => (
<div
key={checkpoint.id}
style={{
border: '1px solid #e6ece8',
borderRadius: 10,
padding: 10,
display: 'grid',
gap: 6,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
<strong>{checkpoint.sequence}. {checkpoint.title}</strong>
{statusBadge(checkpoint.status)}
</div>
{checkpoint.description && (
<p style={{ margin: 0, color: '#68716c' }}>{checkpoint.description}</p>
)}
{checkpoint.outputSummary && (
<p style={{ margin: 0 }}>{checkpoint.outputSummary}</p>
)}
<p style={{ margin: 0, color: '#68716c', fontSize: 13 }}>
agent_run={checkpoint.agentRunId ?? '—'} · {formatTime(checkpoint.updatedAt)}
</p>
{checkpoint.status === 'awaiting_approval' && (
<div>
<button
type="button"
className="btn"
disabled={actionBusy === `approve:${checkpoint.id}`}
onClick={() => onApprove(checkpoint.id)}
>
{actionBusy === `approve:${checkpoint.id}` ? '确认中…' : '管理员确认阶段'}
</button>
</div>
)}
</div>
))}
</div>
);
}
export function GoalRunPage() {
const [summary, setSummary] = useState<GoalRunSummaryResponse | null>(null);
const [items, setItems] = useState<GoalRunListItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [detail, setDetail] = useState<GoalRunDetail | null>(null);
const [statusFilter, setStatusFilter] = useState('');
const [userFilter, setUserFilter] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const [actionBusy, setActionBusy] = useState<string | null>(null);
const reloadDetail = useCallback(async (goalRunId: string) => {
const result = await fetchGoalRunDetail(goalRunId);
setDetail(result.goal);
}, []);
const reloadList = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [summaryRes, listRes] = await Promise.all([
fetchGoalRunSummary(),
fetchGoalRuns({
status: statusFilter || undefined,
userId: userFilter.trim() || undefined,
limit: 50,
}),
]);
setSummary(summaryRes);
setItems(listRes.items);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
}, [statusFilter, userFilter]);
useEffect(() => {
void reloadList();
}, [reloadList]);
useEffect(() => {
if (!selectedId) {
setDetail(null);
return;
}
let cancelled = false;
setDetailLoading(true);
void fetchGoalRunDetail(selectedId)
.then((result) => {
if (!cancelled) setDetail(result.goal);
})
.catch((err) => {
if (!cancelled) {
setError(err instanceof Error ? err.message : '读取详情失败');
}
})
.finally(() => {
if (!cancelled) setDetailLoading(false);
});
return () => {
cancelled = true;
};
}, [selectedId]);
const approveCheckpoint = async (checkpointId: string) => {
if (!selectedId) return;
setActionBusy(`approve:${checkpointId}`);
setError(null);
try {
await approveGoalRunCheckpoint(selectedId, checkpointId);
await reloadDetail(selectedId);
await reloadList();
} catch (err) {
setError(err instanceof Error ? err.message : '确认阶段失败');
} finally {
setActionBusy(null);
}
};
const cancelSelectedGoal = async () => {
if (!selectedId) return;
setActionBusy('cancel');
setError(null);
try {
await cancelGoalRun(selectedId);
await reloadDetail(selectedId);
await reloadList();
} catch (err) {
setError(err instanceof Error ? err.message : '取消目标失败');
} finally {
setActionBusy(null);
}
};
if (loading && !summary) return <p></p>;
return (
<div className="grid">
{error && <p className="alert">{error}</p>}
<div className="card">
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center' }}>
<div>
<h2 style={{ marginTop: 0, marginBottom: 4 }}>Goal Run</h2>
<p style={{ color: '#68716c', margin: 0 }}>
checkpoint memindadm
</p>
</div>
<button type="button" className="btn secondary" onClick={() => void reloadList()}>
</button>
</div>
{summary && (
<div style={{ marginTop: 16, display: 'grid', gap: 10 }}>
<p style={{ margin: 0 }}>
<strong>{summary.runtime.enabled ? '已启用' : '未启用'}</strong>
{summary.runtime.canaryMode
? ` · Canary 用户 ${summary.runtime.canaryUserIds.length}`
: ' · 全员'}
</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
gap: 12,
}}
>
{Object.entries(summary.counts).map(([status, count]) => (
<div key={status}>
<p style={{ color: '#68716c', marginBottom: 4 }}>{status}</p>
<strong style={{ fontSize: 24 }}>{count}</strong>
</div>
))}
</div>
</div>
)}
</div>
<div className="card">
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 16 }}>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ color: '#68716c', fontSize: 13 }}></span>
<select
value={statusFilter}
onChange={(event) => setStatusFilter(event.target.value)}
style={{ minWidth: 160 }}
>
{STATUS_OPTIONS.map((option) => (
<option key={option.value || 'all'} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label style={{ display: 'grid', gap: 4, flex: 1, minWidth: 220 }}>
<span style={{ color: '#68716c', fontSize: 13 }}> ID</span>
<input
value={userFilter}
onChange={(event) => setUserFilter(event.target.value)}
placeholder="可选,精确匹配 user_id"
/>
</label>
<div style={{ alignSelf: 'end' }}>
<button type="button" className="btn" onClick={() => void reloadList()}>
</button>
</div>
</div>
<h3 style={{ marginTop: 0 }}>{items.length}</h3>
{items.length === 0 ? (
<p style={{ color: '#68716c' }}> Goal Run </p>
) : (
<div style={{ display: 'grid', gap: 10 }}>
{items.map((item) => (
<button
key={item.id}
type="button"
onClick={() => setSelectedId(item.id)}
style={{
textAlign: 'left',
border: selectedId === item.id ? '2px solid #1f6feb' : '1px solid #e6ece8',
borderRadius: 12,
padding: 12,
background: '#fff',
cursor: 'pointer',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<strong>{item.title}</strong>
{statusBadge(item.status)}
</div>
<p style={{ margin: '8px 0 0', color: '#68716c', fontSize: 13 }}>
{item.username ?? item.userId} · {item.checkpointCount} · run {item.activeAgentRunCount}
</p>
<p style={{ margin: '4px 0 0', color: '#68716c', fontSize: 13 }}>
{formatTime(item.updatedAt)}
</p>
</button>
))}
</div>
)}
</div>
{selectedId && (
<div className="card">
<h3 style={{ marginTop: 0 }}></h3>
{detailLoading && !detail ? (
<p></p>
) : detail ? (
<div style={{ display: 'grid', gap: 16 }}>
<div>
<p style={{ margin: 0 }}><strong>{detail.title}</strong></p>
<p style={{ margin: '8px 0 0' }}>{detail.intentSummary}</p>
<p style={{ margin: '8px 0 0', color: '#68716c', fontSize: 13 }}>
id={detail.id} · user={detail.username ?? detail.userId}
· canary={detail.canaryEnabled ? '是' : '否'}
</p>
</div>
<div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 }}>
{!['completed', 'cancelled'].includes(detail.status) && (
<button
type="button"
className="btn secondary"
disabled={actionBusy === 'cancel'}
onClick={() => void cancelSelectedGoal()}
>
{actionBusy === 'cancel' ? '取消中…' : '取消目标'}
</button>
)}
</div>
<h4 style={{ marginBottom: 8 }}>Checkpoints</h4>
<CheckpointList
checkpoints={detail.checkpoints}
onApprove={(checkpointId) => void approveCheckpoint(checkpointId)}
actionBusy={actionBusy}
/>
</div>
<div>
<h4 style={{ marginBottom: 8 }}> Agent Runs</h4>
{detail.agentRuns.length === 0 ? (
<p style={{ color: '#68716c', margin: 0 }}> run</p>
) : (
<div style={{ display: 'grid', gap: 8 }}>
{detail.agentRuns.map((run) => (
<div
key={run.id}
style={{
border: '1px solid #e6ece8',
borderRadius: 10,
padding: 10,
display: 'flex',
justifyContent: 'space-between',
gap: 8,
flexWrap: 'wrap',
}}
>
<code style={{ fontSize: 12 }}>{run.id}</code>
{statusBadge(run.status)}
<span style={{ color: '#68716c', fontSize: 13 }}>
checkpoint={run.goalCheckpointId ?? '—'} · {formatTime(run.updatedAt)}
</span>
</div>
))}
</div>
)}
</div>
</div>
) : (
<p style={{ color: '#68716c' }}></p>
)}
</div>
)}
</div>
);
}
+3
View File
@@ -71,6 +71,9 @@ export function SummaryPage() {
<Link className="btn secondary" to="/admin/llm" style={{ textAlign: 'center', textDecoration: 'none' }}>
/ H5
</Link>
<Link className="btn secondary" to="/admin/goal-runs" style={{ textAlign: 'center', textDecoration: 'none' }}>
Goal Run
</Link>
<Link className="btn secondary" to="/admin/users" style={{ textAlign: 'center', textDecoration: 'none' }}>
</Link>