feat(web): W4-A wire console to real endpoints

- Agent page uses contracts (AgentRun/AgentRunDetail/AgentGoalReport) instead of a local
  mirror; ?mock=1 stays as an explicit demo switch; run snapshot refreshes on agent.run_status;
  stream auto-follow only while the user stays near the bottom (abort stays reachable)
- settings: Pi provider select incl. fake, new Agent runtime card (budgets, fakeScript)
- download trace uses /tasks?subjectId= and /organization-records?downloadTaskId= instead of
  time-window engine run matching and path matching; new business-task tab with cancel/retry
- workbench download dialog posts /acquisition-plans and shows per-slot rejection reasons
- preference tag revision history; subscription search by title/kind; identity/alias/subscription
  lists show entity titles; slot cards show current asset summary; policy preview sample titles
- DecisionSummary / IdentityRevisionList / ReleaseIdentityResult / TaskAccepted / PolicyPreviewSummary /
  OrganizePlanArtifactContent taken from contracts
- docs: SSE for console (cookie) vs GET /events?after=&limit= JSON replay for API keys

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 21:32:11 +08:00
1 parent 34e8751b45
commit 1023642ef9
30 files changed
+703 -271

No files matched your search

+20 -57
View File
@@ -1,75 +1,38 @@
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
AgentGoalReport,
AgentRun,
AgentRunDetail,
type AgentRunEvent,
AgentRunEventRow,
type AgentRunMessage,
AgentRunMode,
type AgentRunStatus,
agentRunEventFromRow,
cursorPage,
Id,
NonNegInt,
Timestamp,
} from "@umt/contracts";
import { z } from "zod";
import { MOCK_RUNS, mockCreateRun, mockDetail, mockEventsAfter } from "~/components/agent/mock.ts";
import { TTL_SECOND } from "~/lib/api.ts";
import { apiMutate, apiParsed, cleanParams } from "./parse.ts";
// Agent 会话(W2-D:GET/POST /agent/runs、GET /agent/runs/{id}?afterSeq=、/messages、/abort)。
// 端点未就绪时用 mock 开关(?mock=1 或 VITE_AGENT_MOCK=1)开发。
// 下面的 schema 与 contracts/agent.ts 的 AgentRunDetail / AgentGoalReport 同形;在这里宽松镜像一份,
// 合同字段调整时页面仍能解析(新增字段忽略,缺失的可选字段按未知显示)。
// Agent 会话(W2-D:GET/POST /agent/runs、GET /agent/runs/{id}?afterSeq=、/messages、/abort),
// 形状一律取 contracts(AgentRun / AgentRunDetail / AgentGoalReport)。
// ?mock=1(或 VITE_AGENT_MOCK=1)只作演示开关:不连后端,用本地假数据预览交互。
export const AGENT_BASE = "/api/v1/agent/runs";
/** 运行模式:execute = 授权内执行;plan_only = 只调查出方案,不执行写能力 */
export const AgentMode = z.enum(["execute", "plan_only"]);
export type AgentMode = z.infer<typeof AgentMode>;
export const AgentRunView = AgentRun.extend({
mode: AgentMode.optional(),
dedupeKey: z.string().nullable().optional(),
});
export type AgentRunView = z.infer<typeof AgentRunView>;
export const AgentMode = AgentRunMode;
export type AgentMode = AgentRunMode;
export type AgentRunView = AgentRun;
/** 服务端从业务表汇总的目标完成度(不采信模型文本) */
export const GoalReport = z.object({
state: z.enum(["no_actions", "waiting", "completed", "partial", "failed"]),
summary: z.string(),
submitted: NonNegInt,
imported: NonNegInt,
inProgress: NonNegInt,
failed: NonNegInt,
plans: z.array(z.object({ planId: Id, status: z.string() }).passthrough()),
downloads: z.array(z.object({ downloadTaskId: Id, status: z.string(), name: z.string().nullable() }).passthrough()),
organization: z.array(
z.object({ recordId: Id, status: z.string(), targetPath: z.string().nullable() }).passthrough(),
),
modelText: z.string().nullable(),
generatedAt: Timestamp,
});
export type GoalReport = z.infer<typeof GoalReport>;
export const RunMessage = z.object({
id: Id,
createdAt: Timestamp,
text: z.string(),
status: z.string(),
consumedAt: Timestamp.nullable().optional(),
});
export type RunMessage = z.infer<typeof RunMessage>;
export const RunDetail = z.object({
run: AgentRunView,
events: z.array(AgentRunEventRow),
nextAfterSeq: z.number().int().nullable(),
messages: z.array(RunMessage),
report: GoalReport.nullable(),
});
export type RunDetail = z.infer<typeof RunDetail>;
export const GoalReport = AgentGoalReport;
export type GoalReport = AgentGoalReport;
export type RunMessage = AgentRunMessage;
export type RunDetail = AgentRunDetail;
export type RunSnapshot = Omit<RunDetail, "events" | "nextAfterSeq">;
const RunPage = cursorPage(AgentRunView);
const RunPage = cursorPage(AgentRun);
export function agentMockEnabled(): boolean {
if (import.meta.env.VITE_AGENT_MOCK === "1") return true;
@@ -118,7 +81,7 @@ export function useAgentRun(id: string | undefined) {
enabled: !!id,
queryFn: async ({ signal }): Promise<RunSnapshot> => {
if (mock) return mockDetail(id ?? "");
const d = await apiParsed(RunDetail, `${AGENT_BASE}/:id`, {
const d = await apiParsed(AgentRunDetail, `${AGENT_BASE}/:id`, {
pathParams: { id: id ?? "" },
params: { afterSeq: NO_EVENTS, limit: 1 },
signal,
@@ -135,7 +98,7 @@ export async function fetchRunEvents(id: string, after: number, signal?: AbortSi
const out: AgentRunEvent[] = [];
let cursor = after;
for (let page = 0; page < 20; page++) {
const d = await apiParsed(RunDetail, `${AGENT_BASE}/:id`, {
const d = await apiParsed(AgentRunDetail, `${AGENT_BASE}/:id`, {
pathParams: { id },
params: { afterSeq: cursor, limit: 500 },
signal,
@@ -158,7 +121,7 @@ export function useCreateAgentRun() {
return useMutation({
mutationFn: async (input: { goal: string; mode: AgentMode }) => {
if (agentMockEnabled()) return mockCreateRun(input.goal, input.mode);
return apiMutate(AGENT_BASE, { body: { goal: input.goal, mode: input.mode } }).then((d) => AgentRunView.parse(d));
return apiMutate(AGENT_BASE, { body: { goal: input.goal, mode: input.mode } }).then((d) => AgentRun.parse(d));
},
onSuccess: () => qc.invalidateQueries({ queryKey: agentKeys.all }),
});
@@ -170,7 +133,7 @@ export function useAbortAgentRun() {
mutationFn: async (id: string) => {
if (agentMockEnabled()) return mockDetail(id, { abort: true }).run;
return apiMutate(`${AGENT_BASE}/:id/abort`, { pathParams: { id }, body: { reason: "用户在控制台中止" } }).then(
(d) => AgentRunView.parse(d),
(d) => AgentRun.parse(d),
);
},
onSuccess: () => qc.invalidateQueries({ queryKey: agentKeys.all }),
@@ -183,7 +146,7 @@ export function useAppendAgentMessage() {
mutationFn: async ({ id, text }: { id: string; text: string }) => {
if (agentMockEnabled()) return mockDetail(id, { append: text }).run;
return apiMutate(`${AGENT_BASE}/:id/messages`, { pathParams: { id }, body: { text } }).then((d) =>
AgentRunView.parse(d),
AgentRun.parse(d),
);
},
onSuccess: (_d, v) => qc.invalidateQueries({ queryKey: agentKeys.run(v.id) }),