feat: add project workbench launch

This commit is contained in:
lyon
2026-06-25 17:59:19 +08:00
parent cc6561e506
commit 06284f5287
14 changed files with 575 additions and 40 deletions
@@ -31,6 +31,7 @@ interface ScenarioState {
requestLedger: JsonRecord[];
legacyRequestLedger: JsonRecord[];
chatRequests: JsonRecord[];
projectLinks: JsonRecord[];
listOmitSelected: boolean;
sessionDelayMs: number;
sessionDetailDelayMs: number;
@@ -142,6 +143,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
}
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/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));
@@ -226,7 +228,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
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(response, url, method);
if (path.startsWith("/v1/project-management")) return projectManagementPayload(request, response, url, method);
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);
@@ -285,13 +287,20 @@ function hwpodNodeOpsPayload(): JsonRecord {
};
}
function projectManagementPayload(response: ServerResponse, url: URL, method: string): void {
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 });
async function projectManagementPayload(request: IncomingMessage, response: ServerResponse, url: URL, method: string): Promise<void> {
const path = url.pathname;
const source = projectManagementSource();
const files = projectManagementFiles();
const tasks = projectManagementTasks();
const links = projectManagementLinks();
const links = state.projectLinks;
const tasks = projectManagementTasks(links);
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 }));
}
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({
@@ -303,7 +312,7 @@ function projectManagementPayload(response: ServerResponse, url: URL, method: st
{ id: "projects", label: "Projects", apiRoute: "/v1/project-management/projects" },
{ id: "mdtodo", label: "MDTODO", apiRoute: "/v1/project-management/mdtodo/tasks" }
],
capabilities: { mdtodoProjection: true, workbenchLaunch: false, workbenchLinks: true, sourceOfTruth: "markdown-files" }
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" }
}));
@@ -331,18 +340,79 @@ function projectManagementFiles(): JsonRecord[] {
];
}
function projectManagementTasks(): JsonRecord[] {
function projectManagementTasks(links: JsonRecord[] = []): JsonRecord[] {
const linkCount = (taskRef: string) => links.filter((link) => link.taskRef === taskRef).length;
return [
{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t1", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "t1", title: "实现项目根导航", status: "done", parentTaskRef: null, depth: 0, lineNumber: 4, ordinal: 1, linkCount: 1, updatedAt: "2026-06-25T09:00:00.000Z" },
{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t2", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "t2", title: "展示 MDTODO 任务详情", status: "open", parentTaskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t1", depth: 1, lineNumber: 5, ordinal: 2, linkCount: 0, updatedAt: "2026-06-25T09:00:00.000Z" },
{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_rollout:t1", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_rollout", taskId: "t1", title: "D601 v03 页面验收", status: "blocked", parentTaskRef: null, depth: 0, lineNumber: 3, ordinal: 3, linkCount: 0, updatedAt: "2026-06-25T09:00:00.000Z" }
{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t1", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "t1", title: "实现项目根导航", status: "done", parentTaskRef: null, depth: 0, lineNumber: 4, ordinal: 1, linkCount: linkCount("mdtodo:hwlab-v03-mdtodo:file_plan:t1"), updatedAt: "2026-06-25T09:00:00.000Z" },
{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t2", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_plan", taskId: "t2", title: "展示 MDTODO 任务详情", status: "open", parentTaskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t1", depth: 1, lineNumber: 5, ordinal: 2, linkCount: linkCount("mdtodo:hwlab-v03-mdtodo:file_plan:t2"), updatedAt: "2026-06-25T09:00:00.000Z" },
{ taskRef: "mdtodo:hwlab-v03-mdtodo:file_rollout:t1", projectId: "project_hwlab_v03", sourceId: "hwlab-v03-mdtodo", fileRef: "file_rollout", taskId: "t1", title: "D601 v03 页面验收", status: "blocked", parentTaskRef: null, depth: 0, lineNumber: 3, ordinal: 3, linkCount: linkCount("mdtodo:hwlab-v03-mdtodo:file_rollout:t1"), updatedAt: "2026-06-25T09:00:00.000Z" }
];
}
function projectManagementLinks(): JsonRecord[] {
function projectManagementSeedLinks(): JsonRecord[] {
return [{ linkId: "lnk_project_task_root", projectId: "project_hwlab_v03", taskRef: "mdtodo:hwlab-v03-mdtodo:file_plan:t1", 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 launchContext = body.launchContext && typeof body.launchContext === "object" ? body.launchContext 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 = [
@@ -808,6 +878,7 @@ function createScenarioState(scenarioId: string): ScenarioState {
requestLedger: [],
legacyRequestLedger: [],
chatRequests: [],
projectLinks: projectManagementSeedLinks(),
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,