Merge reply recovery and simplified consultation UI into ongoing chats
This commit is contained in:
@@ -1,9 +1,39 @@
|
||||
import { mkdir, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { atomicWriteJson, readJsonFile } from '../coding-projects/atomic-json';
|
||||
import type { TeacherTopic, TeacherTopicList } from '../../shared/coding-teacher';
|
||||
import type { TeacherRequest, TeacherTopic, TeacherTopicList } from '../../shared/coding-teacher';
|
||||
import { isTeacherReplyEnvelope, parseTeacherReply } from '../../shared/teacher-reply';
|
||||
import { TeacherError } from './config-client';
|
||||
import { parseTeacherDiscussionContent } from '../../shared/teacher-discussion';
|
||||
|
||||
/** Repair only received, completed protocol replies; never resume a model request. */
|
||||
function repairCompletedReply(request: TeacherRequest): void {
|
||||
const raw = request.unparsedResponse;
|
||||
if (request.status !== 'completed' || typeof raw !== 'string'
|
||||
|| !(request.discussionError || request.replyParseError || request.replyIncomplete)
|
||||
|| !isTeacherReplyEnvelope(raw)) return;
|
||||
const parsed = parseTeacherReply(raw);
|
||||
if (parsed.incomplete) {
|
||||
// Keep the previous text and diagnostics as evidence, but do not present a
|
||||
// legacy half-sentence as a complete reply.
|
||||
request.replyIncomplete = true;
|
||||
return;
|
||||
}
|
||||
const previous = request.response.trim();
|
||||
// The retained raw data must extend or confirm the existing prose. A stale or
|
||||
// unrelated archive must not replace a useful answer with different content.
|
||||
if (previous && !parsed.reply.trim().startsWith(previous)
|
||||
&& previous !== '这次回复没有完整解析,请重试。'
|
||||
&& previous !== raw.trim() && !request.replyIncomplete) return;
|
||||
request.response = parsed.reply;
|
||||
request.suggestedQuestions = parsed.quickReplies;
|
||||
delete request.replyIncomplete;
|
||||
if (parsed.parseError) request.replyParseError = parsed.parseError;
|
||||
else {
|
||||
delete request.replyParseError;
|
||||
delete request.discussionError;
|
||||
}
|
||||
// Unlike applying a new reply, reading history never discards the original.
|
||||
}
|
||||
|
||||
export function teacherTopicId(id: string): string {
|
||||
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(id))
|
||||
@@ -60,40 +90,21 @@ export class TeacherTopicStore {
|
||||
if (topic.id !== id || topic.schemaVersion !== 1 || !Array.isArray(topic.requests))
|
||||
throw new TeacherError(409, 'teacher_topic_invalid', '智能体历史无法读取。');
|
||||
let recovered = false;
|
||||
if (topic.discussion) {
|
||||
try {
|
||||
const discussion = topic.discussion;
|
||||
teacherTopicId(discussion.id);
|
||||
if (!Number.isSafeInteger(discussion.revision) || discussion.revision < 1
|
||||
|| !['offered', 'active', 'paused', 'finished'].includes(discussion.status)) throw new Error('Invalid discussion');
|
||||
discussion.content = parseTeacherDiscussionContent(discussion.content);
|
||||
for (const key of ['previousIdeas', 'previousStructure'] as const) {
|
||||
if (!discussion[key]) continue;
|
||||
try {
|
||||
const previous = parseTeacherDiscussionContent(discussion[key]);
|
||||
if (key === 'previousIdeas' && previous.kind === 'ideas') discussion.previousIdeas = previous;
|
||||
else if (key === 'previousStructure' && previous.kind === 'structure') discussion.previousStructure = previous;
|
||||
else throw new Error('Invalid history');
|
||||
} catch { delete discussion[key]; recovered = true; }
|
||||
}
|
||||
} catch { delete topic.discussion; recovered = true; }
|
||||
}
|
||||
// Legacy component fields are opaque archives: never validate, revive or delete them.
|
||||
for (const request of topic.requests) {
|
||||
if (request.discussionSnapshot) {
|
||||
try { request.discussionSnapshot = parseTeacherDiscussionContent(request.discussionSnapshot); }
|
||||
catch { delete request.discussionSnapshot; recovered = true; }
|
||||
}
|
||||
}
|
||||
for (const request of topic.requests)
|
||||
if (request.status === 'preparing' || request.status === 'running') {
|
||||
request.status = 'interrupted';
|
||||
request.error = '应用已重启,本次回复中断。';
|
||||
recovered = true;
|
||||
}
|
||||
}
|
||||
if (recovered) {
|
||||
topic.revision++;
|
||||
await this.save(topic);
|
||||
}
|
||||
// Apply display repair after restart recovery is saved, so reading a
|
||||
// mixed topic does not incidentally persist completed-reply repairs.
|
||||
for (const request of topic.requests) repairCompletedReply(request);
|
||||
return topic;
|
||||
})();
|
||||
this.cache.set(id, pending);
|
||||
|
||||
Reference in New Issue
Block a user