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:
1 parent
34e8751b45
commit
1023642ef9
30 files changed
+703
-271
No files matched your search
+73
-79
@@ -4,19 +4,21 @@ import {
|
||||
cursorPage,
|
||||
Decision,
|
||||
type DownloadTask,
|
||||
EngineRun,
|
||||
EngineRunDetail,
|
||||
type PublicOrganizationRecord,
|
||||
PublicOrganizationRecord,
|
||||
SourceRelease,
|
||||
Task,
|
||||
type TaskKind,
|
||||
type TaskStatus,
|
||||
} from "@umt/contracts";
|
||||
import { PLAN_STATUS_LABEL } from "./downloads.ts";
|
||||
import { apiParsed, cleanParams } from "./parse.ts";
|
||||
import { useRecordsForEntity } from "./records.ts";
|
||||
import { apiParsed } from "./parse.ts";
|
||||
|
||||
// 业务任务视图:引擎 run 关联 + 按 trace 汇总的业务时间线(参考 MovieClaw task-activity)
|
||||
// 关联键:plan → download_task(instanceId + hash)→ organization_record → decision/agent_run。
|
||||
// 业务任务视图:按 trace 汇总的业务时间线(参考 MovieClaw task-activity)
|
||||
// 关联键全部由服务端给出:plan →(/tasks?subjectId=planId)submitDownload → download_task
|
||||
// →(/tasks?subjectId=downloadTaskId)verifyAndImport →(/organization-records?downloadTaskId=)整理记录 → decision/agent_run。
|
||||
|
||||
const EnginePage = cursorPage(EngineRun);
|
||||
const TaskPage = cursorPage(Task);
|
||||
const RecordPage = cursorPage(PublicOrganizationRecord);
|
||||
const DecisionPage = cursorPage(Decision);
|
||||
|
||||
const RESOLUTION_LABEL = {
|
||||
@@ -63,44 +65,55 @@ export function useSubjectDecisions(subjectIds: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 引擎 run 关联:submitDownload 的 input 带 acquisitionPlanId,verifyAndImport 的 input 带 downloadTaskId。
|
||||
* 服务端没有"按业务对象查 run"的端点,这里在时间窗内取少量候选 run 读详情后按 input 精确匹配。
|
||||
*/
|
||||
export function useTraceEngineRuns(opts: { planId?: string | null; downloadTaskId?: string; since?: string }) {
|
||||
const { planId, downloadTaskId, since } = opts;
|
||||
/** 业务任务:submitDownload 的 subject 是计划,verifyAndImport 的 subject 是下载任务(服务端写入 task 表) */
|
||||
export function useTraceTasks(opts: { planId?: string | null; downloadTaskId?: string }) {
|
||||
const ids = [opts.planId, opts.downloadTaskId].filter((x): x is string => !!x);
|
||||
return useQuery({
|
||||
queryKey: ["engine", "trace", planId ?? "", downloadTaskId ?? "", since ?? ""],
|
||||
enabled: !!since && (!!planId || !!downloadTaskId),
|
||||
staleTime: 60_000,
|
||||
queryFn: async ({ signal }) => {
|
||||
const from = since ? new Date(new Date(since).getTime() - 10 * 60_000).toISOString() : undefined;
|
||||
const lists = await Promise.all(
|
||||
["submitDownload", "verifyAndImport"].map((workflowName) =>
|
||||
apiParsed(EnginePage, "/api/v1/engine/runs", {
|
||||
params: cleanParams({ workflowName, from, limit: 15 }),
|
||||
signal,
|
||||
}).catch(() => ({ items: [], nextCursor: null })),
|
||||
),
|
||||
queryKey: ["tasks", "trace", ids],
|
||||
enabled: ids.length > 0,
|
||||
queryFn: async ({ signal }): Promise<Task[]> => {
|
||||
const pages = await Promise.all(
|
||||
ids.map((subjectId) => apiParsed(TaskPage, "/api/v1/tasks", { params: { subjectId, limit: 50 }, signal })),
|
||||
);
|
||||
const candidates = lists.flatMap((l) => l.items);
|
||||
const details = await Promise.all(
|
||||
candidates.map((r) =>
|
||||
apiParsed(EngineRunDetail, "/api/v1/engine/runs/:id", { pathParams: { id: r.id }, signal }).catch(() => null),
|
||||
),
|
||||
);
|
||||
return details.filter((d): d is EngineRunDetail => {
|
||||
if (!d) return false;
|
||||
const input = (d.input ?? {}) as Record<string, unknown>;
|
||||
return (
|
||||
(!!planId && input.acquisitionPlanId === planId) ||
|
||||
(!!downloadTaskId && input.downloadTaskId === downloadTaskId)
|
||||
);
|
||||
});
|
||||
return pages.flatMap((p) => p.items);
|
||||
},
|
||||
refetchInterval: (q) => (q.state.data?.some((t) => ACTIVE_TASK.includes(t.status)) ? 5_000 : false),
|
||||
});
|
||||
}
|
||||
|
||||
const ACTIVE_TASK: TaskStatus[] = ["pending", "running", "waiting"];
|
||||
|
||||
/** 这个下载产生的整理记录(服务端按 download_task_id 关联) */
|
||||
export function useRecordsForDownload(downloadTaskId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["records", "download", downloadTaskId ?? ""],
|
||||
enabled: !!downloadTaskId,
|
||||
queryFn: ({ signal }) =>
|
||||
apiParsed(RecordPage, "/api/v1/organization-records", {
|
||||
params: { downloadTaskId: downloadTaskId ?? "", limit: 50 },
|
||||
signal,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const TASK_KIND_LABEL: Partial<Record<TaskKind, string>> = {
|
||||
submitDownload: "提交下载",
|
||||
verifyAndImport: "核验入库",
|
||||
evaluateAcquisition: "评估候选",
|
||||
cleanupDownloadCache: "清理缓存",
|
||||
resolveDecision: "处理待办",
|
||||
};
|
||||
|
||||
const TASK_STATUS_TONE: Record<TaskStatus, TimelineTone> = {
|
||||
pending: "neutral",
|
||||
running: "neutral",
|
||||
waiting: "warning",
|
||||
succeeded: "success",
|
||||
failed: "destructive",
|
||||
cancelled: "neutral",
|
||||
dead: "destructive",
|
||||
};
|
||||
|
||||
const RECORD_TONE: Record<PublicOrganizationRecord["status"], TimelineTone> = {
|
||||
queued: "neutral",
|
||||
running: "neutral",
|
||||
@@ -110,18 +123,6 @@ const RECORD_TONE: Record<PublicOrganizationRecord["status"], TimelineTone> = {
|
||||
cancelled: "neutral",
|
||||
};
|
||||
|
||||
/** 整理记录与下载的关联:同实例 + 源路径落在下载内容路径下(合同里没有 downloadTaskId,只能按路径匹配) */
|
||||
export function recordsForDownload(records: PublicOrganizationRecord[], task: DownloadTask) {
|
||||
const base = (task.contentPath ?? task.savePath ?? "").replace(/\/+$/, "");
|
||||
return records.filter(
|
||||
(r) =>
|
||||
r.source.downloaderInstanceId === task.instanceId &&
|
||||
!!base &&
|
||||
!!r.source.path &&
|
||||
(r.source.path === base || r.source.path.startsWith(`${base}/`)),
|
||||
);
|
||||
}
|
||||
|
||||
/** 汇总一个下载 trace 的业务事件,按时间升序 */
|
||||
export function buildTimeline(input: {
|
||||
task: DownloadTask;
|
||||
@@ -129,9 +130,9 @@ export function buildTimeline(input: {
|
||||
plan?: AcquisitionPlan | null;
|
||||
records?: PublicOrganizationRecord[];
|
||||
decisions?: Decision[];
|
||||
engineRuns?: EngineRunDetail[];
|
||||
tasks?: Task[];
|
||||
}): TimelineEvent[] {
|
||||
const { task, release, plan, records = [], decisions = [], engineRuns = [] } = input;
|
||||
const { task, release, plan, records = [], decisions = [], tasks = [] } = input;
|
||||
const ev: TimelineEvent[] = [];
|
||||
if (release) {
|
||||
ev.push({
|
||||
@@ -253,21 +254,20 @@ export function buildTimeline(input: {
|
||||
href: `/console/agent/${d.agentRunId}`,
|
||||
});
|
||||
}
|
||||
for (const r of engineRuns) {
|
||||
for (const t of tasks) {
|
||||
const err = t.lastError
|
||||
? String((t.lastError as { message?: unknown }).message ?? JSON.stringify(t.lastError))
|
||||
: null;
|
||||
ev.push({
|
||||
key: `eng-${r.id}`,
|
||||
key: `task-${t.id}`,
|
||||
stage: "engine",
|
||||
at: r.startedAt ?? r.createdAt,
|
||||
title: `引擎 ${r.workflowName}:${r.status}`,
|
||||
detail: `${r.stepAttempts.length} 个步骤尝试 · 第 ${r.attempts} 次`,
|
||||
tone:
|
||||
r.status === "failed"
|
||||
? "destructive"
|
||||
: r.status === "completed" || r.status === "succeeded"
|
||||
? "success"
|
||||
: "neutral",
|
||||
href: `/console/engine/${encodeURIComponent(r.id)}`,
|
||||
ongoing: r.status === "running" || r.status === "pending" || r.status === "sleeping",
|
||||
at: t.startedAt ?? t.createdAt,
|
||||
title: `任务 ${TASK_KIND_LABEL[t.kind] ?? t.kind}:${t.status}`,
|
||||
detail:
|
||||
[t.attempts > 1 ? `第 ${t.attempts} 次尝试` : null, t.waitReason, err].filter(Boolean).join(" · ") || null,
|
||||
tone: TASK_STATUS_TONE[t.status],
|
||||
...(t.engineRunId ? { href: `/console/engine/${encodeURIComponent(t.engineRunId)}` } : {}),
|
||||
ongoing: ACTIVE_TASK.includes(t.status),
|
||||
});
|
||||
}
|
||||
return ev.sort((a, b) => a.at.localeCompare(b.at));
|
||||
@@ -276,30 +276,24 @@ export function buildTimeline(input: {
|
||||
/** 一站式取 trace 数据 */
|
||||
export function useDownloadTrace(task: DownloadTask | undefined, plan: AcquisitionPlan | null | undefined) {
|
||||
const release = useSourceRelease(task?.sourceReleaseId ?? plan?.sourceReleaseId);
|
||||
const entityId = release.data?.mediaEntityId ?? null;
|
||||
const records = useRecordsForEntity(entityId);
|
||||
const records = useRecordsForDownload(task?.id);
|
||||
const decisions = useSubjectDecisions([task?.id ?? "", task?.acquisitionPlanId ?? ""]);
|
||||
const engine = useTraceEngineRuns({
|
||||
planId: task?.acquisitionPlanId,
|
||||
downloadTaskId: task?.id,
|
||||
since: plan?.createdAt ?? task?.createdAt,
|
||||
});
|
||||
const matched = task ? recordsForDownload(records.data?.items ?? [], task) : [];
|
||||
const tasks = useTraceTasks({ planId: task?.acquisitionPlanId, downloadTaskId: task?.id });
|
||||
return {
|
||||
release,
|
||||
records,
|
||||
decisions,
|
||||
engine,
|
||||
tasks,
|
||||
events: task
|
||||
? buildTimeline({
|
||||
task,
|
||||
release: release.data,
|
||||
plan,
|
||||
records: matched,
|
||||
records: records.data?.items ?? [],
|
||||
decisions: decisions.data,
|
||||
engineRuns: engine.data,
|
||||
tasks: tasks.data,
|
||||
})
|
||||
: [],
|
||||
loading: release.isLoading || records.isLoading || decisions.isLoading || engine.isLoading,
|
||||
loading: release.isLoading || records.isLoading || decisions.isLoading || tasks.isLoading,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user