feat(teacher): 连接 Yuxi 老师并提供项目与会话只读工具

This commit is contained in:
2026-09-23 09:56:11 +08:00
parent 1f2ad3fb3a
commit e35a13079e
14 changed files with 886 additions and 30 deletions

View File

@@ -447,6 +447,10 @@ async function installCodingFirstChatHost(
});
const teacherDefinition = {schema_version:1,teacher_id:'coding-teacher',name:'代码老师',description:'',avatar_id:'avatar-01',welcome_message:'一起理解代码',suggested_questions:['解释当前代码'],system_prompt:'教学',skills:[],model:{model_id:'model-a',reasoning_choice:{mode:'default'}},limits:{max_input_tokens:8000,max_output_tokens:1500}};
const teachers = [
{ teacher_id: 'teacher-code', version: 1, is_default: true, definition: { ...teacherDefinition, config_id: 'teacher-code', runtime: 'yuxi', system_prompt: '', yuxi: { agent_slug: 'code', agent_version: 2 } } },
{ teacher_id: 'teacher-algorithm', version: 9, is_default: false, definition: { ...teacherDefinition, config_id: 'teacher-algorithm', runtime: 'yuxi', name: '算法老师', system_prompt: '', yuxi: { agent_slug: 'algorithm', agent_version: 4 } } },
];
let teacherTopic: Record<string, unknown> | null = null;
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (
@@ -521,9 +525,11 @@ async function installCodingFirstChatHost(
}
if (path === '/api/coding/teacher/config') return respond({enabled:true,revision:1,published_version:1,definition:teacherDefinition});
if (path === '/api/coding/teacher/teachers') return respond({ items: teachers });
if (path.endsWith('/teacher-topics') && method === 'GET') return respond({items:teacherTopic?[{id:teacherTopic.id,title:'老师话题'}]:[],lastSelectedTopicId:teacherTopic?.id??null});
if (path.endsWith('/teacher-topics') && method === 'POST') {
teacherTopic={schemaVersion:1,revision:0,id:'teacher-topic',accountId:'e2e',projectId:project.id,sourceConversationId:conversation.id,definition:teacherDefinition,version:1,createdAt:now,updatedAt:now,requests:[]};
const selected = teachers.find(item => item.version === body?.teacherVersion) ?? teachers[0];
teacherTopic={schemaVersion:1,revision:0,id:'teacher-topic',accountId:'e2e',projectId:project.id,sourceConversationId:conversation.id,definition:selected.definition,version:selected.version,createdAt:now,updatedAt:now,requests:[]};
return respond(teacherTopic,201);
}
if (path.endsWith('/teacher-topics/teacher-topic/messages') && method === 'POST') {
@@ -1723,15 +1729,18 @@ test('project teacher side chat returns advice to the coding draft without submi
const teacher=page.getByTestId('teacher-chat-panel');
await expect(teacher.getByText('一起理解代码')).toBeVisible();
await expect(teacher.getByText('结合当前会话和项目文件答疑')).toBeVisible();
await teacher.getByRole('combobox', { name: '新话题使用的老师' }).selectOption('9');
await teacher.getByRole('textbox',{name:'向老师提问'}).fill('帮我理解当前代码');
await teacher.getByRole('button',{name:'提问',exact:true}).click();
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
await expect(teacher.getByRole('heading', { name: '算法老师' })).toBeVisible();
await expect(teacher.getByText('较长的上下文已节选,老师可按需读取原文。')).toBeVisible();
await teacher.getByRole('button',{name:'带回主会话草稿'}).click();
await expect(composer).toHaveValue('保留我的草稿\n\n先理解状态如何随点击变化,再修改代码。');
const requests=(await readState(electronApp)).captured;
expect(requests.filter(item=>item.path.endsWith('/prompt')&&item.method==='POST')).toHaveLength(0);
expect(requests.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
expect(requests.find(item=>item.path.endsWith('/teacher-topics')&&item.method==='POST')?.body?.teacherVersion).toBe(9);
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
await teacher.getByRole('button',{name:'关闭老师'}).click();await expect(teacher).toHaveCount(0);
await page.getByRole('button',{name:'问老师',exact:true}).click();