2709 lines
157 KiB
TypeScript
2709 lines
157 KiB
TypeScript
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-19-p2-terminal-sealed-final-response; PJ2026-010403 API契约 draft-2026-06-18-r1.
|
||
// Responsibility: Same-origin fake Workbench API and static server for Playwright browser regression tests.
|
||
|
||
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
||
import { readFile } from "node:fs/promises";
|
||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||
import { extname, join, resolve, sep } from "node:path";
|
||
|
||
import {
|
||
DEFAULT_WORKBENCH_EMPTY_SESSION_TTL_MS,
|
||
classifyWorkbenchEmptySessionGcCandidate
|
||
} from "../../../internal/cloud/workbench-empty-session-gc.ts";
|
||
|
||
type JsonRecord = Record<string, unknown>;
|
||
|
||
interface SessionRecord extends JsonRecord {
|
||
sessionId: string;
|
||
threadId?: string | null;
|
||
status?: string | null;
|
||
lastTraceId?: string | null;
|
||
messages?: JsonRecord[];
|
||
hidden?: boolean;
|
||
}
|
||
|
||
interface ScenarioState {
|
||
scenarioId: string;
|
||
providerProfile: string;
|
||
selectedSessionId: string;
|
||
sessions: SessionRecord[];
|
||
traces: Record<string, JsonRecord>;
|
||
requestLedger: JsonRecord[];
|
||
legacyRequestLedger: JsonRecord[];
|
||
chatRequests: JsonRecord[];
|
||
projectLinks: JsonRecord[];
|
||
projectFiles: JsonRecord[];
|
||
projectTasks: JsonRecord[];
|
||
projectRevision: number;
|
||
listOmitSelected: boolean;
|
||
sessionDelayMs: number;
|
||
sessionDetailDelayMs: number;
|
||
chatDelayMs: number;
|
||
terminalScript: boolean;
|
||
terminalFailureScript: boolean;
|
||
staleTraceId: string | null;
|
||
liveBackfillTraceId: string | null;
|
||
liveBackfillReadyAtMs: number | null;
|
||
}
|
||
|
||
interface FakePerformanceWindow {
|
||
id: string;
|
||
label: string;
|
||
seconds: number | null;
|
||
windowFrom: string | null;
|
||
windowTo: string;
|
||
generatedAt: string;
|
||
}
|
||
|
||
const cwd = process.cwd();
|
||
const args = new Map<string, string>();
|
||
for (let index = 2; index < process.argv.length; index += 1) {
|
||
const key = process.argv[index];
|
||
const value = process.argv[index + 1];
|
||
if (key?.startsWith("--")) {
|
||
args.set(key.slice(2), value && !value.startsWith("--") ? value : "true");
|
||
if (value && !value.startsWith("--")) index += 1;
|
||
}
|
||
}
|
||
|
||
const port = Number(args.get("port") ?? process.env.HWLAB_WORKBENCH_E2E_PORT ?? 4173);
|
||
const distDir = resolve(cwd, args.get("dist") ?? "dist");
|
||
const capturePath = resolve(cwd, "tests/workbench-e2e/fixtures/real-captures/d601-v03-redacted.json");
|
||
const capture = JSON.parse(await readFile(capturePath, "utf8")) as { scenario: { selectedSessionId: string; providerProfile: string; sessions: SessionRecord[]; traces: Record<string, JsonRecord> } };
|
||
const progressOnlyText = "PROGRESS_ONLY_SHOULD_NOT_RENDER_AS_FINAL";
|
||
const finalOnlyText = "FINAL_ONLY_SHOULD_RENDER";
|
||
const markdownFinalText = [
|
||
"已新增并实际运行:",
|
||
"",
|
||
"- Go:`go test ./...` 保留两个空格",
|
||
"- Rust:`cargo test --release`",
|
||
"",
|
||
"```sh",
|
||
"go test ./...",
|
||
"cargo test --release",
|
||
"```",
|
||
"",
|
||
"| benchmark | Go | Rust |",
|
||
"|---|---:|---:|",
|
||
"| fib | 1.2ms | 1.0ms |",
|
||
"",
|
||
"| runtime | status |",
|
||
"|---|---|",
|
||
"| local | pass |"
|
||
].join("\n");
|
||
const terminalAssistantFinalText = [
|
||
"全部六份数据到手!下面是完整的六语言终极性能对比:",
|
||
"",
|
||
"| language | runtime | status |",
|
||
"|---|---|---|",
|
||
"| Lua | LuaJIT | pass |",
|
||
"| Python | CPython | pass |",
|
||
"| Rust | native | pass |"
|
||
].join("\n");
|
||
const canonicalTraceFinalText = "messageProjection 的 sealed final response 才是主消息正文。";
|
||
const traceDetailConflictText = "TRACE_DETAIL_CONFLICT_SHOULD_NOT_REPLACE_MESSAGE";
|
||
const runtimeConfigScript = `<script>\nwindow.HWLAB_CLOUD_WEB_CONFIG = {\n ...(window.HWLAB_CLOUD_WEB_CONFIG ?? {}),\n displayTime: window.HWLAB_CLOUD_WEB_CONFIG?.displayTime ?? { timeZone: "Asia/Shanghai", locale: "zh-CN", label: "北京时间" },\n workbench: {\n ...(window.HWLAB_CLOUD_WEB_CONFIG?.workbench ?? {}),\n realtimeFeatures: window.HWLAB_CLOUD_WEB_CONFIG?.workbench?.realtimeFeatures ?? { liveKafkaSse: false, kafkaRefreshReplay: false, projectionRealtime: true },\n debugCapabilities: window.HWLAB_CLOUD_WEB_CONFIG?.workbench?.debugCapabilities ?? { isolatedKafka: false, rawHwlabEventWindow: { enabled: false, maxEntries: 1, maxRetainedBytes: 1 } },\n traceTimeline: window.HWLAB_CLOUD_WEB_CONFIG?.workbench?.traceTimeline ?? { autoExpandRunning: false, autoCollapseTerminal: false }\n }\n};\n</script>`;
|
||
|
||
let state = createScenarioState("baseline");
|
||
const sseClients = new Set<ServerResponse>();
|
||
const debugSseClients = new Set<ServerResponse>();
|
||
let debugFakeSseQueue = createDebugFakeSseQueue();
|
||
|
||
const server = createServer((request, response) => {
|
||
void handleRequest(request, response).catch((error) => {
|
||
if (response.headersSent) {
|
||
console.error("workbench-e2e-server stream error", error);
|
||
response.destroy();
|
||
return;
|
||
}
|
||
json(response, 500, { ok: false, status: 500, error: { code: "e2e_server_error", message: error instanceof Error ? error.message : String(error) } });
|
||
});
|
||
});
|
||
|
||
server.listen(port, "127.0.0.1", () => {
|
||
console.log(`workbench-e2e-server listening on http://127.0.0.1:${port}`);
|
||
});
|
||
|
||
async function handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
||
const method = request.method ?? "GET";
|
||
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `127.0.0.1:${port}`}`);
|
||
const path = url.pathname;
|
||
recordLedger(method, url);
|
||
refreshBackgroundProjection();
|
||
|
||
if (path === "/__e2e/health") return json(response, 200, { ok: true, scenarioId: state.scenarioId });
|
||
if (path === "/__e2e/state") return json(response, 200, stateSummary());
|
||
if (path === "/__e2e/reset" && method === "POST") {
|
||
const body = await readJson(request);
|
||
state = createScenarioState(String(body.scenarioId ?? "baseline"));
|
||
return json(response, 200, stateSummary());
|
||
}
|
||
if (path === "/__e2e/scroll-follow/append" && method === "POST") {
|
||
const body = await readJson(request);
|
||
return json(response, 200, appendScrollFollowEvents(positiveInteger(body.count, 1)));
|
||
}
|
||
if (path === "/__e2e/gc-empty-sessions" && method === "POST") {
|
||
const body = await readJson(request);
|
||
return json(response, 200, gcEmptySessions(body));
|
||
}
|
||
|
||
if (path === "/auth/session" || path === "/auth/bootstrap") {
|
||
const payload = authPayload();
|
||
return json(response, payload.authenticated === true ? 200 : 401, payload);
|
||
}
|
||
if (path === "/auth/login" && method === "POST") return authLoginResponse(response);
|
||
if (path === "/v1/workbench/events" && method === "GET") return sse(request, response, url);
|
||
if (path === "/v1/workbench/debug/fake-sse" || path.startsWith("/v1/workbench/debug/fake-sse/")) return debugFakeSse(request, response, url, method);
|
||
if (path === "/v1/workbench/launches" && method === "POST") return json(response, 201, await createWorkbenchLaunch(request));
|
||
if (path === "/v1/workbench/sessions" && method === "GET") {
|
||
await delay(state.sessionDelayMs);
|
||
return json(response, 200, workbenchSessionListPayload(url));
|
||
}
|
||
|
||
const turnMatch = path.match(/^\/v1\/workbench\/turns\/([^/]+)$/u);
|
||
if (turnMatch && method === "GET") {
|
||
const traceId = decodeURIComponent(turnMatch[1] ?? "");
|
||
if (state.scenarioId === "completed-replay-detail-404" && traceId === "trc_completed") return json(response, 404, { ok: false, status: 404, error: { code: "turn_replay_unavailable" } });
|
||
if (traceId === state.staleTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
|
||
return json(response, 200, workbenchTurnPayload(traceId));
|
||
}
|
||
|
||
const traceMatch = path.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
|
||
if (traceMatch && method === "GET") {
|
||
const traceId = decodeURIComponent(traceMatch[1] ?? "");
|
||
if (state.scenarioId === "completed-replay-detail-404" && traceId === "trc_completed") return json(response, 404, { ok: false, status: 404, error: { code: "trace_replay_unavailable" } });
|
||
if (state.scenarioId === "trace-detail-read-timeout-diagnostic" && traceId === "trc_trace_detail_read_timeout") return errorDiagnosticResponse(response, 504, `/v1/workbench/traces/${traceId}/events`, "trace_detail_read_timeout", "Trace 更新超时,运行记录暂不可见。", { traceId: "33333333333333333333333333333333", requestId: "req_e2e_trace_detail_read_timeout", layer: "workbench-read-model", category: "trace-detail-read" });
|
||
if (state.scenarioId === "sealed-final-response-diagnostics" && traceId === "trc_sealed_final_diag") return errorDiagnosticResponse(response, 504, `/v1/workbench/traces/${traceId}/events`, "trace_detail_read_timeout", "Trace 更新超时,运行记录暂不可见。", { traceId: "55555555555555555555555555555555", requestId: "req_e2e_sealed_final_trace_timeout", layer: "workbench-read-model", category: "trace-detail-read" });
|
||
if (traceId === state.staleTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
|
||
return json(response, 200, workbenchTracePayload(traceId, url));
|
||
}
|
||
|
||
const sessionMessagesMatch = path.match(/^\/v1\/workbench\/sessions\/([^/]+)\/messages$/u);
|
||
if (sessionMessagesMatch && method === "GET") {
|
||
const sessionId = canonicalSessionId(decodeURIComponent(sessionMessagesMatch[1] ?? ""));
|
||
await delay(sessionDetailDelayMs(sessionId));
|
||
if (state.scenarioId === "created-session-read-failure" && sessionId === "ses_created_read_failure") return json(response, 503, { ok: false, status: 503, error: { code: "session_messages_unavailable" } });
|
||
const session = visibleSessionById(sessionId);
|
||
return session ? json(response, 200, workbenchSessionMessagesPayload(session, url)) : json(response, 404, { ok: false, status: 404, error: { code: "session_not_found" } });
|
||
}
|
||
|
||
const workbenchSessionMatch = path.match(/^\/v1\/workbench\/sessions\/([^/]+)$/u);
|
||
if (workbenchSessionMatch && method === "GET") {
|
||
const sessionId = canonicalSessionId(decodeURIComponent(workbenchSessionMatch[1] ?? ""));
|
||
await delay(sessionDetailDelayMs(sessionId));
|
||
if (state.scenarioId === "created-session-read-failure" && sessionId === "ses_created_read_failure") return json(response, 503, { ok: false, status: 503, error: { code: "session_detail_unavailable" } });
|
||
if (state.scenarioId === "session-switch-detail-404-isolated" && sessionId === "ses_stale_404") return json(response, 404, { ok: false, status: 404, error: { code: "session_not_found" } });
|
||
if (state.scenarioId === "completed-replay-detail-404" && sessionId === "ses_completed") return json(response, 404, { ok: false, status: 404, error: { code: "session_replay_unavailable" } });
|
||
const session = visibleSessionById(sessionId, { includeArchived: true });
|
||
return session ? json(response, 200, { ok: true, status: "found", session }) : json(response, 404, { ok: false, status: 404, error: { code: "session_not_found" } });
|
||
}
|
||
|
||
if (path === "/v1/agent/chat" && method === "POST") {
|
||
const body = await readJson(request);
|
||
state.chatRequests.push(redactRequestBody(body));
|
||
await delay(state.chatDelayMs);
|
||
if (state.scenarioId === "admission-unavailable") {
|
||
return json(response, 503, {
|
||
ok: false,
|
||
accepted: false,
|
||
status: "failed",
|
||
traceId: body.traceId,
|
||
error: { code: "admission_unavailable", layer: "admission", retryable: true, message: "admission_unavailable: durable turn was not persisted" },
|
||
blocker: { code: "admission_unavailable", layer: "admission", retryable: true, summary: "durable turn was not persisted" },
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
return json(response, 200, acceptChatTurn(body));
|
||
}
|
||
|
||
if (path === "/v1/agent/sessions" && method === "POST") {
|
||
const body = await readJson(request);
|
||
const session = createManualSession(body);
|
||
state.selectedSessionId = session.sessionId;
|
||
state.sessions.unshift(session);
|
||
return json(response, state.scenarioId === "create-while-route-loading" ? 201 : 200, { ok: true, status: "ok", session });
|
||
}
|
||
|
||
const agentSessionMatch = path.match(/^\/v1\/agent\/sessions\/([^/]+)$/u);
|
||
if (agentSessionMatch && method === "DELETE") {
|
||
const session = sessionById(decodeURIComponent(agentSessionMatch[1] ?? ""));
|
||
if (!session) return json(response, 404, { ok: false, status: 404, error: { code: "session_not_found" } });
|
||
session.status = "archived";
|
||
session.updatedAt = new Date().toISOString();
|
||
return json(response, 200, { ok: true, status: "archived", session, archivedCount: 1 });
|
||
}
|
||
if (agentSessionMatch && method === "GET") return json(response, 200, { session: sessionPayload(decodeURIComponent(agentSessionMatch[1] ?? "")) });
|
||
|
||
if (path === "/v1/provider-profiles") {
|
||
const items = [
|
||
{ profile: "codex-api", backendProfile: "codex-api", backendKind: "responses", configured: true, failureKind: null, valuesPrinted: false },
|
||
{ profile: "deepseek", backendProfile: "deepseek", backendKind: "responses", configured: true, failureKind: null, valuesPrinted: false }
|
||
];
|
||
return json(response, 200, {
|
||
ok: true,
|
||
contractVersion: "provider-profile-management-v1",
|
||
count: items.length,
|
||
items,
|
||
data: { items, count: items.length, valuesPrinted: false },
|
||
valuesPrinted: false
|
||
});
|
||
}
|
||
if (path === "/v1/dashboard/summary") return json(response, 200, dashboardSummaryPayload());
|
||
if (path === "/health/live" || path === "/health" || path === "/v1") return json(response, 200, { status: "ok", serviceId: "workbench-e2e", codeAgent: { ready: true, status: "ready" } });
|
||
if (path === "/v1/live-builds") return json(response, 200, { status: "ok", builds: [] });
|
||
if (path === "/v1/hwpod/specs") return json(response, 200, hwpodSpecsPayload());
|
||
if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload());
|
||
if (path.startsWith("/v1/project-management")) return projectManagementPayload(request, response, url, method);
|
||
if (path === "/v1/web-performance" && method === "POST") {
|
||
const body = await readJson(request);
|
||
return json(response, 202, { accepted: true, recorded: Array.isArray(body.events) ? body.events.length : 0, valuesRedacted: true });
|
||
}
|
||
if (path === "/v1/web-performance/summary") {
|
||
if (state.scenarioId === "performance-summary-error") return errorDiagnosticResponse(response, 502, "/v1/web-performance/summary", "upstream_unavailable", "暂时无法连接上游。");
|
||
const matrixStatus = performanceSummaryErrorStatus(state.scenarioId);
|
||
if (matrixStatus !== null) return errorDiagnosticResponse(response, matrixStatus, "/v1/web-performance/summary", `e2e_http_${matrixStatus}`, `模拟 ${matrixStatus} 错误。`, { traceId: statusMatrixTraceId(matrixStatus), requestId: `req_e2e_api_status_${matrixStatus}`, category: matrixStatus >= 500 ? "server" : "client" });
|
||
if (state.scenarioId === "performance-network-error") {
|
||
response.destroy(new Error("e2e performance network reset"));
|
||
return;
|
||
}
|
||
if (state.scenarioId === "performance-empty") return json(response, 200, webPerformanceEmptyPayload(url));
|
||
if (state.scenarioId === "performance-stale-window") return json(response, 200, webPerformanceStaleWindowPayload(url));
|
||
return json(response, 200, webPerformanceSummaryPayload(url));
|
||
}
|
||
if (path.startsWith("/v1/") || path.startsWith("/auth/") || path.startsWith("/health")) return json(response, 500, { ok: false, status: 500, error: { code: "unmocked_request", path } });
|
||
|
||
return staticFile(response, path);
|
||
}
|
||
|
||
function dashboardSummaryPayload(): JsonRecord {
|
||
const observedAt = "2026-07-13T03:30:00.000Z";
|
||
return {
|
||
ok: true,
|
||
status: "partial",
|
||
contractVersion: "cloud-dashboard-summary-v1",
|
||
route: "/v1/dashboard/summary",
|
||
observedAt,
|
||
actor: { id: "usr_e2e_admin", displayName: "E2E Admin", role: "admin" },
|
||
sources: [
|
||
{ id: "work", label: "项目与任务", authority: "hwlab-project-management", state: "ready", observedAt, reason: null, href: "/projects" },
|
||
{ id: "hwpod", label: "HWPOD", authority: "cloud-api-hwpod-domain", state: "partial", observedAt, reason: "1 个节点离线", href: "/hwpods/devices" },
|
||
{ id: "billing", label: "额度与 Plan", authority: "hwlab-user-billing", state: "ready", observedAt, reason: null, href: "/usage" }
|
||
],
|
||
work: { state: "ready", authority: "hwlab-project-management", observedAt, projectCount: 1, projects: [{ projectId: "project_hwlab_v03", title: "HWLAB MDTODO", status: "active", href: "/projects/project_hwlab_v03" }], taskCounts: { open: 2, inProgress: 1, blocked: 1 }, continuations: [{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:R1.1", taskId: "R1.1", title: "展示 MDTODO 任务详情", status: "open", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", updatedAt: observedAt, href: "/projects/mdtodo/sources/hwlab-v03-mdtodo/files/file_plan/tasks/R1.1" }] },
|
||
hwpod: { state: "partial", authority: "cloud-api-hwpod-domain", observedAt, nodeCount: 2, onlineNodeCount: 1, offlineNodeCount: 1, deviceCount: 2, availableDeviceCount: 1, busyDeviceCount: 0, capabilityMismatchCount: 0, invalidSpecCount: 0 },
|
||
billing: { state: "ready", authority: "hwlab-user-billing", observedAt, plan: { id: "default", displayName: "Default", status: "active" }, credits: { balance: 100, reserved: 5, available: 95 }, entitlementCount: 2, activeReservationCount: 1 },
|
||
issues: [{ id: "hwpod-offline-nodes", domain: "hwpod", severity: "error", code: "hwpod_node_offline", title: "HWPOD Node 离线", summary: "1 个已声明 Node 当前未连接。", count: 1, action: { label: "查看 Node", href: "/hwpods/nodes?status=offline" } }],
|
||
valuesRedacted: true
|
||
};
|
||
}
|
||
|
||
function hwpodSpecsPayload(): JsonRecord {
|
||
return {
|
||
ok: true,
|
||
status: "completed",
|
||
count: 1,
|
||
availableCount: 0,
|
||
nodeOpsContractVersion: "hwpod-node-ops-v1",
|
||
specs: [
|
||
{
|
||
name: "d601-f103-v2",
|
||
hwpodId: "d601-f103-v2",
|
||
nodeId: "node-d601-f103-v2",
|
||
workspacePath: "F:\\Work\\D601-HWLAB",
|
||
source: { caseId: "d601-f103-v2-compile" },
|
||
availability: {
|
||
ok: false,
|
||
status: "blocked",
|
||
checkedAt: "2026-06-18T04:30:00.000Z",
|
||
results: [
|
||
{ opId: "op_01_workspace_ls", op: "workspace.ls", ok: false, status: "blocked", blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "no outbound WebSocket hwpod-node is connected" } }
|
||
]
|
||
}
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
function hwpodNodeOpsPayload(): JsonRecord {
|
||
return {
|
||
ok: true,
|
||
status: "ready",
|
||
contractVersion: "hwpod-node-ops-v1",
|
||
route: "/v1/hwpod-node-ops",
|
||
supportedOps: ["node.health", "workspace.ls", "debug.build", "debug.flash"],
|
||
websocket: { connectedCount: 0, pendingCount: 1, nodes: [] },
|
||
events: [
|
||
{ ts: "2026-06-18T04:30:00.000Z", status: "blocked", blocker: { code: "hwpod_node_unavailable", summary: "no outbound WebSocket hwpod-node is connected" } }
|
||
]
|
||
};
|
||
}
|
||
|
||
async function projectManagementPayload(request: IncomingMessage, response: ServerResponse, url: URL, method: string): Promise<void> {
|
||
const path = url.pathname;
|
||
const source = projectManagementSource();
|
||
const files = state.projectFiles;
|
||
const links = state.projectLinks;
|
||
const tasks = projectManagementTasks(links);
|
||
|
||
if (path === "/v1/project-management/mdtodo/sources" && method === "POST") {
|
||
const body = await readJson(request);
|
||
return json(response, 201, projectManagementEnvelope({ source: projectManagementSourceFromBody(body, source) }));
|
||
}
|
||
const sourceMutationMatch = path.match(/^\/v1\/project-management\/mdtodo\/sources\/([^/]+)$/u);
|
||
if (sourceMutationMatch && method === "PATCH") {
|
||
const body = await readJson(request);
|
||
const sourceId = decodeURIComponent(sourceMutationMatch[1] ?? String(source.sourceId));
|
||
return json(response, 200, projectManagementEnvelope({ source: projectManagementSourceFromBody(body, { ...source, sourceId }) }));
|
||
}
|
||
const sourceProbeMatch = path.match(/^\/v1\/project-management\/mdtodo\/sources\/([^/]+)\/probe$/u);
|
||
if (sourceProbeMatch && method === "POST") {
|
||
const sourceId = decodeURIComponent(sourceProbeMatch[1] ?? String(source.sourceId));
|
||
return json(response, 200, projectManagementEnvelope({ probe: { ok: true, status: "ok", sourceId, sourceKind: "hwpod-workspace", entries: files.length, valuesRedacted: true } }));
|
||
}
|
||
const sourceReindexMatch = path.match(/^\/v1\/project-management\/mdtodo\/sources\/([^/]+)\/reindex$/u);
|
||
if (sourceReindexMatch && method === "POST") {
|
||
const sourceId = decodeURIComponent(sourceReindexMatch[1] ?? String(source.sourceId));
|
||
return json(response, 200, projectManagementEnvelope({ projection: { sourceId, sourceKind: "hwpod-workspace", rootRef: "docs/MDTODO/", documentCount: files.length, taskCount: tasks.length, projectedAt: "2026-06-25T09:05:00.000Z", valuesRedacted: true } }));
|
||
}
|
||
if (path === "/v1/project-management/workbench-links" && method === "POST") {
|
||
const body = await readJson(request);
|
||
const link = projectManagementLinkFromBody(body);
|
||
state.projectLinks.unshift(link);
|
||
return json(response, 201, projectManagementEnvelope({ link }));
|
||
}
|
||
const taskMutationMatch = path.match(/^\/v1\/project-management\/mdtodo\/tasks\/(.+)$/u);
|
||
if (path === "/v1/project-management/mdtodo/tasks" && method === "POST") return projectManagementCreateTask(response, await readJson(request));
|
||
if (taskMutationMatch && method === "PATCH") return projectManagementUpdateTask(response, decodeURIComponent(taskMutationMatch[1] ?? ""), await readJson(request));
|
||
if (taskMutationMatch && method === "DELETE") return projectManagementDeleteTask(response, decodeURIComponent(taskMutationMatch[1] ?? ""), await readJson(request));
|
||
if (method !== "GET" && method !== "HEAD") return json(response, 405, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "method_not_allowed" }, valuesRedacted: true });
|
||
|
||
if (path === "/v1/project-management" || path === "/v1/project-management/navigation") {
|
||
return json(response, 200, projectManagementEnvelope({
|
||
navigation: {
|
||
id: "project-management",
|
||
label: "Project Management",
|
||
actor: { id: "usr_e2e_admin", role: "admin", authMethod: "web-session" },
|
||
items: [
|
||
{ id: "projects", label: "Projects", apiRoute: "/v1/project-management/projects" },
|
||
{ id: "mdtodo", label: "MDTODO", apiRoute: "/v1/project-management/mdtodo/tasks" }
|
||
],
|
||
capabilities: { mdtodoProjection: true, workbenchLaunch: true, workbenchLinks: true, sourceOfTruth: "markdown-files" }
|
||
},
|
||
projection: { sourceId: source.sourceId, documentCount: files.length, taskCount: tasks.length, projectedAt: "2026-06-25T09:00:00.000Z" }
|
||
}));
|
||
}
|
||
if (path === "/v1/project-management/projects") return json(response, 200, projectManagementEnvelope({ projects: [{ projectId: "project_hwlab_v03", name: "HWLAB MDTODO", sourceIds: [source.sourceId], sourceOfTruth: "markdown-files", status: "active" }] }));
|
||
if (path === "/v1/project-management/mdtodo/sources") return json(response, 200, projectManagementEnvelope({ sources: [source] }));
|
||
if (path === "/v1/project-management/mdtodo/files") return json(response, 200, projectManagementEnvelope({ files: files.filter((file) => !url.searchParams.get("sourceId") || file.sourceId === url.searchParams.get("sourceId")) }));
|
||
if (path === "/v1/project-management/mdtodo/launch-context") return projectManagementLaunchContext(response, url);
|
||
if (path === "/v1/project-management/mdtodo/task-detail") return projectManagementTaskDetail(response, url);
|
||
if (path === "/v1/project-management/mdtodo/report-preview") return projectManagementReportPreview(response, url);
|
||
if (path === "/v1/project-management/mdtodo/tasks") {
|
||
const search = String(url.searchParams.get("search") ?? "").toLowerCase();
|
||
const filtered = tasks.filter((task) => (!url.searchParams.get("taskRef") || task.taskRef === url.searchParams.get("taskRef"))
|
||
&& (!url.searchParams.get("sourceId") || task.sourceId === url.searchParams.get("sourceId"))
|
||
&& (!url.searchParams.get("fileRef") || task.fileRef === url.searchParams.get("fileRef"))
|
||
&& (!url.searchParams.get("projectId") || task.projectId === url.searchParams.get("projectId"))
|
||
&& (!url.searchParams.get("status") || task.status === url.searchParams.get("status"))
|
||
&& (!search || [task.taskId, task.title, task.taskRef].some((value) => String(value ?? "").toLowerCase().includes(search))));
|
||
const offset = positiveInteger(url.searchParams.get("offset"), 0);
|
||
const limit = positiveInteger(url.searchParams.get("limit"), 200);
|
||
const pageTasks = filtered.slice(offset, offset + limit);
|
||
return json(response, 200, projectManagementEnvelope({ tasks: pageTasks, page: { offset, limit, maxLimit: 500, total: filtered.length, hasMore: offset + pageTasks.length < filtered.length } }));
|
||
}
|
||
if (path === "/v1/project-management/workbench-links") return json(response, 200, projectManagementEnvelope({ links: links.filter((link) => !url.searchParams.get("taskRef") || link.taskRef === url.searchParams.get("taskRef")) }));
|
||
return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "not_found" }, valuesRedacted: true });
|
||
}
|
||
|
||
function projectManagementEnvelope(payload: JsonRecord): JsonRecord {
|
||
return { ok: true, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", ...payload, valuesRedacted: true };
|
||
}
|
||
|
||
function projectManagementSource(): JsonRecord {
|
||
return { sourceId: "hwlab-v03-mdtodo", kind: "mdtodo-file-tree", sourceKind: "hwpod-workspace", displayName: "HWLAB v0.3 MDTODO", projectId: "project_hwlab_v03", status: "active", hwpodId: "constart-71freq-c", nodeId: "D518", workspaceRootRef: "F:\\Work\\ConStart", mdtodoRootRef: "docs/MDTODO/", maxFiles: 120, valuesRedacted: true };
|
||
}
|
||
|
||
function projectManagementSourceFromBody(body: JsonRecord, base: JsonRecord): JsonRecord {
|
||
return {
|
||
...base,
|
||
sourceId: String(body.sourceId ?? base.sourceId ?? "hwlab-v03-mdtodo"),
|
||
sourceKind: String(body.sourceKind ?? "hwpod-workspace"),
|
||
displayName: String(body.displayName ?? base.displayName ?? "HWLAB v0.3 MDTODO"),
|
||
projectId: String(body.projectId ?? base.projectId ?? "project_hwlab_v03"),
|
||
hwpodId: String(body.hwpodId ?? base.hwpodId ?? ""),
|
||
nodeId: String(body.nodeId ?? base.nodeId ?? ""),
|
||
workspaceRootRef: String(body.workspaceRootRef ?? base.workspaceRootRef ?? ""),
|
||
mdtodoRootRef: String(body.mdtodoRootRef ?? base.mdtodoRootRef ?? "docs/MDTODO/"),
|
||
maxFiles: positiveInteger(body.maxFiles, Number(base.maxFiles ?? 120)),
|
||
status: "active",
|
||
valuesRedacted: true
|
||
};
|
||
}
|
||
|
||
function projectManagementFiles(): JsonRecord[] {
|
||
return [
|
||
{ sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", projectId: "project_hwlab_v03", relativePath: "docs/MDTODO/hwlab-v03-mdtodo-web-sample.md", title: "项目管理任务", taskCount: 3, parseError: null, revision: 3, fingerprint: "sha256:e2e-plan", updatedAt: "2026-06-25T09:00:00.000Z" },
|
||
{ sourceId: "hwlab-v03-mdtodo", fileRef: "file_rollout", projectId: "project_hwlab_v03", relativePath: "docs/MDTODO/D601-v03-rollout.md", title: "D601 发布任务", taskCount: 1, parseError: null, revision: 1, fingerprint: "sha256:e2e-rollout", updatedAt: "2026-06-25T09:00:00.000Z" },
|
||
{ sourceId: "hwlab-v03-mdtodo", fileRef: "file_feedback", projectId: "project_hwlab_v03", relativePath: "docs/MDTODO/20260609_频率判断_用户反馈.md", title: "这是用户的反馈,你分析原因:", taskCount: 1, parseError: null, revision: 1, fingerprint: "sha256:e2e-feedback", updatedAt: "2026-06-26T13:00:00.000Z" }
|
||
];
|
||
}
|
||
|
||
function projectManagementTasks(links: JsonRecord[] = []): JsonRecord[] {
|
||
const linkCount = (taskRef: string) => links.filter((link) => link.taskRef === taskRef).length;
|
||
return state.projectTasks.map((task) => ({ ...task, linkCount: linkCount(String(task.taskRef ?? "")) }));
|
||
}
|
||
|
||
function projectManagementSeedTasks(links: JsonRecord[] = []): JsonRecord[] {
|
||
const linkCount = (taskRef: string) => links.filter((link) => link.taskRef === taskRef).length;
|
||
const r1 = "mdtodo:hwlab-v03-mdtodo:file_plan:R1";
|
||
const r11 = "mdtodo:hwlab-v03-mdtodo:file_plan:R1.1";
|
||
const r111 = "mdtodo:hwlab-v03-mdtodo:file_plan:R1.1.1";
|
||
const r2 = "mdtodo:hwlab-v03-mdtodo:file_rollout:R2";
|
||
const feedbackR1 = "mdtodo:hwlab-v03-mdtodo:file_feedback:R1";
|
||
return [
|
||
{ taskRef: r1, projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "R1", title: "实现项目根导航", status: "done", parentTaskRef: null, depth: 0, lineNumber: 4, ordinal: 1, linkCount: linkCount(r1), sourceFingerprint: "sha256:e2e-plan", bodyPreview: "Root notes", body: "Root notes\n\n参考报告 [R1.1](./20260110_LOG/R1.1_log_implementation.md)", updatedAt: "2026-06-25T09:00:00.000Z" },
|
||
{ taskRef: r11, projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "R1.1", title: "展示 MDTODO 任务详情", status: "open", parentTaskRef: r1, depth: 1, lineNumber: 8, ordinal: 2, linkCount: linkCount(r11), sourceFingerprint: "sha256:e2e-plan", bodyPreview: "Detail body", body: "Detail body", updatedAt: "2026-06-25T09:00:00.000Z" },
|
||
{ taskRef: r111, projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "R1.1.1", title: "Source 顶部控制", status: "in_progress", parentTaskRef: r11, depth: 2, lineNumber: 12, ordinal: 3, linkCount: linkCount(r111), sourceFingerprint: "sha256:e2e-plan", bodyPreview: "Toolbar body", body: "Toolbar body", updatedAt: "2026-06-25T09:00:00.000Z" },
|
||
{ taskRef: r2, projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_rollout", taskId: "R2", title: "D601 v03 页面验收", status: "blocked", parentTaskRef: null, depth: 0, lineNumber: 3, ordinal: 4, linkCount: linkCount(r2), sourceFingerprint: "sha256:e2e-rollout", bodyPreview: "Rollout body", body: "Rollout body", updatedAt: "2026-06-25T09:00:00.000Z" },
|
||
{ taskRef: feedbackR1, projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_feedback", taskId: "R1", title: "这是用户的反馈,你分析原因:", status: "open", parentTaskRef: null, depth: 0, lineNumber: 4, ordinal: 5, linkCount: linkCount(feedbackR1), sourceFingerprint: "sha256:e2e-feedback", bodyPreview: "我 reset sys_trip 后,返回 sys_trip=false,但万用表测量端子还是通的。", body: "我 reset sys_trip 后,返回的代码中 sys_trip 已经 false 了,但万用表测量端子还是通的。是因为 int_trip:true 吗?\n\n完成任务后将详细报告写入 [R1](details/20260609_频率判断_用户反馈/20260609_161845_Task_Report.md)。", updatedAt: "2026-06-26T13:00:00.000Z" }
|
||
];
|
||
}
|
||
|
||
function projectManagementTaskDetail(response: ServerResponse, url: URL): void {
|
||
const taskRef = url.searchParams.get("taskRef");
|
||
const task = state.projectTasks.find((item) => item.taskRef === taskRef);
|
||
if (!task) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "task_not_found" }, valuesRedacted: true });
|
||
const body = String(task.body ?? task.bodyPreview ?? "");
|
||
const links = projectManagementTaskLinks(task, body);
|
||
return json(response, 200, projectManagementEnvelope({
|
||
task: { ...task, bodyPreview: body.slice(0, 240), linkCount: links.length },
|
||
detail: {
|
||
taskRef: task.taskRef,
|
||
taskId: task.taskId,
|
||
rxxId: task.taskId,
|
||
projectId: task.projectId,
|
||
sourceId: task.sourceId,
|
||
fileRef: task.fileRef,
|
||
relativePath: projectFileForTask(task).relativePath,
|
||
title: task.title,
|
||
status: task.status,
|
||
body,
|
||
bodyPreview: body.slice(0, 240),
|
||
links,
|
||
valuesRedacted: false
|
||
},
|
||
file: projectFileForTask(task)
|
||
}));
|
||
}
|
||
|
||
function projectManagementReportPreview(response: ServerResponse, url: URL): void {
|
||
const taskRef = url.searchParams.get("taskRef");
|
||
const linkId = url.searchParams.get("linkId");
|
||
const task = state.projectTasks.find((item) => item.taskRef === taskRef);
|
||
if (!task) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "task_not_found" }, valuesRedacted: true });
|
||
const link = projectManagementTaskLinks(task, String(task.body ?? task.bodyPreview ?? "")).find((item) => item.linkId === linkId);
|
||
if (!link) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "report_link_not_found" }, valuesRedacted: true });
|
||
return json(response, 200, projectManagementEnvelope({
|
||
task,
|
||
link,
|
||
report: {
|
||
sourceId: task.sourceId,
|
||
relativePath: link.relativePath,
|
||
content: reportContentForLink(link),
|
||
fingerprint: "sha256:e2e-report",
|
||
revision: "sha256:e2e-report",
|
||
byteCount: 56,
|
||
render: "markdown",
|
||
linkId: link.linkId,
|
||
label: link.label,
|
||
href: link.href,
|
||
valuesRedacted: false
|
||
},
|
||
file: projectFileForTask(task)
|
||
}));
|
||
}
|
||
|
||
function projectManagementLaunchContext(response: ServerResponse, url: URL): void {
|
||
const taskRef = url.searchParams.get("taskRef");
|
||
const payload = projectManagementLaunchContextForTask(taskRef);
|
||
if (!payload) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "task_not_found" }, valuesRedacted: true });
|
||
return json(response, 200, projectManagementEnvelope(payload));
|
||
}
|
||
|
||
function projectManagementLaunchContextForTask(taskRef: string | null): JsonRecord | null {
|
||
const task = state.projectTasks.find((item) => item.taskRef === taskRef);
|
||
if (!task) return null;
|
||
const source = projectManagementSource();
|
||
const file = projectFileForTask(task);
|
||
const workspaceRootRef = String(source.workspaceRootRef ?? "");
|
||
const executionContext = {
|
||
sourceId: task.sourceId,
|
||
sourceKind: source.sourceKind,
|
||
projectId: task.projectId,
|
||
taskRef: task.taskRef,
|
||
fileRef: task.fileRef,
|
||
relativePath: file.relativePath,
|
||
taskId: task.taskId,
|
||
hwpodId: source.hwpodId,
|
||
nodeId: source.nodeId,
|
||
workspaceRootRef,
|
||
workspaceRootHash: "e2e-workspace-root-hash",
|
||
workspaceRootLabel: "ConStart",
|
||
mdtodoRootRef: source.mdtodoRootRef,
|
||
hwpodWorkspaceArgs: `--hwpod-id ${source.hwpodId} --workspace-path '${workspaceRootRef}'`,
|
||
contextFingerprint: `ctx_e2e_${String(task.taskId ?? "task").replace(/[^A-Za-z0-9]/gu, "_")}`,
|
||
capabilities: source.capabilities ?? {},
|
||
valuesRedacted: true
|
||
};
|
||
const launchContext = {
|
||
source: "project-management",
|
||
projectId: task.projectId,
|
||
taskRef: task.taskRef,
|
||
sourceId: task.sourceId,
|
||
sourceKind: source.sourceKind,
|
||
fileRef: task.fileRef,
|
||
relativePath: file.relativePath,
|
||
taskId: task.taskId,
|
||
title: task.title,
|
||
status: task.status,
|
||
hwpodId: executionContext.hwpodId,
|
||
nodeId: executionContext.nodeId,
|
||
mdtodoRootRef: executionContext.mdtodoRootRef,
|
||
hwpodWorkspaceArgs: executionContext.hwpodWorkspaceArgs,
|
||
workspaceRootHash: executionContext.workspaceRootHash,
|
||
workspaceRootLabel: executionContext.workspaceRootLabel,
|
||
contextFingerprint: executionContext.contextFingerprint,
|
||
executionContext,
|
||
route: "/projects/mdtodo",
|
||
valuesRedacted: true
|
||
};
|
||
return { task, source, executionContext, launchContext };
|
||
}
|
||
|
||
function projectManagementTaskLinks(task: JsonRecord, body: string): JsonRecord[] {
|
||
if (String(task.taskId) !== "R1") return [];
|
||
if (body.includes("20260609_161845_Task_Report.md")) {
|
||
return [{
|
||
linkId: "lnk_feedback_r1_report",
|
||
label: "R1",
|
||
href: "details/20260609_频率判断_用户反馈/20260609_161845_Task_Report.md",
|
||
kind: "markdown-report",
|
||
relativePath: "details/20260609_频率判断_用户反馈/20260609_161845_Task_Report.md",
|
||
ordinal: 1,
|
||
valuesRedacted: true
|
||
}];
|
||
}
|
||
if (!body.includes("R1.1_log_implementation.md")) return [];
|
||
return [{
|
||
linkId: "lnk_r11_report",
|
||
label: "R1.1",
|
||
href: "./20260110_LOG/R1.1_log_implementation.md",
|
||
kind: "markdown-report",
|
||
relativePath: "20260110_LOG/R1.1_log_implementation.md",
|
||
ordinal: 1,
|
||
valuesRedacted: true
|
||
}];
|
||
}
|
||
|
||
function reportContentForLink(link: JsonRecord): string {
|
||
if (String(link.relativePath ?? "").includes("20260609_161845_Task_Report.md")) {
|
||
return [
|
||
"# R1 用户反馈分析报告",
|
||
"",
|
||
"停机继电器仍然动作时,需要同时确认外部 sys_trip 和内部 int_trip 的输出投票。",
|
||
"",
|
||
"```json",
|
||
"{",
|
||
" \"result\": {",
|
||
" \"sys_reset\": true,",
|
||
" \"int_trip\": true,",
|
||
" \"sys_trip\": false,",
|
||
" \"sys_alarm\": false",
|
||
" },",
|
||
" \"id\": 222",
|
||
"}",
|
||
"```",
|
||
"",
|
||
"如果 `int_trip` 仍为 true,理论上停机继电器仍可能保持动作。"
|
||
].join("\n");
|
||
}
|
||
return "# R1.1 implementation report\n\nReport body from fake server.";
|
||
}
|
||
|
||
function projectManagementUpdateTask(response: ServerResponse, taskRef: string, body: JsonRecord): void {
|
||
const task = state.projectTasks.find((item) => item.taskRef === taskRef);
|
||
if (!task) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "task_not_found" }, valuesRedacted: true });
|
||
const file = projectFileForTask(task);
|
||
if (!projectManagementExpectedFingerprintOk(response, file, body)) return;
|
||
if (typeof body.title === "string" && body.title.trim()) task.title = body.title.trim();
|
||
if (typeof body.status === "string" && body.status.trim()) task.status = body.status.trim();
|
||
if (typeof body.body === "string") {
|
||
task.body = body.body;
|
||
task.bodyPreview = body.body.slice(0, 240);
|
||
}
|
||
task.updatedAt = new Date().toISOString();
|
||
touchProjectFile(String(task.fileRef));
|
||
return json(response, 200, projectManagementEnvelope({ task, file: projectFileForTask(task), changedTaskId: task.taskId, affectedTaskIds: [task.taskId] }));
|
||
}
|
||
|
||
function projectManagementCreateTask(response: ServerResponse, body: JsonRecord): void {
|
||
const parent = typeof body.parentTaskRef === "string" ? state.projectTasks.find((item) => item.taskRef === body.parentTaskRef) : null;
|
||
const after = typeof body.afterTaskRef === "string" ? state.projectTasks.find((item) => item.taskRef === body.afterTaskRef) : null;
|
||
const sourceId = String(parent?.sourceId ?? after?.sourceId ?? body.sourceId ?? "hwlab-v03-mdtodo");
|
||
const fileRef = String(parent?.fileRef ?? after?.fileRef ?? body.fileRef ?? "file_plan");
|
||
const file = state.projectFiles.find((item) => item.sourceId === sourceId && item.fileRef === fileRef);
|
||
if (!file) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "file_not_found" }, valuesRedacted: true });
|
||
if (!projectManagementExpectedFingerprintOk(response, file, body)) return;
|
||
const title = String(body.title ?? "").trim();
|
||
if (!title) return json(response, 400, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "title_required" }, valuesRedacted: true });
|
||
const taskId = parent ? nextProjectChildTaskId(fileRef, String(parent.taskId)) : after ? nextProjectSiblingTaskId(fileRef, String(after.taskId)) : nextProjectRootTaskId(fileRef);
|
||
const taskRef = `mdtodo:${sourceId}:${fileRef}:${taskId}`;
|
||
const parentTaskRef = parent?.taskRef ?? after?.parentTaskRef ?? null;
|
||
const task = { taskRef, projectId: "project_hwlab_v03", sourceId, fileRef, taskId, title, status: String(body.status ?? "open"), parentTaskRef, depth: taskId.split(".").length - 1, lineNumber: 20 + state.projectTasks.length, ordinal: state.projectTasks.length + 1, linkCount: 0, sourceFingerprint: file.fingerprint, body: typeof body.body === "string" ? body.body : "", bodyPreview: typeof body.body === "string" ? body.body.slice(0, 240) : "", updatedAt: new Date().toISOString() };
|
||
state.projectTasks.push(task);
|
||
touchProjectFile(fileRef);
|
||
return json(response, 201, projectManagementEnvelope({ task, file: projectFileForTask(task), changedTaskId: taskId, affectedTaskIds: [taskId] }));
|
||
}
|
||
|
||
function projectManagementDeleteTask(response: ServerResponse, taskRef: string, body: JsonRecord): void {
|
||
const task = state.projectTasks.find((item) => item.taskRef === taskRef);
|
||
if (!task) return json(response, 404, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "task_not_found" }, valuesRedacted: true });
|
||
const file = projectFileForTask(task);
|
||
if (!projectManagementExpectedFingerprintOk(response, file, body)) return;
|
||
const prefix = `${task.taskId}.`;
|
||
const affected = state.projectTasks.filter((item) => item.taskRef === taskRef || String(item.taskId).startsWith(prefix)).map((item) => String(item.taskId));
|
||
state.projectTasks = state.projectTasks.filter((item) => !(item.taskRef === taskRef || String(item.taskId).startsWith(prefix)));
|
||
touchProjectFile(String(task.fileRef));
|
||
return json(response, 200, projectManagementEnvelope({ task: null, file: projectFileForTask(task), changedTaskId: task.taskId, affectedTaskIds: affected }));
|
||
}
|
||
|
||
function projectManagementExpectedFingerprintOk(response: ServerResponse, file: JsonRecord, body: JsonRecord): boolean {
|
||
const expected = String(body.expectedFingerprint ?? body.revision ?? "");
|
||
if (!expected) {
|
||
json(response, 400, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "expected_fingerprint_required" }, valuesRedacted: true });
|
||
return false;
|
||
}
|
||
if (expected !== file.fingerprint) {
|
||
json(response, 409, { ok: false, contractVersion: "project-management-v1", serviceId: "hwlab-project-management", error: { code: "revision_conflict" }, valuesRedacted: true });
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function projectFileForTask(task: JsonRecord): JsonRecord {
|
||
const file = state.projectFiles.find((item) => item.sourceId === task.sourceId && item.fileRef === task.fileRef) ?? state.projectFiles[0];
|
||
if (!file) throw new Error("project management fake-server file fixture is missing");
|
||
return file;
|
||
}
|
||
|
||
function touchProjectFile(fileRef: string): void {
|
||
state.projectRevision += 1;
|
||
const file = state.projectFiles.find((item) => item.fileRef === fileRef);
|
||
if (!file) return;
|
||
file.revision = Number(file.revision ?? 1) + 1;
|
||
file.fingerprint = `sha256:e2e-${fileRef}-${state.projectRevision}`;
|
||
file.taskCount = state.projectTasks.filter((task) => task.fileRef === fileRef).length;
|
||
file.updatedAt = new Date().toISOString();
|
||
for (const task of state.projectTasks.filter((item) => item.fileRef === fileRef)) task.sourceFingerprint = file.fingerprint;
|
||
}
|
||
|
||
function nextProjectRootTaskId(fileRef: string): string {
|
||
const next = Math.max(0, ...state.projectTasks.filter((task) => task.fileRef === fileRef && !String(task.taskId).includes(".")).map((task) => Number(String(task.taskId).replace(/^R/u, "")) || 0)) + 1;
|
||
return `R${next}`;
|
||
}
|
||
|
||
function nextProjectChildTaskId(fileRef: string, parentTaskId: string): string {
|
||
const prefix = `${parentTaskId}.`;
|
||
const next = Math.max(0, ...state.projectTasks.filter((task) => task.fileRef === fileRef && String(task.taskId).startsWith(prefix)).map((task) => Number(String(task.taskId).slice(prefix.length).split(".")[0]) || 0)) + 1;
|
||
return `${parentTaskId}.${next}`;
|
||
}
|
||
|
||
function nextProjectSiblingTaskId(fileRef: string, afterTaskId: string): string {
|
||
const parent = afterTaskId.includes(".") ? afterTaskId.split(".").slice(0, -1).join(".") : "";
|
||
return parent ? nextProjectChildTaskId(fileRef, parent) : nextProjectRootTaskId(fileRef);
|
||
}
|
||
|
||
function projectManagementSeedLinks(): JsonRecord[] {
|
||
return [{ linkId: "lnk_project_task_root", projectId: "project_hwlab_v03", taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:R1", sessionId: "ses_project_seed", traceId: "trc_project_seed", role: "reference", updatedAt: "2026-06-25T09:00:00.000Z" }];
|
||
}
|
||
|
||
function projectManagementLinkFromBody(body: JsonRecord): JsonRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
linkId: String(body.linkId ?? `lnk_project_launch_${Date.now().toString(36)}`),
|
||
projectId: String(body.projectId ?? "project_hwlab_v03"),
|
||
taskRef: typeof body.taskRef === "string" ? body.taskRef : null,
|
||
sessionId: typeof body.sessionId === "string" ? body.sessionId : null,
|
||
traceId: typeof body.traceId === "string" ? body.traceId : null,
|
||
role: String(body.role ?? "launch"),
|
||
updatedAt: now,
|
||
link: body.link && typeof body.link === "object" ? body.link : { valuesRedacted: true }
|
||
};
|
||
}
|
||
|
||
async function createWorkbenchLaunch(request: IncomingMessage): Promise<JsonRecord> {
|
||
const body = await readJson(request);
|
||
const now = new Date().toISOString();
|
||
const sessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId : `ses_project_launch_${Date.now().toString(36)}`;
|
||
const conversationId = typeof body.conversationId === "string" && body.conversationId.trim() ? body.conversationId : `cnv_project_launch_${Date.now().toString(36)}`;
|
||
const authoritative = projectManagementLaunchContextForTask(typeof body.taskRef === "string" ? body.taskRef : null)?.launchContext as JsonRecord | undefined;
|
||
const launchContext: JsonRecord = authoritative ?? (body.launchContext && typeof body.launchContext === "object" ? body.launchContext as JsonRecord : {} as JsonRecord);
|
||
const session: SessionRecord = {
|
||
sessionId,
|
||
conversationId,
|
||
status: "idle",
|
||
projectId: String(body.projectId ?? launchContext.projectId ?? "project_hwlab_v03"),
|
||
launchContext,
|
||
metadata: { launchContext },
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 0,
|
||
firstUserMessagePreview: null,
|
||
messages: []
|
||
};
|
||
state.sessions.unshift(session);
|
||
const link = projectManagementLinkFromBody({
|
||
linkId: `lnk_project_launch_${sessionId.replace(/^ses_/u, "")}`,
|
||
projectId: session.projectId,
|
||
taskRef: body.taskRef,
|
||
sessionId,
|
||
traceId: null,
|
||
role: "launch",
|
||
link: { launchContext, workbenchUrl: `/workbench/sessions/${sessionId}`, valuesRedacted: true }
|
||
});
|
||
state.projectLinks.unshift(link);
|
||
return {
|
||
ok: true,
|
||
status: "created",
|
||
contractVersion: "workbench-launch-v1",
|
||
sessionId,
|
||
conversationId,
|
||
projectId: session.projectId,
|
||
taskRef: body.taskRef,
|
||
workbenchUrl: `/workbench/sessions/${sessionId}`,
|
||
linkId: link.linkId,
|
||
link,
|
||
launchContext,
|
||
valuesRedacted: true
|
||
};
|
||
}
|
||
|
||
function webPerformanceSummaryPayload(url: URL): JsonRecord {
|
||
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
|
||
const rows = [
|
||
performanceRow({ kind: "workbench-journey", metric: "session-switch-full-load", route: "/workbench/sessions/:sessionId", count: 21, p50: 0.18, p75: 0.31, p95: 1.12, tone: "warn", backend: "agentrun", transport: "sse", targetState: "completed", sourceFamily: "workbench" }, sampleWindow),
|
||
performanceRow({ kind: "workbench-event-phase", metric: "trace-event-projected", route: "/workbench", count: 28, p50: 0.08, p75: 0.14, p95: 0.42, tone: "ok", eventType: "tool_result", phase: "render", source: "rum", sourceFamily: "workbench" }, sampleWindow),
|
||
performanceRow({ kind: "workbench-backend-event", metric: "backend-event-visible", route: "/v1/workbench/events", count: 15, p50: 0.2, p75: 0.34, p95: 1.38, tone: "warn", eventType: "agentrun.stdout", backend: "agentrun", cache: "miss", sourceFamily: "backend" }, sampleWindow),
|
||
performanceRow({ kind: "web-vital", metric: "LCP", route: "/performance", count: 9, p50: 1.2, p75: 1.8, p95: 2.9, tone: "ok", sourceFamily: "web" }, sampleWindow),
|
||
performanceRow({ kind: "api", metric: "api_request", route: "/v1/web-performance/summary", method: "GET", statusClass: "2xx", outcome: "ok", count: 17, p50: 0.09, p75: 0.15, p95: 0.33, tone: "ok", sourceFamily: "web", backendEvidence: fakeBackendEvidence({ route: "/v1/web-performance/summary", count: 16, p95: 0.18, diagnostic: "backend_matched", diagnosticLabel: "后端样本已匹配" }) }, sampleWindow),
|
||
performanceRow({ kind: "api", metric: "api_request", route: "/v1/workbench/sessions", method: "GET", statusClass: "2xx", outcome: "ok", count: 3, p50: 2.5, p75: 5, p95: 10, tone: "warn", problem: "slow-api_request", sampleState: "low-sample", freshness: "low-sample", aggregationKind: "histogram", approximation: "bucketed", sourceFamily: "web", backendEvidence: fakeBackendEvidence({ route: "/v1/workbench/sessions", count: 4, p95: 0.24, diagnostic: "rum_slower_than_backend", diagnosticLabel: "RUM 慢于后端" }) }, sampleWindow),
|
||
performanceRow({ kind: "long-task", metric: "longtask", route: "/workbench", count: 3, p50: 0.08, p75: 0.11, p95: 0.2, tone: "warn", problem: "main-thread busy", sourceFamily: "web" }, sampleWindow)
|
||
];
|
||
const sampleCount = performanceSampleCount(rows);
|
||
const collectionHealth = performanceCollectionHealthPayload(rows, sampleWindow);
|
||
return {
|
||
ok: true,
|
||
status: "ready",
|
||
source: "fake-server",
|
||
observedAt: sampleWindow.generatedAt,
|
||
namespace: "hwlab-v03",
|
||
gitopsTarget: "D601/v03",
|
||
sampleWindow: performanceSampleWindowPayload(sampleWindow),
|
||
summary: performanceSummaryContract(sampleWindow, rows, { status: "ready", sampleCount, sampleSeries: 12, problemCount: rows.filter((row) => row.tone === "warn").length }),
|
||
backendEvidence: fakeBackendEvidenceSummary(rows, sampleWindow),
|
||
collectionHealth,
|
||
dashboard: webPerformanceDashboardPayload(rows, sampleWindow, collectionHealth),
|
||
rows,
|
||
problems: rows.filter((row) => row.tone === "warn"),
|
||
webVitals: rows.filter((row) => row.kind === "web-vital"),
|
||
apiRoutes: rows.filter((row) => row.kind === "api"),
|
||
longTasks: rows.filter((row) => row.kind === "long-task"),
|
||
workbenchJourneys: rows.filter((row) => row.kind === "workbench-journey"),
|
||
workbenchEventPhases: rows.filter((row) => row.kind === "workbench-event-phase"),
|
||
workbenchBackendEvents: rows.filter((row) => row.kind === "workbench-backend-event")
|
||
};
|
||
}
|
||
|
||
function fakeBackendEvidence(input: { route: string; count: number; p95: number; diagnostic: string; diagnosticLabel: string }): JsonRecord {
|
||
return {
|
||
status: "matched",
|
||
statusLabel: "已匹配后端",
|
||
diagnostic: input.diagnostic,
|
||
diagnosticLabel: input.diagnosticLabel,
|
||
detail: `fake-server backend evidence for ${input.route}`,
|
||
route: input.route,
|
||
method: "GET",
|
||
count: input.count,
|
||
p50: Math.max(0.02, input.p95 / 3),
|
||
p75: Math.max(0.04, input.p95 / 2),
|
||
p95: input.p95,
|
||
responseBytesP95: 4096,
|
||
lastObservedAt: "2026-06-18T04:29:59.000Z",
|
||
phases: [{ phase: "workbench_session_page_query", outcome: "ok", count: input.count, p95: Math.min(input.p95, 0.2) }]
|
||
};
|
||
}
|
||
|
||
function fakeBackendEvidenceSummary(rows: JsonRecord[], sampleWindow: FakePerformanceWindow): JsonRecord {
|
||
const evidenceRows = rows
|
||
.map((row) => row.backendEvidence)
|
||
.filter((evidence): evidence is JsonRecord => Boolean(evidence && typeof evidence === "object" && !Array.isArray(evidence)))
|
||
.map((evidence) => ({ route: evidence.route, method: evidence.method, statusClass: "2xx", outcome: "ok", count: evidence.count, average: evidence.p50, p50: evidence.p50, p75: evidence.p75, p95: evidence.p95, firstObservedAt: sampleWindow.windowFrom, lastObservedAt: evidence.lastObservedAt, responseBytes: { count: evidence.count, average: 2048, p50: 2048, p75: 3072, p95: evidence.responseBytesP95 }, phases: evidence.phases, valuesRedacted: true }));
|
||
return { schemaVersion: "hwlab-backend-performance-evidence-v1", source: "fake-server-backend-performance-store", observedAt: sampleWindow.generatedAt, sampleCount: evidenceRows.reduce((sum, row) => sum + Number(row.count ?? 0), 0), retainedSampleCount: evidenceRows.length, routeCount: evidenceRows.length, rows: evidenceRows, available: true, valuesRedacted: true };
|
||
}
|
||
|
||
function webPerformanceEmptyPayload(url: URL): JsonRecord {
|
||
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
|
||
const rows: JsonRecord[] = [];
|
||
const collectionHealth = performanceCollectionHealthPayload(rows, sampleWindow);
|
||
return {
|
||
ok: true,
|
||
status: "waiting",
|
||
source: "fake-server",
|
||
observedAt: sampleWindow.generatedAt,
|
||
namespace: "hwlab-v03",
|
||
gitopsTarget: "D601/v03",
|
||
sampleWindow: performanceSampleWindowPayload(sampleWindow),
|
||
summary: performanceSummaryContract(sampleWindow, rows, { status: "waiting", sampleCount: 0, sampleSeries: 0, problemCount: 0 }),
|
||
collectionHealth,
|
||
dashboard: webPerformanceDashboardPayload(rows, sampleWindow, collectionHealth),
|
||
rows,
|
||
problems: [],
|
||
webVitals: [],
|
||
apiRoutes: [],
|
||
longTasks: [],
|
||
workbenchJourneys: [],
|
||
workbenchEventPhases: [],
|
||
workbenchBackendEvents: []
|
||
};
|
||
}
|
||
|
||
function webPerformanceStaleWindowPayload(url: URL): JsonRecord {
|
||
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
|
||
const rows: JsonRecord[] = [];
|
||
const collectionHealth = {
|
||
schemaVersion: "hwlab-web-performance-collection-health-v1",
|
||
status: "waiting",
|
||
statusLabel: "等待数据",
|
||
reason: "no_samples_in_window",
|
||
reasonLabel: "当前窗口暂无样本",
|
||
sampleCount: 0,
|
||
retainedSampleCount: 37,
|
||
sampleSeries: 0,
|
||
lowSampleThreshold: 5,
|
||
firstSampleAt: null,
|
||
lastSampleAt: null,
|
||
lastRetainedSampleAt: "2026-06-18T03:20:00.000Z",
|
||
secondsSinceLastSample: 4200,
|
||
windowCoverageSeconds: null,
|
||
sourceFamilyCounts: [{ sourceFamily: "web", rowCount: 0, sampleCount: 37 }],
|
||
unknownAttribution: { workbenchRows: 0, rowsWithUnknown: 0, samplesWithUnknown: 0, backendUnknownRows: 0, transportUnknownRows: 0 },
|
||
diagnostics: [{ code: "no_samples_in_window", label: "当前窗口无样本", tone: "warn", detail: "保留样本存在,但当前窗口没有性能样本。" }]
|
||
};
|
||
return {
|
||
ok: true,
|
||
status: "waiting",
|
||
source: "fake-server",
|
||
observedAt: sampleWindow.generatedAt,
|
||
namespace: "hwlab-v03",
|
||
gitopsTarget: "D601/v03",
|
||
sampleWindow: performanceSampleWindowPayload(sampleWindow),
|
||
summary: performanceSummaryContract(sampleWindow, rows, { status: "waiting", sampleCount: 0, retainedSampleCount: 37, sampleSeries: 0, problemCount: 0 }),
|
||
backendEvidence: { schemaVersion: "hwlab-backend-performance-evidence-v1", source: "fake-server-backend-performance-store", observedAt: sampleWindow.generatedAt, sampleCount: 0, retainedSampleCount: 37, routeCount: 0, rows: [], available: true, valuesRedacted: true },
|
||
collectionHealth,
|
||
dashboard: webPerformanceDashboardPayload(rows, sampleWindow, collectionHealth),
|
||
rows,
|
||
problems: [],
|
||
webVitals: [],
|
||
apiRoutes: [],
|
||
longTasks: [],
|
||
workbenchJourneys: [],
|
||
workbenchEventPhases: [],
|
||
workbenchBackendEvents: []
|
||
};
|
||
}
|
||
|
||
function performanceRow(row: JsonRecord, sampleWindow: FakePerformanceWindow): JsonRecord {
|
||
const count = Number(row.count ?? 0);
|
||
const sampleState = count === 0 ? "empty" : count < 5 ? "low-sample" : "ok";
|
||
return {
|
||
method: "GET",
|
||
statusClass: "2xx",
|
||
outcome: "ok",
|
||
unit: "seconds",
|
||
sampleState,
|
||
sampleCount: count,
|
||
sourceFamily: "mixed",
|
||
freshness: sampleState === "ok" ? "fresh" : sampleState,
|
||
aggregationKind: "exact",
|
||
approximation: "exact",
|
||
windowFrom: sampleWindow.windowFrom,
|
||
windowTo: sampleWindow.windowTo,
|
||
generatedAt: sampleWindow.generatedAt,
|
||
...row
|
||
};
|
||
}
|
||
|
||
function webPerformanceDashboardPayload(rows: JsonRecord[], sampleWindow: FakePerformanceWindow, collectionHealth: JsonRecord = performanceCollectionHealthPayload(rows, sampleWindow)): JsonRecord {
|
||
const workbench = rows.filter((row) => String(row.kind).startsWith("workbench"));
|
||
const api = rows.filter((row) => row.kind === "api");
|
||
const vital = rows.filter((row) => row.kind === "web-vital");
|
||
const longTask = rows.filter((row) => row.kind === "long-task");
|
||
const sampleCount = performanceSampleCount(rows);
|
||
const problemCount = rows.filter((row) => row.tone === "warn").length;
|
||
return {
|
||
schemaVersion: "hwlab-web-performance-dashboard-v1",
|
||
generatedAt: sampleWindow.generatedAt,
|
||
sampleWindow: { ...performanceSampleWindowPayload(sampleWindow), buckets: [{ id: sampleWindow.id, label: sampleWindow.label, observedAt: sampleWindow.generatedAt, sampleCount }] },
|
||
freshness: { observedAt: sampleWindow.generatedAt, generatedAt: sampleWindow.generatedAt, windowFrom: sampleWindow.windowFrom, windowTo: sampleWindow.windowTo, sampleCount, source: "fake-server", sourceFamily: "mixed", namespace: "hwlab-v03", gitopsTarget: "D601/v03", status: collectionHealth.status, statusLabel: collectionHealth.statusLabel, reason: collectionHealth.reason, reasonLabel: collectionHealth.reasonLabel, lowSampleThreshold: 5, lastSampleAt: collectionHealth.lastSampleAt, lastRetainedSampleAt: collectionHealth.lastRetainedSampleAt, secondsSinceLastSample: collectionHealth.secondsSinceLastSample },
|
||
collectionHealth,
|
||
cards: [
|
||
{ id: "health", label: "采集状态", value: collectionHealth.statusLabel, unit: "状态", tone: sampleCount === 0 ? "source" : collectionHealth.status === "fresh" ? "ok" : "warn", detail: collectionHealth.reasonLabel },
|
||
{ id: "samples", label: "样本数", value: sampleCount, unit: "样本", tone: sampleCount === 0 ? "source" : "ok", detail: `${sampleWindow.label} · ${rows.length} 个序列` },
|
||
{ id: "workbench", label: "工作台序列", value: workbench.length, unit: "序列", tone: workbench.length > 0 ? "ok" : "source", detail: "hwlab-v03 / D601/v03" },
|
||
{ id: "problems", label: "问题行", value: problemCount, unit: "条", tone: problemCount > 0 ? "warn" : "ok", detail: "低样本阈值 5 个样本" }
|
||
],
|
||
trends: [
|
||
{ id: "workbench-experience", title: "工作台体感趋势", subtitle: "首屏、会话切换和提交首个可见结果的 P95", unit: "seconds", points: workbench.map(dashboardPoint) },
|
||
{ id: "api-timing", title: "同源 API 耗时", subtitle: "Cloud Web 同源 API 请求 P95", unit: "seconds", points: api.map(dashboardPoint) },
|
||
{ id: "web-vitals", title: "浏览器核心指标", subtitle: "浏览器体感当前窗口", unit: "mixed", points: vital.map(dashboardPoint) }
|
||
],
|
||
distributions: [
|
||
{ id: "status", title: "状态分布", subtitle: "按健康状态聚合当前窗口行数", unit: "count", buckets: [{ label: "正常", count: 4, tone: "ok" }, { label: "预警", count: 2, tone: "warn" }] },
|
||
{ id: "samples", title: "样本状态", subtitle: "正常、低样本和暂无数据分布", unit: "count", buckets: [{ label: "正常样本", count: 6, tone: "ok" }] }
|
||
],
|
||
topN: [
|
||
{ id: "top-api", title: "慢 API TopN", subtitle: "同源 API P95 最慢路由", unit: "seconds", rows: api.map(dashboardTopRow) },
|
||
{ id: "top-workbench", title: "工作台慢路径 TopN", subtitle: "工作台 journey 和事件可见延迟", unit: "seconds", rows: workbench.map(dashboardTopRow) },
|
||
{ id: "top-long-task", title: "长任务 TopN", subtitle: "主线程长任务当前窗口", unit: "seconds", rows: longTask.map(dashboardTopRow) }
|
||
]
|
||
};
|
||
}
|
||
|
||
function performanceCollectionHealthPayload(rows: JsonRecord[], sampleWindow: FakePerformanceWindow): JsonRecord {
|
||
const sampleCount = performanceSampleCount(rows);
|
||
const sourceFamilyCounts = performanceSourceFamilyCounts(rows);
|
||
const unknownAttribution = performanceUnknownAttribution(rows);
|
||
const diagnostics: JsonRecord[] = [];
|
||
let status = "fresh";
|
||
let reason = "window_has_samples";
|
||
let reasonLabel = "当前窗口采集正常";
|
||
|
||
if (sampleCount <= 0) {
|
||
status = "waiting";
|
||
reason = "no_samples_retained";
|
||
reasonLabel = "采集器尚未上报样本";
|
||
diagnostics.push({ code: reason, label: "暂无采集样本", tone: "source", detail: "fake-server 当前场景没有性能样本。" });
|
||
} else if (sampleCount < 5) {
|
||
status = "low-sample";
|
||
reason = "low_sample_window";
|
||
reasonLabel = "当前窗口低样本";
|
||
diagnostics.push({ code: reason, label: "低样本窗口", tone: "warn", detail: `当前窗口只有 ${sampleCount} 个样本,低于 5 个样本阈值。` });
|
||
}
|
||
|
||
const rowsWithUnknown = Number(unknownAttribution.rowsWithUnknown ?? 0);
|
||
if (rowsWithUnknown > 0) {
|
||
if (status === "fresh") {
|
||
status = "degraded";
|
||
reason = "unknown_attribution";
|
||
reasonLabel = "归因字段不完整";
|
||
}
|
||
diagnostics.push({ code: "unknown_attribution", label: "工作台归因不完整", tone: "warn", detail: `${rowsWithUnknown} 条工作台序列存在 backend 或 transport unknown。` });
|
||
}
|
||
|
||
if (sourceFamilyCounts.length > 1) diagnostics.push({ code: "mixed_source_family", label: "多来源样本", tone: "ok", detail: `当前窗口包含 ${sourceFamilyCounts.length} 类低基数来源。` });
|
||
if (!diagnostics.length) diagnostics.push({ code: "ok", label: "采集健康", tone: "ok", detail: "当前窗口有足够样本,且没有发现低样本、缺样或 unknown 归因问题。" });
|
||
|
||
return {
|
||
schemaVersion: "hwlab-web-performance-collection-health-v1",
|
||
status,
|
||
statusLabel: status === "fresh" ? "采集正常" : status === "low-sample" ? "低样本" : status === "degraded" ? "归因不完整" : "等待数据",
|
||
reason,
|
||
reasonLabel,
|
||
sampleCount,
|
||
retainedSampleCount: sampleCount,
|
||
sampleSeries: rows.length,
|
||
lowSampleThreshold: 5,
|
||
firstSampleAt: sampleCount > 0 ? sampleWindow.windowFrom : null,
|
||
lastSampleAt: sampleCount > 0 ? sampleWindow.generatedAt : null,
|
||
lastRetainedSampleAt: sampleCount > 0 ? sampleWindow.generatedAt : null,
|
||
secondsSinceLastSample: sampleCount > 0 ? 0 : null,
|
||
windowCoverageSeconds: sampleCount > 0 ? sampleWindow.seconds : null,
|
||
sourceFamilyCounts,
|
||
unknownAttribution,
|
||
diagnostics
|
||
};
|
||
}
|
||
|
||
function performanceSourceFamilyCounts(rows: JsonRecord[]): JsonRecord[] {
|
||
const counts = new Map<string, { sourceFamily: string; rowCount: number; sampleCount: number }>();
|
||
for (const row of rows) {
|
||
const sourceFamily = String(row.sourceFamily ?? "mixed");
|
||
const entry = counts.get(sourceFamily) ?? { sourceFamily, rowCount: 0, sampleCount: 0 };
|
||
entry.rowCount += 1;
|
||
entry.sampleCount += Math.max(0, Number(row.count) || 0);
|
||
counts.set(sourceFamily, entry);
|
||
}
|
||
return [...counts.values()].sort((left, right) => right.sampleCount - left.sampleCount || left.sourceFamily.localeCompare(right.sourceFamily));
|
||
}
|
||
|
||
function performanceUnknownAttribution(rows: JsonRecord[]): JsonRecord {
|
||
const workbenchRows = rows.filter((row) => String(row.kind ?? "").startsWith("workbench"));
|
||
const isUnknown = (value: unknown) => !String(value ?? "").trim() || String(value ?? "").trim().toLowerCase() === "unknown" || String(value ?? "").trim() === "-";
|
||
const rowsWithUnknown = workbenchRows.filter((row) => isUnknown(row.backend) || isUnknown(row.transport)).length;
|
||
return {
|
||
workbenchRows: workbenchRows.length,
|
||
rowsWithUnknown,
|
||
samplesWithUnknown: workbenchRows.reduce((sum, row) => sum + ((isUnknown(row.backend) || isUnknown(row.transport)) ? Math.max(0, Number(row.count) || 0) : 0), 0),
|
||
backendUnknownRows: workbenchRows.filter((row) => isUnknown(row.backend)).length,
|
||
transportUnknownRows: workbenchRows.filter((row) => isUnknown(row.transport)).length
|
||
};
|
||
}
|
||
|
||
function fakePerformanceWindow(value: string | null): FakePerformanceWindow {
|
||
const id = ["5m", "15m", "1h", "6h", "24h", "all"].includes(String(value)) ? String(value) : "15m";
|
||
const generatedAt = "2026-06-18T04:30:00.000Z";
|
||
const specs: Record<string, Omit<FakePerformanceWindow, "id" | "generatedAt" | "windowTo">> = {
|
||
"5m": { label: "最近 5 分钟", seconds: 300, windowFrom: "2026-06-18T04:25:00.000Z" },
|
||
"15m": { label: "最近 15 分钟", seconds: 900, windowFrom: "2026-06-18T04:15:00.000Z" },
|
||
"1h": { label: "最近 1 小时", seconds: 3600, windowFrom: "2026-06-18T03:30:00.000Z" },
|
||
"6h": { label: "最近 6 小时", seconds: 21600, windowFrom: "2026-06-17T22:30:00.000Z" },
|
||
"24h": { label: "最近 24 小时", seconds: 86400, windowFrom: "2026-06-17T04:30:00.000Z" },
|
||
all: { label: "全部累计", seconds: null, windowFrom: null }
|
||
};
|
||
const spec = specs[id] ?? { label: "最近 15 分钟", seconds: 900, windowFrom: "2026-06-18T04:15:00.000Z" };
|
||
return { id, generatedAt, windowTo: generatedAt, ...spec };
|
||
}
|
||
|
||
function performanceSampleWindowPayload(sampleWindow: FakePerformanceWindow): JsonRecord {
|
||
return { id: sampleWindow.id, label: sampleWindow.label, seconds: sampleWindow.seconds, windowFrom: sampleWindow.windowFrom, windowTo: sampleWindow.windowTo, generatedAt: sampleWindow.generatedAt, aggregationKind: "exact", approximation: "exact" };
|
||
}
|
||
|
||
function performanceSummaryContract(sampleWindow: FakePerformanceWindow, rows: JsonRecord[], summary: JsonRecord): JsonRecord {
|
||
return {
|
||
routeCount: rows.length,
|
||
durationSeries: rows.filter((row) => row.unit === "seconds").length,
|
||
lowSampleThreshold: 5,
|
||
workbenchJourneySeries: rows.filter((row) => row.kind === "workbench-journey").length,
|
||
workbenchEventPhaseSeries: rows.filter((row) => row.kind === "workbench-event-phase").length,
|
||
workbenchBackendEventVisibleSeries: rows.filter((row) => row.kind === "workbench-backend-event").length,
|
||
window: sampleWindow.id,
|
||
windowLabel: sampleWindow.label,
|
||
windowSeconds: sampleWindow.seconds,
|
||
windowFrom: sampleWindow.windowFrom,
|
||
windowTo: sampleWindow.windowTo,
|
||
generatedAt: sampleWindow.generatedAt,
|
||
aggregationKind: "exact",
|
||
approximation: "exact",
|
||
...summary
|
||
};
|
||
}
|
||
|
||
function performanceSampleCount(rows: JsonRecord[]): number {
|
||
return rows.reduce((total, row) => total + (Number(row.count) || 0), 0);
|
||
}
|
||
|
||
function dashboardPoint(row: JsonRecord): JsonRecord {
|
||
return { label: dashboardMetricLabel(row), detail: dashboardRouteLabel(row.route), value: row.p95, p50: row.p50, p75: row.p75, p95: row.p95, count: row.count, tone: row.tone, sampleState: row.sampleState };
|
||
}
|
||
|
||
function dashboardTopRow(row: JsonRecord): JsonRecord {
|
||
return { label: dashboardMetricLabel(row), detail: dashboardRouteLabel(row.route), value: row.p95, unit: row.unit ?? "seconds", count: row.count, tone: row.tone };
|
||
}
|
||
|
||
function dashboardRouteLabel(route: unknown): string {
|
||
const value = String(route ?? "").trim();
|
||
if (!value || value === "-") return "当前窗口";
|
||
return value
|
||
.replace(/:sessionId/gu, ":会话")
|
||
.replace(/:traceId/gu, ":轨迹")
|
||
.replace(/:runId/gu, ":运行")
|
||
.replace(/:threadId/gu, ":线程")
|
||
.replace(/:turnId/gu, ":轮次");
|
||
}
|
||
|
||
function dashboardMetricLabel(row: JsonRecord): string {
|
||
const metric = String(row.metric ?? "unknown");
|
||
const labels: Record<string, string> = {
|
||
"session-switch-full-load": "切换会话完整加载",
|
||
"trace-event-projected": "轨迹事件投影",
|
||
"backend-event-visible": "后端事件到可见",
|
||
LCP: "最大内容绘制",
|
||
fetchJson: "同源 API 请求",
|
||
longtask: "主线程长任务"
|
||
};
|
||
return labels[metric] ?? metric;
|
||
}
|
||
|
||
function createScenarioState(scenarioId: string): ScenarioState {
|
||
const base = structuredClone(capture.scenario);
|
||
const id = scenarioId || "baseline";
|
||
const sessions = base.sessions;
|
||
const traces = base.traces;
|
||
if (id === "cross-project-detail-boundary") sessions.push(hiddenBoundarySession());
|
||
if (id === "legacy-cnv-deeplink-canonical") {
|
||
sessions.unshift(legacyCanonicalSession());
|
||
traces.trc_40badc15523146c9 = legacyCanonicalTrace();
|
||
}
|
||
if (id === "submit-authority-race") {
|
||
sessions.unshift(submitSplitSession());
|
||
traces.trc_submit_split = submitSplitTrace();
|
||
}
|
||
if (id === "session-switch-delayed-detail-frame") {
|
||
markSessionText(sessions, "ses_running", "SESSION_A_ONLY", "SESSION_A_ONLY running source session");
|
||
markSessionText(sessions, "ses_completed", "SESSION_B_ONLY", "SESSION_B_ONLY completed target session");
|
||
}
|
||
if (id === "session-rail-many") {
|
||
for (let index = 0; index < 51; index += 1) sessions.push(manyRailSession(index));
|
||
}
|
||
if (id === "session-switch-detail-404-isolated") {
|
||
sessions.unshift(staleDetailSession());
|
||
sessions.push(emptySession());
|
||
}
|
||
if (id === "terminal-empty-trace") {
|
||
sessions.push(terminalEmptyTraceSession());
|
||
traces.trc_terminal_empty = terminalEmptyTrace();
|
||
}
|
||
if (id === "markdown-final-response") {
|
||
sessions.unshift(markdownFinalSession());
|
||
traces.trc_markdown_final = markdownFinalTrace();
|
||
}
|
||
if (id === "terminal-assistant-event-final-response") {
|
||
sessions.unshift(terminalAssistantFinalSession());
|
||
traces.trc_terminal_assistant_final = terminalAssistantFinalTrace();
|
||
}
|
||
if (id === "progress-only-final-response") markRunningProgressOnly(sessions, traces);
|
||
if (id === "terminal-completed-no-final-response") markRunningNoFinalResponse(sessions, traces);
|
||
if (id === "tool-completed-projection-running") markToolCompletedProjectionRunning(sessions, traces);
|
||
if (id === "terminal-canceled-late-timing") markTerminalCanceledLateTiming(sessions, traces);
|
||
if (id === "projection-degraded-diagnostics") {
|
||
sessions.unshift(projectionDegradedSession());
|
||
traces.trc_projection_degraded = projectionDegradedTrace();
|
||
}
|
||
if (id === "projection-sse-error") {
|
||
sessions.unshift(projectionSseErrorSession());
|
||
traces.trc_projection_sse_error = projectionSseErrorTrace();
|
||
}
|
||
if (id === "trace-detail-read-timeout-diagnostic") {
|
||
sessions.unshift(traceDetailReadTimeoutSession());
|
||
traces.trc_trace_detail_read_timeout = traceDetailReadTimeoutTrace();
|
||
}
|
||
if (id === "sealed-final-response-diagnostics") {
|
||
sessions.unshift(sealedFinalResponseDiagnosticSession());
|
||
traces.trc_sealed_final_diag = sealedFinalResponseDiagnosticTrace();
|
||
}
|
||
if (id === "trace-final-response-not-message-authority") {
|
||
sessions.unshift(traceFinalResponseConflictSession());
|
||
traces.trc_trace_final_conflict = traceFinalResponseConflictTrace();
|
||
}
|
||
if (id === "scroll-follow-long-trace") {
|
||
const session = scrollFollowSession();
|
||
sessions.unshift(session);
|
||
traces.trc_scroll_follow = scrollFollowTrace(90);
|
||
}
|
||
if (id === "projector-resume-from-checkpoint") {
|
||
const session = projectorResumeSession();
|
||
sessions.unshift(session);
|
||
traces.trc_f2456023233a485b = projectorResumeEarlyTrace();
|
||
}
|
||
if (id === "cross-session-late-events") {
|
||
sessions.unshift(crossSessionLateB(), crossSessionLateA());
|
||
traces.trc_late_A = crossSessionLateTraceA();
|
||
}
|
||
if (id === "deleted-session-deeplink") sessions.unshift(archivedDeletedSession());
|
||
if (id === "session-switch-empty-reload") sessions.push(emptySession());
|
||
if (id === "empty-session-gc") sessions.unshift(staleEmptyTtlSession(), freshEmptyTtlSession(), staleMessagedTtlSession());
|
||
const selectedSessionId = id === "scroll-follow-long-trace"
|
||
? "ses_scroll_follow"
|
||
: id === "deep-link" || id === "stale-nested-trace"
|
||
? "ses_failed"
|
||
: id === "legacy-cnv-deeplink-canonical"
|
||
? "ses_running"
|
||
: id === "session-switch-detail-404-isolated" || id === "completed-replay-detail-404" || id === "deleted-session-deeplink" || id === "terminal-turn-stale-session-active"
|
||
? "ses_completed"
|
||
: id === "terminal-empty-trace"
|
||
? "ses_terminal_empty"
|
||
: id === "markdown-final-response"
|
||
? "ses_markdown_final"
|
||
: id === "terminal-assistant-event-final-response"
|
||
? "ses_terminal_assistant_final"
|
||
: id === "progress-only-final-response" || id === "terminal-completed-no-final-response" || id === "tool-completed-projection-running" || id === "terminal-canceled-late-timing"
|
||
? "ses_running"
|
||
: id === "projector-resume-from-checkpoint"
|
||
? "ses_317f78e1-ed91-4b09-a72a-3668d5469f59"
|
||
: id === "projection-degraded-diagnostics"
|
||
? "ses_projection_degraded"
|
||
: id === "projection-sse-error"
|
||
? "ses_projection_sse_error"
|
||
: id === "trace-detail-read-timeout-diagnostic"
|
||
? "ses_trace_detail_read_timeout"
|
||
: id === "sealed-final-response-diagnostics"
|
||
? "ses_sealed_final_diag"
|
||
: id === "trace-final-response-not-message-authority"
|
||
? "ses_trace_final_conflict"
|
||
: id === "create-while-route-loading"
|
||
? "ses_running"
|
||
: base.selectedSessionId;
|
||
const staleTraceId = id === "stale-nested-trace" || id === "stale-submit-restore" ? "trc_stale_502" : null;
|
||
const projectLinks = projectManagementSeedLinks();
|
||
return {
|
||
scenarioId: id,
|
||
providerProfile: base.providerProfile,
|
||
selectedSessionId,
|
||
sessions,
|
||
traces,
|
||
requestLedger: [],
|
||
legacyRequestLedger: [],
|
||
chatRequests: [],
|
||
projectLinks,
|
||
projectFiles: projectManagementFiles(),
|
||
projectTasks: projectManagementSeedTasks(projectLinks),
|
||
projectRevision: 1,
|
||
listOmitSelected: id === "selected-missing-from-list",
|
||
sessionDelayMs: id === "loading" ? 2_500 : 0,
|
||
sessionDetailDelayMs: id === "legacy-cnv-deeplink-canonical" ? 1_500 : id === "session-switch-delayed-detail-frame" ? 900 : id === "submit-authority-race" ? 1_000 : 0,
|
||
chatDelayMs: id === "submit-authority-race" ? 1_000 : 0,
|
||
terminalScript: id === "event-replay" || id === "running-to-terminal" || id === "stale-submit-restore" || id === "progress-only-final-response" || id === "terminal-completed-no-final-response" || id === "terminal-canceled-late-timing",
|
||
terminalFailureScript: id === "stale-submit-restore",
|
||
staleTraceId,
|
||
liveBackfillTraceId: id === "projector-resume-from-checkpoint" ? "trc_f2456023233a485b" : null,
|
||
liveBackfillReadyAtMs: id === "projector-resume-from-checkpoint" ? Date.now() + 5_000 : null
|
||
};
|
||
}
|
||
|
||
function refreshBackgroundProjection(): void {
|
||
if (state.scenarioId !== "projector-resume-from-checkpoint") return;
|
||
if (!state.liveBackfillReadyAtMs || Date.now() < state.liveBackfillReadyAtMs) return;
|
||
const session = sessionById("ses_317f78e1-ed91-4b09-a72a-3668d5469f59");
|
||
if (session?.status === "completed") return;
|
||
commitProjectorResumeProjection();
|
||
}
|
||
|
||
function projectorResumeSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
const trace = projectorResumeEarlyTrace();
|
||
return {
|
||
sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59",
|
||
threadId: "thr_projector_resume_1596",
|
||
status: "running",
|
||
lastTraceId: "trc_f2456023233a485b",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "issue 1596 durable projector resume sample",
|
||
turnSummary: { traceId: "trc_f2456023233a485b", status: "running", running: true, terminal: false, ...projectorResumeProjection("projecting", 4) },
|
||
messages: [
|
||
{ id: "msg_projector_resume_user", messageId: "msg_projector_resume_user", role: "user", title: "用户", text: "issue 1596 durable projector resume sample", status: "sent", createdAt: now, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", traceId: "trc_f2456023233a485b", turnId: "turn_projector_resume_1596" },
|
||
{ id: "msg_projector_resume_agent", messageId: "msg_projector_resume_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", traceId: "trc_f2456023233a485b", turnId: "turn_projector_resume_1596", runnerTrace: trace, ...projectorResumeProjection("projecting", 4) }
|
||
]
|
||
};
|
||
}
|
||
|
||
function projectorResumeEarlyTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const projection = projectorResumeProjection("projecting", 4);
|
||
const events = [
|
||
{ seq: 1, sourceSeq: 1, createdAt, label: "agentrun:request:accepted", type: "backend_status", status: "running", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704" } },
|
||
{ seq: 2, sourceSeq: 2, createdAt, label: "agentrun:run:created", type: "backend_status", status: "running", payload: { runId: "run_58ef1811071548539f881515912560d8", runStatus: "claimed" } },
|
||
{ seq: 3, sourceSeq: 3, createdAt, label: "agentrun:command:created", type: "backend_status", status: "running", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", commandState: "running" } },
|
||
{ seq: 4, sourceSeq: 4, createdAt, label: "agentrun:runner-job:created", type: "backend_status", status: "running", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", namespace: "agentrun-v01" } }
|
||
];
|
||
return { traceId: "trc_f2456023233a485b", status: "running", running: true, terminal: false, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", turnId: "turn_projector_resume_1596", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, ...projection };
|
||
}
|
||
|
||
function projectorResumeCompletedTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const finalText = "fake projector resume completed from durable checkpoint.";
|
||
const projection = projectorResumeProjection("caught-up", 7);
|
||
const events = [
|
||
...(projectorResumeEarlyTrace().events as JsonRecord[]),
|
||
{ seq: 5, sourceSeq: 2748, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", message: finalText, payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704" } },
|
||
{ seq: 6, sourceSeq: 2749, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", stdout: "OK\n", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", exitCode: 0 } },
|
||
{ seq: 7, sourceSeq: 2750, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true, payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", terminalStatus: "completed" } }
|
||
];
|
||
return { traceId: "trc_f2456023233a485b", status: "completed", running: false, terminal: true, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", turnId: "turn_projector_resume_1596", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, finalResponse: { text: finalText, status: "completed" }, traceSummary: { source: "durable-projector-resume", terminalStatus: "completed", valuesPrinted: false }, ...projection };
|
||
}
|
||
|
||
function projectorResumeProjection(status: "projecting" | "caught-up", lastProjectedSeq: number): JsonRecord {
|
||
const caughtUp = status === "caught-up";
|
||
const projection = {
|
||
projectionStatus: status,
|
||
projectionHealth: status,
|
||
lastProjectedSeq,
|
||
sourceLatestSeq: 2750,
|
||
sourceRunId: "run_58ef1811071548539f881515912560d8",
|
||
sourceCommandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704",
|
||
staleMs: caughtUp ? 0 : 90_000,
|
||
blocker: caughtUp ? null : { code: "projector_resume_pending", layer: "workbench-projector", retryable: true, message: "后台投影恢复中,已从 checkpoint 记录 lastProjectedSeq=4。", valuesPrinted: false },
|
||
valuesRedacted: true
|
||
};
|
||
return { projection, ...projection };
|
||
}
|
||
|
||
function commitProjectorResumeProjection(): void {
|
||
const completed = projectorResumeCompletedTrace();
|
||
state.traces.trc_f2456023233a485b = completed;
|
||
const session = sessionById("ses_317f78e1-ed91-4b09-a72a-3668d5469f59");
|
||
if (!session) return;
|
||
session.status = "completed";
|
||
session.updatedAt = new Date().toISOString();
|
||
session.turnSummary = { traceId: "trc_f2456023233a485b", status: "completed", running: false, terminal: true, ...projectorResumeProjection("caught-up", 7) };
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role !== "agent" || message.traceId !== "trc_f2456023233a485b") return message;
|
||
return { ...message, text: "fake projector resume completed from durable checkpoint.", status: "completed", runnerTrace: completed, ...projectorResumeProjection("caught-up", 7) };
|
||
});
|
||
const terminalEvent = (completed.events as JsonRecord[]).at(-1) ?? null;
|
||
if (terminalEvent) broadcastSse("workbench.trace.event", { type: "trace.event", sessionId: session.sessionId, threadId: session.threadId ?? null, traceId: "trc_f2456023233a485b", event: terminalEvent, snapshot: completed });
|
||
broadcastSse("workbench.turn.snapshot", { type: "turn.snapshot", sessionId: session.sessionId, threadId: session.threadId ?? null, traceId: "trc_f2456023233a485b", turn: turnPayload("trc_f2456023233a485b") });
|
||
}
|
||
|
||
function markSessionText(sessions: SessionRecord[], sessionId: string, userText: string, agentText: string): void {
|
||
const session = sessions.find((item) => item.sessionId === sessionId);
|
||
if (!session) return;
|
||
session.firstUserMessagePreview = userText;
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role === "user") return { ...message, text: userText };
|
||
if (message.role === "agent") return { ...message, text: agentText };
|
||
return message;
|
||
});
|
||
}
|
||
|
||
function markRunningProgressOnly(sessions: SessionRecord[], traces: Record<string, JsonRecord>): void {
|
||
const createdAt = new Date().toISOString();
|
||
const progressEvent = { seq: 1, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "running", replyAuthority: false, final: false, message: progressOnlyText };
|
||
const trace = { traceId: "trc_running", status: "running", sessionId: "ses_running", threadId: "thr_running", turnId: "turn_running", events: [progressEvent], eventCount: 1, fullTraceLoaded: false, hasMore: false };
|
||
traces.trc_running = trace;
|
||
const session = sessions.find((item) => item.sessionId === "ses_running");
|
||
if (!session) return;
|
||
session.status = "running";
|
||
session.lastTraceId = "trc_running";
|
||
session.firstUserMessagePreview = "progress-only running turn";
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role === "user") return { ...message, text: "progress-only running turn" };
|
||
if (message.role !== "agent" || message.traceId !== "trc_running") return message;
|
||
return { ...message, text: "", status: "running", runnerTrace: trace };
|
||
});
|
||
}
|
||
|
||
function markRunningNoFinalResponse(sessions: SessionRecord[], traces: Record<string, JsonRecord>): void {
|
||
const trace = noFinalRunningTrace();
|
||
traces.trc_running = trace;
|
||
const session = sessions.find((item) => item.sessionId === "ses_running");
|
||
if (!session) return;
|
||
session.status = "running";
|
||
session.lastTraceId = "trc_running";
|
||
session.firstUserMessagePreview = "completed turn without final response";
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role === "user") return { ...message, text: "completed turn without final response" };
|
||
if (message.role !== "agent" || message.traceId !== "trc_running") return message;
|
||
return { ...message, text: "", status: "running", runnerTrace: trace };
|
||
});
|
||
}
|
||
|
||
function markTerminalCanceledLateTiming(sessions: SessionRecord[], traces: Record<string, JsonRecord>): void {
|
||
const trace = terminalCanceledLateTimingRunningTrace();
|
||
traces.trc_running = trace;
|
||
const session = sessions.find((item) => item.sessionId === "ses_running");
|
||
if (!session) return;
|
||
session.status = "running";
|
||
session.lastTraceId = "trc_running";
|
||
session.firstUserMessagePreview = "terminal canceled timing should stay sealed";
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role === "user") return { ...message, text: "terminal canceled timing should stay sealed" };
|
||
if (message.role !== "agent" || message.traceId !== "trc_running") return message;
|
||
return { ...message, text: "", status: "running", timing: trace.timing, startedAt: trace.startedAt, lastEventAt: trace.lastEventAt, finishedAt: null, durationMs: null, runnerTrace: trace };
|
||
});
|
||
}
|
||
|
||
function terminalCanceledLateTimingRunningTrace(): JsonRecord {
|
||
const startedAt = "2026-06-17T09:39:00.000Z";
|
||
const lastEventAt = "2026-06-17T09:39:04.000Z";
|
||
const timing = { startedAt, lastEventAt, finishedAt: null, durationMs: null, valuesRedacted: true };
|
||
return {
|
||
traceId: "trc_running",
|
||
status: "running",
|
||
sessionId: "ses_running",
|
||
threadId: "thr_running",
|
||
turnId: "turn_running",
|
||
timing,
|
||
startedAt,
|
||
lastEventAt,
|
||
finishedAt: null,
|
||
durationMs: null,
|
||
events: [{ seq: 1, createdAt: lastEventAt, label: "agentrun:backend:turn/running", type: "backend_status", status: "running" }],
|
||
eventCount: 1,
|
||
fullTraceLoaded: false,
|
||
hasMore: false
|
||
};
|
||
}
|
||
|
||
function applyTerminalCanceledLateTiming(durationMs: number, finishedAt: string): JsonRecord | null {
|
||
const startedAt = "2026-06-17T09:39:00.000Z";
|
||
const timing = { startedAt, lastEventAt: finishedAt, finishedAt, durationMs, valuesRedacted: true };
|
||
const event = { seq: 2, createdAt: finishedAt, label: "agentrun:terminal:canceled", type: "backend_status", status: "canceled", terminal: true };
|
||
const trace = {
|
||
traceId: "trc_running",
|
||
status: "canceled",
|
||
sessionId: "ses_running",
|
||
threadId: "thr_running",
|
||
turnId: "turn_running",
|
||
timing,
|
||
startedAt,
|
||
lastEventAt: finishedAt,
|
||
finishedAt,
|
||
durationMs,
|
||
events: [event],
|
||
eventCount: 2,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
finalResponse: { text: "hwlab-user-cancel", status: "canceled" }
|
||
};
|
||
state.traces.trc_running = trace;
|
||
const session = sessionById("ses_running");
|
||
if (!session) return null;
|
||
session.status = "canceled";
|
||
session.updatedAt = finishedAt;
|
||
session.messages = (session.messages ?? []).map((message) => message.role === "agent" && message.traceId === "trc_running"
|
||
? { ...message, text: "hwlab-user-cancel", status: "canceled", timing, startedAt, lastEventAt: finishedAt, finishedAt, durationMs, runnerTrace: trace }
|
||
: message);
|
||
return trace;
|
||
}
|
||
|
||
function markToolCompletedProjectionRunning(sessions: SessionRecord[], traces: Record<string, JsonRecord>): void {
|
||
const trace = toolCompletedProjectionRunningTrace();
|
||
traces.trc_running = trace;
|
||
const session = sessions.find((item) => item.sessionId === "ses_running");
|
||
if (!session) return;
|
||
session.status = "running";
|
||
session.lastTraceId = "trc_running";
|
||
session.firstUserMessagePreview = "tool completed but turn still running";
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role === "user") return { ...message, text: "tool completed but turn still running" };
|
||
if (message.role !== "agent" || message.traceId !== "trc_running") return message;
|
||
return { ...message, text: "", status: "running", runnerTrace: trace };
|
||
});
|
||
}
|
||
|
||
function toolCompletedProjectionRunningTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const events = [
|
||
{ seq: 1, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "running", terminal: false, message: "正在安装 Python。" },
|
||
{ seq: 2, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", terminal: false, command: "ls -ld .", stdout: "OK\n" }
|
||
];
|
||
return {
|
||
traceId: "trc_running",
|
||
status: "completed",
|
||
turnStatus: "running",
|
||
projectionStatus: "projecting",
|
||
sessionId: "ses_running",
|
||
threadId: "thr_running",
|
||
turnId: "turn_running",
|
||
events,
|
||
eventCount: events.length,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
finalResponse: null,
|
||
assistantText: null
|
||
};
|
||
}
|
||
|
||
function projectionDiagnostic(input: { code: string; message: string; health?: string; category?: string; layer?: string; traceId: string; runId?: string; commandId?: string; staleMs?: number }): JsonRecord {
|
||
const diagnostic = {
|
||
contractVersion: "hwlab-error-diagnostic-v1",
|
||
traceId: input.traceId,
|
||
requestId: `req_e2e_${input.code}`,
|
||
route: "/v1/workbench/events",
|
||
layer: input.layer ?? "agentrun",
|
||
category: input.category ?? "upstream-timeout",
|
||
code: input.code,
|
||
httpStatus: 0,
|
||
source: "server",
|
||
observedAt: new Date().toISOString(),
|
||
retryable: true,
|
||
valuesPrinted: false
|
||
};
|
||
return {
|
||
projectionStatus: "projecting",
|
||
projectionHealth: input.health ?? "degraded",
|
||
lastProjectedSeq: 137,
|
||
sourceRunId: input.runId ?? "run_projection_diag",
|
||
sourceCommandId: input.commandId ?? "cmd_projection_diag",
|
||
staleMs: input.staleMs ?? 12_345,
|
||
blocker: {
|
||
code: input.code,
|
||
layer: input.layer ?? "agentrun",
|
||
category: input.category ?? "upstream-timeout",
|
||
message: input.message,
|
||
retryable: true,
|
||
timeoutMs: 2500,
|
||
runId: input.runId ?? "run_projection_diag",
|
||
commandId: input.commandId ?? "cmd_projection_diag",
|
||
traceId: diagnostic.traceId,
|
||
requestId: diagnostic.requestId,
|
||
route: diagnostic.route,
|
||
source: diagnostic.source,
|
||
httpStatus: diagnostic.httpStatus,
|
||
diagnostic,
|
||
valuesPrinted: false
|
||
},
|
||
diagnostic,
|
||
valuesRedacted: true
|
||
};
|
||
}
|
||
|
||
function projectionDegradedTrace(): JsonRecord {
|
||
const projection = projectionDiagnostic({ traceId: "trc_projection_degraded", code: "agentrun_result_timeout", message: "状态更新超时,最近投影 seq=137,AgentRun 结果暂不可见。" });
|
||
return { traceId: "trc_projection_degraded", status: "running", sessionId: "ses_projection_degraded", threadId: "thr_projection_degraded", turnId: "turn_projection_degraded", events: [], eventCount: 0, fullTraceLoaded: true, hasMore: false, projection, ...projection };
|
||
}
|
||
|
||
function projectionDegradedSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
const projection = projectionDiagnostic({ traceId: "trc_projection_degraded", code: "agentrun_result_timeout", message: "状态更新超时,最近投影 seq=137,AgentRun 结果暂不可见。" });
|
||
return {
|
||
sessionId: "ses_projection_degraded",
|
||
threadId: "thr_projection_degraded",
|
||
status: "running",
|
||
lastTraceId: "trc_projection_degraded",
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "projection degraded visibility",
|
||
turnSummary: { traceId: "trc_projection_degraded", status: "running", running: true, terminal: false, ...projection },
|
||
messages: [
|
||
{ id: "msg_projection_degraded_user", messageId: "msg_projection_degraded_user", role: "user", title: "用户", text: "projection degraded visibility", status: "sent", createdAt: now, sessionId: "ses_projection_degraded", threadId: "thr_projection_degraded", turnId: "turn_projection_degraded" },
|
||
{ id: "msg_projection_degraded_agent", messageId: "msg_projection_degraded_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_projection_degraded", threadId: "thr_projection_degraded", traceId: "trc_projection_degraded", turnId: "turn_projection_degraded", projection, runnerTrace: projectionDegradedTrace() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function projectionSseErrorTrace(): JsonRecord {
|
||
return { traceId: "trc_projection_sse_error", status: "running", sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", turnId: "turn_projection_sse_error", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false };
|
||
}
|
||
|
||
function projectionSseErrorSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_projection_sse_error",
|
||
threadId: "thr_projection_sse_error",
|
||
status: "running",
|
||
lastTraceId: "trc_projection_sse_error",
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "projection SSE error visibility",
|
||
messages: [
|
||
{ id: "msg_projection_sse_error_user", messageId: "msg_projection_sse_error_user", role: "user", title: "用户", text: "projection SSE error visibility", status: "sent", createdAt: now, sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", turnId: "turn_projection_sse_error" },
|
||
{ id: "msg_projection_sse_error_agent", messageId: "msg_projection_sse_error_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", traceId: "trc_projection_sse_error", turnId: "turn_projection_sse_error", runnerTrace: projectionSseErrorTrace() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function traceDetailReadTimeoutTrace(): JsonRecord {
|
||
return { traceId: "trc_trace_detail_read_timeout", status: "running", sessionId: "ses_trace_detail_read_timeout", threadId: "thr_trace_detail_read_timeout", turnId: "turn_trace_detail_read_timeout", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: true };
|
||
}
|
||
|
||
function traceDetailReadTimeoutSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_trace_detail_read_timeout",
|
||
threadId: "thr_trace_detail_read_timeout",
|
||
status: "running",
|
||
lastTraceId: "trc_trace_detail_read_timeout",
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "trace detail read timeout visibility",
|
||
messages: [
|
||
{ id: "msg_trace_detail_read_timeout_user", messageId: "msg_trace_detail_read_timeout_user", role: "user", title: "用户", text: "trace detail read timeout visibility", status: "sent", createdAt: now, sessionId: "ses_trace_detail_read_timeout", threadId: "thr_trace_detail_read_timeout", turnId: "turn_trace_detail_read_timeout" },
|
||
{ id: "msg_trace_detail_read_timeout_agent", messageId: "msg_trace_detail_read_timeout_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_trace_detail_read_timeout", threadId: "thr_trace_detail_read_timeout", traceId: "trc_trace_detail_read_timeout", turnId: "turn_trace_detail_read_timeout", runnerTrace: traceDetailReadTimeoutTrace() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function sealedFinalResponseDiagnosticTrace(): JsonRecord {
|
||
const now = new Date().toISOString();
|
||
const finalText = "已完成的 sealed final response 不应被诊断覆盖。";
|
||
return {
|
||
traceId: "trc_sealed_final_diag",
|
||
status: "completed",
|
||
sessionId: "ses_sealed_final_diag",
|
||
threadId: "thr_sealed_final_diag",
|
||
turnId: "turn_sealed_final_diag",
|
||
events: [],
|
||
eventCount: 1,
|
||
fullTraceLoaded: false,
|
||
hasMore: true,
|
||
finalResponse: { text: finalText, status: "completed" },
|
||
terminalEvidence: { terminal: true, status: "completed" },
|
||
sealedAt: now
|
||
};
|
||
}
|
||
|
||
function sealedFinalResponseDiagnosticSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
const finalText = "已完成的 sealed final response 不应被诊断覆盖。";
|
||
const trace = sealedFinalResponseDiagnosticTrace();
|
||
return {
|
||
sessionId: "ses_sealed_final_diag",
|
||
threadId: "thr_sealed_final_diag",
|
||
status: "completed",
|
||
lastTraceId: "trc_sealed_final_diag",
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "sealed final response diagnostic separation",
|
||
turnSummary: { traceId: "trc_sealed_final_diag", status: "completed", running: false, terminal: true, sealedAt: now },
|
||
messages: [
|
||
{ id: "msg_sealed_final_user", messageId: "msg_sealed_final_user", role: "user", title: "用户", text: "sealed final response diagnostic separation", status: "sent", createdAt: now, sessionId: "ses_sealed_final_diag", threadId: "thr_sealed_final_diag", turnId: "turn_sealed_final_diag" },
|
||
{ id: "msg_sealed_final_agent", messageId: "msg_sealed_final_agent", role: "agent", title: "Code Agent", text: finalText, status: "completed", createdAt: now, updatedAt: now, sessionId: "ses_sealed_final_diag", threadId: "thr_sealed_final_diag", traceId: "trc_sealed_final_diag", turnId: "turn_sealed_final_diag", sealedAt: now, runnerTrace: trace, finalResponse: { text: finalText, status: "completed" } }
|
||
]
|
||
};
|
||
}
|
||
|
||
function traceFinalResponseConflictTrace(): JsonRecord {
|
||
const now = new Date().toISOString();
|
||
const event = { seq: 1, sourceSeq: 91, createdAt: now, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", terminal: true, final: true, replyAuthority: true, message: traceDetailConflictText };
|
||
return {
|
||
traceId: "trc_trace_final_conflict",
|
||
status: "completed",
|
||
sessionId: "ses_trace_final_conflict",
|
||
threadId: "thr_trace_final_conflict",
|
||
turnId: "turn_trace_final_conflict",
|
||
events: [event],
|
||
eventCount: 1,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
finalResponse: { text: traceDetailConflictText, status: "completed" },
|
||
terminalEvidence: { terminal: true, status: "completed" },
|
||
sealedAt: now
|
||
};
|
||
}
|
||
|
||
function traceFinalResponseConflictSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_trace_final_conflict",
|
||
threadId: "thr_trace_final_conflict",
|
||
status: "completed",
|
||
lastTraceId: "trc_trace_final_conflict",
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "trace final response authority separation",
|
||
turnSummary: { traceId: "trc_trace_final_conflict", status: "completed", running: false, terminal: true, sealedAt: now },
|
||
messages: [
|
||
{ id: "msg_trace_final_conflict_user", messageId: "msg_trace_final_conflict_user", role: "user", title: "用户", text: "trace final response authority separation", status: "sent", createdAt: now, sessionId: "ses_trace_final_conflict", threadId: "thr_trace_final_conflict", turnId: "turn_trace_final_conflict" },
|
||
{ id: "msg_trace_final_conflict_agent", messageId: "msg_trace_final_conflict_agent", role: "agent", title: "Code Agent", text: canonicalTraceFinalText, status: "completed", createdAt: now, updatedAt: now, sessionId: "ses_trace_final_conflict", threadId: "thr_trace_final_conflict", traceId: "trc_trace_final_conflict", turnId: "turn_trace_final_conflict", sealedAt: now, finalResponse: { text: canonicalTraceFinalText, status: "completed" }, runnerTrace: { traceId: "trc_trace_final_conflict", status: "completed", sessionId: "ses_trace_final_conflict", threadId: "thr_trace_final_conflict", events: [], eventCount: 1, fullTraceLoaded: false, hasMore: true } }
|
||
]
|
||
};
|
||
}
|
||
|
||
function noFinalRunningTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
return {
|
||
traceId: "trc_running",
|
||
status: "running",
|
||
sessionId: "ses_running",
|
||
threadId: "thr_running",
|
||
turnId: "turn_running",
|
||
events: [{ seq: 1, createdAt, label: "agentrun:backend:turn/running", type: "backend_status", status: "running" }],
|
||
eventCount: 1,
|
||
fullTraceLoaded: false,
|
||
hasMore: false
|
||
};
|
||
}
|
||
|
||
function completedNoFinalTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const events = [
|
||
{ seq: 1, createdAt, label: "agentrun:backend:turn/running", type: "backend_status", status: "running" },
|
||
{ seq: 2, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true }
|
||
];
|
||
return { traceId: "trc_running", status: "completed", sessionId: "ses_running", threadId: "thr_running", turnId: "turn_running", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false };
|
||
}
|
||
|
||
function scrollFollowSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
const trace = scrollFollowTrace(90);
|
||
const history = Array.from({ length: 18 }, (_, index) => {
|
||
const seq = index + 1;
|
||
return seq % 2 === 0
|
||
? { id: `msg_scroll_history_agent_${seq}`, messageId: `msg_scroll_history_agent_${seq}`, role: "agent", title: "Code Agent", text: `历史回复 ${seq}\n\n用于撑开主工作区滚动高度。`, status: "completed", createdAt: now, sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", turnId: `turn_scroll_history_${seq}` }
|
||
: { id: `msg_scroll_history_user_${seq}`, messageId: `msg_scroll_history_user_${seq}`, role: "user", title: "用户", text: `历史问题 ${seq}:保持主工作区可滚动。`, status: "sent", createdAt: now, sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", turnId: `turn_scroll_history_${seq}` };
|
||
});
|
||
const messages = [
|
||
...history,
|
||
{ id: "msg_scroll_follow_user", messageId: "msg_scroll_follow_user", role: "user", title: "用户", text: "scroll follow long trace probe", status: "sent", createdAt: now, sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", traceId: "trc_scroll_follow", turnId: "turn_scroll_follow" },
|
||
{ id: "msg_scroll_follow_agent", messageId: "msg_scroll_follow_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", traceId: "trc_scroll_follow", turnId: "turn_scroll_follow", runnerTrace: trace }
|
||
];
|
||
return {
|
||
sessionId: "ses_scroll_follow",
|
||
threadId: "thr_scroll_follow",
|
||
status: "running",
|
||
lastTraceId: "trc_scroll_follow",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: messages.length,
|
||
firstUserMessagePreview: "scroll follow long trace probe",
|
||
messages
|
||
};
|
||
}
|
||
|
||
function scrollFollowTrace(count: number): JsonRecord {
|
||
const events = Array.from({ length: count }, (_, index) => scrollFollowEvent(index + 1));
|
||
return { traceId: "trc_scroll_follow", status: "running", sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", turnId: "turn_scroll_follow", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false };
|
||
}
|
||
|
||
function scrollFollowEvent(seq: number): JsonRecord {
|
||
const createdAt = new Date(Date.parse("2026-06-18T18:00:00.000Z") + seq * 1000).toISOString();
|
||
if (seq % 5 === 0) {
|
||
return { seq, sourceSeq: seq, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "running", message: `滚动跟随采样 assistant row ${seq}\n\n- markdown 行 ${seq}\n- append 后不能闪到顶部` };
|
||
}
|
||
return {
|
||
seq,
|
||
sourceSeq: seq,
|
||
createdAt,
|
||
label: "item/commandExecution:running",
|
||
type: "commandExecution",
|
||
status: "running",
|
||
itemId: `scroll-follow-${seq}`,
|
||
command: `printf 'scroll follow row ${seq}\\n'`,
|
||
stdout: `scroll-follow stdout ${seq}\nline ${seq} keeps the row tall enough for scroll sampling\n`
|
||
};
|
||
}
|
||
|
||
function appendScrollFollowEvents(count: number): JsonRecord {
|
||
if (state.scenarioId !== "scroll-follow-long-trace") return { ok: false, status: 409, error: { code: "scenario_mismatch", scenarioId: state.scenarioId } };
|
||
const trace = state.traces.trc_scroll_follow ?? scrollFollowTrace(0);
|
||
const events = Array.isArray(trace.events) ? [...trace.events as JsonRecord[]] : [];
|
||
const appended: JsonRecord[] = [];
|
||
for (let index = 0; index < count; index += 1) {
|
||
const seq = Number(events.at(-1)?.seq ?? events.length) + 1;
|
||
const event = scrollFollowEvent(seq);
|
||
events.push(event);
|
||
appended.push(event);
|
||
}
|
||
const nextTrace = { ...trace, status: "running", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false };
|
||
state.traces.trc_scroll_follow = nextTrace;
|
||
const session = sessionById("ses_scroll_follow");
|
||
if (session) {
|
||
session.status = "running";
|
||
session.updatedAt = new Date().toISOString();
|
||
session.messages = (session.messages ?? []).map((message) => message.role === "agent" && message.traceId === "trc_scroll_follow" ? { ...message, status: "running", runnerTrace: nextTrace } : message);
|
||
}
|
||
for (const event of appended) {
|
||
broadcastSse("workbench.trace.event", { type: "trace.event", sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", traceId: "trc_scroll_follow", event, snapshot: nextTrace });
|
||
}
|
||
broadcastSse("workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_scroll_follow", threadId: "thr_scroll_follow", traceId: "trc_scroll_follow", turn: turnPayload("trc_scroll_follow") });
|
||
return { ok: true, status: "appended", appended: appended.length, eventCount: events.length };
|
||
}
|
||
|
||
function createManualSession(body: JsonRecord): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
const token = state.scenarioId === "server-authoritative-create"
|
||
? "server_created"
|
||
: state.scenarioId === "create-while-route-loading"
|
||
? "created_while_running"
|
||
: state.scenarioId === "created-session-read-failure"
|
||
? "created_read_failure"
|
||
: Date.now().toString(36);
|
||
const sessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId : `ses_${token}`;
|
||
const threadId = typeof body.threadId === "string" && body.threadId.trim() ? body.threadId : null;
|
||
return { sessionId, threadId, status: "active", startedAt: now, updatedAt: now, messageCount: 0, firstUserMessagePreview: null, messages: [] };
|
||
}
|
||
|
||
function hiddenBoundarySession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return { sessionId: "ses_cross_project", threadId: "thr_cross_project", hidden: true, status: "completed", lastTraceId: "trc_cross_project", startedAt: now, updatedAt: now, messageCount: 2, firstUserMessagePreview: "边界外 session 不应泄露", messages: [] };
|
||
}
|
||
|
||
function terminalEmptyTraceSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_terminal_empty",
|
||
threadId: "thr_terminal_empty",
|
||
status: "completed",
|
||
lastTraceId: "trc_terminal_empty",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "完成态空 Trace 文案验证",
|
||
messages: [
|
||
{ id: "msg_terminal_empty_user", role: "user", title: "用户", text: "完成态空 Trace 文案验证", status: "sent", createdAt: now, sessionId: "ses_terminal_empty", threadId: "thr_terminal_empty", turnId: "turn_terminal_empty" },
|
||
{ id: "msg_terminal_empty_agent", role: "agent", title: "Code Agent", text: "终态空 Trace 已完成。", status: "completed", createdAt: now, sessionId: "ses_terminal_empty", threadId: "thr_terminal_empty", traceId: "trc_terminal_empty", turnId: "turn_terminal_empty", runnerTrace: terminalEmptyTrace() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function terminalEmptyTrace(): JsonRecord {
|
||
return { traceId: "trc_terminal_empty", status: "completed", sessionId: "ses_terminal_empty", threadId: "thr_terminal_empty", events: [], eventCount: 0, fullTraceLoaded: true, hasMore: false, finalResponse: { text: "终态空 Trace 已完成。" } };
|
||
}
|
||
|
||
function legacyCanonicalSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_c5e1330558c94d8e",
|
||
conversationId: "cnv_c5e1330558c94d8e",
|
||
threadId: "thr_c5e1330558c94d8e",
|
||
status: "completed",
|
||
lastTraceId: "trc_40badc15523146c9",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "legacy cnv deep link canonical smoke",
|
||
messages: [
|
||
{ id: "msg_c5_user", role: "user", title: "用户", text: "legacy cnv deep link canonical smoke", status: "sent", createdAt: now, sessionId: "ses_c5e1330558c94d8e", threadId: "thr_c5e1330558c94d8e", traceId: "trc_40badc15523146c9" },
|
||
{ id: "msg_c5_agent", role: "agent", title: "Code Agent", text: "已完成:新增 Python benchmark 脚本。", status: "completed", createdAt: now, sessionId: "ses_c5e1330558c94d8e", threadId: "thr_c5e1330558c94d8e", traceId: "trc_40badc15523146c9", runnerTrace: legacyCanonicalTrace() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function legacyCanonicalTrace(): JsonRecord {
|
||
return {
|
||
traceId: "trc_40badc15523146c9",
|
||
status: "completed",
|
||
sessionId: "ses_c5e1330558c94d8e",
|
||
threadId: "thr_c5e1330558c94d8e",
|
||
events: [{ seq: 1, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", terminal: true, final: true, message: "已完成:新增 Python benchmark 脚本。" }],
|
||
eventCount: 1,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
finalResponse: { text: "已完成:新增 Python benchmark 脚本。", status: "completed" }
|
||
};
|
||
}
|
||
|
||
function markdownFinalSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_markdown_final",
|
||
conversationId: "cnv_markdown_final",
|
||
threadId: "thr_markdown_final",
|
||
status: "completed",
|
||
lastTraceId: "trc_markdown_final",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "render markdown final response",
|
||
messages: [
|
||
{ id: "msg_markdown_user", messageId: "msg_markdown_user", role: "user", title: "用户", text: "render markdown final response", status: "sent", createdAt: now, sessionId: "ses_markdown_final", threadId: "thr_markdown_final", traceId: "trc_markdown_final", turnId: "trc_markdown_final" },
|
||
{ id: "msg_markdown_agent", messageId: "msg_markdown_agent", role: "agent", title: "Code Agent", text: markdownFinalText, parts: [{ type: "text", text: markdownFinalText }], status: "completed", createdAt: now, updatedAt: now, sessionId: "ses_markdown_final", threadId: "thr_markdown_final", traceId: "trc_markdown_final", turnId: "trc_markdown_final", runnerTrace: markdownFinalTrace() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function markdownFinalTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const event = { seq: 1, sourceSeq: 88, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", terminal: true, final: true, replyAuthority: true, message: markdownFinalText };
|
||
return {
|
||
traceId: "trc_markdown_final",
|
||
status: "completed",
|
||
sessionId: "ses_markdown_final",
|
||
threadId: "thr_markdown_final",
|
||
events: [event],
|
||
eventCount: 1,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
finalResponse: { text: markdownFinalText, status: "completed" }
|
||
};
|
||
}
|
||
|
||
function terminalAssistantFinalSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
const trace = terminalAssistantFinalTrace();
|
||
return {
|
||
sessionId: "ses_terminal_assistant_final",
|
||
conversationId: "cnv_terminal_assistant_final",
|
||
threadId: "thr_terminal_assistant_final",
|
||
status: "completed",
|
||
lastTraceId: "trc_terminal_assistant_final",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "再把lua加入对比测试",
|
||
messages: [
|
||
{ id: "msg_terminal_assistant_user", messageId: "msg_terminal_assistant_user", role: "user", title: "用户", text: "再把lua加入对比测试", status: "sent", createdAt: now, sessionId: "ses_terminal_assistant_final", threadId: "thr_terminal_assistant_final", traceId: "trc_terminal_assistant_final", turnId: "trc_terminal_assistant_final" },
|
||
{ id: "msg_terminal_assistant_agent", messageId: "msg_terminal_assistant_agent", role: "agent", title: "Code Agent", text: terminalAssistantFinalText, parts: [{ type: "text", text: terminalAssistantFinalText, status: "completed" }], status: "completed", createdAt: now, updatedAt: now, sessionId: "ses_terminal_assistant_final", threadId: "thr_terminal_assistant_final", traceId: "trc_terminal_assistant_final", turnId: "trc_terminal_assistant_final", runnerTrace: trace, finalResponse: { text: terminalAssistantFinalText, status: "completed" } }
|
||
]
|
||
};
|
||
}
|
||
|
||
function terminalAssistantFinalTrace(): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const events = [
|
||
{ seq: 1, sourceSeq: 21, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "running", replyAuthority: false, final: false, terminal: false, message: "现在写 Lua 基准测试脚本。" },
|
||
{ seq: 2, sourceSeq: 28, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", replyAuthority: true, final: true, terminal: true, message: terminalAssistantFinalText },
|
||
{ seq: 3, sourceSeq: 29, createdAt, label: "agentrun:terminal:completed", type: "result", status: "completed", terminal: true, message: "AgentRun command completed." }
|
||
];
|
||
return {
|
||
traceId: "trc_terminal_assistant_final",
|
||
status: "completed",
|
||
sessionId: "ses_terminal_assistant_final",
|
||
threadId: "thr_terminal_assistant_final",
|
||
events,
|
||
eventCount: events.length,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
assistantText: terminalAssistantFinalText,
|
||
finalResponse: { text: terminalAssistantFinalText, status: "completed" }
|
||
};
|
||
}
|
||
|
||
function manyRailSession(index: number): SessionRecord {
|
||
const id = String(index).padStart(2, "0");
|
||
const now = new Date(Date.now() - (index + 1) * 1000).toISOString();
|
||
return { sessionId: `ses_many_${id}`, threadId: `thr_many_${id}`, status: "completed", lastTraceId: `trc_many_${id}`, startedAt: now, updatedAt: now, messageCount: 0, firstUserMessagePreview: `历史 session ${id}`, messages: [] };
|
||
}
|
||
|
||
function staleDetailSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return { sessionId: "ses_stale_404", threadId: "thr_stale_404", status: "active", startedAt: now, updatedAt: now, messageCount: 0, firstUserMessagePreview: "已失效的列表项", messages: [] };
|
||
}
|
||
|
||
function emptySession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return { sessionId: "ses_empty", threadId: "thr_empty", status: "active", startedAt: now, updatedAt: now, messageCount: 0, firstUserMessagePreview: "空白会话", messages: [] };
|
||
}
|
||
|
||
function staleEmptyTtlSession(): SessionRecord {
|
||
const at = "2026-06-18T16:00:00.000Z";
|
||
return { sessionId: "ses_empty_ttl", threadId: "thr_empty_ttl", status: "idle", startedAt: at, updatedAt: at, messageCount: 0, firstUserMessagePreview: null, messages: [] };
|
||
}
|
||
|
||
function freshEmptyTtlSession(): SessionRecord {
|
||
const at = "2026-06-18T16:15:30.000Z";
|
||
return { sessionId: "ses_empty_fresh", threadId: "thr_empty_fresh", status: "idle", startedAt: at, updatedAt: at, messageCount: 0, firstUserMessagePreview: null, messages: [] };
|
||
}
|
||
|
||
function staleMessagedTtlSession(): SessionRecord {
|
||
const at = "2026-06-18T15:50:00.000Z";
|
||
return {
|
||
sessionId: "ses_empty_ttl_has_message",
|
||
threadId: "thr_empty_ttl_has_message",
|
||
status: "completed",
|
||
startedAt: at,
|
||
updatedAt: at,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "超过 TTL 但已有消息,不能被 GC",
|
||
messages: [
|
||
{ id: "msg_empty_ttl_user", role: "user", title: "用户", text: "超过 TTL 但已有消息,不能被 GC", status: "sent", createdAt: at, sessionId: "ses_empty_ttl_has_message", threadId: "thr_empty_ttl_has_message" },
|
||
{ id: "msg_empty_ttl_agent", role: "agent", title: "Code Agent", text: "保留已有消息 session。", status: "completed", createdAt: at, sessionId: "ses_empty_ttl_has_message", threadId: "thr_empty_ttl_has_message" }
|
||
]
|
||
};
|
||
}
|
||
|
||
function archivedDeletedSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return { sessionId: "ses_deleted", threadId: "thr_deleted", status: "archived", startedAt: now, updatedAt: now, messageCount: 0, firstUserMessagePreview: "已删除 session", messages: [] };
|
||
}
|
||
|
||
function submitSplitSession(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_submit_split",
|
||
threadId: "thr_submit_split",
|
||
status: "idle",
|
||
lastTraceId: "trc_submit_split",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "workbench submit race probe redacted: 请只回复 OK。",
|
||
messages: [
|
||
{ id: "msg_submit_split_user", messageId: "msg_submit_split_user", role: "user", title: "用户", text: "", status: "sent", createdAt: now, sessionId: "ses_submit_split", threadId: "thr_submit_split", traceId: "trc_submit_split", turnId: "turn_submit_split" },
|
||
{ id: "msg_submit_split_agent", messageId: "msg_submit_split_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_submit_split", threadId: "thr_submit_split", traceId: "trc_submit_split", turnId: "turn_submit_split", runnerTrace: { traceId: "trc_submit_split", status: "running", sessionId: "ses_submit_split", threadId: "thr_submit_split", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false } }
|
||
]
|
||
};
|
||
}
|
||
|
||
function submitSplitTrace(): JsonRecord {
|
||
return { traceId: "trc_submit_split", status: "running", sessionId: "ses_submit_split", threadId: "thr_submit_split", turnId: "turn_submit_split", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false };
|
||
}
|
||
|
||
function crossSessionLateA(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_late_A",
|
||
threadId: "thr_late_A",
|
||
status: "completed",
|
||
lastTraceId: "trc_late_A",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "写一个python脚本测试性能",
|
||
messages: [
|
||
{ id: "msg_late_A_user", messageId: "msg_late_A_user", role: "user", title: "用户", text: "写一个python脚本测试性能", status: "sent", createdAt: now, sessionId: "ses_late_A", threadId: "thr_late_A", traceId: "trc_late_A", turnId: "turn_late_A" },
|
||
{ id: "msg_late_A_agent", messageId: "msg_late_A_agent", role: "agent", title: "Code Agent", text: "已写好 perf_test.py,脚本可用。", status: "completed", createdAt: now, sessionId: "ses_late_A", threadId: "thr_late_A", traceId: "trc_late_A", turnId: "turn_late_A", runnerTrace: crossSessionLateTraceA() }
|
||
]
|
||
};
|
||
}
|
||
|
||
function crossSessionLateB(): SessionRecord {
|
||
const now = new Date().toISOString();
|
||
return {
|
||
sessionId: "ses_late_B",
|
||
threadId: "thr_late_B",
|
||
status: "active",
|
||
lastTraceId: "trc_late_A",
|
||
startedAt: now,
|
||
updatedAt: now,
|
||
messageCount: 2,
|
||
firstUserMessagePreview: "B session waiting",
|
||
messages: [
|
||
{ id: "msg_late_B_user", messageId: "msg_late_B_user", role: "user", title: "用户", text: "B session waiting", status: "sent", createdAt: now, sessionId: "ses_late_B", threadId: "thr_late_B", traceId: "trc_late_A", turnId: "turn_late_B" },
|
||
{ id: "msg_late_B_agent", messageId: "msg_late_B_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_late_B", threadId: "thr_late_B", traceId: "trc_late_A", turnId: "turn_late_B", runnerTrace: { traceId: "trc_late_A", status: "running", sessionId: "ses_late_B", threadId: "thr_late_B", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false } }
|
||
]
|
||
};
|
||
}
|
||
|
||
function crossSessionLateTraceA(): JsonRecord {
|
||
const now = new Date().toISOString();
|
||
const finalText = "已写好 perf_test.py,脚本可用。";
|
||
const event = { seq: 1, createdAt: now, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", replyAuthority: true, final: true, message: finalText, terminal: true };
|
||
return { traceId: "trc_late_A", status: "completed", sessionId: "ses_late_A", threadId: "thr_late_A", turnId: "turn_late_A", events: [event], eventCount: 1, fullTraceLoaded: true, hasMore: false, finalResponse: { text: finalText, status: "completed" }, assistantText: finalText };
|
||
}
|
||
|
||
function sessionDetailDelayMs(sessionId: string): number {
|
||
if (state.scenarioId === "cross-session-late-events" && sessionId === "ses_late_A") return 900;
|
||
if (state.scenarioId === "create-while-route-loading" && (sessionId === "ses_running" || sessionId === "ses_created_while_running")) return 4_000;
|
||
return state.sessionDetailDelayMs;
|
||
}
|
||
|
||
function acceptChatTurn(body: JsonRecord): JsonRecord {
|
||
const sessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId : state.selectedSessionId;
|
||
const session = sessionById(sessionId) ?? createManualSession({ sessionId });
|
||
if (!state.sessions.includes(session)) state.sessions.unshift(session);
|
||
const threadId = typeof body.threadId === "string" && body.threadId.trim() ? body.threadId : null;
|
||
const traceId = state.scenarioId === "live-rest-backfill-without-terminal-sse"
|
||
? "trc_live_backfill"
|
||
: typeof body.traceId === "string" && body.traceId.trim() ? body.traceId : `trc_${Date.now().toString(16)}`;
|
||
const message = typeof body.message === "string" ? body.message : "";
|
||
const now = new Date().toISOString();
|
||
session.status = "running";
|
||
session.lastTraceId = traceId;
|
||
session.updatedAt = now;
|
||
session.firstUserMessagePreview = session.firstUserMessagePreview ?? message;
|
||
session.messageCount = (session.messages?.length ?? 0) + 2;
|
||
session.messages = [
|
||
...(session.messages ?? []),
|
||
{ id: `msg_${traceId}_user`, role: "user", title: "用户", text: message, status: "sent", createdAt: now, sessionId, threadId, traceId },
|
||
{ id: `msg_${traceId}_agent`, role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId, threadId, traceId, runnerTrace: { traceId, status: "running", sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false } }
|
||
];
|
||
if (state.scenarioId === "live-rest-backfill-without-terminal-sse") {
|
||
state.liveBackfillTraceId = traceId;
|
||
state.liveBackfillReadyAtMs = Date.now() + 2_000;
|
||
state.traces[traceId] = liveBackfillEarlyTrace(sessionId, threadId, traceId);
|
||
scheduleLiveBackfillProjection(sessionId, threadId, traceId, state.liveBackfillReadyAtMs);
|
||
} else {
|
||
state.traces[traceId] = { traceId, status: "running", sessionId, threadId, events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false };
|
||
}
|
||
return { status: "running", accepted: true, running: true, terminal: false, traceId, sessionId, threadId, turnId: traceId };
|
||
}
|
||
|
||
function liveBackfillEarlyTrace(sessionId: string, threadId: string | null, traceId: string): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const agentRun = liveBackfillAgentRun("pending", null, 4);
|
||
return {
|
||
traceId,
|
||
status: "running",
|
||
running: true,
|
||
terminal: false,
|
||
sessionId,
|
||
threadId,
|
||
agentRun,
|
||
events: [
|
||
{ seq: 1, sourceSeq: 1, createdAt, label: "agentrun:request:accepted", type: "backend_status", status: "running", payload: { commandId: agentRun.commandId } },
|
||
{ seq: 2, sourceSeq: 2, createdAt, label: "agentrun:run:created", type: "backend_status", status: "running", payload: { runId: agentRun.runId, runStatus: agentRun.runStatus } },
|
||
{ seq: 3, sourceSeq: 3, createdAt, label: "agentrun:command:created", type: "backend_status", status: "running", payload: { commandId: agentRun.commandId, commandState: agentRun.commandState } },
|
||
{ seq: 4, sourceSeq: 4, createdAt, label: "agentrun:runner-job:created", type: "backend_status", status: "running", payload: { commandId: agentRun.commandId, jobName: agentRun.jobName, namespace: agentRun.namespace } }
|
||
],
|
||
eventCount: 4,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
traceSummary: { source: "agentrun-events", agentRun, valuesPrinted: false }
|
||
};
|
||
}
|
||
|
||
function liveBackfillCompletedTrace(sessionId: string, threadId: string | null, traceId: string): JsonRecord {
|
||
const createdAt = new Date().toISOString();
|
||
const finalText = "fake AgentRun completed after detail backfill without terminal SSE.";
|
||
const agentRun = liveBackfillAgentRun("completed", "completed", 23);
|
||
const events = [
|
||
...(liveBackfillEarlyTrace(sessionId, threadId, traceId).events as JsonRecord[]),
|
||
{ seq: 5, sourceSeq: 21, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", message: finalText, payload: { commandId: agentRun.commandId } },
|
||
{ seq: 6, sourceSeq: 22, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", stdout: "OK\n", payload: { commandId: agentRun.commandId, exitCode: 0 } },
|
||
{ seq: 7, sourceSeq: 23, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true, payload: { commandId: agentRun.commandId, phase: "command-terminal", terminalStatus: "completed" } }
|
||
];
|
||
return {
|
||
traceId,
|
||
status: "completed",
|
||
running: false,
|
||
terminal: true,
|
||
sessionId,
|
||
threadId,
|
||
agentRun,
|
||
events,
|
||
eventCount: events.length,
|
||
fullTraceLoaded: true,
|
||
hasMore: false,
|
||
finalResponse: { text: finalText, status: "completed" },
|
||
traceSummary: { source: "agentrun-command-result", terminalStatus: "completed", agentRun, valuesPrinted: false }
|
||
};
|
||
}
|
||
|
||
function liveBackfillAgentRun(commandState: string, terminalStatus: string | null, lastSeq: number): JsonRecord {
|
||
return {
|
||
adapter: "agentrun-v01",
|
||
runId: "run_live_backfill_issue1555",
|
||
commandId: "cmd_live_backfill_issue1555",
|
||
status: commandState,
|
||
runStatus: "claimed",
|
||
commandState,
|
||
terminalStatus,
|
||
runnerStatus: "running",
|
||
namespace: "agentrun-v02",
|
||
jobName: "agentrun-v01-runner-live-backfill-issue1555",
|
||
lastSeq,
|
||
valuesPrinted: false
|
||
};
|
||
}
|
||
|
||
function scheduleLiveBackfillProjection(sessionId: string, threadId: string | null, traceId: string, readyAtMs: number): void {
|
||
const scenarioId = state.scenarioId;
|
||
setTimeout(() => {
|
||
if (state.scenarioId !== scenarioId || state.liveBackfillTraceId !== traceId) return;
|
||
commitLiveBackfillProjection(sessionId, threadId, traceId);
|
||
}, Math.max(0, readyAtMs - Date.now()));
|
||
}
|
||
|
||
function commitLiveBackfillProjection(sessionId: string, threadId: string | null, traceId: string): void {
|
||
const completed = liveBackfillCompletedTrace(sessionId, threadId, traceId);
|
||
const finalText = typeof (completed.finalResponse as JsonRecord | undefined)?.text === "string" ? String((completed.finalResponse as JsonRecord).text) : "";
|
||
state.traces[traceId] = completed;
|
||
const session = sessionById(sessionId);
|
||
if (!session) return;
|
||
session.status = "completed";
|
||
session.updatedAt = new Date().toISOString();
|
||
session.messages = (session.messages ?? []).map((message) => {
|
||
if (message.role !== "agent" || message.traceId !== traceId) return message;
|
||
return { ...message, text: finalText, status: "completed", runnerTrace: completed };
|
||
});
|
||
}
|
||
|
||
function sessionSummary(session: SessionRecord): SessionRecord {
|
||
const { messages: _messages, hidden: _hidden, ...rest } = session;
|
||
const status = typeof rest.status === "string" ? rest.status : "unknown";
|
||
return { ...rest, status, running: isRunningStatus(status), terminal: isTerminalStatus(status) } as SessionRecord;
|
||
}
|
||
|
||
function isRunningStatus(status: string): boolean {
|
||
return ["active", "accepted", "pending", "running"].includes(status);
|
||
}
|
||
|
||
function isTerminalStatus(status: string): boolean {
|
||
return ["blocked", "canceled", "cancelled", "completed", "failed", "timeout"].includes(status);
|
||
}
|
||
|
||
function workbenchSessionListPayload(url: URL): JsonRecord {
|
||
if (url.searchParams.has("projectId") || url.searchParams.has("workspaceId")) return { ok: false, status: 400, error: { code: "workbench_authority_removed" } };
|
||
const includeRouteId = url.searchParams.get("includeSessionId") || "";
|
||
const includeSessionId = includeRouteId ? canonicalSessionId(includeRouteId) : "";
|
||
const limit = boundedPageLimit(url.searchParams.get("limit"));
|
||
const offset = cursorOffset(url.searchParams.get("cursor") || url.searchParams.get("after"));
|
||
const visible = state.sessions.filter((item) => !item.hidden && (item.status !== "archived" || item.sessionId === includeSessionId));
|
||
const authorityVisible = state.scenarioId === "create-while-route-loading" && includeSessionId === "ses_running"
|
||
? visible.filter((item) => item.sessionId !== "ses_created_while_running")
|
||
: visible;
|
||
const listed = state.listOmitSelected && includeSessionId ? authorityVisible.filter((item) => item.sessionId !== includeSessionId) : authorityVisible;
|
||
const page = listed.slice(offset, offset + limit);
|
||
const included = includeSessionId && !page.some((session) => session.sessionId === includeSessionId) ? listed.find((session) => session.sessionId === includeSessionId) ?? null : null;
|
||
const summaries = [...(included ? [included] : []), ...page].map((session) => sessionSummary(session));
|
||
const hasMore = offset + limit < listed.length;
|
||
return { ok: true, status: "succeeded", contractVersion: "workbench-sessions-v1", sessions: summaries, count: summaries.length, total: listed.length, cursor: offset > 0 ? cursorFromOffset(offset) : null, hasMore, nextCursor: hasMore ? cursorFromOffset(offset + limit) : null };
|
||
}
|
||
|
||
function boundedPageLimit(value: string | null): number {
|
||
const parsed = Number.parseInt(value ?? "", 10);
|
||
return Math.min(100, Math.max(1, Number.isInteger(parsed) && parsed > 0 ? parsed : 50));
|
||
}
|
||
|
||
function cursorOffset(value: string | null): number {
|
||
const text = typeof value === "string" ? value.trim() : "";
|
||
if (!text) return 0;
|
||
if (text.startsWith("idx:")) return Math.max(0, Number.parseInt(text.slice(4), 10) || 0);
|
||
return Math.max(0, Number.parseInt(text, 10) || 0);
|
||
}
|
||
|
||
function cursorFromOffset(offset: number): string {
|
||
return `idx:${Math.max(0, Math.trunc(offset))}`;
|
||
}
|
||
|
||
function workbenchSessionMessagesPayload(session: SessionRecord, url: URL): JsonRecord {
|
||
const messages = Array.isArray(session.messages) ? session.messages : [];
|
||
const requestedLimit = Number(url.searchParams.get("limit") ?? messages.length);
|
||
const limit = Math.max(1, Number.isFinite(requestedLimit) ? Math.trunc(requestedLimit) : messages.length || 100);
|
||
const page = messages.slice(0, limit);
|
||
return { ok: true, status: "succeeded", sessionId: session.sessionId, messages: page, count: page.length, total: messages.length, hasMore: messages.length > page.length };
|
||
}
|
||
|
||
function sessionById(id: string): SessionRecord | null {
|
||
return state.sessions.find((session) => session.sessionId === id) ?? null;
|
||
}
|
||
|
||
function canonicalSessionId(id: string): string {
|
||
const text = id.trim();
|
||
if (!text.startsWith("cnv_")) return text;
|
||
const conversation = state.sessions.find((session) => session.conversationId === text);
|
||
return conversation?.sessionId ?? `ses_${text.slice("cnv_".length)}`;
|
||
}
|
||
|
||
function visibleSessionById(id: string, options: { includeArchived?: boolean } = {}): SessionRecord | null {
|
||
const session = sessionById(id);
|
||
if (!session || session.hidden) return null;
|
||
if (session.status === "archived" && !options.includeArchived) return null;
|
||
return session;
|
||
}
|
||
|
||
function sessionPayload(sessionId: string): JsonRecord {
|
||
const session = sessionById(sessionId);
|
||
if (state.scenarioId === "terminal-turn-stale-session-active" && sessionId === "ses_completed") return { sessionId, status: "active", lastTraceId: "trc_completed", updatedAt: new Date().toISOString() };
|
||
return { sessionId, status: session?.status ?? "unknown", lastTraceId: session?.lastTraceId ?? null, updatedAt: new Date().toISOString() };
|
||
}
|
||
|
||
function turnPayload(traceId: string): JsonRecord {
|
||
if (state.scenarioId === "live-rest-backfill-without-terminal-sse" && traceId === state.liveBackfillTraceId) {
|
||
const session = state.sessions.find((item) => item.lastTraceId === traceId) ?? state.sessions[0];
|
||
const sessionId = session?.sessionId ?? state.selectedSessionId;
|
||
const threadId = typeof session?.threadId === "string" ? session.threadId : null;
|
||
return state.traces[traceId] ?? liveBackfillEarlyTrace(sessionId, threadId, traceId);
|
||
}
|
||
const trace = state.traces[traceId] ?? { traceId, status: "unknown", events: [] };
|
||
if (state.scenarioId === "tool-completed-projection-running" && traceId === "trc_running") return { ...trace, traceId, status: "running", running: true, terminal: false, projectionStatus: "projecting", trace: { status: trace.status, eventCount: trace.eventCount ?? 0 } };
|
||
if (state.scenarioId === "terminal-turn-stale-session-active" && traceId === "trc_completed") return { ...trace, traceId, status: "active", running: true, terminal: false, trace: { status: "completed", eventCount: trace.eventCount ?? 0 } };
|
||
const status = String(trace.status ?? "unknown");
|
||
return { ...trace, traceId, status, running: ["running", "pending", "accepted"].includes(status), terminal: ["completed", "failed", "blocked", "timeout", "canceled"].includes(status) };
|
||
}
|
||
|
||
function workbenchTurnPayload(traceId: string): JsonRecord {
|
||
const turn = turnPayload(traceId);
|
||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", turn, ...projectionEnvelope(turn) };
|
||
}
|
||
|
||
function tracePayload(traceId: string, url: URL): JsonRecord {
|
||
const turn = turnPayload(traceId);
|
||
const events = Array.isArray(turn.events) ? turn.events as JsonRecord[] : [];
|
||
const afterProjectedSeq = traceAfterProjectedSeq(url);
|
||
const requestedLimit = Number(url.searchParams.get("limit") ?? events.length);
|
||
const limit = Math.max(1, Number.isFinite(requestedLimit) ? Math.trunc(requestedLimit) : events.length || 100);
|
||
const indexedEvents = events.map((event, index) => {
|
||
const projectedSeq = nonNegativeInteger(event.projectedSeq ?? event.seq, index + 1);
|
||
return { event: { ...event, seq: projectedSeq, projectedSeq }, projectedSeq };
|
||
});
|
||
const filtered = afterProjectedSeq > 0 ? indexedEvents.filter((item) => item.projectedSeq > afterProjectedSeq) : indexedEvents;
|
||
const pageRecords = filtered.slice(0, limit);
|
||
const page = pageRecords.map((item) => item.event);
|
||
const firstProjectedSeq = pageRecords.length > 0 ? pageRecords[0]?.projectedSeq ?? null : null;
|
||
const lastProjectedSeq = pageRecords.length > 0 ? pageRecords.at(-1)?.projectedSeq ?? null : null;
|
||
const hasMore = filtered.length > page.length;
|
||
return { ...turn, events: page, eventCount: events.length, hasMore, fullTraceLoaded: !hasMore, nextProjectedSeq: page.length > 0 ? lastProjectedSeq : afterProjectedSeq, nextCursor: hasMore && lastProjectedSeq ? `projected:${lastProjectedSeq}` : null, range: { afterProjectedSeq, fromProjectedSeq: firstProjectedSeq, toProjectedSeq: lastProjectedSeq, limit, returned: page.length, total: events.length } };
|
||
}
|
||
|
||
function traceAfterProjectedSeq(url: URL): number {
|
||
const cursor = url.searchParams.get("cursor") ?? "";
|
||
if (cursor.startsWith("projected:")) return nonNegativeInteger(cursor.slice("projected:".length), 0);
|
||
return nonNegativeInteger(url.searchParams.get("afterProjectedSeq"), 0);
|
||
}
|
||
|
||
function workbenchTracePayload(traceId: string, url: URL): JsonRecord {
|
||
const payload = tracePayload(traceId, url);
|
||
if (state.scenarioId === "tool-completed-projection-running" && traceId === "trc_running") {
|
||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: "completed", timing: payload.timing, startedAt: payload.startedAt, lastEventAt: payload.lastEventAt, finishedAt: payload.finishedAt, durationMs: payload.durationMs, events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextProjectedSeq: payload.nextProjectedSeq, nextCursor: payload.nextCursor, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, projectionStatus: "projecting", terminalEvidence: null, finalResponse: null, traceSummary: payload.traceSummary, retention: payload.retention };
|
||
}
|
||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: payload.status, timing: payload.timing, startedAt: payload.startedAt, lastEventAt: payload.lastEventAt, finishedAt: payload.finishedAt, durationMs: payload.durationMs, events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextProjectedSeq: payload.nextProjectedSeq, nextCursor: payload.nextCursor, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, terminalEvidence: payload.terminalEvidence, finalResponse: payload.finalResponse, traceSummary: payload.traceSummary, retention: payload.retention, ...projectionEnvelope(payload) };
|
||
}
|
||
|
||
function projectionEnvelope(payload: JsonRecord): JsonRecord {
|
||
const projection = payload.projection && typeof payload.projection === "object" ? payload.projection as JsonRecord : null;
|
||
if (!projection) return {};
|
||
return {
|
||
projection,
|
||
projectionStatus: projection.projectionStatus,
|
||
projectionHealth: projection.projectionHealth,
|
||
lastProjectedSeq: projection.lastProjectedSeq,
|
||
sourceRunId: projection.sourceRunId,
|
||
sourceCommandId: projection.sourceCommandId,
|
||
staleMs: projection.staleMs,
|
||
blocker: projection.blocker
|
||
};
|
||
}
|
||
|
||
function sse(request: IncomingMessage, response: ServerResponse, url: URL): void {
|
||
response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache", connection: "keep-alive" });
|
||
sseClients.add(response);
|
||
request.on("close", () => sseClients.delete(response));
|
||
writeSse(response, "workbench.connected", { type: "connected", sessionId: url.searchParams.get("sessionId") });
|
||
if (state.scenarioId === "projection-sse-error") {
|
||
const scenarioId = state.scenarioId;
|
||
setTimeout(() => {
|
||
if (state.scenarioId !== scenarioId) return;
|
||
const projection = projectionDiagnostic({ traceId: "trc_projection_sse_error", code: "workbench_sse_projection_error", message: "SSE 投影事件异常,实时状态暂不可见。", category: "realtime-error", layer: "workbench-sse", runId: "run_projection_sse_error", commandId: "cmd_projection_sse_error" });
|
||
writeSse(response, "workbench.error", { type: "error", sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", traceId: "trc_projection_sse_error", projection, ...projectionEnvelope({ projection }), error: projection.blocker });
|
||
}, 350);
|
||
}
|
||
if (state.scenarioId === "cross-session-late-events") {
|
||
const scenarioId = state.scenarioId;
|
||
setTimeout(() => {
|
||
if (state.scenarioId !== scenarioId) return;
|
||
const trace = crossSessionLateTraceA();
|
||
const event = (trace.events as JsonRecord[])[0];
|
||
writeSse(response, "workbench.trace.event", { type: "trace.event", sessionId: "ses_late_A", threadId: "thr_late_A", traceId: "trc_late_A", event, snapshot: trace });
|
||
writeSse(response, "workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_late_A", threadId: "thr_late_A", traceId: "trc_late_A", turn: trace });
|
||
}, 650);
|
||
return;
|
||
}
|
||
if (!state.terminalScript) return;
|
||
const scenarioId = state.scenarioId;
|
||
setTimeout(() => {
|
||
if (state.scenarioId !== scenarioId) return;
|
||
if (state.scenarioId === "terminal-canceled-late-timing") {
|
||
const trace = applyTerminalCanceledLateTiming(12_000, "2026-06-17T09:39:12.000Z");
|
||
const event = Array.isArray(trace?.events) ? trace.events.at(-1) as JsonRecord : null;
|
||
if (event) writeSse(response, "workbench.trace.event", { type: "trace.event", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", event, snapshot: trace });
|
||
const message = runningAgentMessageSnapshot();
|
||
if (message) writeSse(response, "workbench.message.snapshot", { type: "message.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", message });
|
||
writeSse(response, "workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", turn: turnPayload("trc_running") });
|
||
setTimeout(() => {
|
||
if (state.scenarioId !== scenarioId) return;
|
||
applyTerminalCanceledLateTiming(14_000, "2026-06-17T09:39:14.000Z");
|
||
const late = runningAgentMessageSnapshot();
|
||
if (late) writeSse(response, "workbench.message.snapshot", { type: "message.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", message: late });
|
||
writeSse(response, "workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", turn: turnPayload("trc_running") });
|
||
}, 650);
|
||
return;
|
||
}
|
||
if (state.scenarioId === "terminal-completed-no-final-response") {
|
||
const trace = completedNoFinalTrace();
|
||
const event = (trace.events as JsonRecord[]).at(-1) ?? { status: "completed", terminal: true };
|
||
writeSse(response, "workbench.trace.event", { type: "trace.event", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", event, snapshot: trace });
|
||
finishRunningNoFinalResponse(trace);
|
||
const message = runningAgentMessageSnapshot();
|
||
if (message) writeSse(response, "workbench.message.snapshot", { type: "message.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", message });
|
||
writeSse(response, "workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", turn: turnPayload("trc_running") });
|
||
return;
|
||
}
|
||
const terminalStatus = state.terminalFailureScript ? "failed" : "completed";
|
||
const terminalText = state.terminalFailureScript ? "恢复后失败:缺少受控依赖。" : state.scenarioId === "progress-only-final-response" ? finalOnlyText : "事件重放后完成。";
|
||
const event = { seq: 3, createdAt: new Date().toISOString(), label: "agentrun:assistant:message", type: "assistant_message", status: terminalStatus, replyAuthority: true, final: true, message: terminalText, terminal: true };
|
||
writeSse(response, "workbench.trace.event", { type: "trace.event", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", event, snapshot: { traceId: "trc_running", sessionId: "ses_running", threadId: "thr_running", status: terminalStatus, events: [event], eventCount: 3, fullTraceLoaded: true, finalResponse: { text: terminalText, status: terminalStatus } } });
|
||
finishRunningSession(terminalStatus, terminalText);
|
||
const message = runningAgentMessageSnapshot();
|
||
if (message) writeSse(response, "workbench.message.snapshot", { type: "message.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", message });
|
||
writeSse(response, "workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", turn: turnPayload("trc_running") });
|
||
}, state.scenarioId === "progress-only-final-response" ? 5_000 : state.scenarioId === "event-replay" ? 1_500 : 350);
|
||
}
|
||
|
||
async function debugFakeSse(request: IncomingMessage, response: ServerResponse, url: URL, method: string): Promise<void> {
|
||
const suffix = url.pathname.replace(/^\/v1\/workbench\/debug\/fake-sse/u, "");
|
||
if (suffix === "" || suffix === "/sequences") return json(response, 200, debugFakeSsePayload());
|
||
if (suffix === "/events") {
|
||
response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache", connection: "keep-alive" });
|
||
debugSseClients.add(response);
|
||
request.on("close", () => debugSseClients.delete(response));
|
||
return writeSse(response, "workbench.connected", { type: "connected", queue: debugFakeSseQueueSummary() });
|
||
}
|
||
if (method !== "POST") return json(response, 405, { ok: false, error: { code: "method_not_allowed" } });
|
||
if (suffix === "/reset") {
|
||
debugFakeSseQueue = createDebugFakeSseQueue();
|
||
return json(response, 200, debugFakeSsePayload());
|
||
}
|
||
if (suffix === "/append") {
|
||
const body = await readJson(request);
|
||
const events = Array.isArray(body.events) ? body.events.filter((event): event is JsonRecord => Boolean(event && typeof event === "object" && !Array.isArray(event))) : [];
|
||
debugFakeSseQueue.events.push(...events.map((event, index) => debugRealtimeEvent(event, debugFakeSseQueue.events.length + index + 1)));
|
||
return json(response, 200, { ok: true, appended: events.length, queue: debugFakeSseQueueSummary() });
|
||
}
|
||
if (suffix === "/next") return json(response, 200, { ok: true, delivered: debugDeliverNext(), queue: debugFakeSseQueueSummary() });
|
||
if (suffix === "/run") {
|
||
let count = 0;
|
||
while (debugFakeSseQueue.cursor < debugFakeSseQueue.events.length) {
|
||
debugDeliverNext();
|
||
count += 1;
|
||
}
|
||
return json(response, 200, { ok: true, delivered: { ok: true, count }, queue: debugFakeSseQueueSummary() });
|
||
}
|
||
return json(response, 404, { ok: false, error: { code: "debug_fake_sse_route_not_found" } });
|
||
}
|
||
|
||
function debugDeliverNext(): JsonRecord {
|
||
const event = debugFakeSseQueue.events[debugFakeSseQueue.cursor] as JsonRecord | undefined;
|
||
if (!event) return { ok: false, reason: "queue-empty", deliveredTo: 0 };
|
||
debugFakeSseQueue.cursor += 1;
|
||
const eventName = debugEventName(event);
|
||
for (const client of Array.from(debugSseClients)) writeSse(client, eventName, event);
|
||
return { ok: true, eventName, eventType: event.type, traceId: event.traceId, deliveredTo: debugSseClients.size };
|
||
}
|
||
|
||
function debugFakeSsePayload(): JsonRecord {
|
||
return { ok: true, contractVersion: "workbench-debug-fake-sse-v1", sequences: [{ sequenceId: "trace-card-basic", label: "Trace card basic", eventCount: debugFakeSseQueue.events.length }], queue: debugFakeSseQueueSummary() };
|
||
}
|
||
|
||
function debugFakeSseQueueSummary(): JsonRecord {
|
||
return { queueId: "trace-card", sequenceId: "trace-card-basic", cursor: debugFakeSseQueue.cursor, eventCount: debugFakeSseQueue.events.length, remaining: Math.max(0, debugFakeSseQueue.events.length - debugFakeSseQueue.cursor), connectedClients: debugSseClients.size, updatedAt: new Date().toISOString() };
|
||
}
|
||
|
||
function createDebugFakeSseQueue(): { cursor: number; events: JsonRecord[] } {
|
||
const sessionId = "ses_debug_fake_sse";
|
||
const threadId = "thr_debug_fake_sse";
|
||
const traceId = "trc_debug_fake_sse";
|
||
const finalText = "Fake SSE Trace 卡片渲染完成。";
|
||
const events = [
|
||
{ traceId, projectedSeq: 1, createdAt: "2026-07-09T08:00:01.000Z", label: "assistant:message", type: "assistant_message", status: "running", message: "读取 Workbench 组件和 Trace 渲染上下文。", elapsedMs: 1000 },
|
||
{ traceId, projectedSeq: 2, createdAt: "2026-07-09T08:00:03.000Z", label: "agentrun:tool:completed", type: "tool_call", status: "completed", toolName: "commandExecution", itemId: "tool_debug_rg", command: "rg -n \"TraceTimeline|message-card\" web/hwlab-cloud-web/src", stdoutSummary: "TraceTimeline.vue\\nConversationPanel.vue\\nWorkbenchMessageCard.vue", exitCode: 0, elapsedMs: 3000 },
|
||
{ traceId, projectedSeq: 3, createdAt: "2026-07-09T08:00:05.000Z", label: "assistant:message", type: "assistant_message", status: "running", message: "Trace 卡片已收到 tool 事件,继续等待 terminal snapshot。", elapsedMs: 5000 },
|
||
{ traceId, projectedSeq: 4, createdAt: "2026-07-09T08:00:07.000Z", label: "assistant:completed", type: "assistant_message", status: "completed", message: finalText, final: true, terminal: true, replyAuthority: true, elapsedMs: 7000 },
|
||
{ traceId, projectedSeq: 5, createdAt: "2026-07-09T08:00:07.500Z", label: "turn:completed", type: "completion", status: "completed", terminal: true, elapsedMs: 7500 }
|
||
];
|
||
return {
|
||
cursor: 0,
|
||
events: [
|
||
debugRealtimeEvent({ type: "message.snapshot", sessionId, threadId, traceId, message: debugAgentMessage(sessionId, threadId, traceId, "running", []) }, 1),
|
||
...events.slice(0, 3).map((event, index) => debugRealtimeEvent({ type: "trace.event", sessionId, threadId, traceId, event, snapshot: debugTraceSnapshot(sessionId, threadId, traceId, "running", events.slice(0, index + 1)) }, index + 2)),
|
||
debugRealtimeEvent({ type: "trace.event", sessionId, threadId, traceId, event: events[3], snapshot: debugTraceSnapshot(sessionId, threadId, traceId, "completed", events.slice(0, 4)) }, 5),
|
||
debugRealtimeEvent({ type: "message.snapshot", sessionId, threadId, traceId, message: debugAgentMessage(sessionId, threadId, traceId, "completed", events) }, 6)
|
||
]
|
||
};
|
||
}
|
||
|
||
function debugRealtimeEvent(event: JsonRecord, version: number): JsonRecord {
|
||
const type = String(event.type ?? "trace.event");
|
||
const traceId = String(event.traceId ?? "trc_debug_fake_sse");
|
||
const message = event.message && typeof event.message === "object" ? event.message as JsonRecord : null;
|
||
return {
|
||
contractVersion: "workbench-events-v1",
|
||
realtimeAuthority: "workbench-realtime-authority-v2",
|
||
sessionId: event.sessionId ?? "ses_debug_fake_sse",
|
||
threadId: event.threadId ?? "thr_debug_fake_sse",
|
||
traceId,
|
||
...event,
|
||
type,
|
||
entity: event.entity ?? { family: type === "message.snapshot" ? "messages" : "traceEvents", id: type === "message.snapshot" ? message?.messageId ?? "msg_debug_agent" : `${traceId}:${version}`, version, projectionRevision: `debug-${version}`, authority: "workbench-realtime-authority-v2" },
|
||
cursor: event.cursor ?? { traceSeq: version, outboxSeq: version }
|
||
};
|
||
}
|
||
|
||
function debugAgentMessage(sessionId: string, threadId: string, traceId: string, status: string, events: JsonRecord[]): JsonRecord {
|
||
const finalText = "Fake SSE Trace 卡片渲染完成。";
|
||
return { id: "msg_debug_agent", messageId: "msg_debug_agent", role: "agent", title: "Code Agent", text: status === "completed" ? finalText : "", status, createdAt: "2026-07-09T08:00:00.000Z", updatedAt: "2026-07-09T08:00:07.000Z", sessionId, threadId, traceId, turnId: traceId, runnerTrace: debugTraceSnapshot(sessionId, threadId, traceId, status, events) };
|
||
}
|
||
|
||
function debugTraceSnapshot(sessionId: string, threadId: string, traceId: string, status: string, events: JsonRecord[]): JsonRecord {
|
||
const finalText = "Fake SSE Trace 卡片渲染完成。";
|
||
return { traceId, sessionId, threadId, status, events, eventCount: events.length, fullTraceLoaded: status === "completed", hasMore: false, startedAt: "2026-07-09T08:00:00.000Z", lastEventAt: events.at(-1)?.createdAt ?? "2026-07-09T08:00:00.000Z", finishedAt: status === "completed" ? "2026-07-09T08:00:07.000Z" : null, durationMs: status === "completed" ? 7000 : null, finalResponse: status === "completed" ? { text: finalText, status: "completed" } : undefined };
|
||
}
|
||
|
||
function debugEventName(event: JsonRecord): string {
|
||
if (event.type === "message.snapshot") return "workbench.message.snapshot";
|
||
if (event.type === "trace.snapshot") return "workbench.trace.snapshot";
|
||
if (event.type === "turn.snapshot") return "workbench.turn.snapshot";
|
||
return event.type === "error" ? "workbench.error" : "workbench.trace.event";
|
||
}
|
||
|
||
function broadcastSse(eventName: string, payload: JsonRecord): void {
|
||
for (const client of Array.from(sseClients)) writeSse(client, eventName, payload);
|
||
}
|
||
|
||
function writeSse(response: ServerResponse, eventName: string, payload: JsonRecord): void {
|
||
response.write(`event: ${eventName}\n`);
|
||
response.write(`data: ${JSON.stringify(payload)}\n\n`);
|
||
}
|
||
|
||
function finishRunningSession(status: "completed" | "failed", text: string): void {
|
||
const session = sessionById("ses_running");
|
||
if (!session) return;
|
||
session.status = status;
|
||
session.updatedAt = new Date().toISOString();
|
||
const trace = state.traces.trc_running ?? { traceId: "trc_running", status: "running", events: [] };
|
||
const finalEvent = { seq: 4, createdAt: new Date().toISOString(), label: `agentrun:terminal:${status}`, status, terminal: true };
|
||
const events = [...(Array.isArray(trace.events) ? trace.events as JsonRecord[] : []), finalEvent];
|
||
state.traces.trc_running = { ...trace, status, events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, assistantText: text, finalResponse: { text, status } };
|
||
session.messages = (session.messages ?? []).map((message) => message.role === "agent" ? { ...message, text, status, runnerTrace: state.traces.trc_running } : message);
|
||
}
|
||
|
||
function finishRunningNoFinalResponse(trace: JsonRecord = completedNoFinalTrace()): void {
|
||
state.traces.trc_running = trace;
|
||
const session = sessionById("ses_running");
|
||
if (!session) return;
|
||
session.status = "completed";
|
||
session.updatedAt = new Date().toISOString();
|
||
session.messages = (session.messages ?? []).map((message) => message.role === "agent" ? { ...message, text: "", status: "completed", runnerTrace: trace } : message);
|
||
}
|
||
|
||
function runningAgentMessageSnapshot(): JsonRecord | null {
|
||
const session = sessionById("ses_running");
|
||
const message = session?.messages?.find((item) => item.role === "agent" && item.traceId === "trc_running");
|
||
return message ?? null;
|
||
}
|
||
|
||
function stateSummary(): JsonRecord {
|
||
return { scenarioId: state.scenarioId, selectedSessionId: state.selectedSessionId, requestLedger: state.requestLedger, legacyRequestLedger: state.legacyRequestLedger, chatRequests: state.chatRequests, staleTraceId: state.staleTraceId, sessions: state.sessions.map((item) => ({ sessionId: item.sessionId, threadId: item.threadId, status: item.status, lastTraceId: item.lastTraceId, hidden: item.hidden === true })) };
|
||
}
|
||
|
||
function gcEmptySessions(body: JsonRecord): JsonRecord {
|
||
const ttlMs = positiveInteger(body.ttlMs, DEFAULT_WORKBENCH_EMPTY_SESSION_TTL_MS);
|
||
const nowMs = Date.parse(typeof body.now === "string" ? body.now : "2026-06-18T16:20:30.000Z");
|
||
const archivedSessionIds: string[] = [];
|
||
const keptByReason: Record<string, number> = {};
|
||
for (const session of state.sessions) {
|
||
if (session.status === "archived") continue;
|
||
const decision = classifyWorkbenchEmptySessionGcCandidate(toBackendSessionRecord(session), { nowMs, ttlMs });
|
||
if (!decision.eligible) {
|
||
keptByReason[decision.reason] = (keptByReason[decision.reason] ?? 0) + 1;
|
||
continue;
|
||
}
|
||
session.status = "archived";
|
||
session.updatedAt = new Date(nowMs).toISOString();
|
||
archivedSessionIds.push(session.sessionId);
|
||
}
|
||
return { ok: true, status: "completed", ttlMs, archived: archivedSessionIds.length, archivedSessionIds, keptByReason, valuesRedacted: true };
|
||
}
|
||
|
||
function toBackendSessionRecord(session: SessionRecord): JsonRecord {
|
||
return {
|
||
id: session.sessionId,
|
||
status: session.status,
|
||
startedAt: session.startedAt,
|
||
updatedAt: session.updatedAt,
|
||
lastTraceId: session.lastTraceId,
|
||
session: {
|
||
createdAt: session.startedAt,
|
||
updatedAt: session.updatedAt,
|
||
messageCount: session.messageCount,
|
||
messages: session.messages ?? [],
|
||
lastTraceId: session.lastTraceId,
|
||
valuesRedacted: true,
|
||
secretMaterialStored: false
|
||
}
|
||
};
|
||
}
|
||
|
||
function recordLedger(method: string, url: URL): void {
|
||
if (!url.pathname.startsWith("/v1/")) return;
|
||
const entry = { method, path: url.pathname, query: Object.fromEntries(url.searchParams.entries()) };
|
||
state.requestLedger.push(entry);
|
||
if (isLegacyWorkbenchPath(url.pathname)) state.legacyRequestLedger.push(entry);
|
||
}
|
||
|
||
function isLegacyWorkbenchPath(path: string): boolean {
|
||
if (/^\/v1\/workbench\/(sessions(?:\/[^/]+(?:\/messages)?)?|events|turns\/[^/]+|traces\/[^/]+\/events)$/u.test(path)) return false;
|
||
if (path === "/v1/workbench/debug/fake-sse" || path.startsWith("/v1/workbench/debug/fake-sse/")) return false;
|
||
if (path === "/v1/agent/chat" || /^\/v1\/agent\/sessions(?:\/[^/]+(?:\/select)?)?$/u.test(path)) return false;
|
||
return path.startsWith("/v1/workbench/") || path.startsWith("/v1/agent/");
|
||
}
|
||
|
||
function authPayload(): JsonRecord {
|
||
const deniedCapabilities = { web: "forbidden", workbench: "forbidden", codeAgent: "forbidden", billing: "forbidden", apiKeys: "forbidden", admin: "forbidden" };
|
||
if (state.scenarioId === "auth-upstream-unavailable" || state.scenarioId === "auth-invalid-credentials") {
|
||
return { authenticated: false, mode: "server", authMethod: null, identityAuthority: null, sessionKind: null, actor: null, user: null, capabilities: deniedCapabilities, expiresAt: null, valuesRedacted: true, error: { code: "auth_required", message: "Authentication is required", status: 401 } };
|
||
}
|
||
const actor = { id: "usr_e2e_admin", username: "e2e-admin", role: "admin", status: "active" };
|
||
return { authenticated: true, mode: "server", authMethod: "web-session", identityAuthority: "hwlab-local", sessionKind: "browser", actor, user: actor, capabilities: { web: "available", workbench: "available", codeAgent: "available", billing: "unlinked", apiKeys: "available", admin: "available" }, access: { nav: { profileId: "e2e-admin", allowedIds: ["*"], valuesRedacted: true } }, expiresAt: null, valuesRedacted: true };
|
||
}
|
||
|
||
function authLoginResponse(response: ServerResponse): void {
|
||
if (state.scenarioId === "auth-upstream-unavailable") return json(response, 502, { ok: false, status: "failed", error: { code: "upstream_unavailable", message: "auth upstream unavailable", retryable: true, layer: "proxy" } });
|
||
if (state.scenarioId === "auth-invalid-credentials") return json(response, 401, { ok: false, status: "failed", error: { code: "invalid_credentials", message: "invalid credentials" } });
|
||
response.setHeader("Set-Cookie", "hwlab_session=authenticated; Path=/; HttpOnly; SameSite=Lax");
|
||
return json(response, 200, authPayload());
|
||
}
|
||
|
||
function performanceSummaryErrorStatus(scenarioId: string): number | null {
|
||
const match = /^performance-summary-error-(400|401|403|404|409|500)$/u.exec(scenarioId);
|
||
return match ? Number(match[1]) : null;
|
||
}
|
||
|
||
function statusMatrixTraceId(status: number): string {
|
||
return String(status).repeat(11).slice(0, 32);
|
||
}
|
||
|
||
function redactRequestBody(value: JsonRecord): JsonRecord {
|
||
const output: JsonRecord = {};
|
||
for (const [key, item] of Object.entries(value)) output[key] = /secret|token|key|password|authorization/iu.test(key) ? "<redacted>" : item;
|
||
return output;
|
||
}
|
||
|
||
async function readJson(request: IncomingMessage): Promise<JsonRecord> {
|
||
const chunks: Buffer[] = [];
|
||
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||
if (chunks.length === 0) return {};
|
||
try {
|
||
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||
return parsed && typeof parsed === "object" ? parsed as JsonRecord : {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function errorDiagnosticResponse(response: ServerResponse, status: number, route: string, code: string, message: string, overrides: Partial<JsonRecord> = {}): void {
|
||
const diagnostic = {
|
||
contractVersion: "hwlab-error-diagnostic-v1",
|
||
traceId: String(overrides.traceId ?? "11111111111111111111111111111111"),
|
||
requestId: String(overrides.requestId ?? "req_e2e_api_error_diagnostic"),
|
||
route,
|
||
layer: String(overrides.layer ?? "api"),
|
||
category: String(overrides.category ?? "server"),
|
||
code,
|
||
httpStatus: status,
|
||
source: String(overrides.source ?? "server"),
|
||
observedAt: new Date().toISOString(),
|
||
valuesPrinted: false
|
||
};
|
||
json(response, status, { ok: false, status, error: { code, message, diagnostic } }, {
|
||
traceparent: `00-${diagnostic.traceId}-2222222222222222-01`,
|
||
"x-hwlab-otel-trace-id": diagnostic.traceId,
|
||
"x-request-id": diagnostic.requestId
|
||
});
|
||
}
|
||
|
||
function json(response: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {
|
||
response.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
|
||
response.end(JSON.stringify(body));
|
||
}
|
||
|
||
function staticFile(response: ServerResponse, path: string): void {
|
||
const filePath = safeStaticPath(path);
|
||
const target = filePath && existsSync(filePath) && statSync(filePath).isFile() ? filePath : join(distDir, "index.html");
|
||
response.writeHead(200, { "content-type": contentType(target) });
|
||
if (extname(target) === ".html") {
|
||
response.end(injectRuntimeConfig(readFileSync(target, "utf8")));
|
||
return;
|
||
}
|
||
createReadStream(target).pipe(response);
|
||
}
|
||
|
||
function injectRuntimeConfig(html: string): string {
|
||
if (html.includes("HWLAB_CLOUD_WEB_CONFIG")) return html;
|
||
return html.replace("</head>", ` ${runtimeConfigScript}\n </head>`);
|
||
}
|
||
|
||
function safeStaticPath(path: string): string | null {
|
||
const normalized = resolve(distDir, `.${decodeURIComponent(path === "/" ? "/index.html" : path)}`);
|
||
return normalized === distDir || normalized.startsWith(`${distDir}${sep}`) ? normalized : null;
|
||
}
|
||
|
||
function contentType(filePath: string): string {
|
||
switch (extname(filePath)) {
|
||
case ".html": return "text/html; charset=utf-8";
|
||
case ".js": return "text/javascript; charset=utf-8";
|
||
case ".css": return "text/css; charset=utf-8";
|
||
case ".svg": return "image/svg+xml";
|
||
case ".ico": return "image/x-icon";
|
||
default: return "application/octet-stream";
|
||
}
|
||
}
|
||
|
||
function delay(ms: number): Promise<void> {
|
||
return ms > 0 ? new Promise((resolveDelay) => setTimeout(resolveDelay, ms)) : Promise.resolve();
|
||
}
|
||
|
||
function positiveInteger(value: unknown, fallback: number): number {
|
||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||
}
|
||
|
||
function nonNegativeInteger(value: unknown, fallback: number): number {
|
||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : Math.max(0, Math.trunc(fallback));
|
||
}
|