import type { IncomingMessage, ServerResponse } from 'node:http'; import type { HostApiContext } from '../context'; import { flushStreamingHeaders, parseJsonBody, sendJson, writeStreamingChunk, } from '../route-utils'; import { TeacherError } from '../../coding-teacher/config-client'; import type { TeacherScope } from '../../coding-teacher/service'; import type { TeacherSend } from '../../../shared/coding-teacher'; import { takeTeacherPreviewRevision } from '../../main/app-deep-link'; export async function handleCodingTeacherRoutes( req: IncomingMessage, res: ServerResponse, url: URL, ctx: HostApiContext ): Promise { const source = url.pathname.match( /^\/api\/coding\/projects\/([^/]+)\/conversations\/([^/]+)\/teacher-topics(?:\/([^/]+))?(?:\/(messages|events|save|requests\/([^/]+)\/cancel))?$/ ); const preview = url.pathname.match( /^\/api\/coding\/teacher-preview\/topics(?:\/([^/]+))?(?:\/(messages|events|save|requests\/([^/]+)\/cancel))?$/ ); const config = url.pathname === '/api/coding/teacher/config'; const draft = url.pathname === '/api/coding/teacher-preview'; const pending = url.pathname === '/api/coding/teacher-preview/pending-link'; if (!source && !preview && !config && !draft && !pending) return false; if ((config || draft || pending) && req.method !== 'GET') { sendJson(res, 405, { error: '不支持此操作。' }); return true; } if (pending && req.method === 'GET') { sendJson(res, 200, { draftRevision: takeTeacherPreviewRevision() }); return true; } const service = ctx.codingProducts?.teacher; if (!service) { sendJson(res, 503, { error: '老师服务暂不可用。', code: 'teacher_unavailable' }); return true; } try { if (config && req.method === 'GET') { sendJson(res, 200, await service.definition()); return true; } if (draft && req.method === 'GET') { const revision = Number(url.searchParams.get('draftRevision')); if (!Number.isSafeInteger(revision) || revision < 1) throw new TeacherError(400, 'teacher_preview_invalid', '草稿版本无效。'); sendJson(res, 200, await service.previewDefinition(revision)); return true; } const scope: TeacherScope = source ? { projectId: decodeURIComponent(source[1]), sourceId: decodeURIComponent(source[2]) } : { projectId: 'preview', sourceId: 'preview' }; const id = source?.[3] ?? preview?.[1], action = source?.[4] ?? preview?.[2], requestId = source?.[5] ?? preview?.[3]; if (!id && req.method === 'GET') { sendJson(res, 200, await service.list(scope)); return true; } if (!id && req.method === 'POST') { const body = await parseJsonBody<{ draftRevision?: number; sampleContext?: string }>(req); sendJson(res, 201, await service.create(scope, body.draftRevision, body.sampleContext)); return true; } if (id && !action && req.method === 'GET') { sendJson(res, 200, await service.read(scope, id)); return true; } if (id && action === 'messages' && req.method === 'POST') { sendJson(res, 202, await service.send(scope, id, await parseJsonBody(req))); return true; } if (id && action === 'save' && req.method === 'POST') { sendJson(res, 200, await service.save(scope, id)); return true; } if (id && requestId && req.method === 'POST') { sendJson(res, 200, await service.cancel(scope, id, requestId)); return true; } if (id && action === 'events' && req.method === 'GET') { let closed = false; const buffered: unknown[] = []; let started = false; const close = await service.subscribe(scope, id, (topic) => { if (!started) { buffered.push(topic); return; } if (!closed) void writeStreamingChunk( res, 'event: snapshot\ndata: ' + JSON.stringify(topic) + '\n\n' ).catch(() => { closed = true; }); }); let timer: ReturnType | undefined; const cleanup = () => { closed = true; if (timer) clearInterval(timer); close(); }; req.once('close', cleanup); res.once('close', cleanup); try { res.statusCode = 200; res.setHeader('Content-Type', 'text/event-stream; charset=utf-8'); res.setHeader('Cache-Control', 'private, no-store'); res.setHeader('Connection', 'keep-alive'); flushStreamingHeaders(res); started = true; for (const topic of buffered) { if (!await writeStreamingChunk(res, 'event: snapshot\ndata: ' + JSON.stringify(topic) + '\n\n')) { cleanup(); return true; } } timer = setInterval(() => { if (!closed) void writeStreamingChunk(res, ': keepalive\n\n').then(written => { if (!written) cleanup(); }).catch(cleanup); }, 15000); } catch { cleanup(); res.end(); } return true; } sendJson(res, 405, { error: '不支持此操作。' }); } catch (error) { if (error instanceof TeacherError) sendJson(res, error.status, { error: error.message, code: error.code }); else sendJson(res, 500, { error: '老师操作失败,请保留当前内容后重试。', code: 'teacher_operation_failed', }); } return true; }