@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
package/AGENTS.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Goal
|
|
4
|
+
|
|
5
|
+
This project is a local Vite + Express MVP for personalized junior high math practice for 周子烊.
|
|
6
|
+
|
|
7
|
+
The product goal is a chapter-level learning feedback loop:
|
|
8
|
+
|
|
9
|
+
1. **章节知识验证**: select a chapter and understand which knowledge points exist and what the student currently appears to know.
|
|
10
|
+
2. **盲点评估**: generate assessment papers, let the student complete them on paper, upload the completed result as PDF, grade it, and identify weak knowledge points.
|
|
11
|
+
3. **盲点覆盖**: generate new targeted practice papers from those weak points, then repeat the same upload/grading/update loop.
|
|
12
|
+
|
|
13
|
+
The UI should stay workflow-focused and simple. For a selected chapter, the right side should mainly show:
|
|
14
|
+
|
|
15
|
+
- chapter mastery data and practice counts,
|
|
16
|
+
- inline knowledge extraction status/actions, plus secondary-page entries for knowledge coverage, mistake repair, and configured ability assessment,
|
|
17
|
+
- printable paper links,
|
|
18
|
+
- PDF upload and grading progress,
|
|
19
|
+
- grading summary, weak points, correction pack, and confirmation actions.
|
|
20
|
+
|
|
21
|
+
Overview practice entry cards should navigate to the relevant secondary page. Knowledge extraction lives in the inline overview module and starts only after the user opens the extraction settings/confirmation flow. Do not start generation, upload, grading, or confirmation directly from overview cards. Ability state should be shown as a compact ability status panel and must not be mixed into the knowledge heat map.
|
|
22
|
+
|
|
23
|
+
Avoid over-designing dashboards before the loop is reliable.
|
|
24
|
+
|
|
25
|
+
## Architecture Principle: No Fallback Design
|
|
26
|
+
|
|
27
|
+
The product should be designed as one complete, maintainable, well-structured system.
|
|
28
|
+
|
|
29
|
+
- Do not add compatibility layers, silent fallbacks, rollback-style product paths, or hidden local substitutes.
|
|
30
|
+
- When an API, data model, paper format, or workflow changes, use an explicit migration and update the project assets. Do not keep two product paths alive indefinitely.
|
|
31
|
+
- If an external model call, preprocessing step, PDF grading step, or long-running job fails, expose a clear failed state with actionable progress/error details instead of fabricating usable results.
|
|
32
|
+
- Frontend screens should not silently replace API data with bundled/demo data. They may show an explicit unavailable or setup-required state.
|
|
33
|
+
- Diagnostic smoke paths may exist only for local verification, and they must not update mastery, mistakes, archive state, or product-facing learning records.
|
|
34
|
+
- Initial knowledge extraction and re-extraction are different operations. Initial extraction creates chapter knowledge assets. Re-extraction changes the knowledge-point coordinate system and must reset that chapter's practice loop: generated practices, submissions, reports, generated coverage, mistake queue, mastery evidence, and ability assessment state restart from the newly extracted knowledge.
|
|
35
|
+
|
|
36
|
+
## Current Architecture
|
|
37
|
+
|
|
38
|
+
- Frontend: `src/main.jsx`, `src/styles.css`
|
|
39
|
+
- API server: `server/index.js`
|
|
40
|
+
- File store and data helpers: `server/fileStore.js`
|
|
41
|
+
- Practice generation: `server/practiceService.js`, `server/practiceGenerator.js`, `server/practiceReviewer.js`
|
|
42
|
+
- Practice rendering: `server/practicePaperHtml.js`
|
|
43
|
+
- PDF grading pipeline: `server/pdfSubmissionGrader.js`
|
|
44
|
+
- Knowledge feedback and reports: `server/knowledgeFeedback.js`
|
|
45
|
+
- Ability assessment config and state: `server/abilityService.js`
|
|
46
|
+
- Long-task job registry: `server/jobStore.js`
|
|
47
|
+
- Agent prompts: `prompts/`
|
|
48
|
+
- CLI scripts: `scripts/`
|
|
49
|
+
|
|
50
|
+
Run locally:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If running separately:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run dev:server
|
|
60
|
+
npm run dev:client
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The Vite frontend runs on `127.0.0.1:5173`; the Express API defaults to `127.0.0.1:4173`.
|
|
64
|
+
|
|
65
|
+
## Core Data Assets
|
|
66
|
+
|
|
67
|
+
Global data:
|
|
68
|
+
|
|
69
|
+
- `data/global/student_profile.json`: student profile.
|
|
70
|
+
- `data/global/chapters.json`: 34 chapter list and chapter-level status.
|
|
71
|
+
- `data/global/mastery_index.json`: cross-chapter mastery summary.
|
|
72
|
+
- `data/global/mistakes_index.json`: cross-chapter mistake summary.
|
|
73
|
+
- `data/global/ability_index.json`: derived cross-chapter ability assessment summary.
|
|
74
|
+
|
|
75
|
+
Chapter-scoped data:
|
|
76
|
+
|
|
77
|
+
- `data/chapters/<chapter-id>/chapter.json`: chapter metadata.
|
|
78
|
+
- `data/chapters/<chapter-id>/knowledge/knowledge.json`: chapter knowledge-point dictionary.
|
|
79
|
+
- `data/chapters/<chapter-id>/knowledge/knowledge.md`: readable knowledge document.
|
|
80
|
+
- `data/chapters/<chapter-id>/mastery.json`: chapter knowledge-point mastery state.
|
|
81
|
+
- `data/chapters/<chapter-id>/abilities.json`: configured chapter ability assessment state.
|
|
82
|
+
- `data/chapters/<chapter-id>/mistakes.json`: chapter mistakes.
|
|
83
|
+
- `data/chapters/<chapter-id>/practices/`: generated papers.
|
|
84
|
+
- `data/chapters/<chapter-id>/submissions/`: PDF/photo submissions, grading artifacts, reports.
|
|
85
|
+
- `data/chapters/<chapter-id>/reports/`: archived learning reports.
|
|
86
|
+
- `data/chapters/<chapter-id>/context/`: generation history, coverage summaries, plans.
|
|
87
|
+
- `data/chapters/<chapter-id>/context/generated_coverage.json`: first-round generated-paper coverage derived from knowledge coverage practice JSON. This is not mastery evidence.
|
|
88
|
+
|
|
89
|
+
Long-lived project maps live under `.agents/story/` and must be kept current when behavior, data models, APIs, modules, architecture constraints, or coding standards change.
|
|
90
|
+
|
|
91
|
+
## Practice Paper Format
|
|
92
|
+
|
|
93
|
+
HTML is now the preferred paper format.
|
|
94
|
+
HTML is the only supported practice paper format.
|
|
95
|
+
|
|
96
|
+
Each generated practice should write:
|
|
97
|
+
|
|
98
|
+
- `<practice-id>.json`: source of truth for questions, answers, rubrics, `knowledgePointIds`, and ability metadata when applicable.
|
|
99
|
+
- `<practice-id>.html`: printable student paper.
|
|
100
|
+
- `<practice-id>.answers.html`: printable answer/rubric sheet.
|
|
101
|
+
|
|
102
|
+
Practice generation must not write Markdown paper files. UI and printing work must use `htmlUrl` / `answersHtmlUrl`.
|
|
103
|
+
|
|
104
|
+
Practice JSON must preserve:
|
|
105
|
+
|
|
106
|
+
- `questions[].id`
|
|
107
|
+
- `questions[].stem`
|
|
108
|
+
- `questions[].answer`
|
|
109
|
+
- `questions[].solutionSteps`
|
|
110
|
+
- `questions[].rubric`
|
|
111
|
+
- `questions[].score`
|
|
112
|
+
- `questions[].knowledgePointIds`
|
|
113
|
+
- `questions[].knowledgePoints`
|
|
114
|
+
- `questions[].expectedErrorTypes`
|
|
115
|
+
- `questions[].abilityIds` for `ability_assessment`
|
|
116
|
+
- `questions[].skillAtoms` for `ability_assessment`
|
|
117
|
+
- `questions[].expectedAbilityErrors` for `ability_assessment`
|
|
118
|
+
|
|
119
|
+
PDF grading must treat the active `practice.json` as the source of truth. The uploaded PDF only provides student answer evidence.
|
|
120
|
+
|
|
121
|
+
## PDF Grading Loop
|
|
122
|
+
|
|
123
|
+
The reusable service is:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
gradePdfSubmission({
|
|
127
|
+
practiceId,
|
|
128
|
+
pdfPath,
|
|
129
|
+
notes,
|
|
130
|
+
autoArchive,
|
|
131
|
+
forceArchive,
|
|
132
|
+
submissionId,
|
|
133
|
+
useAgent,
|
|
134
|
+
maxRechecks,
|
|
135
|
+
gradingBatchSize,
|
|
136
|
+
onProgress
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Pipeline:
|
|
141
|
+
|
|
142
|
+
1. render PDF pages to PNG,
|
|
143
|
+
2. extract visible answer fragments page by page,
|
|
144
|
+
3. stitch answers by `questionId` across pages,
|
|
145
|
+
4. grade in batches against practice answers and rubrics,
|
|
146
|
+
5. run optional second-pass rechecks for uncertain items,
|
|
147
|
+
6. write grading artifacts,
|
|
148
|
+
7. confirm/archive only after trusted grading or explicit confirmation.
|
|
149
|
+
|
|
150
|
+
Submission artifacts include:
|
|
151
|
+
|
|
152
|
+
- `pages/page-*.png`
|
|
153
|
+
- `page_extracts/page-*.json`
|
|
154
|
+
- `stitched_answers.json`
|
|
155
|
+
- `grading.json`
|
|
156
|
+
- `knowledge_evidence.json`
|
|
157
|
+
- `weak_points.json`
|
|
158
|
+
- `positive_report.md`
|
|
159
|
+
- `correction_pack.md`
|
|
160
|
+
- `self_check.json`
|
|
161
|
+
- `process_log.jsonl`
|
|
162
|
+
|
|
163
|
+
`needs_review` should be minimized. If the first pass produces `needs_review`, the pipeline should attempt a focused second pass before leaving it for humans.
|
|
164
|
+
|
|
165
|
+
## Web API Surface
|
|
166
|
+
|
|
167
|
+
Baseline:
|
|
168
|
+
|
|
169
|
+
- `GET /api/profile`
|
|
170
|
+
- `GET /api/chapters`
|
|
171
|
+
- `GET /api/settings/llm`
|
|
172
|
+
- `PUT /api/settings/llm`
|
|
173
|
+
- `GET /api/knowledge`
|
|
174
|
+
- `GET /api/knowledge/:chapterId`
|
|
175
|
+
- `POST /api/knowledge/:chapterId/extract`
|
|
176
|
+
- `GET /api/practices`
|
|
177
|
+
- `GET /api/practices/:id`
|
|
178
|
+
- `POST /api/practices/generate`
|
|
179
|
+
- `POST /api/practices/:id/submissions`
|
|
180
|
+
- `POST /api/submissions/:submissionId/photos`
|
|
181
|
+
- `GET /api/submissions/:id`
|
|
182
|
+
- `POST /api/submissions/:id/confirm`
|
|
183
|
+
- `GET /api/archive`
|
|
184
|
+
|
|
185
|
+
Chapter workflow:
|
|
186
|
+
|
|
187
|
+
- `GET /api/chapters/:chapterId/practices`
|
|
188
|
+
- `GET /api/chapters/:chapterId/generated-coverage`
|
|
189
|
+
- `GET /api/chapters/:chapterId/abilities`
|
|
190
|
+
- `GET /api/chapters/:chapterId/submissions`
|
|
191
|
+
- `GET /api/chapters/:chapterId/weak-points`
|
|
192
|
+
- `POST /api/practices/:id/pdf-submissions`
|
|
193
|
+
- `GET /api/submissions/:id/artifacts`
|
|
194
|
+
- `GET /api/submissions/:id/artifacts/:name`
|
|
195
|
+
|
|
196
|
+
Long tasks:
|
|
197
|
+
|
|
198
|
+
- `POST /api/jobs/knowledge-extract`
|
|
199
|
+
- `POST /api/jobs/practice-generate`
|
|
200
|
+
- `GET /api/jobs/:jobId`
|
|
201
|
+
- `GET /api/jobs/:jobId/events`
|
|
202
|
+
|
|
203
|
+
Long-running operations must use job/progress APIs or equivalent UI progress. Do not make the frontend wait silently on long requests.
|
|
204
|
+
|
|
205
|
+
LLM settings:
|
|
206
|
+
|
|
207
|
+
- `GET /api/settings/llm` returns local inference endpoint, model, API-key presence, and `.env.local` path; it must never return the API key value.
|
|
208
|
+
- `PUT /api/settings/llm` writes `AI_BASE_URL`, `AI_MODEL`, and optionally `AI_API_KEY` to `.env.local`, then updates the running server process environment so new model calls use the saved values without restart.
|
|
209
|
+
- In packaged `math-ati start` mode, `.env.local` belongs to the user workspace and is passed through `MATH_AGENT_ENV_FILE`; do not write provider credentials into the installed package directory.
|
|
210
|
+
|
|
211
|
+
`POST /api/practices/generate` and `POST /api/jobs/practice-generate` support `type: "ability_assessment"` with `abilityIds` for chapters that explicitly enable those abilities.
|
|
212
|
+
|
|
213
|
+
The frontend derives flow records as `knowledge_coverage -> knowledge`, `mistake_repair -> mistake`, and `ability_assessment -> ability`. Ability assessment should reuse the same practice workspace, PDF upload, grading, and confirmation loop rather than creating a parallel workflow.
|
|
214
|
+
|
|
215
|
+
## Long-Task UX Rules
|
|
216
|
+
|
|
217
|
+
Practice generation, knowledge extraction, and PDF grading can be slow.
|
|
218
|
+
|
|
219
|
+
Every web flow for those operations should show progress using `event.message`, for example:
|
|
220
|
+
|
|
221
|
+
- `正在把 PDF 渲染成逐页图片`
|
|
222
|
+
- `正在识别第 3/6 页作答`
|
|
223
|
+
- `正在批改第 2/7 批:q6、q7、q8、q9、q10`
|
|
224
|
+
- `正在生成批改文件、知识点证据、薄弱点和讲解包`
|
|
225
|
+
|
|
226
|
+
The frontend should support polling `GET /api/jobs/:jobId` and, when useful, SSE via `GET /api/jobs/:jobId/events`.
|
|
227
|
+
|
|
228
|
+
## Knowledge Feedback Rules
|
|
229
|
+
|
|
230
|
+
Grading updates long-term learning state only after confirmation/trusted archive.
|
|
231
|
+
|
|
232
|
+
Correct answers create positive evidence for linked knowledge points.
|
|
233
|
+
Wrong, partial, unrecognized, or unresolved answers create weak-point evidence.
|
|
234
|
+
Mistake repair uses `data/chapters/<chapter-id>/mistakes.json` as the active work queue. Each mistake should use `status: open|practicing|resolved|regressed`; default weak-point APIs should expose only actionable `open|regressed` mistakes.
|
|
235
|
+
|
|
236
|
+
Confirmed `ability_assessment` submissions update `data/chapters/<chapter-id>/abilities.json` and `data/global/ability_index.json`. They should not update knowledge mastery directly, though wrong answers may still feed the mistake lifecycle through their linked knowledge points and error types.
|
|
237
|
+
|
|
238
|
+
Important files:
|
|
239
|
+
|
|
240
|
+
- `data/chapters/<chapter-id>/mastery.json`
|
|
241
|
+
- `data/mastery.json`
|
|
242
|
+
- `data/chapters/<chapter-id>/mistakes.json`
|
|
243
|
+
- `data/chapters/<chapter-id>/abilities.json`
|
|
244
|
+
- `data/mistakes.json`
|
|
245
|
+
- `data/chapters/<chapter-id>/submissions/<submission-id>/weak_points.json`
|
|
246
|
+
- `data/chapters/<chapter-id>/submissions/<submission-id>/knowledge_evidence.json`
|
|
247
|
+
|
|
248
|
+
Generation for blind-spot repair should use `knowledgePointIds` from weak points when possible.
|
|
249
|
+
|
|
250
|
+
## Development Guidelines
|
|
251
|
+
|
|
252
|
+
- Keep the workflow simple until the chapter loop is reliable.
|
|
253
|
+
- Prefer chapter-scoped data under `data/chapters/<chapter-id>/...`.
|
|
254
|
+
- Keep server-side provider calls and secrets out of browser code.
|
|
255
|
+
- Preserve existing file-based source of truth; do not introduce a database unless explicitly requested.
|
|
256
|
+
- Do not add compatibility or fallback layers; prefer explicit migrations and one source of truth.
|
|
257
|
+
- Use HTML for printable practice papers. Do not generate Markdown practice papers.
|
|
258
|
+
- Use `process_log.jsonl` and job events for progress; do not rely only on console logs.
|
|
259
|
+
- When adding or changing APIs, modules, data models, or workflow behavior, update `.agents/story/` as well as this file if the change affects future agents.
|
|
260
|
+
|
|
261
|
+
## Upaseo Assets
|
|
262
|
+
|
|
263
|
+
- `.paseo/` stores runtime plans, todos, handoffs, and recovery context only.
|
|
264
|
+
- `.agents/story/` stores long-lived project assets.
|
|
265
|
+
|
|
266
|
+
Story assets:
|
|
267
|
+
|
|
268
|
+
- `.agents/story/stories.md`
|
|
269
|
+
- `.agents/story/data_models.md`
|
|
270
|
+
- `.agents/story/apis.md`
|
|
271
|
+
- `.agents/story/modules.md`
|
|
272
|
+
- `.agents/story/architecture_constraints.md`
|
|
273
|
+
- `.agents/story/coding_standards.md`
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Math ATI 加油站
|
|
2
|
+
|
|
3
|
+
本包提供本地运行的数学 ATI 学习闭环:章节知识点覆盖、易错题修复、能力评估、纸质试卷打印、PDF 批改和掌握状态回写。
|
|
4
|
+
|
|
5
|
+
## 安装与初始化
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @zhouchangui/math-ati
|
|
9
|
+
mkdir my-math-ati
|
|
10
|
+
cd my-math-ati
|
|
11
|
+
math-ati init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
初始化会创建本地 `data/` 和 `.env.local`。编辑 `.env.local`,填入 LLM 推理地址、模型和 API Key。
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
math-ati start --open
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
默认启动地址是 `http://127.0.0.1:4173`。
|
|
21
|
+
|
|
22
|
+
## 常用命令
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
math-ati init [workspace]
|
|
26
|
+
math-ati start [workspace] --open --port 4173
|
|
27
|
+
math-ati doctor [workspace]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 数据边界
|
|
31
|
+
|
|
32
|
+
`data/` 是本地孩子档案、练习、批改和掌握状态,不会随 npm 包发布。包内只包含干净的初始化模板、服务端、前端构建产物和提示词。
|
|
33
|
+
|
|
34
|
+
教材原始图片与完整课程资源应通过独立资源包安装到本地 workspace,再由 `MATH_AGENT_IMAGE_ROOT` 指向资源目录。
|
package/bin/math-ati.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { access, cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const templateRoot = path.join(packageRoot, 'templates', 'workspace');
|
|
11
|
+
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`math-ati
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
math-ati init [workspace]
|
|
17
|
+
math-ati start [workspace] [--open] [--port 4173] [--data-dir <dir>] [--env-file <file>]
|
|
18
|
+
math-ati doctor [workspace] [--data-dir <dir>] [--env-file <file>]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init Create a local workspace with clean data and .env.local template.
|
|
22
|
+
start Start the local API and production web UI.
|
|
23
|
+
doctor Print package, workspace, and configuration status.
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = [];
|
|
29
|
+
const flags = {};
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const item = argv[index];
|
|
32
|
+
if (!item.startsWith('--')) {
|
|
33
|
+
args.push(item);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const key = item.slice(2);
|
|
37
|
+
if (['open', 'force', 'help'].includes(key)) {
|
|
38
|
+
flags[key] = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
flags[key] = argv[index + 1];
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
return { args, flags };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function exists(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
await access(filePath, constants.F_OK);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function ensureWorkspaceTemplate() {
|
|
57
|
+
if (await exists(templateRoot)) return;
|
|
58
|
+
throw new Error(`workspace_template_missing:${templateRoot}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function copyTemplateSafely(targetDir, options) {
|
|
62
|
+
await ensureWorkspaceTemplate();
|
|
63
|
+
await mkdir(targetDir, { recursive: true });
|
|
64
|
+
const stack = [''];
|
|
65
|
+
while (stack.length) {
|
|
66
|
+
const rel = stack.pop();
|
|
67
|
+
const sourceDir = path.join(templateRoot, rel);
|
|
68
|
+
const targetSubdir = path.join(targetDir, rel);
|
|
69
|
+
await mkdir(targetSubdir, { recursive: true });
|
|
70
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const source = path.join(sourceDir, entry.name);
|
|
73
|
+
const destination = path.join(targetSubdir, entry.name);
|
|
74
|
+
const childRel = path.join(rel, entry.name);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
stack.push(childRel);
|
|
77
|
+
} else if (options.force || !(await exists(destination))) {
|
|
78
|
+
await cp(source, destination, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function initWorkspace(argv) {
|
|
85
|
+
const { args, flags } = parseArgs(argv);
|
|
86
|
+
const workspaceDir = path.resolve(args[0] || process.cwd());
|
|
87
|
+
await copyTemplateSafely(workspaceDir, { force: Boolean(flags.force) });
|
|
88
|
+
const envLocal = path.join(workspaceDir, '.env.local');
|
|
89
|
+
const envExample = path.join(workspaceDir, '.env.local.example');
|
|
90
|
+
if (!(await exists(envLocal)) && (await exists(envExample))) {
|
|
91
|
+
await writeFile(envLocal, await readFile(envExample, 'utf8'), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
console.log(`[math-ati.init] workspace=${workspaceDir}`);
|
|
94
|
+
console.log(`[math-ati.init] data=${path.join(workspaceDir, 'data')}`);
|
|
95
|
+
console.log('[math-ati.init] edit .env.local, then run: math-ati start --open');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function workspaceFromArgs(args) {
|
|
99
|
+
return path.resolve(args[0] || process.cwd());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dataDirFromArgs(args, flags) {
|
|
103
|
+
return path.resolve(flags['data-dir'] || path.join(workspaceFromArgs(args), 'data'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function envFileFromArgs(args, flags) {
|
|
107
|
+
return path.resolve(flags['env-file'] || path.join(workspaceFromArgs(args), '.env.local'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function openBrowser(url) {
|
|
111
|
+
const command = os.platform() === 'darwin'
|
|
112
|
+
? 'open'
|
|
113
|
+
: os.platform() === 'win32'
|
|
114
|
+
? 'cmd'
|
|
115
|
+
: 'xdg-open';
|
|
116
|
+
const args = os.platform() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
117
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
118
|
+
child.unref();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function startServer(argv) {
|
|
122
|
+
const { args, flags } = parseArgs(argv);
|
|
123
|
+
const port = String(flags.port || process.env.PORT || 4173);
|
|
124
|
+
const dataDir = dataDirFromArgs(args, flags);
|
|
125
|
+
const envFile = envFileFromArgs(args, flags);
|
|
126
|
+
const chaptersPath = path.join(dataDir, 'global', 'chapters.json');
|
|
127
|
+
if (!(await exists(chaptersPath))) {
|
|
128
|
+
throw new Error(`workspace_not_initialized:${dataDir}. Run "math-ati init" first.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const url = `http://127.0.0.1:${port}`;
|
|
132
|
+
const server = spawn(process.execPath, [path.join(packageRoot, 'server', 'index.js')], {
|
|
133
|
+
cwd: packageRoot,
|
|
134
|
+
stdio: 'inherit',
|
|
135
|
+
env: {
|
|
136
|
+
...process.env,
|
|
137
|
+
PORT: port,
|
|
138
|
+
MATH_AGENT_DATA_DIR: dataDir,
|
|
139
|
+
MATH_AGENT_ENV_FILE: envFile
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
if (flags.open) setTimeout(() => openBrowser(url), 900);
|
|
143
|
+
|
|
144
|
+
const stop = (signal) => {
|
|
145
|
+
if (!server.killed) server.kill(signal);
|
|
146
|
+
};
|
|
147
|
+
process.on('SIGINT', () => stop('SIGINT'));
|
|
148
|
+
process.on('SIGTERM', () => stop('SIGTERM'));
|
|
149
|
+
server.on('exit', (code, signal) => {
|
|
150
|
+
if (signal) process.kill(process.pid, signal);
|
|
151
|
+
process.exit(code ?? 0);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function doctor(argv) {
|
|
156
|
+
const { args, flags } = parseArgs(argv);
|
|
157
|
+
const dataDir = dataDirFromArgs(args, flags);
|
|
158
|
+
const envLocal = envFileFromArgs(args, flags);
|
|
159
|
+
const distIndex = path.join(packageRoot, 'dist', 'index.html');
|
|
160
|
+
console.log(JSON.stringify({
|
|
161
|
+
packageRoot,
|
|
162
|
+
dataDir,
|
|
163
|
+
hasData: await exists(path.join(dataDir, 'global', 'chapters.json')),
|
|
164
|
+
hasEnvLocal: await exists(envLocal),
|
|
165
|
+
hasBuiltUi: await exists(distIndex),
|
|
166
|
+
node: process.version
|
|
167
|
+
}, null, 2));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
const [command = 'help', ...argv] = process.argv.slice(2);
|
|
172
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
173
|
+
printHelp();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (command === 'init') {
|
|
177
|
+
await initWorkspace(argv);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (command === 'start') {
|
|
181
|
+
await startServer(argv);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (command === 'doctor') {
|
|
185
|
+
await doctor(argv);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`unknown_command:${command}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
console.error(`[math-ati.error] ${error.message}`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|