Merge project conversations and cloud coding teacher into main
This commit is contained in:
145
electron/api/routes/coding-teacher.ts
Normal file
145
electron/api/routes/coding-teacher.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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<boolean> {
|
||||
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<TeacherSend>(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<typeof setInterval> | 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;
|
||||
}
|
||||
Reference in New Issue
Block a user