@zhouchangui/math-ati 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local.example +6 -0
- package/AGENTS.md +273 -0
- package/README.md +34 -0
- package/bin/math-ati.js +194 -0
- package/dist/assets/index-BYFoutza.js +22 -0
- package/dist/assets/index-Bk2WFPoL.css +1 -0
- package/dist/index.html +13 -0
- package/package.json +72 -0
- package/prompts/grading.system.md +129 -0
- package/prompts/knowledge-extract.system.md +123 -0
- package/prompts/knowledge-summarize.system.md +127 -0
- package/prompts/learning-summary.system.md +123 -0
- package/prompts/pdf-grading.system.md +80 -0
- package/prompts/pdf-page-extract.system.md +52 -0
- package/prompts/pdf-recheck.system.md +43 -0
- package/prompts/practice-generate.system.md +161 -0
- package/prompts/practice-review.system.md +65 -0
- package/prompts/practice-revise.system.md +56 -0
- package/server/abilityService.js +259 -0
- package/server/agentClient.js +202 -0
- package/server/env.js +4 -0
- package/server/fileStore.js +726 -0
- package/server/grading.js +116 -0
- package/server/index.js +655 -0
- package/server/jobStore.js +169 -0
- package/server/knowledgeBase.js +30 -0
- package/server/knowledgeExtractor.js +360 -0
- package/server/knowledgeFeedback.js +299 -0
- package/server/llmConfig.js +96 -0
- package/server/mistakeLifecycle.js +251 -0
- package/server/pdfSubmissionGrader.js +846 -0
- package/server/practiceGenerator.js +908 -0
- package/server/practicePaperHtml.js +313 -0
- package/server/practiceReviewer.js +307 -0
- package/server/practiceService.js +331 -0
- package/server/promptStore.js +16 -0
- package/server/submissionService.js +184 -0
- package/templates/workspace/.env.local.example +6 -0
- package/templates/workspace/data/global/ability_index.json +5 -0
- package/templates/workspace/data/global/chapters.json +621 -0
- package/templates/workspace/data/global/mastery_index.json +6 -0
- package/templates/workspace/data/global/mistakes_index.json +7 -0
- package/templates/workspace/data/global/student_profile.json +11 -0
- package/templates/workspace/data/knowledge_points.json +1264 -0
- package/templates/workspace/data/mistakes.json +1 -0
- package/vite.config.js +21 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const jobs = new Map();
|
|
4
|
+
|
|
5
|
+
function publicJob(job) {
|
|
6
|
+
return {
|
|
7
|
+
id: job.id,
|
|
8
|
+
type: job.type,
|
|
9
|
+
status: job.status,
|
|
10
|
+
createdAt: job.createdAt,
|
|
11
|
+
updatedAt: job.updatedAt,
|
|
12
|
+
progress: job.progress,
|
|
13
|
+
events: job.events.slice(-20),
|
|
14
|
+
result: job.result,
|
|
15
|
+
error: job.error,
|
|
16
|
+
submissionId: job.submissionId,
|
|
17
|
+
practiceId: job.practiceId,
|
|
18
|
+
chapterId: job.chapterId
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createJob({ type, practiceId = null, chapterId = null, submissionId = null } = {}) {
|
|
23
|
+
const job = {
|
|
24
|
+
id: `job-${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID().slice(0, 8)}`,
|
|
25
|
+
type,
|
|
26
|
+
practiceId,
|
|
27
|
+
chapterId,
|
|
28
|
+
submissionId,
|
|
29
|
+
status: 'queued',
|
|
30
|
+
progress: { message: '任务已创建,等待开始。' },
|
|
31
|
+
events: [],
|
|
32
|
+
result: null,
|
|
33
|
+
error: null,
|
|
34
|
+
listeners: new Set(),
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
updatedAt: new Date().toISOString()
|
|
37
|
+
};
|
|
38
|
+
jobs.set(job.id, job);
|
|
39
|
+
return publicJob(job);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getJob(jobId) {
|
|
43
|
+
const job = jobs.get(jobId);
|
|
44
|
+
return job ? publicJob(job) : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getJobInternal(jobId) {
|
|
48
|
+
return jobs.get(jobId) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function updateJob(jobId, patch = {}) {
|
|
52
|
+
const job = jobs.get(jobId);
|
|
53
|
+
if (!job) return null;
|
|
54
|
+
Object.assign(job, patch, { updatedAt: new Date().toISOString() });
|
|
55
|
+
if (patch.progress) {
|
|
56
|
+
job.events.push({
|
|
57
|
+
ts: new Date().toISOString(),
|
|
58
|
+
...patch.progress
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
broadcastJob(job);
|
|
62
|
+
return publicJob(job);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function addJobEvent(jobId, event) {
|
|
66
|
+
const job = jobs.get(jobId);
|
|
67
|
+
if (!job) return null;
|
|
68
|
+
const nextEvent = {
|
|
69
|
+
ts: new Date().toISOString(),
|
|
70
|
+
...event
|
|
71
|
+
};
|
|
72
|
+
job.progress = nextEvent;
|
|
73
|
+
job.events.push(nextEvent);
|
|
74
|
+
job.updatedAt = new Date().toISOString();
|
|
75
|
+
broadcastJob(job);
|
|
76
|
+
return nextEvent;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function completeJob(jobId, result) {
|
|
80
|
+
const job = jobs.get(jobId);
|
|
81
|
+
if (!job) return null;
|
|
82
|
+
job.status = 'done';
|
|
83
|
+
job.result = result;
|
|
84
|
+
job.progress = {
|
|
85
|
+
ts: new Date().toISOString(),
|
|
86
|
+
step: 'job.done',
|
|
87
|
+
message: '任务完成。'
|
|
88
|
+
};
|
|
89
|
+
job.events.push(job.progress);
|
|
90
|
+
job.updatedAt = new Date().toISOString();
|
|
91
|
+
broadcastJob(job);
|
|
92
|
+
closeJob(job);
|
|
93
|
+
return publicJob(job);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function failJob(jobId, error) {
|
|
97
|
+
const job = jobs.get(jobId);
|
|
98
|
+
if (!job) return null;
|
|
99
|
+
job.status = 'failed';
|
|
100
|
+
job.error = {
|
|
101
|
+
message: error?.message || String(error),
|
|
102
|
+
reason: error?.reason || null,
|
|
103
|
+
detail: error?.detail || null,
|
|
104
|
+
status: error?.status || null,
|
|
105
|
+
stack: error?.stack || null
|
|
106
|
+
};
|
|
107
|
+
job.progress = {
|
|
108
|
+
ts: new Date().toISOString(),
|
|
109
|
+
step: 'job.failed',
|
|
110
|
+
message: `任务失败:${job.error.message}${job.error.detail ? `(${job.error.detail}` : ''}${job.error.detail ? ')' : ''}`
|
|
111
|
+
};
|
|
112
|
+
job.events.push(job.progress);
|
|
113
|
+
job.updatedAt = new Date().toISOString();
|
|
114
|
+
broadcastJob(job);
|
|
115
|
+
closeJob(job);
|
|
116
|
+
return publicJob(job);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function startJob(jobId) {
|
|
120
|
+
return updateJob(jobId, {
|
|
121
|
+
status: 'running',
|
|
122
|
+
progress: {
|
|
123
|
+
step: 'job.running',
|
|
124
|
+
message: '任务开始执行。'
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function subscribeJob(jobId, res) {
|
|
130
|
+
const job = jobs.get(jobId);
|
|
131
|
+
if (!job) return false;
|
|
132
|
+
job.listeners.add(res);
|
|
133
|
+
for (const event of job.events.slice(-20)) {
|
|
134
|
+
sendSse(res, event);
|
|
135
|
+
}
|
|
136
|
+
sendSse(res, {
|
|
137
|
+
step: 'job.snapshot',
|
|
138
|
+
message: job.progress?.message || '任务状态已同步。',
|
|
139
|
+
job: publicJob(job)
|
|
140
|
+
});
|
|
141
|
+
res.on('close', () => {
|
|
142
|
+
job.listeners.delete(res);
|
|
143
|
+
});
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sendSse(res, event) {
|
|
148
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function broadcastJob(job) {
|
|
152
|
+
const event = {
|
|
153
|
+
...job.progress,
|
|
154
|
+
job: publicJob(job)
|
|
155
|
+
};
|
|
156
|
+
for (const res of job.listeners) sendSse(res, event);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function closeJob(job) {
|
|
160
|
+
for (const res of job.listeners) {
|
|
161
|
+
sendSse(res, {
|
|
162
|
+
step: 'job.closed',
|
|
163
|
+
message: '任务连接结束。',
|
|
164
|
+
job: publicJob(job)
|
|
165
|
+
});
|
|
166
|
+
res.end();
|
|
167
|
+
}
|
|
168
|
+
job.listeners.clear();
|
|
169
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function flattenKnowledgePoints(knowledgeDoc) {
|
|
2
|
+
return knowledgeDoc.sections.flatMap((section) =>
|
|
3
|
+
section.points.map((point) => ({
|
|
4
|
+
...point,
|
|
5
|
+
sectionTitle: section.title
|
|
6
|
+
}))
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function knowledgeToMarkdown(chapter, knowledgeDoc) {
|
|
11
|
+
const lines = [`# ${chapter.fullTitle} 知识点文档`, ''];
|
|
12
|
+
for (const section of knowledgeDoc.sections) {
|
|
13
|
+
lines.push(`## ${section.title}`, '');
|
|
14
|
+
for (const point of section.points) {
|
|
15
|
+
lines.push(`### ${point.title}`, '');
|
|
16
|
+
lines.push(point.summary, '');
|
|
17
|
+
if (point.formulas?.length) {
|
|
18
|
+
lines.push('**公式/定律**', '');
|
|
19
|
+
for (const formula of point.formulas) lines.push(`- ${formula}`);
|
|
20
|
+
lines.push('');
|
|
21
|
+
}
|
|
22
|
+
if (point.pitfalls?.length) {
|
|
23
|
+
lines.push('**易错点**', '');
|
|
24
|
+
for (const pitfall of point.pitfalls) lines.push(`- ${pitfall}`);
|
|
25
|
+
lines.push('');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return `${lines.join('\n').trim()}\n`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { callChatAgent, callVisionAgent } from './agentClient.js';
|
|
4
|
+
import {
|
|
5
|
+
chapterDataPaths,
|
|
6
|
+
ensureChapterDataDirs,
|
|
7
|
+
ensureChapterWorkspace,
|
|
8
|
+
paths,
|
|
9
|
+
readJson,
|
|
10
|
+
resetChapterLearningLoop,
|
|
11
|
+
saveKnowledgeDoc,
|
|
12
|
+
writeJson
|
|
13
|
+
} from './fileStore.js';
|
|
14
|
+
import { promptPayload, readPrompt } from './promptStore.js';
|
|
15
|
+
|
|
16
|
+
function extractionDir(chapterId) {
|
|
17
|
+
return chapterDataPaths(chapterId).pageExtracts;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pageExtractPath(chapterId, imageFile) {
|
|
21
|
+
return path.join(extractionDir(chapterId), `${path.basename(imageFile, path.extname(imageFile))}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function summaryPath(chapterId) {
|
|
25
|
+
return chapterDataPaths(chapterId).knowledgeSummary;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function chapterImages(chapter) {
|
|
29
|
+
const folderPath = path.join(paths.imageRoot, chapter.imageFolder);
|
|
30
|
+
const files = (await readdir(folderPath))
|
|
31
|
+
.filter((file) => /\.(png|jpe?g|webp)$/i.test(file))
|
|
32
|
+
.sort();
|
|
33
|
+
return files.map((file) => path.join(folderPath, file));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeExtractProfile(profile = {}) {
|
|
37
|
+
const detailLevels = new Set(['exam_focus', 'balanced', 'fine_grained']);
|
|
38
|
+
const baselines = new Set(['strong', 'normal', 'weak']);
|
|
39
|
+
const allowedFocus = new Set(['exam_points', 'error_prone', 'prerequisite_gaps', 'calculation_links']);
|
|
40
|
+
const focus = Array.isArray(profile.focus)
|
|
41
|
+
? profile.focus.filter((item) => allowedFocus.has(item))
|
|
42
|
+
: [];
|
|
43
|
+
return {
|
|
44
|
+
detailLevel: detailLevels.has(profile.detailLevel) ? profile.detailLevel : 'exam_focus',
|
|
45
|
+
studentBaseline: baselines.has(profile.studentBaseline) ? profile.studentBaseline : 'strong',
|
|
46
|
+
focus: focus.length ? focus : ['exam_points', 'error_prone'],
|
|
47
|
+
displayLayer: profile.displayLayer === 'fine_grained_wall' ? 'fine_grained_wall' : 'exam_point_wall'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePageExtract(chapter, imagePath, pageIndex, data, source = 'agent') {
|
|
52
|
+
const points = Array.isArray(data?.knowledgePoints) ? data.knowledgePoints : [];
|
|
53
|
+
const mistakes = Array.isArray(data?.easyMistakes) ? data.easyMistakes : [];
|
|
54
|
+
return {
|
|
55
|
+
chapterId: chapter.id,
|
|
56
|
+
chapterTitle: chapter.fullTitle,
|
|
57
|
+
imageFile: path.basename(imagePath),
|
|
58
|
+
pageIndex,
|
|
59
|
+
source,
|
|
60
|
+
extractedAt: new Date().toISOString(),
|
|
61
|
+
pageTitle: data?.pageTitle || '',
|
|
62
|
+
rawOutline: Array.isArray(data?.rawOutline) ? data.rawOutline : [],
|
|
63
|
+
knowledgePoints: points.map((point, index) => ({
|
|
64
|
+
title: point.title || `知识点 ${index + 1}`,
|
|
65
|
+
summary: point.summary || '',
|
|
66
|
+
formulas: Array.isArray(point.formulas) ? point.formulas : [],
|
|
67
|
+
examples: Array.isArray(point.examples) ? point.examples : [],
|
|
68
|
+
prerequisite: point.prerequisite || '',
|
|
69
|
+
difficulty: point.difficulty || 'basic'
|
|
70
|
+
})),
|
|
71
|
+
easyMistakes: mistakes.map((mistake) => ({
|
|
72
|
+
title: mistake.title || mistake.errorType || '易错点',
|
|
73
|
+
errorType: mistake.errorType || mistake.title || '易错点',
|
|
74
|
+
description: mistake.description || '',
|
|
75
|
+
correction: mistake.correction || ''
|
|
76
|
+
})),
|
|
77
|
+
exerciseHints: Array.isArray(data?.exerciseHints) ? data.exerciseHints : []
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function knowledgeExtractionError(reason, detail = '') {
|
|
82
|
+
const error = new Error(`knowledge_extraction_failed:${reason}`);
|
|
83
|
+
error.status = 502;
|
|
84
|
+
error.reason = reason;
|
|
85
|
+
error.detail = detail;
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function extractChapterPage({ chapter, imagePath, pageIndex, force = false, extractProfile = null }) {
|
|
90
|
+
await ensureChapterWorkspace(chapter);
|
|
91
|
+
const outputPath = pageExtractPath(chapter.id, imagePath);
|
|
92
|
+
if (!force) {
|
|
93
|
+
const existing = await readJson(outputPath, null);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
}
|
|
96
|
+
const systemPrompt = await readPrompt('knowledge-extract.system.md');
|
|
97
|
+
const agent = await callVisionAgent({
|
|
98
|
+
timeoutMs: 60000,
|
|
99
|
+
system: systemPrompt,
|
|
100
|
+
text: promptPayload({
|
|
101
|
+
task: '从这一页提分笔记图片中逐项提取知识点、公式、例题线索和易错点。',
|
|
102
|
+
context: {
|
|
103
|
+
chapter: {
|
|
104
|
+
id: chapter.id,
|
|
105
|
+
title: chapter.fullTitle,
|
|
106
|
+
track: chapter.track
|
|
107
|
+
},
|
|
108
|
+
extractProfile: normalizeExtractProfile(extractProfile || {})
|
|
109
|
+
},
|
|
110
|
+
requirements: [
|
|
111
|
+
'knowledgePoints 要覆盖页面出现的每个概念、性质、公式、方法或题型。',
|
|
112
|
+
'easyMistakes 要提取页面明确写出的易错点,也可以从页面中的提醒、比较、条件限制中归纳,但不能凭空添加。',
|
|
113
|
+
'summary 用学生能懂的短句;公式必须使用 $...$。',
|
|
114
|
+
'exerciseHints 只写题型方向,不写完整答案。'
|
|
115
|
+
],
|
|
116
|
+
schema: {
|
|
117
|
+
pageTitle: 'string',
|
|
118
|
+
rawOutline: ['string'],
|
|
119
|
+
knowledgePoints: [{
|
|
120
|
+
title: 'string',
|
|
121
|
+
summary: 'string',
|
|
122
|
+
formulas: ['string with LaTeX'],
|
|
123
|
+
examples: ['short example or expression'],
|
|
124
|
+
prerequisite: 'string',
|
|
125
|
+
difficulty: 'basic|medium|challenge'
|
|
126
|
+
}],
|
|
127
|
+
easyMistakes: [{
|
|
128
|
+
title: 'string',
|
|
129
|
+
errorType: 'string',
|
|
130
|
+
description: 'string',
|
|
131
|
+
correction: 'string'
|
|
132
|
+
}],
|
|
133
|
+
exerciseHints: ['string']
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
imagePaths: [imagePath]
|
|
137
|
+
});
|
|
138
|
+
if (!agent.ok) {
|
|
139
|
+
throw knowledgeExtractionError(agent.reason || 'agent_failed', agent.detail || '');
|
|
140
|
+
}
|
|
141
|
+
const extract = normalizePageExtract(chapter, imagePath, pageIndex, agent.data, 'agent');
|
|
142
|
+
await writeJson(outputPath, extract);
|
|
143
|
+
await writeJson(path.join(paths.knowledgeExtracts, chapter.id, `${path.basename(imagePath, path.extname(imagePath))}.json`), extract);
|
|
144
|
+
return extract;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function dedupeByTitle(items) {
|
|
148
|
+
const seen = new Map();
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
const key = String(item.title || item.errorType || '').replace(/\s+/g, '');
|
|
151
|
+
if (!key) continue;
|
|
152
|
+
if (!seen.has(key)) {
|
|
153
|
+
seen.set(key, { ...item });
|
|
154
|
+
} else {
|
|
155
|
+
const current = seen.get(key);
|
|
156
|
+
current.summary ||= item.summary;
|
|
157
|
+
current.description ||= item.description;
|
|
158
|
+
current.formulas = [...new Set([...(current.formulas || []), ...(item.formulas || [])])];
|
|
159
|
+
current.examples = [...new Set([...(current.examples || []), ...(item.examples || [])])].slice(0, 6);
|
|
160
|
+
current.sources = [...new Set([...(current.sources || []), ...(item.sources || [])])];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return [...seen.values()];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function localMergeChapter(chapter, pageExtracts) {
|
|
167
|
+
const points = dedupeByTitle(pageExtracts.flatMap((page) =>
|
|
168
|
+
page.knowledgePoints.map((point) => ({
|
|
169
|
+
...point,
|
|
170
|
+
sources: [page.imageFile]
|
|
171
|
+
}))
|
|
172
|
+
));
|
|
173
|
+
const mistakes = dedupeByTitle(pageExtracts.flatMap((page) =>
|
|
174
|
+
page.easyMistakes.map((mistake) => ({
|
|
175
|
+
...mistake,
|
|
176
|
+
sources: [page.imageFile]
|
|
177
|
+
}))
|
|
178
|
+
));
|
|
179
|
+
return {
|
|
180
|
+
sections: [
|
|
181
|
+
{
|
|
182
|
+
title: '知识点覆盖',
|
|
183
|
+
points: points.map((point, index) => ({
|
|
184
|
+
id: `${chapter.id}-kp-${String(index + 1).padStart(2, '0')}`,
|
|
185
|
+
title: point.title,
|
|
186
|
+
summary: point.summary || '从章节图片提取,待人工复核完善。',
|
|
187
|
+
formulas: point.formulas || [],
|
|
188
|
+
pitfalls: mistakes
|
|
189
|
+
.filter((mistake) => mistake.description?.includes(point.title) || mistake.title?.includes(point.title))
|
|
190
|
+
.map((mistake) => mistake.errorType)
|
|
191
|
+
.slice(0, 4),
|
|
192
|
+
examples: point.examples || [],
|
|
193
|
+
questionTemplates: [
|
|
194
|
+
[
|
|
195
|
+
`围绕「${point.title}」完成一道基础覆盖题,并写出关键结论。`,
|
|
196
|
+
point.summary || '答案需符合知识点定义、公式或方法。',
|
|
197
|
+
point.title
|
|
198
|
+
]
|
|
199
|
+
],
|
|
200
|
+
sources: point.sources || []
|
|
201
|
+
}))
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
title: '易错题专项',
|
|
205
|
+
points: mistakes.map((mistake, index) => ({
|
|
206
|
+
id: `${chapter.id}-mistake-${String(index + 1).padStart(2, '0')}`,
|
|
207
|
+
title: mistake.title || mistake.errorType,
|
|
208
|
+
summary: mistake.description || mistake.errorType,
|
|
209
|
+
formulas: [],
|
|
210
|
+
pitfalls: [mistake.errorType || mistake.title].filter(Boolean),
|
|
211
|
+
examples: [],
|
|
212
|
+
questionTemplates: [
|
|
213
|
+
[
|
|
214
|
+
`针对易错点「${mistake.errorType || mistake.title}」设计一道辨析或改错题。`,
|
|
215
|
+
mistake.correction || '指出错误原因并给出正确做法。',
|
|
216
|
+
mistake.errorType || mistake.title
|
|
217
|
+
]
|
|
218
|
+
],
|
|
219
|
+
sources: mistake.sources || []
|
|
220
|
+
}))
|
|
221
|
+
}
|
|
222
|
+
].filter((section) => section.points.length)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function summarizeChapterExtraction({
|
|
227
|
+
chapter,
|
|
228
|
+
pageExtracts,
|
|
229
|
+
extractProfile = null,
|
|
230
|
+
resetLearningState = false
|
|
231
|
+
}) {
|
|
232
|
+
const local = localMergeChapter(chapter, pageExtracts);
|
|
233
|
+
const normalizedProfile = normalizeExtractProfile(extractProfile || {});
|
|
234
|
+
const systemPrompt = await readPrompt('knowledge-summarize.system.md');
|
|
235
|
+
const agent = await callChatAgent({
|
|
236
|
+
timeoutMs: 60000,
|
|
237
|
+
temperature: 0.1,
|
|
238
|
+
system: systemPrompt,
|
|
239
|
+
user: promptPayload({
|
|
240
|
+
task: '把逐页提取结果合并成章节知识文档,并做覆盖检查。',
|
|
241
|
+
context: {
|
|
242
|
+
chapter,
|
|
243
|
+
pageExtracts,
|
|
244
|
+
localDraft: local,
|
|
245
|
+
extractProfile: normalizedProfile
|
|
246
|
+
},
|
|
247
|
+
requirements: [
|
|
248
|
+
'合并同义知识点,保留来源页。',
|
|
249
|
+
normalizedProfile.detailLevel === 'fine_grained'
|
|
250
|
+
? '当前提取画像要求细粒度拆分:保留必要前置概念、步骤性方法和基础易错点。'
|
|
251
|
+
: '当前提取画像要求考点优先:优先保留考试常见考点、易错点、变式边界和必要前置关系,不把教材说明拆得过碎。',
|
|
252
|
+
'sections 至少包含“知识点覆盖”;如果有易错点,单独包含“易错题专项”。',
|
|
253
|
+
'每个知识点必须有 id、title、summary、formulas、pitfalls、questionTemplates。',
|
|
254
|
+
'questionTemplates 用于后续出题,题干只写题目,不写解题过程。',
|
|
255
|
+
'review.missingOrWeak 列出疑似遗漏或需要人工复核的点。'
|
|
256
|
+
],
|
|
257
|
+
schema: {
|
|
258
|
+
sections: [{
|
|
259
|
+
title: '知识点覆盖',
|
|
260
|
+
points: [{
|
|
261
|
+
id: `${chapter.id}-kp-01`,
|
|
262
|
+
title: 'string',
|
|
263
|
+
summary: 'string',
|
|
264
|
+
formulas: ['string with LaTeX'],
|
|
265
|
+
pitfalls: ['string'],
|
|
266
|
+
examples: ['string'],
|
|
267
|
+
questionTemplates: [['stem', 'answer', 'expectedErrorType']],
|
|
268
|
+
sources: ['image filename']
|
|
269
|
+
}]
|
|
270
|
+
}],
|
|
271
|
+
review: {
|
|
272
|
+
passed: true,
|
|
273
|
+
coverageSummary: 'string',
|
|
274
|
+
missingOrWeak: ['string'],
|
|
275
|
+
duplicateMerged: ['string']
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
if (!agent.ok || !Array.isArray(agent.data?.sections)) {
|
|
281
|
+
throw knowledgeExtractionError(agent.reason || 'invalid_agent_response', agent.detail || '');
|
|
282
|
+
}
|
|
283
|
+
const merged = agent.data;
|
|
284
|
+
const extractedAt = new Date().toISOString();
|
|
285
|
+
const normalized = await saveKnowledgeDoc(chapter, merged, 'agent', {
|
|
286
|
+
extractProfile: normalizedProfile,
|
|
287
|
+
extractorVersion: 1,
|
|
288
|
+
extractedAt,
|
|
289
|
+
resetLearningState
|
|
290
|
+
});
|
|
291
|
+
const summary = {
|
|
292
|
+
chapterId: chapter.id,
|
|
293
|
+
chapterTitle: chapter.fullTitle,
|
|
294
|
+
source: 'agent',
|
|
295
|
+
pageCount: pageExtracts.length,
|
|
296
|
+
updatedAt: extractedAt,
|
|
297
|
+
extractProfile: normalizedProfile,
|
|
298
|
+
extractorVersion: 1,
|
|
299
|
+
review: merged.review || null,
|
|
300
|
+
knowledgePointCount: normalized.sections.reduce((sum, section) => sum + section.points.length, 0)
|
|
301
|
+
};
|
|
302
|
+
await writeJson(summaryPath(chapter.id), summary);
|
|
303
|
+
await writeJson(path.join(paths.knowledgeExtracts, chapter.id, 'summary.json'), summary);
|
|
304
|
+
const reset = resetLearningState ? await resetChapterLearningLoop(chapter.id) : null;
|
|
305
|
+
return { knowledge: normalized, summary, reset };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function extractChapterKnowledge({
|
|
309
|
+
chapterId,
|
|
310
|
+
limitPages = 0,
|
|
311
|
+
force = false,
|
|
312
|
+
extractProfile = null,
|
|
313
|
+
resetLearningState = false
|
|
314
|
+
} = {}) {
|
|
315
|
+
const chapters = await readJson(paths.chapters, []);
|
|
316
|
+
const selected = chapterId ? chapters.filter((chapter) => chapter.id === chapterId) : chapters;
|
|
317
|
+
const results = [];
|
|
318
|
+
const normalizedProfile = normalizeExtractProfile(extractProfile || {});
|
|
319
|
+
for (const chapter of selected) {
|
|
320
|
+
const images = await chapterImages(chapter);
|
|
321
|
+
const scopedImages = limitPages > 0 ? images.slice(0, limitPages) : images;
|
|
322
|
+
const pageExtracts = [];
|
|
323
|
+
for (let index = 0; index < scopedImages.length; index += 1) {
|
|
324
|
+
pageExtracts.push(await extractChapterPage({
|
|
325
|
+
chapter,
|
|
326
|
+
imagePath: scopedImages[index],
|
|
327
|
+
pageIndex: index + 1,
|
|
328
|
+
force,
|
|
329
|
+
extractProfile: normalizedProfile
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
results.push(await summarizeChapterExtraction({
|
|
333
|
+
chapter,
|
|
334
|
+
pageExtracts,
|
|
335
|
+
extractProfile: normalizedProfile,
|
|
336
|
+
resetLearningState
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function extractKnowledgeFromImages({ chapterId, imagePaths, force = false } = {}) {
|
|
343
|
+
const chapters = await readJson(paths.chapters, []);
|
|
344
|
+
const chapter = chapters.find((item) => item.id === chapterId);
|
|
345
|
+
if (!chapter) {
|
|
346
|
+
const error = new Error('chapter_not_found');
|
|
347
|
+
error.status = 404;
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
const pageExtracts = [];
|
|
351
|
+
for (let index = 0; index < imagePaths.length; index += 1) {
|
|
352
|
+
pageExtracts.push(await extractChapterPage({
|
|
353
|
+
chapter,
|
|
354
|
+
imagePath: imagePaths[index],
|
|
355
|
+
pageIndex: index + 1,
|
|
356
|
+
force
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
return summarizeChapterExtraction({ chapter, pageExtracts });
|
|
360
|
+
}
|