import type { IncomingMessage, ServerResponse } from 'node:http'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { IMAGE_WORKSPACE_API_PATH, IMAGE_WORKSPACE_UNAVAILABLE_CODE, IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE, type DesignConfirmGenerationInput, type DesignCreateWorkspaceInput, type DesignRenameWorkspaceInput, type DesignSubmitMessageInput, } from '../../../shared/image-workspace'; import { DesignWorkspaceModuleError } from '../../image-workspace/module'; import type { HostApiContext } from '../context'; import { flushStreamingHeaders, parseJsonBody, sendJson, writeStreamingChunk, } from '../route-utils'; function decodedSegments(pathname: string): string[] | null { const suffix = pathname.slice(IMAGE_WORKSPACE_API_PATH.length).replace(/^\/+/, ''); if (!suffix) return []; try { return suffix.split('/').map((segment) => decodeURIComponent(segment)); } catch { return null; } } function asString(value: unknown): string { return typeof value === 'string' ? value : ''; } function asInteger(value: unknown): number { return typeof value === 'number' && Number.isInteger(value) ? value : -1; } function asStringArray(value: unknown): string[] { return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : []; } function sendData(res: ServerResponse, data: unknown): void { sendJson(res, 200, { success: true, status: 200, data }); } function sendRouteError(res: ServerResponse, error: unknown): void { if (res.headersSent) { res.destroy(error instanceof Error ? error : undefined); return; } if (error instanceof DesignWorkspaceModuleError) { sendJson(res, error.status, { success: false, status: error.status, code: error.code, error: error.message, }); return; } if (error instanceof SyntaxError) { sendJson(res, 400, { success: false, status: 400, code: 'IMAGE_WORKSPACE_INVALID_JSON', error: '请求内容不是有效 JSON', }); return; } sendJson(res, 500, { success: false, status: 500, code: 'IMAGE_WORKSPACE_REQUEST_FAILED', error: error instanceof Error ? error.message : 'AI 设计请求失败', }); } async function relayAssetContent( req: IncomingMessage, res: ServerResponse, ctx: HostApiContext, workspaceId: string, assetId: string, ): Promise { const content = await ctx.imageWorkspace!.openAssetContent( workspaceId, assetId, typeof req.headers.range === 'string' ? req.headers.range : undefined, ); res.statusCode = content.status; for (const header of [ 'accept-ranges', 'cache-control', 'content-length', 'content-range', 'content-type', 'etag', 'last-modified', ]) { const value = content.headers.get(header); if (value) res.setHeader(header, value); } if (!content.body || req.method === 'HEAD') { res.end(); return; } await pipeline(Readable.fromWeb(content.body), res); } async function relayWorkspaceEvents( req: IncomingMessage, res: ServerResponse, ctx: HostApiContext, workspaceId: string, ): Promise { if (!ctx.imageWorkspace?.openWorkspaceEvents) { throw new DesignWorkspaceModuleError( 501, 'DESIGN_EVENT_STREAM_UNAVAILABLE', 'AI 设计任务实时状态暂时不可用', ); } const header = req.headers['last-event-id']; const afterEventId = Array.isArray(header) ? header[0] : header; const subscription = await ctx.imageWorkspace.openWorkspaceEvents({ workspaceId, ...(afterEventId ? { afterEventId } : {}), }); let closed = false; const close = () => { if (closed) return; closed = true; subscription.close(); }; res.once('close', close); try { res.statusCode = 200; res.setHeader('Content-Type', 'text/event-stream; charset=utf-8'); res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); flushStreamingHeaders(res); if (!await writeStreamingChunk(res, ': connected\n\n')) return; for await (const event of subscription.events) { if (!await writeStreamingChunk( res, `id: ${event.id}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`, )) { return; } } if (!res.writableEnded) res.end(); } finally { res.off('close', close); close(); } } export async function handleImageWorkspaceRoutes( req: IncomingMessage, res: ServerResponse, url: URL, ctx: HostApiContext, ): Promise { if (url.pathname !== IMAGE_WORKSPACE_API_PATH && !url.pathname.startsWith(`${IMAGE_WORKSPACE_API_PATH}/`)) { return false; } if (!ctx.imageWorkspace) { sendJson(res, 501, { success: false, status: 501, code: IMAGE_WORKSPACE_UNAVAILABLE_CODE, error: IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE, }); return true; } const segments = decodedSegments(url.pathname); if (!segments) { sendJson(res, 400, { success: false, status: 400, error: '无效的 AI 设计路径' }); return true; } try { if (segments.length === 0 && req.method === 'GET') { sendData(res, await ctx.imageWorkspace.bootstrap()); return true; } if (segments.length === 1 && segments[0] === 'workspaces' && req.method === 'POST') { const body = await parseJsonBody>(req); const input: DesignCreateWorkspaceInput = { clientWorkspaceId: asString(body.clientWorkspaceId), title: asString(body.title), }; sendData(res, await ctx.imageWorkspace.createWorkspace(input)); return true; } if (segments.length === 1 && segments[0] === 'local-data' && req.method === 'DELETE') { if (!ctx.imageWorkspace.reset) { throw new DesignWorkspaceModuleError( 405, 'IMAGE_WORKSPACE_RESET_NOT_ALLOWED', '云端 AI 设计不支持清空本地数据', ); } sendData(res, await ctx.imageWorkspace.reset()); return true; } if (segments.length === 2 && segments[0] === 'workspaces' && req.method === 'GET') { sendData(res, await ctx.imageWorkspace.getWorkspace(segments[1])); return true; } if (segments.length === 2 && segments[0] === 'workspaces' && req.method === 'PATCH') { const body = await parseJsonBody>(req); const input: DesignRenameWorkspaceInput = { workspaceId: segments[1], title: asString(body.title), }; sendData(res, await ctx.imageWorkspace.renameWorkspace(input)); return true; } if (segments.length === 3 && segments[0] === 'workspaces' && segments[2] === 'messages' && req.method === 'POST') { const body = await parseJsonBody>(req); const input: DesignSubmitMessageInput = { workspaceId: segments[1], clientTurnId: asString(body.clientTurnId), expectedTurnRevision: asInteger(body.expectedTurnRevision), message: asString(body.message), attachmentAssetIds: asStringArray(body.attachmentAssetIds), }; sendData(res, await ctx.imageWorkspace.submitMessage(input)); return true; } if (segments.length === 3 && segments[0] === 'workspaces' && segments[2] === 'tasks' && req.method === 'GET') { sendData(res, await ctx.imageWorkspace.listTasks(segments[1])); return true; } if (segments.length === 3 && segments[0] === 'workspaces' && segments[2] === 'events' && req.method === 'GET') { await relayWorkspaceEvents(req, res, ctx, segments[1]); return true; } if (segments.length === 5 && segments[0] === 'workspaces' && segments[2] === 'quotes' && segments[4] === 'confirm' && req.method === 'POST') { const body = await parseJsonBody>(req); const input: DesignConfirmGenerationInput = { workspaceId: segments[1], quoteId: segments[3], clientTurnId: asString(body.clientTurnId), expectedTurnRevision: asInteger(body.expectedTurnRevision), }; sendData(res, await ctx.imageWorkspace.confirmGeneration(input)); return true; } if (segments.length === 5 && segments[0] === 'workspaces' && segments[2] === 'assets' && segments[4] === 'content' && (req.method === 'GET' || req.method === 'HEAD')) { await relayAssetContent(req, res, ctx, segments[1], segments[3]); return true; } sendJson(res, 404, { success: false, status: 404, code: 'IMAGE_WORKSPACE_ROUTE_NOT_FOUND', error: `没有对应的 AI 设计接口:${req.method ?? 'GET'} ${url.pathname}`, }); } catch (error) { sendRouteError(res, error); } return true; }