Merge remote main preserving project chats and reply recovery
This commit is contained in:
@@ -9,7 +9,6 @@ import type {
|
||||
TeacherCheckInInput,
|
||||
TeacherCheckInResult,
|
||||
TeacherDefinition,
|
||||
TeacherDiscussionAction,
|
||||
TeacherReference,
|
||||
TeacherRequest,
|
||||
TeacherSend,
|
||||
@@ -35,8 +34,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 {
|
||||
@@ -419,11 +417,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' && (
|
||||
@@ -462,9 +456,7 @@ export class CodingTeacherService {
|
||||
(scope.agentId && existing.projectId !== scope.projectId) ||
|
||||
(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);
|
||||
@@ -485,19 +477,19 @@ export class CodingTeacherService {
|
||||
topic.version = selected.version;
|
||||
topic.projectId = scope.projectId;
|
||||
topic.sourceConversationId = 'project';
|
||||
topic.discussion = topic.conversation!.discussions[scope.projectId];
|
||||
const previous = topic.requests.at(-1);
|
||||
const reuse = previous && !previous.origin && previous.projectId === scope.projectId
|
||||
&& previous.sourceConversationId === input.sourceConversationId
|
||||
&& previous.presentation === 'reply-v1'
|
||||
&& previous.teacherVersion === selected.version && previous.runtimeThreadId;
|
||||
newConversationSegment = !reuse;
|
||||
// Yuxi binds both project and Pi source to a thread. Preserve its native
|
||||
// summary middleware within a segment; explicitly carry bounded public
|
||||
// history only when a new scope/release starts another segment.
|
||||
// history only when a new scope, release, or reply protocol starts another
|
||||
// segment. Older checkpoints may still contain retired component instructions.
|
||||
topic.conversation!.runtimeThreadId = reuse || randomUUID();
|
||||
topic.conversation!.segmentTurns = reuse ? topic.conversation!.segmentTurns + 1 : 1;
|
||||
}
|
||||
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) {
|
||||
@@ -584,7 +576,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,
|
||||
@@ -594,7 +586,7 @@ export class CodingTeacherService {
|
||||
references,
|
||||
model.inputLimit,
|
||||
intent,
|
||||
structuredReply ? discussionInstructions(topic, discussionContext) : undefined,
|
||||
structuredReply ? teacherReplyInstructions() : undefined,
|
||||
scope.projectId !== 'preview',
|
||||
model.measureInput
|
||||
);
|
||||
@@ -617,8 +609,7 @@ export class CodingTeacherService {
|
||||
teacherVersion: topic.version, runtimeThreadId: topic.conversation!.runtimeThreadId } : {}),
|
||||
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,
|
||||
@@ -650,13 +641,13 @@ export class CodingTeacherService {
|
||||
const release = this.options.acquireLease?.(key) ?? (() => undefined);
|
||||
const finish = async () => {
|
||||
const current = topic.requests.find(request => request.id === input.requestId)!;
|
||||
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;
|
||||
@@ -664,12 +655,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';
|
||||
@@ -679,6 +665,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;
|
||||
@@ -686,7 +675,6 @@ export class CodingTeacherService {
|
||||
topic.updatedAt = new Date().toISOString();
|
||||
topic.revision++;
|
||||
try {
|
||||
if (topic.conversation && topic.discussion) topic.conversation.discussions[scope.projectId] = topic.discussion;
|
||||
await store.save(topic, current.id);
|
||||
} catch {
|
||||
topic.unsaved = true;
|
||||
@@ -702,31 +690,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);
|
||||
if (next.conversation) {
|
||||
if (!input.projectId) throw new TeacherError(422, 'teacher_project_required', '请选择讨论所属的项目。');
|
||||
if (input.projectId !== scope.projectId) throw new TeacherError(422, 'teacher_project_mismatch', '讨论不属于当前项目。');
|
||||
await this.options.projects.getProject(input.projectId);
|
||||
next.discussion = next.conversation.discussions[input.projectId];
|
||||
}
|
||||
editDiscussion(next, input);
|
||||
if (next.conversation && next.discussion) next.conversation.discussions[input.projectId!] = next.discussion;
|
||||
next.revision++;
|
||||
next.updatedAt = new Date().toISOString();
|
||||
this.assertAccount(account);
|
||||
await store.save(next, scope.agentId ? '' : undefined);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user