@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.
Files changed (46) hide show
  1. package/.env.local.example +6 -0
  2. package/AGENTS.md +273 -0
  3. package/README.md +34 -0
  4. package/bin/math-ati.js +194 -0
  5. package/dist/assets/index-BYFoutza.js +22 -0
  6. package/dist/assets/index-Bk2WFPoL.css +1 -0
  7. package/dist/index.html +13 -0
  8. package/package.json +72 -0
  9. package/prompts/grading.system.md +129 -0
  10. package/prompts/knowledge-extract.system.md +123 -0
  11. package/prompts/knowledge-summarize.system.md +127 -0
  12. package/prompts/learning-summary.system.md +123 -0
  13. package/prompts/pdf-grading.system.md +80 -0
  14. package/prompts/pdf-page-extract.system.md +52 -0
  15. package/prompts/pdf-recheck.system.md +43 -0
  16. package/prompts/practice-generate.system.md +161 -0
  17. package/prompts/practice-review.system.md +65 -0
  18. package/prompts/practice-revise.system.md +56 -0
  19. package/server/abilityService.js +259 -0
  20. package/server/agentClient.js +202 -0
  21. package/server/env.js +4 -0
  22. package/server/fileStore.js +726 -0
  23. package/server/grading.js +116 -0
  24. package/server/index.js +655 -0
  25. package/server/jobStore.js +169 -0
  26. package/server/knowledgeBase.js +30 -0
  27. package/server/knowledgeExtractor.js +360 -0
  28. package/server/knowledgeFeedback.js +299 -0
  29. package/server/llmConfig.js +96 -0
  30. package/server/mistakeLifecycle.js +251 -0
  31. package/server/pdfSubmissionGrader.js +846 -0
  32. package/server/practiceGenerator.js +908 -0
  33. package/server/practicePaperHtml.js +313 -0
  34. package/server/practiceReviewer.js +307 -0
  35. package/server/practiceService.js +331 -0
  36. package/server/promptStore.js +16 -0
  37. package/server/submissionService.js +184 -0
  38. package/templates/workspace/.env.local.example +6 -0
  39. package/templates/workspace/data/global/ability_index.json +5 -0
  40. package/templates/workspace/data/global/chapters.json +621 -0
  41. package/templates/workspace/data/global/mastery_index.json +6 -0
  42. package/templates/workspace/data/global/mistakes_index.json +7 -0
  43. package/templates/workspace/data/global/student_profile.json +11 -0
  44. package/templates/workspace/data/knowledge_points.json +1264 -0
  45. package/templates/workspace/data/mistakes.json +1 -0
  46. package/vite.config.js +21 -0
