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

@@ -1,9 +1,8 @@
import type { AcquisitionSlot, WorkbenchRelease } from "@umt/contracts";
import type { AcquisitionSlot, ManualAcquisitionResult, WorkbenchRelease } from "@umt/contracts";
import { Bot, Download } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { isMissingEndpoint } from "~/api/parse.ts";
import { useSubmitRelease } from "~/api/releases.ts";
import { openAgentDrawer } from "~/components/agent/agent-store.ts";
import { FilterSelect } from "~/components/console/filter-bar.tsx";
@@ -42,21 +41,28 @@ export function DownloadDialog({
}) {
const submit = useSubmitRelease();
const [slotId, setSlotId] = useState("");
const [missing, setMissing] = useState(false);
const [rejected, setRejected] = useState<ManualAcquisitionResult["rejected"] | null>(null);
const [done, setDone] = useState<string | null>(null);
const slotName = (id: string | null) => slots.find((s) => s.id === id)?.name ?? null;
const go = () =>
submit.mutate(
{ sourceReleaseId: release.id, ...(slotId ? { slotId } : {}) },
{
onSuccess: (plan) => {
setDone(plan.id);
toast.success(`已生成下载计划(${plan.status}),进度见“下载与任务”`);
},
onError: (e) => {
if (isMissingEndpoint(e)) setMissing(true);
else toast.error(errorMessage(e));
sourceReleaseId: release.id,
...(slotId ? { slotId } : release.entity ? { entityId: release.entity.id } : {}),
},
{
onSuccess: (r) => {
if (r.submitted && r.plan) {
setRejected(null);
setDone(r.plan.id);
toast.success(`已提交(计划${slotName(r.slotId) ? ` · ${slotName(r.slotId)}` : ""}),进度见“下载与任务”`);
} else {
setRejected(r.rejected);
toast.warning("没有生成下载计划,原因见对话框");
}
},
onError: (e) => toast.error(errorMessage(e)),
},
);
@@ -65,7 +71,7 @@ export function DownloadDialog({
open={open}
onOpenChange={(o) => {
if (!o) {
setMissing(false);
setRejected(null);
setDone(null);
}
onOpenChange(o);
@@ -97,14 +103,21 @@ export function DownloadDialog({
) : (
<p className="text-meta text-muted-foreground">
{release.entity
? "该作品还没有版本目标,提交时按默认规则建临时目标。"
: "资源尚未识别到本地作品,提交后先进入识别。"}
? "该作品还没有版本目标(Slot),需要先在订阅里建目标才能提交。"
: "资源尚未识别到本地作品,请先在“身份与元数据”指定作品。"}
</p>
)}
{missing ? (
{rejected ? (
<div className="rounded-xl border border-warning/40 bg-warning/10 px-3 py-2.5 text-caption text-warning">
后端还没有开放手动提交端点(POST /acquisition-plans)。可以委托 Agent 用 download.plan / download.submit
能力走同一流程提交。
<p className="mb-1">没有生成下载计划:</p>
<ul className="list-disc space-y-0.5 pl-5">
{rejected.map((r, i) => (
<li key={`${r.slotId}-${r.code}-${i}`} className="break-words">
{slotName(r.slotId) ? `${slotName(r.slotId)}:` : ""}
{r.message}
</li>
))}
</ul>
</div>
) : null}
{done ? (
@@ -132,7 +145,7 @@ export function DownloadDialog({
<Bot />
委托 Agent
</Button>
<Button onClick={go} disabled={submit.isPending || !!done}>
<Button onClick={go} disabled={submit.isPending || !!done || !release.entity}>
<Download />
{submit.isPending ? "提交中…" : "提交下载"}
</Button>