Skip to content

Commit b296cbb

Browse files
committed
merge: 导入时保留语文与无课程库学段的知识点标签
2 parents 6f8572e + 297fe7f commit b296cbb

9 files changed

Lines changed: 137 additions & 8 deletions

File tree

‎src/__tests__/unit/ai-prompts.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ describe('AI Prompts', () => {
9090
const prompt = generateAnalyzePrompt('zh', null, '数学');
9191
expect(prompt).toContain('数学');
9292
});
93+
it('未指定学科时列出语文等全部学科候选标签', () => {
94+
const prompt = generateAnalyzePrompt('zh', null, null, {
95+
prefetchedChineseTags: ['声母', '韵母'],
96+
prefetchedHistoryTags: ['辛亥革命'],
97+
});
98+
expect(prompt).toContain('**语文标签 (Chinese Tags):**');
99+
expect(prompt).toContain('"声母", "韵母"');
100+
expect(prompt).toContain('**历史标签 (History Tags):**');
101+
expect(prompt).toContain('"辛亥革命"');
102+
});
93103

94104
it('应该根据年级过滤数学标签(初一)', () => {
95105
const prompt = generateAnalyzePrompt('zh', 7, '数学');

‎src/__tests__/unit/ai/knowledge-points.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ describe('AI 知识点候选集合校验', () => {
1111
it('未指定错题本时,按实际识别的学科选择预取清单', () => {
1212
expect(normalizeAiQuestionTags({ subject: '物理', knowledgePoints: ['加法', '力学'] }, { prefetchedMathTags: ['加法'], prefetchedPhysicsTags: ['力学'] }).knowledgePoints).toEqual(['力学']);
1313
});
14+
it('未指定错题本时,语文按语文预取清单过滤,保留语文学科标签', () => {
15+
expect(normalizeAiQuestionTags(
16+
{ subject: '语文', knowledgePoints: ['声母', '拼音', '一元一次方程'] },
17+
{ prefetchedChineseTags: ['声母', '韵母'], prefetchedMathTags: ['一元一次方程'] },
18+
).knowledgePoints).toEqual(['声母']);
19+
});
20+
it('未指定错题本且该学科没有内置知识点时,允许 AI 自行命名', () => {
21+
expect(normalizeAiQuestionTags(
22+
{ subject: '历史', knowledgePoints: ['辛亥革命', '洋务运动'] },
23+
{ prefetchedHistoryTags: [] },
24+
).knowledgePoints).toEqual(['辛亥革命', '洋务运动']);
25+
});
1426
it('即使没有提供候选集合,AI 标签也最多五个且不重复', () => {
1527
expect(normalizeAiQuestionTags({ knowledgePoints: ['1', '1', '2', '3', '4', '5', '6'] }, {}).knowledgePoints).toEqual(['1', '2', '3', '4', '5']);
1628
});

‎src/__tests__/unit/ai/prompt-requests.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ describe('AI 请求的材料边界', () => {
5959
expect(JSON.parse(request.user).data.gradeSemester).toContain(material);
6060
});
6161

62+
it('识别请求把 allowCustom 传给提示词,按自行命名规则选择知识点', () => {
63+
const request = generateAnalyzeRequest('zh', null, '数学', {
64+
candidateTags: { subjectName: '数学', tags: [], allowCustom: true },
65+
});
66+
expect(request.system).toContain('原样复用');
67+
expect(request.system).not.toContain('必须从上述列表中选择精确匹配的标签');
68+
});
6269

6370
});
6471

‎src/__tests__/unit/ai/tag-service-scope.test.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const findMany = vi.hoisted(() => vi.fn());
77
vi.mock('@/lib/prisma', () => ({ prisma: { knowledgeTag: { findMany } } }));
88
vi.mock('@/lib/logger', () => ({ createLogger: vi.fn(() => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() })) }));
99

10-
import { getNotebookCandidateTags, getScopeTagsFromDB } from '@/lib/ai/tag-service';
10+
import { getNotebookCandidateTags, getScopeTagsFromDB, prefetchSubjectTags } from '@/lib/ai/tag-service';
1111

1212
// 年级 → 章节 → 知识点;小学两个年级、初中一个年级
1313
const tree = [
@@ -51,3 +51,28 @@ describe('getScopeTagsFromDB', () => {
5151
expect(await getScopeTagsFromDB('math', 'primary')).toEqual([]);
5252
});
5353
});
54+
55+
describe('prefetchSubjectTags', () => {
56+
beforeEach(() => {
57+
findMany.mockReset();
58+
findMany.mockResolvedValue(tree);
59+
});
60+
61+
it('未指定学科时预取全部学科标签,语文不再缺省为空', async () => {
62+
const tags = await prefetchSubjectTags(null, 5);
63+
expect(Object.keys(tags).sort()).toEqual([
64+
'prefetchedBiologyTags', 'prefetchedChemistryTags', 'prefetchedChineseTags',
65+
'prefetchedEnglishTags', 'prefetchedGeographyTags', 'prefetchedHistoryTags',
66+
'prefetchedMathTags', 'prefetchedPhysicsTags', 'prefetchedPoliticsTags',
67+
]);
68+
// 数据库查询由 Prisma 过滤学科与叶子节点,mock 只验证预取键已包含语文
69+
expect(tags.prefetchedChineseTags).toContain('10以内数的认识');
70+
});
71+
72+
it('指定学科时只预取该学科标签', async () => {
73+
const tags = await prefetchSubjectTags('语文', 5);
74+
expect(tags.prefetchedChineseTags.length).toBeGreaterThan(0);
75+
expect(tags.prefetchedMathTags).toEqual([]);
76+
expect(tags.prefetchedEnglishTags).toEqual([]);
77+
});
78+
});

