Merge cloud-owned teaching policy and bounded new shortcuts

This commit is contained in:
鲨鱼辣椒
2026-09-28 18:06:30 +08:00
12 changed files with 319 additions and 32 deletions

View File

@@ -625,6 +625,28 @@ describe('teacher contextual discussion entry points', () => {
});
});
it.each([false, true])('persists prose and exact raw diagnostics instead of excess new shortcuts (cloud=%s)', async (cloudTeacher) => {
const f = await fixture(cloudTeacher ? { cloudTeacher: true, mockCloud: true } : {});
const scope = newScope(f);
const topic = await f.service.create(scope);
const reply = '我们先看看规则会怎样影响结局。';
const quickReplies = ['看懂规则|现在是什么规则?', '想想影响|摸头会改变什么?', '试试结果|怎样验证?', '换个话题|先聊别的。'];
const raw = JSON.stringify({ reply, quickReplies }, null, 2);
f.replyWith(raw);
await f.service.send(scope, topic.id, { requestId, intent: 'suggestions', text: '帮我看看' });
const completed = await finishRequest(f, scope, topic.id);
expect(completed.requests[0]).toMatchObject({
status: 'completed', response: reply, suggestedQuestions: [],
replyParseError: expect.stringContaining('最多 3 条'), unparsedResponse: raw,
});
const disk = JSON.parse(await readFile(path.join(
f.created.project.path, '.makelore/teacher-conversations', topic.accountId, 'project', topic.id + '.json'
), 'utf8'));
expect(disk.requests[0]).toMatchObject({
response: reply, suggestedQuestions: [], replyParseError: completed.requests[0].replyParseError, unparsedResponse: raw,
});
});
it('does not treat a cancelled response as valid suggestions even when its JSON is complete', async () => {
const f = await fixture();
const scope = newScope(f);
@@ -695,7 +717,7 @@ describe('teacher contextual discussion entry points', () => {
expect(saved.requests[0].suggestedQuestions).toBeUndefined();
});
it('adds a one-question guided opening for this round without changing the teacher system instructions', async () => {
it('marks the guided-help entry without choosing a teaching strategy or changing the system instructions', async () => {
const f = await fixture();
const scope = newScope(f);
const topic = await f.service.create(scope);
@@ -707,7 +729,7 @@ describe('teacher contextual discussion entry points', () => {
});
expect(completed.requests[0].suggestedQuestions).toEqual([]);
const messages = f.run.mock.calls[0][0];
expect(messages.at(-1).content).toContain('推荐一个具体切入点');
expect(messages.at(-1).content).toContain('学生主动求助,表示暂时说不清想问什么');
expect(messages[0]).toEqual(compileTeacherContext(definition, context, [], '问题', [], undefined, 'question', undefined, true).messages[0]);
});
@@ -752,26 +774,27 @@ describe('teacher contextual discussion entry points', () => {
expect(f.run).not.toHaveBeenCalled();
});
it('budgets the round-specific instructions and honestly starts from ideas with no source', () => {
it('budgets the entry facts even when no source conversation is available', () => {
const empty = { ...context, messages: [] };
const ordinary = compileTeacherContext(definition, empty, [], '老师,帮我看看', []);
const suggestions = compileTeacherContext(definition, empty, [], '老师,帮我看看', [], 8000, 'suggestions');
expect(suggestions.messages[0]).toEqual(ordinary.messages[0]);
expect(suggestions.messages.at(-1)?.content).toContain('没有可用上下文时');
expect(suggestions.messages.at(-1)?.content).toContain('不编造');
expect(suggestions.messages.at(-1)?.content).toContain('学生通过“帮我看看”主动请求帮助');
expect(suggestions.messages.some(message => message.content.startsWith('来源编程会话'))).toBe(false);
expect(() => compileTeacherContext(definition, empty, [], '老师,帮我看看', [], estimateTeacherTokens(ordinary.messages), 'suggestions'))
.toThrow('超过上下文预算');
});
it.each(['suggestions', 'guided-help'] as const)('recommends one concrete starting point for %s without local expression policy', (intent) => {
it.each(['suggestions', 'guided-help'] as const)('passes %s entry facts without local teaching or expression policy', (intent) => {
const selected = { ...definition, system_prompt: '云端配置:正文长度随内容,选项保留【我想聊】前缀。' };
const compiled = compileTeacherContext(selected, context, [], '帮我看看', [], 8000, intent, teacherReplyInstructions());
const current = compiled.messages.at(-1)!.content;
expect(current).toContain('推荐一个具体切入点');
expect(current).toContain('快捷回复围绕这个切入点');
expect(current).toContain(intent === 'suggestions'
? '学生通过“帮我看看”主动请求帮助' : '学生主动求助,表示暂时说不清想问什么');
expect(compiled.messages[0].content).toContain(selected.system_prompt);
const local = compiled.messages.slice(1).map(message => message.content).join('\n');
for (const removed of ['400 字', '120 字', '2–3', '简短中文', '学生口吻', 'Alice', '组件', '先这些'])
for (const removed of ['400 字', '120 字', '2–3', '简短中文', '学生口吻', 'Alice', '组件', '先这些',
'推荐一个具体切入点', '说明为何', '快捷回复围绕', '从构思切入', '整理想法', '理解关系', '承接已确认的共识'])
expect(local).not.toContain(removed);
});
@@ -819,9 +842,8 @@ describe('project teacher check-ins', () => {
expect(JSON.stringify(messages)).toContain('创建计数器');
expect(JSON.stringify(messages)).toContain('变量是什么意思');
expect(messages.at(-1)).toMatchObject({ role: 'system' });
expect(messages.at(-1).content).toContain('表达方式遵循已配置的要求');
expect(messages.at(-1).content).toContain('按照已配置的人设和职责');
expect(messages.at(-1).content).toContain('不声称实际运行');
expect(messages.at(-1).content).toContain('程序触发的项目进展检查,学生没有在本轮主动提问');
expect(messages.at(-1).content).toContain('回应方式遵循当前智能体的云端配置');
expect(f.prepareModel).toHaveBeenLastCalledWith(expect.anything(), selected.definition, expect.objectContaining({
projectPath: f.created.project.path, source: context, assertCurrent: expect.any(Function),
}), { finalOnly: false });
@@ -1435,7 +1457,7 @@ describe('project consultations with selected cloud teachers', () => {
expect(result.topic?.id).toBe(topic.id);
expect(result.topic?.requests[0]).toMatchObject({ intent: 'check-in', sourceConversationId: f.scope.sourceId });
const messages = f.run.mock.calls[0][0] as Array<{ role: string; content: string }>;
expect(messages.some(message => message.role === 'system' && message.content.includes('不是用户提问'))).toBe(true);
expect(messages.some(message => message.role === 'system' && message.content.includes('学生没有在本轮主动提问'))).toBe(true);
expect(messages[0].content).not.toContain(TEACHER_BEHAVIOR_PROMPT);
expect(f.prepareCloud.mock.calls[0][3]).toMatchObject({ projectPath: f.created.project.path, source: context });
expect(f.prepareModel).not.toHaveBeenCalled();