Files
makelore/tests/e2e/fixtures/teacher-reply-api.ts
鲨鱼辣椒 cafebca057
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
fix(chat): reconcile reply recovery with continuous agent conversations
2026-09-28 18:14:12 +08:00

79 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Deterministic responses for layout/interaction tests only; never imported by the app.
import type { TeacherDefinition, TeacherTopic, TeacherSend } from '../../../shared/coding-teacher';
import { parseTeacherReply } from '../../../shared/teacher-reply';
const definition: TeacherDefinition = { config_id: 'coding-teacher', schema_version: 1, teacher_id: 'coding-teacher', name: '麦洛智能体', description: '', avatar_id: 'avatar-01', welcome_message: '', suggested_questions: [], system_prompt: '测试', skills: [], model: { model_id: 'fixture', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1500 } };
const samples: Record<string, unknown> = {
ideas: { kind: 'ideas', title: '我的小狗世界', items: [{ id: 'dog', text: '养一只自己的小狗', state: 'kept' }, { id: 'breed', parentId: 'dog', text: '是一只柯基', state: 'kept' }, { id: 'garden', text: '有一片可以种东西的花园', state: 'kept' }, { id: 'food', parentId: 'garden', text: '种出来的东西可以喂小狗', state: 'suggested' }] },
structure: { kind: 'structure', title: '小狗和花园怎么连起来?', nodes: [{ id: 'world', label: '我的小狗世界' }, { id: 'dog', label: '照顾小狗', parentId: 'world' }, { id: 'breed', label: '一只柯基', parentId: 'dog', relation: '它的样子' }, { id: 'garden', label: '种植花园', parentId: 'world' }, { id: 'food', label: '收获食物,喂给小狗', parentId: 'garden', relation: '收获后' }] },
flow: { kind: 'flow', title: '我回家时,小狗会做什么?', nodes: [{ id: 'home', kind: 'event', label: '我回到家' }, { id: 'awake', kind: 'condition', label: '小狗醒着吗?' }, { id: 'welcome', kind: 'outcome', label: '跑过来欢迎我' }, { id: 'sleep', kind: 'outcome', label: '先睡觉,醒来再过来' }], edges: [{ id: 'one', from: 'home', to: 'awake' }, { id: 'yes', from: 'awake', to: 'welcome', label: '醒着' }, { id: 'no', from: 'awake', to: 'sleep', label: '睡着了' }] },
comparison: { kind: 'comparison', title: '哪一种更像你想要的小狗?', columns: [{ id: 'click', label: '点了才回应' }, { id: 'active', label: '会主动找我' }], rows: [{ id: 'home', label: '刚回到家的时候', cells: [{ columnId: 'click', text: '等我点一下,才跳起来。' }, { columnId: 'active', text: '看见我就跑过来,还带着玩具。' }] }, { id: 'busy', label: '我忙着种东西的时候', cells: [{ columnId: 'click', text: '安静等着,不打扰我。' }, { columnId: 'active', text: '有时来蹭一蹭,可能也会打断我。' }] }] },
};
const scenario = new URLSearchParams(location.search).get('kind') ?? 'ideas';
export const fixtureAgent = { teacher_id: definition.teacher_id, version: 1, is_default: true, definition };
const continuous = new URLSearchParams(location.search).get('chat') === 'continuous';
let current: TeacherTopic = { schemaVersion: 1, revision: 1, id: 'discussion-fixture', accountId: 'fixture', projectId: 'fixture', sourceConversationId: 'project', definition, version: 1, createdAt: 'now', updatedAt: 'now', requests: Array.from({ length: 8 }, (_, i) => ({ id: `r${i}`, text: i === 7 ? '我还没想好,我们边聊边想。' : '我希望这个游戏更有意思。', references: [], createdAt: 'now', sourceCursor: { workerGeneration: 0, seq: i }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: i === 7 ? '可以,我们先把已经想到的放在一起。哪里还不确定,就留着继续聊。' : '先从你最在意的一个小地方开始想。', ...(i === 7 ? { suggestedQuestions: ['有什么好玩的想法?'] } : {}) })), ...(samples[scenario] ? { discussion: { id: 'tool', revision: 1, status: 'active', content: structuredClone(samples[scenario]) } } : {}) };
if (continuous) current.conversation = { agentId: definition.teacher_id, segmentTurns: 1, discussions: {} };
const stream = Object.assign(new EventTarget(), { close() {}, onerror: null, onopen: null });
if (scenario === 'recovered' || scenario === 'incomplete') {
const raw = '{"reply":"它早就不是"刚搭好架子"那种阶段了——骨架、玩法、成长线都在跑。\n现在的阶段是「能玩,正在调手感」。'
+ (scenario === 'recovered' ? '","quickReplies":[],"tool":{"kind":"structure","nodes":[]}}' : '');
const parsed = parseTeacherReply(raw);
current.requests = [{
...current.requests[0], projectId: 'fixture', text: '现在项目是什么阶段?', sourceConversationId: 'original-source',
references: [{ messageId: 'original-message', text: '原来的项目记录', role: 'assistant' }],
response: parsed.reply, replyIncomplete: parsed.incomplete, replyParseError: parsed.parseError,
suggestedQuestions: parsed.quickReplies, unparsedResponse: raw,
}];
}
if (scenario === 'retry') {
const status = new URLSearchParams(location.search).get('status') === 'cancelled' ? 'cancelled' : 'failed';
current.requests = [{
...current.requests[0], text: '给我一点提示', intent: 'suggestions', status, response: '',
...(status === 'failed' ? { error: '这次没连上,我们可以再试一次。' } : {}),
}];
}
if (scenario === 'rich-text') {
current.requests = [{
...current.requests[0], text: '帮我看看项目和最近的讨论',
response: '## 项目建议\n\n**先核对已有设计**,再决定下一步。\n\n- 检查入口文件\n- 保留当前玩法\n\n| 文件 | 作用 | 需要确认 |\n| --- | --- | --- |\n| src/main.ts | 初始化项目 | 游戏循环与计时 |\n| public/assets | 图片资源 | 构建后路径是否可用 |\n\n```js\nconst message = "保留字符串里的\\n,不把它变成实际换行";\nconst resourcePath = "/assets/" + "very-long-file-name-".repeat(15);\n```\n\n运动距离:$s = vt$。\n\n![项目截图](https://example.com/teacher-preview.png)\n\n[项目说明](https://example.com/readme)\n\n> 先验证观察到的现象。',
discussionError: '这次整理没有完成,先保留原来的内容。',
unparsedResponse: '{"reply":"正文已保留","tool":{"kind":"structure",',
}];
}
let timer: ReturnType<typeof setTimeout> | undefined;
const sentInputs: TeacherSend[] = [];
Object.defineProperty(window, '__teacherReplyInputs', { value: sentInputs });
const snapshot = () => { current.revision++; stream.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(current) })); };
export const teacherApi = {
conversation: async () => ({ topic: structuredClone(current), before: null }),
seenConversation: async (_agentId: string, requestId: string) => {
if (current.conversation) current.conversation.seenThrough = requestId;
return structuredClone(current);
},
sendConversation: async (_agentId: string, input: TeacherSend): Promise<TeacherTopic> => teacherApi.send('/fixture', current.id, input),
catalog: async () => ({ items: [{ teacher_id: definition.teacher_id, version: 1, is_default: true, definition }] }),
config: async () => ({ enabled: true, definition }),
list: async () => ({ items: [{ id: current.id, title: '我的小狗世界' }], lastSelectedTopicId: current.id }),
read: async () => structuredClone(current),
create: async () => structuredClone(current),
events: async () => stream,
send: async (_base: string, _id: string, input: TeacherSend) => {
if ('presentation' in input || 'discussion' in input) throw new Error('Retired component fields must not be sent');
sentInputs.push(structuredClone(input));
current.requests.push({ id: input.requestId, text: input.text, intent: input.intent, references: [], createdAt: 'now', status: 'running', sourceCursor: { workerGeneration: 0, seq: 1 }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, response: '', presentation: 'reply-v1' });
snapshot();
timer = setTimeout(() => {
const request = current.requests.at(-1)!;
request.status = 'completed';
request.response = input.intent === 'suggestions' ? '我们可以先从一个小地方开始。' : '好,我们就从这一个问题聊起。';
if (input.intent === 'suggestions') request.suggestedQuestions = ['可以从哪个小地方开始?'];
snapshot();
}, 350);
return structuredClone(current);
},
cancel: async () => { clearTimeout(timer); current.requests.at(-1)!.status = 'cancelled'; snapshot(); return structuredClone(current); },
};
export const teacherTopicsPath = () => '/fixture';
export const legacyTopicBase = (base: string) => base;