‎src/lib/ai/knowledge-points.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { PromptOptions } from './prompts';
33

44
const SUBJECT_TAG_FIELDS = {
55
数学: 'prefetchedMathTags', 物理: 'prefetchedPhysicsTags', 化学: 'prefetchedChemistryTags',
6-
生物: 'prefetchedBiologyTags', 英语: 'prefetchedEnglishTags',
6+
生物: 'prefetchedBiologyTags', 英语: 'prefetchedEnglishTags', 语文: 'prefetchedChineseTags',
7+
历史: 'prefetchedHistoryTags', 地理: 'prefetchedGeographyTags', 政治: 'prefetchedPoliticsTags',
78
} as const;
89

910
/**
@@ -18,10 +19,13 @@ export function normalizeAiQuestionTags<T extends { knowledgePoints: string[]; s
1819
if (!options.candidateTags) {
1920
const field = SUBJECT_TAG_FIELDS[question.subject as keyof typeof SUBJECT_TAG_FIELDS];
2021
if (field) {
21-
allowedTags = options[field];
22+
// 该学科暂无内置知识点(如历史、地理、政治)时允许 AI 自行命名
23+
const subjectTags = options[field];
24+
allowedTags = subjectTags?.length ? subjectTags : undefined;
2225
} else {
23-
const available = Object.values(SUBJECT_TAG_FIELDS).flatMap(key => options[key] ? [options[key]] : []);
24-
allowedTags = available.length ? available.flat() : undefined;
26+
const available = Object.values(SUBJECT_TAG_FIELDS)
27+
.flatMap(key => options[key] ?? []);
28+
allowedTags = available.length ? available : undefined;
2529
}
2630
}
2731
}

‎src/lib/ai/prompt-requests.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,20 @@ function inputOptions(options: PromptOptions): Record<string, unknown> {
3333

3434
function referenceOptions(options: PromptOptions): PromptOptions {
3535
const result = { ...options };
36-
const fields = ['prefetchedMathTags', 'prefetchedPhysicsTags', 'prefetchedChemistryTags', 'prefetchedBiologyTags', 'prefetchedEnglishTags'] as const;
36+
const fields = [
37+
'prefetchedMathTags', 'prefetchedPhysicsTags', 'prefetchedChemistryTags',
38+
'prefetchedBiologyTags', 'prefetchedEnglishTags', 'prefetchedChineseTags',
39+
'prefetchedHistoryTags', 'prefetchedGeographyTags', 'prefetchedPoliticsTags',
40+
] as const;
3741
for (const field of fields) {
3842
if (options[field]) result[field] = options[field].length ? [ref('tagOptions.' + field)] : [];
3943
}
4044
if (options.candidateTags) {
4145
result.candidateTags = {
4246
subjectName: safeSubject(options.candidateTags.subjectName) || '本题学科',
4347
tags: options.candidateTags.tags.length ? [ref('tagOptions.candidateTags.tags')] : [],
48+
// 该学段没有课程库时允许自行命名,提示词必须据此切换选择规则
49+
...(options.candidateTags.allowCustom ? { allowCustom: true } : {}),
4450
};
4551
}
4652
result.originalFigureSvg = options.originalFigureSvg ? ref('tagOptions.originalFigureSvg') : null;

‎src/lib/ai/prompts/analyze.ts‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ export function generateAnalyzePrompt(
177177
const englishTags = options?.prefetchedEnglishTags || [];
178178
const englishTagsString = englishTags.length > 0 ? englishTags.map(tag => `"${tag}"`).join(", ") : '(无可用标签)';
179179

180+
const chineseTags = options?.prefetchedChineseTags || [];
181+
const chineseTagsString = chineseTags.length > 0 ? chineseTags.map(tag => `"${tag}"`).join(", ") : '(无可用标签)';
182+
183+
const historyTags = options?.prefetchedHistoryTags || [];
184+
const historyTagsString = historyTags.length > 0 ? historyTags.map(tag => `"${tag}"`).join(", ") : '(无可用标签)';
185+
186+
const geographyTags = options?.prefetchedGeographyTags || [];
187+
const geographyTagsString = geographyTags.length > 0 ? geographyTags.map(tag => `"${tag}"`).join(", ") : '(无可用标签)';
188+
189+
const politicsTags = options?.prefetchedPoliticsTags || [];
190+
const politicsTagsString = politicsTags.length > 0 ? politicsTags.map(tag => `"${tag}"`).join(", ") : '(无可用标签)';
191+
180192
// 根据科目决定显示哪些标签(节省 token,提高准确性)
181193
let tagsSection = "";
182194

@@ -236,6 +248,38 @@ ${biologyTagsString}
236248
使用课程大纲中的**精确标签名称**,可选标签如下:
237249
${englishTagsString}
238250
251+
**重要提示**:
252+
- 必须从上述列表中选择精确匹配的标签
253+
- 每题最多 5 个标签`;
254+
} else if (subject === '语文') {
255+
tagsSection = `**语文标签 (Chinese Tags):**
256+
使用课程大纲中的**精确标签名称**,可选标签如下:
257+
${chineseTagsString}
258+
259+
**重要提示**:
260+
- 必须从上述列表中选择精确匹配的标签
261+
- 每题最多 5 个标签`;
262+
} else if (subject === '历史') {
263+
tagsSection = `**历史标签 (History Tags):**
264+
使用课程大纲中的**精确标签名称**,可选标签如下:
265+
${historyTagsString}
266+
267+
**重要提示**:
268+
- 必须从上述列表中选择精确匹配的标签
269+
- 每题最多 5 个标签`;
270+
} else if (subject === '地理') {
271+
tagsSection = `**地理标签 (Geography Tags):**
272+
使用课程大纲中的**精确标签名称**,可选标签如下:
273+
${geographyTagsString}
274+
275+
**重要提示**:
276+
- 必须从上述列表中选择精确匹配的标签
277+
- 每题最多 5 个标签`;
278+
} else if (subject === '政治') {
279+
tagsSection = `**政治标签 (Politics Tags):**
280+
使用课程大纲中的**精确标签名称**,可选标签如下:
281+
${politicsTagsString}
282+
239283
**重要提示**:
240284
- 必须从上述列表中选择精确匹配的标签
241285
- 每题最多 5 个标签`;
@@ -254,7 +298,19 @@ ${chemistryTagsString}
254298
${biologyTagsString}
255299
256300
**英语标签 (English Tags):**
257-
${englishTagsString}`;
301+
${englishTagsString}
302+
303+
**语文标签 (Chinese Tags):**
304+
${chineseTagsString}
305+
306+
**历史标签 (History Tags):**
307+
${historyTagsString}
308+
309+
**地理标签 (Geography Tags):**
310+
${geographyTagsString}
311+
312+
**政治标签 (Politics Tags):**
313+
${politicsTagsString}`;
258314
}
259315

260316
const template = options?.customTemplate || DEFAULT_ANALYZE_TEMPLATE;

‎src/lib/ai/prompts/shared.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ export interface PromptOptions {
185185
prefetchedChemistryTags?: string[];
186186
prefetchedBiologyTags?: string[];
187187
prefetchedEnglishTags?: string[];
188+
prefetchedChineseTags?: string[];
189+
prefetchedHistoryTags?: string[];
190+
prefetchedGeographyTags?: string[];
191+
prefetchedPoliticsTags?: string[];
188192
// 错题本学段 + 学科下的全部知识点;提供时直接使用,不再按学科、年级取标签
189193
candidateTags?: CandidateTags;
190194
// 举一反三时原题的 SVG 配图,供 AI 参考重新画图

‎src/lib/ai/tag-service.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ export async function getNotebookCandidateTags(scope: NotebookScope, tagSet: str
238238
}
239239

240240
/**
241-
* 未指定错题本时的旧逻辑:数学按学生年级取标签,其他学科取全部;未知学科时各科都取,让 AI 判断
241+
* 未指定错题本时的候选标签:数学按学生年级取标签,其他学科取该学科全部知识点;
242+
* 未知学科时各科都取,让 AI 判断后再按学科过滤。
242243
*/
243244
export async function prefetchSubjectTags(subject: string | null | undefined, grade: GradeLevel | null | undefined, tagSet: string = 'default') {
244245
return {
@@ -247,5 +248,9 @@ export async function prefetchSubjectTags(subject: string | null | undefined, gr
247248
prefetchedChemistryTags: (subject === '化学' || !subject) ? await getTagsFromDB('chemistry', tagSet) : [],
248249
prefetchedBiologyTags: (subject === '生物' || !subject) ? await getTagsFromDB('biology', tagSet) : [],
249250
prefetchedEnglishTags: (subject === '英语' || !subject) ? await getTagsFromDB('english', tagSet) : [],
251+
prefetchedChineseTags: (subject === '语文' || !subject) ? await getTagsFromDB('chinese', tagSet) : [],
252+
prefetchedHistoryTags: (subject === '历史' || !subject) ? await getTagsFromDB('history', tagSet) : [],
253+
prefetchedGeographyTags: (subject === '地理' || !subject) ? await getTagsFromDB('geography', tagSet) : [],
254+
prefetchedPoliticsTags: (subject === '政治' || !subject) ? await getTagsFromDB('politics', tagSet) : [],
250255
};
251256
}

0 commit comments

Comments
 (0)