feat(coding): keep one ongoing conversation per delivered agent

This commit is contained in:
2026-09-28 14:19:02 +08:00
parent e54f3d234d
commit 7aa81da616
23 changed files with 1205 additions and 160 deletions

View File

@@ -17,6 +17,8 @@ export async function handleCodingTeacherRoutes(
url: URL,
ctx: HostApiContext
): Promise<boolean> {
const legacy = url.pathname.match(/^\/api\/coding\/projects\/([^/]+)\/agent-history(?:\/([^/]+))?$/);
const conversation = url.pathname.match(/^\/api\/coding\/agent-conversations\/([^/]+)(?:\/(messages|events|save|seen|discussion|requests\/([^/]+)\/cancel))?$/);
const source = url.pathname.match(
/^\/api\/coding\/projects\/([^/]+)\/conversations\/([^/]+)\/teacher-topics(?:\/([^/]+))?(?:\/(messages|events|save|discussion|requests\/([^/]+)\/cancel))?$/
);
@@ -32,7 +34,7 @@ export async function handleCodingTeacherRoutes(
const catalog = url.pathname === '/api/coding/teacher/teachers';
const draft = url.pathname === '/api/coding/teacher-preview';
const pending = url.pathname === '/api/coding/teacher-preview/pending-link';
if (!source && !preview && !projectTopics && !checkIn && !config && !catalog && !draft && !pending) return false;
if (!legacy && !conversation && !source && !preview && !projectTopics && !checkIn && !config && !catalog && !draft && !pending) return false;
if ((config || catalog || draft || pending) && req.method !== 'GET') {
sendJson(res, 405, { error: '不支持此操作。' });
return true;
@@ -47,6 +49,27 @@ export async function handleCodingTeacherRoutes(
return true;
}
try {
if (legacy) {
if (req.method !== 'GET') sendJson(res, 405, { error: '旧记录仅供查看。' });
else sendJson(res, 200, await service.legacyHistory(decodeURIComponent(legacy[1]), legacy[2] ? {
id: decodeURIComponent(legacy[2]), sourceId: url.searchParams.get('source') ?? 'project', friend: url.searchParams.get('friend') === 'true',
} : undefined));
return true;
}
const agentId = conversation ? decodeURIComponent(conversation[1]) : undefined;
if (agentId && !conversation![2] && req.method === 'GET') {
sendJson(res, 200, await service.conversation(agentId, url.searchParams.get('before') ?? undefined));
return true;
}
if (agentId && conversation![2] === 'messages' && req.method === 'POST') {
sendJson(res, 202, await service.sendConversation(agentId, await parseJsonBody<TeacherSend>(req)));
return true;
}
if (agentId && conversation![2] === 'seen' && req.method === 'POST') {
const input = await parseJsonBody<{ requestId: string }>(req);
sendJson(res, 200, await service.markConversationSeen(agentId, input.requestId));
return true;
}
if (checkIn) {
if (req.method !== 'POST') sendJson(res, 405, { error: '不支持此操作。' });
else sendJson(res, 200, await service.checkIn(
@@ -70,14 +93,16 @@ export async function handleCodingTeacherRoutes(
sendJson(res, 200, await service.previewDefinition(revision));
return true;
}
const scope: TeacherScope = source
const continuous = agentId ? await service.conversation(agentId) : undefined;
if (agentId && !continuous?.topic) throw new TeacherError(404, 'teacher_topic_not_found', '智能体聊天尚未开始。');
const scope: TeacherScope = agentId ? { projectId: '', sourceId: 'project', agentId } : source
? { projectId: decodeURIComponent(source[1]), sourceId: decodeURIComponent(source[2]) }
: projectTopics
? { projectId: decodeURIComponent(projectTopics[1]), sourceId: 'project', role }
: { projectId: 'preview', sourceId: 'preview' };
const id = source?.[3] ?? projectTopics?.[3] ?? preview?.[1],
action = source?.[4] ?? projectTopics?.[4] ?? preview?.[2],
requestId = source?.[5] ?? projectTopics?.[5] ?? preview?.[3];
const id = continuous?.topic?.id ?? source?.[3] ?? projectTopics?.[3] ?? preview?.[1],
action = conversation?.[2] ?? source?.[4] ?? projectTopics?.[4] ?? preview?.[2],
requestId = conversation?.[3] ?? source?.[5] ?? projectTopics?.[5] ?? preview?.[3];
if (!id && req.method === 'GET') {
sendJson(res, 200, await service.list(scope));
return true;
@@ -112,6 +137,9 @@ export async function handleCodingTeacherRoutes(
const buffered: unknown[] = [];
let started = false;
const close = await service.subscribe(scope, id, (topic) => {
// GET owns paged history. Stream only the current turn and metadata;
// Renderer merges by request ID/revision without replacing older pages.
if (conversation) topic = { ...topic, requests: topic.requests.slice(-1) };
if (!started) {
buffered.push(topic);
return;