@@ -0,0 +1,331 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { readFile, readdir, writeFile } from 'node:fs/promises';
4
+ import {
5
+ chapterDataPaths,
6
+ ensureChapterDataDirs,
7
+ ensureChapterWorkspace,
8
+ ensureDataDirs,
9
+ getKnowledgeBundle,
10
+ paths,
11
+ readJson,
12
+ writeJson
13
+ } from './fileStore.js';
14
+ import { generatePracticeContent } from './practiceGenerator.js';
15
+ import { reviewPractice } from './practiceReviewer.js';
16
+ import {
17
+ practiceAnswersToHtml,
18
+ practiceQuestionsToHtml
19
+ } from './practicePaperHtml.js';
20
+ import { flattenKnowledgePoints } from './knowledgeBase.js';
21
+ import { markRepairPracticeStarted } from './mistakeLifecycle.js';
22
+ import { assertChapterAbilitiesEnabled } from './abilityService.js';
23
+
24
+ export async function findPracticePath(id) {
25
+ const fs = await import('node:fs/promises');
26
+ const chapters = (await fs.readdir(paths.chapterData, { withFileTypes: true }))
27
+ .filter((entry) => entry.isDirectory())
28
+ .map((entry) => entry.name);
29
+ for (const chapterId of chapters) {
30
+ const candidate = path.join(chapterDataPaths(chapterId).practices, `${id}.json`);
31
+ if (await readJson(candidate, null)) return candidate;
32
+ }
33
+ const error = new Error(`practice_not_found:${id}`);
34
+ error.status = 404;
35
+ throw error;
36
+ }
37
+
38
+ export async function readPractice(id) {
39
+ return ensurePracticeHtml(await readJson(await findPracticePath(id)));
40
+ }
41
+
42
+ export async function updatePracticeStatus(id, status) {
43
+ const filePath = await findPracticePath(id);
44
+ const practice = await readJson(filePath);
45
+ if (status === 'printed' && practice.status !== 'previewed' && practice.status !== 'printed') {
46
+ const error = new Error('practice_must_be_previewed_before_print');
47
+ error.status = 409;
48
+ throw error;
49
+ }
50
+ const next = {
51
+ ...practice,
52
+ status,
53
+ updatedAt: new Date().toISOString()
54
+ };
55
+ if (status === 'previewed' && !next.previewedAt) next.previewedAt = new Date().toISOString();
56
+ if (status === 'printed' && !next.printedAt) next.printedAt = new Date().toISOString();
57
+ await writeJson(filePath, next);
58
+ return next;
59
+ }
60
+
61
+ function practiceFileUrl(relativePath) {
62
+ if (!relativePath) return '';
63
+ if (relativePath.startsWith('data/chapters/')) {
64
+ return `/chapter-data/${relativePath.slice('data/chapters/'.length)}`;
65
+ }
66
+ return '';
67
+ }
68
+
69
+ function practiceDisplayTitle(title) {
70
+ return String(title || '')
71
+ .replace(/最终版/g, '')
72
+ .replace(/[((][^))]*(?:修订版|第\s*\d+(?:\s*\/\s*\d+)?\s*批)[^))]*[))]/g, '')
73
+ .replace(/[((]\s*[·||::-]*\s*[))]/g, '')
74
+ .replace(/修订版/g, '')
75
+ .replace(/[((]\s*第\s*\d+\s*\/\s*\d+\s*批\s*[))]/g, '')
76
+ .replace(/[((]\s*第\s*\d+\s*批\s*[))]/g, '')
77
+ .replace(/第\s*\d+\s*\/\s*\d+\s*批/g, '')
78
+ .replace(/第\s*\d+\s*批/g, '')
79
+ .replace(/[||]\s*$/g, '')
80
+ .replace(/[::]\s*$/g, '')
81
+ .replace(/·\s*$/g, '')
82
+ .replace(/\s+(/g, '(')
83
+ .replace(/·\s+/g, '·')
84
+ .replace(/\s{2,}/g, ' ')
85
+ .trim();
86
+ }
87
+
88
+ export function practiceSummary(practice) {
89
+ const knowledgePointIds = [
90
+ ...new Set((practice.questions || [])
91
+ .flatMap((question) => Array.isArray(question.knowledgePointIds) ? question.knowledgePointIds : [])
92
+ .filter(Boolean))
93
+ ];
94
+ const abilityIds = [
95
+ ...new Set([
96
+ ...(Array.isArray(practice.abilityIds) ? practice.abilityIds : []),
97
+ ...(practice.questions || [])
98
+ .flatMap((question) => Array.isArray(question.abilityIds) ? question.abilityIds : [])
99
+ ].filter(Boolean))
100
+ ];
101
+ return {
102
+ id: practice.id,
103
+ title: practiceDisplayTitle(practice.title),
104
+ chapterId: practice.chapterId,
105
+ chapterTitle: practice.chapterTitle,
106
+ type: practice.type,
107
+ createdAt: practice.createdAt,
108
+ status: practice.status,
109
+ questionCount: practice.questions?.length || 0,
110
+ knowledgePointIds,
111
+ abilityIds,
112
+ coveragePlan: practice.coveragePlan || null,
113
+ source: practice.source,
114
+ htmlPath: practice.htmlPath,
115
+ answersHtmlPath: practice.answersHtmlPath,
116
+ htmlUrl: practiceFileUrl(practice.htmlPath),
117
+ answersHtmlUrl: practiceFileUrl(practice.answersHtmlPath)
118
+ };
119
+ }
120
+
121
+ async function readPracticeJsonFiles(chapterId) {
122
+ const chapterPaths = chapterDataPaths(chapterId);
123
+ try {
124
+ const files = (await readdir(chapterPaths.practices))
125
+ .filter((file) => file.endsWith('.json'))
126
+ .sort();
127
+ const practices = [];
128
+ for (const file of files) {
129
+ const practice = await readJson(path.join(chapterPaths.practices, file), null);
130
+ if (practice) practices.push(practice);
131
+ }
132
+ return practices;
133
+ } catch (error) {
134
+ if (error.code === 'ENOENT') return [];
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ export async function buildGeneratedCoverage(chapterId) {
140
+ const knowledgeBundle = await getKnowledgeBundle(chapterId);
141
+ const allPointIds = flattenKnowledgePoints(knowledgeBundle.knowledge || {}).map((point) => point.id);
142
+ const allPointIdSet = new Set(allPointIds);
143
+ const practices = (await readPracticeJsonFiles(chapterId))
144
+ .filter((practice) => practice.type === 'knowledge_coverage');
145
+ const generatedPointIds = [
146
+ ...new Set(practices
147
+ .flatMap((practice) => practice.questions || [])
148
+ .flatMap((question) => Array.isArray(question.knowledgePointIds) ? question.knowledgePointIds : [])
149
+ .filter((pointId) => allPointIdSet.has(pointId)))
150
+ ].sort();
151
+ const remainingPointIds = allPointIds.filter((pointId) => !generatedPointIds.includes(pointId));
152
+ return {
153
+ chapterId,
154
+ evidenceSource: 'knowledge_coverage_practices',
155
+ updatedAt: new Date().toISOString(),
156
+ totalKnowledgePointCount: allPointIds.length,
157
+ generatedCount: generatedPointIds.length,
158
+ remainingCount: remainingPointIds.length,
159
+ practiceCount: practices.length,
160
+ questionCount: practices.reduce((sum, practice) => sum + (practice.questions?.length || 0), 0),
161
+ generatedPointIds,
162
+ remainingPointIds,
163
+ practices: practices.map((practice) => ({
164
+ id: practice.id,
165
+ status: practice.status,
166
+ createdAt: practice.createdAt,
167
+ questionCount: practice.questions?.length || 0,
168
+ knowledgePointIds: [
169
+ ...new Set((practice.questions || [])
170
+ .flatMap((question) => Array.isArray(question.knowledgePointIds) ? question.knowledgePointIds : [])
171
+ .filter((pointId) => allPointIdSet.has(pointId)))
172
+ ].sort()
173
+ }))
174
+ };
175
+ }
176
+
177
+ export async function syncGeneratedCoverage(chapterId) {
178
+ const coverage = await buildGeneratedCoverage(chapterId);
179
+ const chapterPaths = await ensureChapterDataDirs(chapterId);
180
+ await writeJson(chapterPaths.generatedCoverage, coverage);
181
+ return coverage;
182
+ }
183
+
184
+ function shouldRegeneratePracticeHtml(html, answersHtml) {
185
+ return [
186
+ !html.includes('katex.min.js') || !answersHtml.includes('katex.min.js'),
187
+ !html.includes('node_modules/katex/dist') || !answersHtml.includes('node_modules/katex/dist'),
188
+ !html.includes('column-count: 2'),
189
+ html.includes('姓名:__________'),
190
+ html.includes('日期:__________'),
191
+ html.includes('用时:__________'),
192
+ !html.includes('data-question-id='),
193
+ !answersHtml.includes('data-question-id='),
194
+ answersHtml.includes('批改后生成讲解'),
195
+ html.includes('border: 1px dashed #cfc7b9'),
196
+ html.includes('repeating-linear-gradient')
197
+ ].some(Boolean);
198
+ }
199
+
200
+ export async function ensurePracticeHtml(practice) {
201
+ if (!practice?.id || !practice?.chapterId) {
202
+ const error = new Error('invalid_practice_record');
203
+ error.status = 422;
204
+ throw error;
205
+ }
206
+ if (!practice.htmlPath || !practice.answersHtmlPath) {
207
+ const error = new Error(`practice_html_missing:${practice.id}`);
208
+ error.status = 422;
209
+ throw error;
210
+ }
211
+ const htmlPath = path.join(paths.rootDir, practice.htmlPath);
212
+ const answersHtmlPath = path.join(paths.rootDir, practice.answersHtmlPath);
213
+ const [html, answersHtml] = await Promise.all([
214
+ readFile(htmlPath, 'utf8').catch(() => ''),
215
+ readFile(answersHtmlPath, 'utf8').catch(() => '')
216
+ ]);
217
+ if (shouldRegeneratePracticeHtml(html, answersHtml)) {
218
+ await writeFile(htmlPath, practiceQuestionsToHtml(practice), 'utf8');
219
+ await writeFile(answersHtmlPath, practiceAnswersToHtml(practice), 'utf8');
220
+ }
221
+ return practice;
222
+ }
223
+
224
+ export function cleanPracticeOptions(options) {
225
+ const persisted = { ...options };
226
+ delete persisted.knowledgeDoc;
227
+ return persisted;
228
+ }
229
+
230
+ export async function createPractice({
231
+ chapterId,
232
+ type = 'knowledge_coverage',
233
+ questionCount = null,
234
+ difficulty = '基础巩固',
235
+ includeMistakes = false,
236
+ knowledgePointId = '',
237
+ knowledgePointIds = [],
238
+ abilityIds = [],
239
+ questionKind = 'auto',
240
+ onProgress = () => {}
241
+ }) {
242
+ await ensureDataDirs();
243
+ const profile = await readJson(paths.profile);
244
+ const chapters = await readJson(paths.chapters, []);
245
+ const chapter = chapters.find((item) => item.id === chapterId);
246
+ if (!chapter) {
247
+ const error = new Error('chapter_not_found');
248
+ error.status = 404;
249
+ throw error;
250
+ }
251
+ await ensureChapterWorkspace(chapter);
252
+ const knowledgeBundle = await getKnowledgeBundle(chapter.id);
253
+ const scopedKnowledgePointIds = Array.isArray(knowledgePointIds) && knowledgePointIds.length
254
+ ? knowledgePointIds
255
+ : knowledgePointId
256
+ ? [knowledgePointId]
257
+ : [];
258
+ const scopedAbilityIds = type === 'ability_assessment'
259
+ ? await assertChapterAbilitiesEnabled(chapter.id, Array.isArray(abilityIds) ? abilityIds : [])
260
+ : [];
261
+ const requestedQuestionCount = Number(questionCount);
262
+ const hasRequestedQuestionCount = Number.isFinite(requestedQuestionCount) && requestedQuestionCount > 0;
263
+ const maxQuestionCount = scopedKnowledgePointIds.length === 1
264
+ ? Math.max(1, Math.min(3, hasRequestedQuestionCount ? requestedQuestionCount : 1))
265
+ : type === 'mistake_repair'
266
+ ? Math.max(3, Math.min(30, hasRequestedQuestionCount ? requestedQuestionCount : 8))
267
+ : Math.max(6, Math.min(24, hasRequestedQuestionCount ? requestedQuestionCount : 18));
268
+ const adaptiveQuestionCount = !hasRequestedQuestionCount && (type === 'knowledge_coverage' || type === 'mastery_check');
269
+ const options = {
270
+ type,
271
+ questionCount: adaptiveQuestionCount ? null : maxQuestionCount,
272
+ maxQuestionCount,
273
+ adaptiveQuestionCount,
274
+ difficulty,
275
+ includeMistakes: Boolean(includeMistakes),
276
+ knowledgePointId: scopedKnowledgePointIds.length === 1 ? scopedKnowledgePointIds[0] : '',
277
+ knowledgePointIds: scopedKnowledgePointIds,
278
+ abilityIds: scopedAbilityIds,
279
+ questionKind,
280
+ knowledgeDoc: knowledgeBundle.knowledge,
281
+ mastery: knowledgeBundle.mastery
282
+ };
283
+ onProgress({ step: 'practice_generate.context', message: '已读取学生画像、章节知识点和历史练习。' });
284
+ const content = await generatePracticeContent({ profile, chapter, options, onProgress });
285
+ const id = `practice-${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID().slice(0, 8)}`;
286
+ let practice = {
287
+ id,
288
+ chapterId: chapter.id,
289
+ chapterTitle: chapter.fullTitle,
290
+ track: chapter.track,
291
+ type: options.type,
292
+ options: cleanPracticeOptions(options),
293
+ abilityIds: scopedAbilityIds,
294
+ title: content.title,
295
+ personalizationBasis: content.personalizationBasis,
296
+ questions: content.questions,
297
+ coveragePlan: content.coveragePlan || null,
298
+ source: content.source,
299
+ agentReason: content.agentReason,
300
+ createdAt: new Date().toISOString(),
301
+ status: 'generated'
302
+ };
303
+ const structuralReview = await reviewPractice(practice, options);
304
+ if (!structuralReview.passed) {
305
+ const error = new Error(`practice_structural_review_failed:${structuralReview.findings.map((item) => item.type).join('|')}`);
306
+ error.status = 422;
307
+ error.review = structuralReview;
308
+ throw error;
309
+ }
310
+ practice.review = structuralReview;
311
+ practice.llmReview = content.llmReview || null;
312
+ practice.initialLlmReview = content.initialLlmReview || null;
313
+ practice.structuralReview = structuralReview;
314
+ practice.revision = content.revision;
315
+ const chapterPaths = await ensureChapterDataDirs(chapter.id);
316
+ const chapterPracticePath = path.join(chapterPaths.practices, `${id}.json`);
317
+ const chapterHtmlPath = path.join(chapterPaths.practices, `${id}.html`);
318
+ const chapterAnswersHtmlPath = path.join(chapterPaths.practices, `${id}.answers.html`);
319
+ practice.htmlPath = path.relative(paths.rootDir, chapterHtmlPath);
320
+ practice.answersHtmlPath = path.relative(paths.rootDir, chapterAnswersHtmlPath);
321
+ await writeJson(chapterPracticePath, practice);
322
+ await writeFile(chapterHtmlPath, practiceQuestionsToHtml(practice), 'utf8');
323
+ await writeFile(chapterAnswersHtmlPath, practiceAnswersToHtml(practice), 'utf8');
324
+ if (practice.type === 'knowledge_coverage') {
325
+ await syncGeneratedCoverage(chapter.id);
326
+ } else if (practice.type === 'mistake_repair') {
327
+ await markRepairPracticeStarted(practice);
328
+ }
329
+ onProgress({ step: 'practice_generate.write', message: '已写入练习 JSON、试卷 HTML 和答案 HTML。' });
330
+ return practice;
331
+ }
@@ -0,0 +1,16 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { paths } from './fileStore.js';
4
+
5
+ export async function readPrompt(name) {
6
+ return readFile(path.join(paths.rootDir, 'prompts', name), 'utf8');
7
+ }
8
+
9
+ export function promptPayload({ task, context, schema, requirements = [] }) {
10
+ return JSON.stringify({
11
+ task,
12
+ requirements,
13
+ context,
14
+ schema
15
+ });
16
+ }
@@ -0,0 +1,184 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { rm, writeFile } from 'node:fs/promises';
4
+ import {
5
+ chapterDataPaths,
6
+ ensureChapterDataDirs,
7
+ ensureDataDirs,
8
+ paths,
9
+ readJson,
10
+ updateChapterAfterArchive,
11
+ updateKnowledgeMasteryAfterArchive,
12
+ writeJson
13
+ } from './fileStore.js';
14
+ import { readPractice } from './practiceService.js';
15
+ import { gradeSubmissionImages } from './grading.js';
16
+ import { updateMistakesAfterArchive } from './mistakeLifecycle.js';
17
+ import { updateAbilitiesAfterArchive } from './abilityService.js';
18
+
19
+ export function chapterSubmissionPath(chapterId, id) {
20
+ return path.join(chapterDataPaths(chapterId).submissions, id, 'metadata.json');
21
+ }
22
+
23
+ export function chapterReportPath(chapterId, id) {
24
+ return path.join(chapterDataPaths(chapterId).reports, `${id}.md`);
25
+ }
26
+
27
+ export async function findSubmissionPath(id) {
28
+ const fs = await import('node:fs/promises');
29
+ let chapters = [];
30
+ try {
31
+ chapters = (await fs.readdir(paths.chapterData, { withFileTypes: true }))
32
+ .filter((entry) => entry.isDirectory())
33
+ .map((entry) => entry.name);
34
+ } catch {
35
+ chapters = [];
36
+ }
37
+ for (const chapterId of chapters) {
38
+ const candidate = chapterSubmissionPath(chapterId, id);
39
+ if (await readJson(candidate, null)) return candidate;
40
+ }
41
+ const error = new Error(`submission_not_found:${id}`);
42
+ error.status = 404;
43
+ throw error;
44
+ }
45
+
46
+ export async function readSubmission(id) {
47
+ return readJson(await findSubmissionPath(id));
48
+ }
49
+
50
+ export async function deleteSubmission(id) {
51
+ const metadataPath = await findSubmissionPath(id);
52
+ const submission = await readJson(metadataPath);
53
+ if (submission?.parentConfirmed || submission?.status === 'archived') {
54
+ const error = new Error(`confirmed_submission_cannot_delete:${id}`);
55
+ error.status = 409;
56
+ throw error;
57
+ }
58
+ await rm(path.dirname(metadataPath), { recursive: true, force: true });
59
+ return {
60
+ deleted: true,
61
+ submissionId: id,
62
+ practiceId: submission?.practiceId || null,
63
+ chapterId: submission?.chapterId || null
64
+ };
65
+ }
66
+
67
+ export async function writeSubmissionMirrors(submission) {
68
+ await ensureChapterDataDirs(submission.chapterId);
69
+ await writeJson(chapterSubmissionPath(submission.chapterId, submission.id), submission);
70
+ }
71
+
72
+ function gradingStats(grading) {
73
+ const total = grading.reduce((sum, item) => sum + Number(item.maxScore || 0), 0);
74
+ const earned = grading.reduce((sum, item) => sum + Number(item.score || 0), 0);
75
+ const accuracy = total > 0 ? Math.round((earned / total) * 100) : 0;
76
+ const errorTypes = [...new Set(grading.flatMap((item) => item.errorTypes || []).filter(Boolean))].slice(0, 6);
77
+ return { total, earned, accuracy, errorTypes };
78
+ }
79
+
80
+ export async function createSubmissionRecord(practiceId, notes = '') {
81
+ await ensureDataDirs();
82
+ const practice = await readPractice(practiceId);
83
+ const id = `submission-${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID().slice(0, 8)}`;
84
+ const submission = {
85
+ id,
86
+ practiceId: practice.id,
87
+ chapterId: practice.chapterId,
88
+ status: 'created',
89
+ notes,
90
+ photoPaths: [],
91
+ gradingResult: null,
92
+ parentConfirmed: false,
93
+ createdAt: new Date().toISOString()
94
+ };
95
+ await writeSubmissionMirrors(submission);
96
+ return submission;
97
+ }
98
+
99
+ export async function gradePracticeImages({ practiceId, imagePaths, notes = '', autoConfirm = true }) {
100
+ await ensureDataDirs();
101
+ const profile = await readJson(paths.profile);
102
+ const practice = await readPractice(practiceId);
103
+ let submission = await createSubmissionRecord(practice.id, notes);
104
+ const gradingResult = await gradeSubmissionImages({
105
+ practice,
106
+ profile,
107
+ imagePaths,
108
+ notes
109
+ });
110
+ submission = {
111
+ ...submission,
112
+ notes,
113
+ photoPaths: imagePaths,
114
+ gradingResult,
115
+ status: 'graded',
116
+ gradedAt: new Date().toISOString()
117
+ };
118
+ await writeSubmissionMirrors(submission);
119
+ if (!autoConfirm) return { submission, practice };
120
+ const archived = await confirmSubmission({
121
+ submissionId: submission.id,
122
+ grading: gradingResult.grading,
123
+ parentSummary: gradingResult.summary || ''
124
+ });
125
+ return { submission: archived, practice };
126
+ }
127
+
128
+ export async function confirmSubmission({ submissionId, grading = null, parentSummary = '' }) {
129
+ const submission = await readSubmission(submissionId);
130
+ const practice = await readPractice(submission.practiceId);
131
+ const confirmedGrading = grading || submission.gradingResult?.grading || [];
132
+ const { accuracy, errorTypes } = gradingStats(confirmedGrading);
133
+ const updated = {
134
+ ...submission,
135
+ gradingResult: {
136
+ ...submission.gradingResult,
137
+ grading: confirmedGrading,
138
+ parentSummary
139
+ },
140
+ parentConfirmed: true,
141
+ status: 'archived',
142
+ accuracy,
143
+ confirmedAt: new Date().toISOString()
144
+ };
145
+ await writeSubmissionMirrors(updated);
146
+ await updateChapterAfterArchive(practice.chapterId, accuracy, errorTypes);
147
+ if (practice.type === 'ability_assessment') {
148
+ await updateAbilitiesAfterArchive({
149
+ practice,
150
+ grading: confirmedGrading,
151
+ submissionId: submission.id
152
+ });
153
+ } else {
154
+ await updateKnowledgeMasteryAfterArchive(
155
+ practice,
156
+ confirmedGrading.map((item) => ({ ...item, submissionId: submission.id }))
157
+ );
158
+ }
159
+
160
+ await updateMistakesAfterArchive({
161
+ practice,
162
+ grading: confirmedGrading,
163
+ submissionId: submission.id
164
+ });
165
+
166
+ const report = [
167
+ `# ${practice.chapterTitle} 学习报告`,
168
+ '',
169
+ `- 学生:周子烊`,
170
+ `- 练习:${practice.title}`,
171
+ `- 正确率:${accuracy}%`,
172
+ `- 错因:${errorTypes.join('、') || '暂无'}`,
173
+ '',
174
+ '## 本次总结',
175
+ '',
176
+ updated.gradingResult?.summary || '已完成归档。',
177
+ '',
178
+ '## 下一步建议',
179
+ '',
180
+ updated.gradingResult?.nextPracticeSuggestion || '继续完成同章知识点探测。'
181
+ ].join('\n');
182
+ await writeFile(chapterReportPath(practice.chapterId, submission.id), `${report}\n`, 'utf8');
183
+ return updated;
184
+ }
@@ -0,0 +1,6 @@
1
+ AI_BASE_URL=https://code.shoestravel.xin
2
+ AI_API_KEY=
3
+ AI_MODEL=gpt-5.5
4
+ IMAGE_BASE_URL=https://code.shoestravel.xin
5
+ IMAGE_API_KEY=
6
+ IMAGE_MODEL=gpt-image-2
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "updatedAt": null,
4
+ "chapters": {}
5
+ }