import type { AgentRunEvent, AgentRunEventBody, AgentUsageTotals } from "@umt/contracts"; import type { AgentMode, AgentRunView, GoalReport, RunMessage, RunSnapshot } from "~/api/agent.ts"; // Agent 会话 mock(W2-D 端点就绪前开发用;?mock=1 打开)。事件按时间逐步"到达",模拟流式输出。 type Step = { delayMs: number; body: AgentRunEventBody }; type Entry = { run: AgentRunView; steps: Step[]; startedAt: number; abortedAt: number | null; appended: RunMessage[]; }; const store = new Map(); let seeded = false; const uuid = (n: number) => `00000000-0000-4000-8000-${String(n).padStart(12, "0")}`; const iso = (ms: number) => new Date(ms).toISOString(); const USAGE: AgentUsageTotals = { inputTokens: 18_420, outputTokens: 1_236, cacheReadTokens: 12_800, cacheWriteTokens: 2_048, totalTokens: 19_656, costUsd: 0.042, }; function chunks(text: string, size = 6): string[] { const out: string[] = []; for (let i = 0; i < text.length; i += size) out.push(text.slice(i, i + size)); return out; } function script(goal: string, mode: AgentMode): Step[] { const s: Step[] = []; let t = 0; const push = (dt: number, body: AgentRunEventBody) => { t += dt; s.push({ delayMs: t, body }); }; const say = (text: string) => { for (const c of chunks(text)) push(60, { type: "text_delta", delta: c }); }; push(0, { type: "run_started", goal, resumed: false }); say("先确认本地库存与版本目标,再去站点找候选。"); push(400, { type: "tool_call_start", toolCallId: "t1", capabilityId: "inventory.query", inputSummary: '{"query":"沙丘:第二部","year":2024}', recovery: false, }); push(900, { type: "tool_call_end", toolCallId: "t1", capabilityId: "inventory.query", isError: false, outputSummary: "作品 沙丘:第二部 (2024):已有 1080p WEB-DL(已核验);Slot「archival」缺 2160p;无下载中任务", recovery: false, }); push(300, { type: "tool_call_start", toolCallId: "t2", capabilityId: "release.search", inputSummary: '{"site":"audiences","q":"Dune Part Two 2160p","passkey":"[REDACTED]"}', recovery: false, }); push(1200, { type: "tool_call_end", toolCallId: "t2", capabilityId: "release.search", isError: false, outputSummary: "3 条候选:① Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.DV-FraMeSToR 78.4 GB 免费 · 做种 41;② 2160p WEB-DL DDP5.1 Atmos 24.1 GB;③ 2160p x265 10bit 18.9 GB", recovery: false, }); push(300, { type: "compaction", phase: "start", reason: "threshold" }); push(700, { type: "compaction", phase: "end", reason: "threshold", status: "completed" }); say("①符合 archival 的硬条件(REMUX、HDR),免费截止时间在 6 小时后。"); if (mode === "execute") { push(300, { type: "tool_call_start", toolCallId: "t3", capabilityId: "download.plan", inputSummary: '{"sourceReleaseId":"…c3f1","slot":"archival"}', recovery: false, }); push(800, { type: "tool_call_end", toolCallId: "t3", capabilityId: "download.plan", isError: false, outputSummary: "计划已预留:78.4 GB,缓存卷 cache-a 使用率 71% → 预计 86%(硬阈值 90%)", recovery: false, }); push(300, { type: "tool_call_rejected", toolCallId: "t4", toolName: "qb_delete_torrent", capabilityId: "download.cleanup", reason: "未授权:删除下载器任务不在本次委托范围内(需要显式授权)", }); push(300, { type: "tool_call_start", toolCallId: "t5", capabilityId: "download.submit", inputSummary: '{"planId":"…9a2e"}', recovery: false, }); push(1000, { type: "tool_call_end", toolCallId: "t5", capabilityId: "download.submit", isError: false, outputSummary: "已提交到 qb-main,标签 umt-managed,任务 ID …41d0", recovery: false, }); say("已提交 4K REMUX 下载。完成后会自动核验并 Copy 入库,原 1080p 保留。"); } else { say("只要方案:建议提交①,预计占用缓存 78.4 GB,完成后 Copy 入库、保留 1080p。未执行任何写操作。"); } push(200, { type: "run_completed", finalText: "", usage: USAGE }); return s; } function baseRun(id: string, goal: string, mode: AgentMode, createdAt: number, status: AgentRunView["status"]) { return { id, createdAt: iso(createdAt), updatedAt: iso(createdAt), taskId: null, parentRunId: null, trigger: "user", goal, status, provider: "anthropic", model: "claude-sonnet", thinkingLevel: "medium", scope: { mode }, budget: { maxToolCalls: 20, maxTokens: 200_000, maxDurationSec: 600, maxCostUsd: 1 }, usage: {}, evidenceVersion: null, sessionState: null, summary: null, toolCallCount: 0, lastEventSeq: 0, error: null, cancelRequestedAt: null, startedAt: iso(createdAt), finishedAt: null, createdBy: "admin", mode, dedupeKey: null, depth: 0, spawnReason: null, } satisfies AgentRunView; } function add(goal: string, mode: AgentMode, startedAt: number, n: number): Entry { const id = uuid(n); const e: Entry = { run: baseRun(id, goal, mode, startedAt, "running"), steps: script(goal, mode), startedAt, abortedAt: null, appended: [], }; store.set(id, e); return e; } function seed() { if (seeded) return; seeded = true; const now = Date.now(); add("把 沙丘:第二部 的 4K 收进来", "execute", now - 3_600_000, 1); add("对比一下 奥本海默 现有版本和站点上的 4K,只给方案", "plan_only", now - 7_200_000, 2); const failed = add("检查 qb-main 为什么占满磁盘", "execute", now - 86_400_000, 3); failed.steps = [ { delayMs: 0, body: { type: "run_started", goal: failed.run.goal, resumed: false } }, { delayMs: 100, body: { type: "tool_call_start", toolCallId: "x1", capabilityId: "task.inspect", inputSummary: '{"instance":"qb-main"}', recovery: false, }, }, { delayMs: 200, body: { type: "tool_call_end", toolCallId: "x1", capabilityId: "task.inspect", isError: true, outputSummary: "下载器连接超时(qb-main 10.0.0.12:8080)", recovery: false, }, }, { delayMs: 300, body: { type: "run_failed", reason: "budget", budget: "duration", message: "超出单次运行时长预算(600 秒)", usage: { ...USAGE, totalTokens: 4_210, inputTokens: 3_900, outputTokens: 310, costUsd: 0.009 }, }, }, ]; } function revealed(e: Entry, now = Date.now()): AgentRunEvent[] { const cut = e.abortedAt ?? now; const out: AgentRunEvent[] = []; let seq = 0; for (const st of e.steps) { if (e.startedAt + st.delayMs > cut) break; out.push({ ...st.body, runId: e.run.id, seq: seq++, at: iso(e.startedAt + st.delayMs) } as AgentRunEvent); } if (e.abortedAt && !out.some((x) => x.type === "run_completed" || x.type === "run_failed")) { out.push({ type: "run_failed", reason: "aborted", message: "用户中止", usage: USAGE, runId: e.run.id, seq: seq++, at: iso(e.abortedAt), }); } return out; } function view(e: Entry): AgentRunView { const evs = revealed(e); const end = evs.find((x) => x.type === "run_completed" || x.type === "run_failed"); const tools = evs.filter((x) => x.type === "tool_call_start").length; const text = evs .filter((x) => x.type === "text_delta") .map((x) => (x.type === "text_delta" ? x.delta : "")) .join(""); const status: AgentRunView["status"] = !end ? "running" : end.type === "run_completed" ? "succeeded" : end.reason === "budget" ? "budget_exceeded" : end.reason === "aborted" ? "cancelled" : "failed"; const frac = end ? 1 : Math.min(1, evs.length / Math.max(1, e.steps.length)); return { ...e.run, status, toolCallCount: tools, lastEventSeq: evs.length ? evs[evs.length - 1]!.seq : 0, usage: end ? { ...end.usage, durationMs: new Date(end.at).getTime() - e.startedAt } : { inputTokens: Math.round(USAGE.inputTokens * frac), outputTokens: Math.round(USAGE.outputTokens * frac), totalTokens: Math.round(USAGE.totalTokens * frac), costUsd: +(USAGE.costUsd * frac).toFixed(4), durationMs: Date.now() - e.startedAt, }, summary: end ? (end.type === "run_failed" ? end.message : text) : null, error: end?.type === "run_failed" ? { reason: end.reason, message: end.message } : null, finishedAt: end ? end.at : null, updatedAt: evs.length ? evs[evs.length - 1]!.at : e.run.updatedAt, }; } export function MOCK_RUNS(): AgentRunView[] { seed(); return [...store.values()].map(view).sort((a, b) => b.createdAt.localeCompare(a.createdAt)); } export function mockDetail(id: string, op?: { abort?: boolean; append?: string }): RunSnapshot { seed(); const e = store.get(id); if (!e) throw Object.assign(new Error("会话不存在(mock)"), { code: "not_found" }); if (op?.abort && !e.abortedAt) e.abortedAt = Date.now(); if (op?.append) { // 追加消息:模拟 Agent 回一句确认 const at = Date.now() - e.startedAt; e.appended.push({ id: uuid(9000 + e.appended.length), runId: id, createdAt: iso(Date.now()), updatedAt: iso(Date.now()), text: op.append, status: "pending", createdBy: "user:mock", consumedAt: null, }); const last = e.steps.findIndex((s) => s.body.type === "run_completed" || s.body.type === "run_failed"); const reply: Step[] = chunks(`收到补充:「${op.append}」,按此调整。`).map((c, i) => ({ delayMs: at + 200 + i * 60, body: { type: "text_delta", delta: c }, })); if (last >= 0 && e.startedAt + e.steps[last]!.delayMs > Date.now()) e.steps.splice(last, 0, ...reply); e.steps.sort((a, b) => a.delayMs - b.delayMs); } const run = view(e); return { run, messages: e.appended, report: report(e, run) }; } /** 服务端目标汇报的 mock:只按"业务表"口径给数,模型文本单独放 modelText */ function report(e: Entry, run: AgentRunView): GoalReport | null { if (!run.finishedAt) return null; const evs = revealed(e); const submitted = evs.some((x) => x.type === "tool_call_end" && x.capabilityId === "download.submit" && !x.isError); const text = evs .filter((x) => x.type === "text_delta") .map((x) => (x.type === "text_delta" ? x.delta : "")) .join(""); return { state: submitted ? "waiting" : run.status === "succeeded" ? "no_actions" : "failed", summary: submitted ? "1 个下载已进入下载器(qb-main),尚未入库;入库以整理记录 succeeded 为准" : run.status === "succeeded" ? "本次没有产生下载或整理动作" : "运行未完成,没有产生业务动作", submitted: submitted ? 1 : 0, imported: 0, inProgress: submitted ? 1 : 0, failed: 0, plans: submitted ? [{ planId: uuid(7001), status: "submitted", slotId: uuid(7003), sourceReleaseId: uuid(7004) }] : [], downloads: submitted ? [ { downloadTaskId: uuid(7002), planId: uuid(7001), status: "downloading", name: "Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX", }, ] : [], organization: [], modelText: text || null, generatedAt: run.finishedAt, }; } export function mockEventsAfter(id: string, after: number): AgentRunEvent[] { seed(); const e = store.get(id); if (!e) return []; return revealed(e).filter((x) => x.seq > after); } export function mockCreateRun(goal: string, mode: AgentMode): AgentRunView { seed(); const e = add(goal, mode, Date.now(), store.size + 1); return view(e); }