feat(teacher): stream Yuxi reasoning and answer previews

This commit is contained in:
brother7 committed 2026-09-29 18:18:56 +08:00
1 parent f443f3bc66
commit 8ccc4650a2
12 files changed
+364 -42

No files matched your search

+50 -1
View File
@@ -759,6 +759,7 @@ it.each(['suggestions', 'reply-v1', 'discussion-v1'] as const)(
let firstReads = 0, resumedReads = 0;
const text = vi.fn();
const activity = vi.fn();
const stream = vi.fn();
const transport: TeacherCloudTransport = {
json: vi.fn(async (url) => {
if (url === '/questions') return { request_id: requestId, run_id: 'before-read' };
@@ -783,18 +784,31 @@ it.each(['suggestions', 'reply-v1', 'discussion-v1'] as const)(
payload: { items: [{ stream_event: { type: 'message_delta', message_id: id, content } }] },
});
if (url.includes('/before-read/')) {
accept('message', { thread_id: 'teacher-thread', payload: { items: [{ stream_event: {
type: 'message_delta', message_id: 'preamble', reasoning_content: '需要先读取项目。',
} }] } }, '0-1');
accept('message', message('teacher-thread', 'preamble', '我先检查重力。'), '1-0');
accept('message', message('teacher-thread', 'draft', '{"reply":"读取前的草案"}'), '2-0');
} else {
accept('message', message('child-thread', 'child', '子线程内容'), '1-0');
accept('message', { thread_id: 'teacher-thread', payload: { items: [{ stream_event: {
type: 'message_delta', message_id: 'final', reasoning_content: '已经获得项目证据。',
} }] } }, '1-1');
accept('message', message('teacher-thread', 'final', finalOutput.slice(0, 12)), '2-0');
expect(stream).toHaveBeenLastCalledWith({ response: finalOutput.slice(0, 12) });
expect(text).not.toHaveBeenCalled();
}
}),
};
await prepareCloudTeacher(
f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity
f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity, stream
).run([{ role: 'user', content: '一起讨论' }], new AbortController().signal, text);
expect(text.mock.calls).toEqual([[finalOutput]]);
expect(stream.mock.calls).toContainEqual([{ response: '' }]);
expect(stream.mock.calls).toContainEqual([{ response: finalOutput.slice(0, 12) }]);
expect(stream.mock.calls.flatMap(([update]) => update.reasoning ?? []).join(''))
.toBe('需要先读取项目。\n\n已经获得项目证据。');
expect(JSON.stringify(stream.mock.calls)).not.toContain('子线程内容');
expect(activity.mock.calls).toEqual([
[{ id: requestId + ':file', name: 'read_project_file', status: 'running' }],
[{ id: requestId + ':file', name: 'read_project_file', status: 'completed' }],
@@ -870,3 +884,38 @@ it('keeps only the cloud main-thread answer while advancing past child events on
expect(transport.events).toHaveBeenCalledTimes(2);
expect(streamUrls[1]).toContain('after_seq=2-0');
});
it('streams main-thread reasoning through reconnect and drains a completed run before finalizing', async () => {
const f = await fixture();
f.topic.requests.push({ id: requestId, text: '检查', references: [], createdAt: 'now',
sourceCursor: f.access.source.cursor, sourceCapturedAt: 'now', includedSourceMessageIds: [],
omittedMessages: 0, status: 'running', response: '', presentation: 'reply-v1' });
let reads = 0;
const stream = vi.fn(), text = vi.fn();
const transport: TeacherCloudTransport = {
json: vi.fn(async url => url === '/questions' ? { request_id: requestId, run_id: 'one' }
: ++reads === 1 ? { status: 'running', thread_id: 'teacher-thread' }
: { status: 'completed', thread_id: 'teacher-thread', output: '{"reply":"最后正文","quickReplies":[]}' }),
events: vi.fn(async (url, _signal, accept) => {
const event = (thread_id: string, message_id: string, fields: Record<string, string>) => ({
thread_id, payload: { chunk: { stream_event: { type: 'message_delta', message_id, ...fields } } },
});
if (url.endsWith('0-0')) {
accept('message', event('teacher-thread', 'answer', { reasoning_content: '先看代码。' }), '1-0');
expect(stream).toHaveBeenLastCalledWith({ reasoning: '先看代码。' });
accept('message', event('child-thread', 'child', { reasoning_content: 'child-private' }), '2-0');
throw new Error('stream interrupted');
}
expect(url).toContain('after_seq=2-0');
accept('message', event('teacher-thread', 'answer', { additional_reasoning_content: '再确认结果。' }), '3-0');
accept('message', event('teacher-thread', 'answer', { content: '{"reply":"最后' }), '4-0');
expect(stream).toHaveBeenLastCalledWith({ response: '{"reply":"最后' });
expect(text).not.toHaveBeenCalled();
}),
};
await prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest,
transport, undefined, stream).run([], new AbortController().signal, text);
expect(stream.mock.calls.flatMap(([update]) => update.reasoning ?? []).join('')).toBe('先看代码。再确认结果。');
expect(text.mock.calls).toEqual([['{"reply":"最后正文","quickReplies":[]}']]);
expect(transport.events).toHaveBeenCalledTimes(2);
});