refactor(teacher): retire discussion components and keep text replies

This commit is contained in:
鲨鱼辣椒
2026-09-28 13:54:55 +08:00
parent ae0f72d8fa
commit 998796d77c
32 changed files with 1060 additions and 1794 deletions

View File

@@ -9,7 +9,6 @@ import type {
TeacherCheckInInput,
TeacherCheckInResult,
TeacherDefinition,
TeacherDiscussionAction,
TeacherReference,
TeacherSend,
TeacherSourceContext,
@@ -31,8 +30,7 @@ import { compileTeacherContext } from './context';
import { prepareTeacherModel } from './model-runner';
import { prepareCloudTeacher } from './cloud-runner';
import { readTeacherSource } from './source-reader';
import { parseTeacherSuggestions } from './suggestions';
import { applyDiscussionReply, discussionInstructions, editDiscussion, validateDiscussionContext } from './discussion';
import { applyTeacherReply, teacherReplyInstructions } from './reply';
import { subscribeWorksSquareSession } from '../services/works-square-session';
export interface TeacherScope {
@@ -317,11 +315,7 @@ export class CodingTeacherService {
): Promise<TeacherTopic> {
teacherTopicId(input.requestId);
const intent = input.intent === undefined ? 'question' : input.intent;
if (input.presentation !== undefined && input.presentation !== 'discussion-v1')
throw new TeacherError(422, 'teacher_presentation_invalid', '智能体的显示方式无效。');
if (input.discussion !== undefined && input.presentation !== 'discussion-v1')
throw new TeacherError(422, 'teacher_discussion_invalid', '讨论方式无效。');
const structuredReply = input.presentation === 'discussion-v1' && (intent === 'question' || intent === 'guided-help');
const structuredReply = intent !== 'check-in';
if (!['question', 'suggestions', 'guided-help', 'check-in'].includes(intent) || (intent === 'check-in' && !checkIn))
throw new TeacherError(422, 'teacher_intent_invalid', '提问方式无效,请重新打开智能体后再试。');
if (intent !== 'question' && (
@@ -355,9 +349,7 @@ export class CodingTeacherService {
existing.text !== input.text ||
(existing.intent ?? 'question') !== intent ||
JSON.stringify(existing.references) !== JSON.stringify(refs) ||
(existing.sourceConversationId ?? undefined) !== (input.sourceConversationId ?? undefined) ||
existing.presentation !== input.presentation ||
JSON.stringify(existing.discussionContext) !== JSON.stringify(input.discussion)
(existing.sourceConversationId ?? undefined) !== (input.sourceConversationId ?? undefined)
)
throw new TeacherError(409, 'teacher_request_conflict', '同一请求标识不能用于不同问题。');
return structuredClone(topic);
@@ -369,7 +361,6 @@ export class CodingTeacherService {
)
)
throw new TeacherError(409, 'teacher_topic_busy', '请等待当前回复完成,或先停止。');
const discussionContext = validateDiscussionContext(topic, input.discussion);
if (topic.draftRevision) {
await (this.options.preview ?? teacherPreview)(account, topic.draftRevision);
} else if (topic.definition.runtime !== 'yuxi' && topic.definition.config_id) {
@@ -455,7 +446,7 @@ export class CodingTeacherService {
})
: await (this.options.prepareModel ?? prepareTeacherModel)(account, topic.definition,
access,
{ finalOnly: structuredReply || intent === 'suggestions' });
{ finalOnly: structuredReply });
if (checkIn) this.assertAccount(account);
const compiled = compileTeacherContext(
topic.definition,
@@ -465,7 +456,7 @@ export class CodingTeacherService {
references,
model.inputLimit,
intent,
structuredReply ? discussionInstructions(topic, discussionContext) : undefined,
structuredReply ? teacherReplyInstructions() : undefined,
scope.projectId !== 'preview',
model.measureInput
);
@@ -485,8 +476,7 @@ export class CodingTeacherService {
const request = {
id: input.requestId,
intent,
...(input.presentation ? { presentation: input.presentation } : {}),
...(discussionContext ? { discussionContext } : {}),
...(structuredReply ? { presentation: 'reply-v1' as const } : {}),
...(input.sourceConversationId ? { sourceConversationId: input.sourceConversationId } : {}),
...(checkIn ? { checkInSourceFingerprint: checkIn.fingerprint } : {}),
text: input.text,
@@ -518,13 +508,13 @@ export class CodingTeacherService {
const release = this.options.acquireLease?.(key) ?? (() => undefined);
const finish = async () => {
const current = topic.requests.at(-1)!;
let structuredText = '';
try {
this.assertAccount(account);
if (controller.signal.aborted) throw controller.signal.reason;
current.status = 'running';
topic.revision++;
this.events.emit(key, structuredClone(topic));
let structuredText = '';
current.usage = await model.run(compiled.messages, controller.signal, (delta) => {
if (structuredReply) { structuredText += delta; return; }
current.response += delta;
@@ -532,12 +522,7 @@ export class CodingTeacherService {
this.events.emit(key, structuredClone(topic));
});
this.assertAccount(account);
if (!controller.signal.aborted && structuredReply) applyDiscussionReply(topic, current, structuredText);
if (!controller.signal.aborted && intent === 'suggestions') {
const suggestions = parseTeacherSuggestions(current.response);
current.response = suggestions.intro;
current.suggestedQuestions = suggestions.questions;
}
if (!controller.signal.aborted && structuredReply) applyTeacherReply(current, structuredText);
current.status = controller.signal.aborted ? 'cancelled' : 'completed';
} catch (error) {
current.status = controller.signal.aborted ? 'cancelled' : 'failed';
@@ -547,6 +532,9 @@ export class CodingTeacherService {
? error.message
: '智能体回复失败,已保留本次问题与收到的内容。';
} finally {
// Keep received bytes inert even when an aborted runner resolves normally.
if (structuredReply && current.status !== 'completed' && structuredText)
current.unparsedResponse = structuredText;
if (intent === 'suggestions' && current.status !== 'completed') {
current.response = '';
delete current.suggestedQuestions;
@@ -569,24 +557,6 @@ export class CodingTeacherService {
return structuredClone(topic);
});
}
async updateDiscussion(scope: TeacherScope, id: string, input: TeacherDiscussionAction) {
const account = await this.account();
this.assertWritable(scope);
const key = this.key(account, scope, id);
return await this.serialize(key, async () => {
const { store, topic } = await this.readOwned(account, scope, id);
if (this.active.has(key) || topic.requests.some(request => ['preparing', 'running'].includes(request.status)))
throw new TeacherError(409, 'teacher_topic_busy', '请等待智能体回复,或先停止。');
const next = structuredClone(topic);
editDiscussion(next, input);
next.revision++;
next.updatedAt = new Date().toISOString();
this.assertAccount(account);
await store.save(next);
this.events.emit(key, structuredClone(next));
return structuredClone(next);
});
}
async cancel(scope: TeacherScope, id: string, requestId: string) {
const account = await this.account();
const { topic } = await this.readOwned(account, scope, id);