mathgen-cli 0.0.0-stage → 1.0.0-rc2
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/README.md +88 -2
- package/cli.js +189 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mathgen-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
小学数学老师用的**出题 / 打印 / 批改**工具 `mathgen` 的 **npm 启动器** —— 一条命令在本机拉起老师端与学生端。
|
|
4
|
+
不需要注册、不需要联网、不需要服务器。
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npx mathgen-cli@rc # 或写死版本:npx mathgen-cli@1.0.0-rc2
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> **当前是预发布版(release candidate)**:npm 上 `rc` 与 `latest` 都指向 `1.0.0-rc2`,
|
|
11
|
+
> 所以 `@rc`、`@1.0.0-rc2` 与裸写 `npx mathgen-cli` 拿到的是同一份;要钉住版本就写死版本号。
|
|
12
|
+
>
|
|
13
|
+
> **包名为什么不是 `mathgen`?** npm 上那个名字被判定与已有的 `math-gen` 太像(去标点后同名),
|
|
14
|
+
> `npm publish` 直接 403。包名改成 `mathgen-cli`,但里面的命令仍然是 `mathgen`,
|
|
15
|
+
> 用法与仓库 README 里写的完全一样。
|
|
16
|
+
|
|
17
|
+
跑起来后浏览器打开 <http://127.0.0.1:8000/teacher>。
|
|
18
|
+
|
|
19
|
+
> 这个包是**启动器**,不是把后端用 JS 重写了一遍。它会:
|
|
20
|
+
> ① 找一个 Python 3.12+ → ② 在 `~/.mathgen/npm-<版本>/venv` 建一个**独立**虚拟环境
|
|
21
|
+
> → ③ 从 [GitHub Release](https://github.com/EdwardXiao-bit/mathgen/releases) 下载并安装后端 wheel
|
|
22
|
+
> (**wheel 里自带前端产物**)→ ④ 需要时下 Chromium(PDF 导出用)→ ⑤ 启动服务。
|
|
23
|
+
>
|
|
24
|
+
> 所以它**需要本机有 Python 3.12+**:Python 应用没法在 Node 里跑。
|
|
25
|
+
> 不想装 Python 就用 Docker 方式,见仓库 README。
|
|
26
|
+
|
|
27
|
+
## 用法
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx mathgen-cli@rc # 默认 127.0.0.1:8000,自动打开浏览器
|
|
31
|
+
npx mathgen-cli@rc --port 9000 # 换端口
|
|
32
|
+
npx mathgen-cli@rc --host 0.0.0.0 # 让同一局域网的学生也能连(教室场景)
|
|
33
|
+
npx mathgen-cli@rc --data-dir D:\math # 数据目录
|
|
34
|
+
npx mathgen-cli@rc --no-browser # 不要自动开浏览器
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
参数会**原样转交**给 `mathgen` 命令,完整列表:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
--host HOST 监听地址,默认 127.0.0.1
|
|
41
|
+
--port PORT 端口,默认 8000
|
|
42
|
+
--data-dir DIR 数据目录,默认 ~/.mathgen/data
|
|
43
|
+
--db PATH 直接指定数据库文件
|
|
44
|
+
--static-dir DIR 前端产物目录
|
|
45
|
+
--no-browser 启动后不要自动打开浏览器
|
|
46
|
+
--version 打印版本号
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 它做了什么、没做什么
|
|
50
|
+
|
|
51
|
+
**做**:把 Python 环境和后端装到 `~/.mathgen/npm-<版本>` 下(删掉这个目录就干净了),
|
|
52
|
+
然后把参数转交给它。不会动你的系统 Python,不需要管理员权限,不写全局配置。
|
|
53
|
+
|
|
54
|
+
**不做**:不常驻后台、不开机自启、不对外暴露——它是给「教室一台电脑」用的。
|
|
55
|
+
|
|
56
|
+
## 数据在哪
|
|
57
|
+
|
|
58
|
+
`~/.mathgen/data/mathgen.db`(一个 SQLite 文件)。备份就是拷这个文件。
|
|
59
|
+
用 `--data-dir` 可以换地方。
|
|
60
|
+
|
|
61
|
+
## 常见问题
|
|
62
|
+
|
|
63
|
+
**PDF 导出报 503?** Chromium 没装好。`npx mathgen-cli@rc` 首次会自己装;
|
|
64
|
+
如果当时网络不通,手动补一次:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
# 用启动器建出来的那个环境
|
|
68
|
+
~/.mathgen/npm-1.0.0-rc2/venv/bin/python -m playwright install chromium
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
(Windows 上是 `%USERPROFILE%\.mathgen\npm-1.0.0-rc2\venv\Scripts\python.exe`)
|
|
72
|
+
|
|
73
|
+
**下载 wheel 失败?** 可以指定别的下载地址或本地文件:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
MATHGEN_WHEEL_URL=https://你的镜像/mathgen_backend-1.0.0rc2-py3-none-any.whl npx mathgen-cli@rc
|
|
77
|
+
MATHGEN_WHEEL_PATH=D:\build\mathgen_backend-1.0.0rc2-py3-none-any.whl npx mathgen-cli@rc
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**想彻底卸载?** 删掉 `~/.mathgen` 即可。
|
|
81
|
+
|
|
82
|
+
## 链接
|
|
83
|
+
|
|
84
|
+
- 仓库与完整文档:<https://github.com/EdwardXiao-bit/mathgen>
|
|
85
|
+
- 问题反馈:<https://github.com/EdwardXiao-bit/mathgen/issues>
|
|
86
|
+
|
|
87
|
+
## 许可
|
|
88
|
+
|
|
89
|
+
MIT © 2026 肖铭皓、胡宇书
|
package/cli.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mathgen-cli —— mathgen 的 npm 启动器。
|
|
4
|
+
*
|
|
5
|
+
* npx mathgen-cli@1.0.0-rc1 # 直接跑(首次会准备环境,之后秒起)
|
|
6
|
+
* npx mathgen-cli@1.0.0-rc1 --host 0.0.0.0 # 让局域网里的学生也能连
|
|
7
|
+
*
|
|
8
|
+
* 包名为什么是 mathgen-cli 而不是 mathgen:npm 判定 `mathgen` 与早已存在的 `math-gen`
|
|
9
|
+
* 太像(去标点后同名),`npm publish mathgen` 直接 403。包名换了,里面转交的命令仍是 `mathgen`。
|
|
10
|
+
*
|
|
11
|
+
* 它**不是**把 Python 后端重新实现一遍,也不自带运行时:Python 应用没法在 Node 里跑。
|
|
12
|
+
* 它做的是「把 Python 那套东西装到用户目录下,然后把参数转交给 `mathgen` 命令」:
|
|
13
|
+
*
|
|
14
|
+
* 1. 找一个 Python 3.12+(没有就明确告诉用户去哪装)
|
|
15
|
+
* 2. 在 ~/.mathgen/npm-<版本>/venv 建一个**独立**虚拟环境
|
|
16
|
+
* 3. 从 GitHub Release 装后端 wheel(**wheel 里自带前端产物**,不用再下第二次)
|
|
17
|
+
* 4. 需要的话下 Chromium(PDF 导出用)
|
|
18
|
+
* 5. exec `mathgen`,把命令行参数原样传过去
|
|
19
|
+
*
|
|
20
|
+
* 为什么从 Release 装而不是 `pip install mathgen-backend`:这个项目**没有发 PyPI**
|
|
21
|
+
* (PyPI 上的 `mathgen` 是别人的包)。Release 里的 wheel 是同一个构建产物,只是换个下载地址。
|
|
22
|
+
*
|
|
23
|
+
* 刻意不做:不用 root、不动系统 Python、不写全局配置、不常驻后台。
|
|
24
|
+
*/
|
|
25
|
+
'use strict'
|
|
26
|
+
|
|
27
|
+
const { spawnSync } = require('node:child_process')
|
|
28
|
+
const fs = require('node:fs')
|
|
29
|
+
const https = require('node:https')
|
|
30
|
+
const os = require('node:os')
|
|
31
|
+
const path = require('node:path')
|
|
32
|
+
|
|
33
|
+
const OWNER_REPO = 'EdwardXiao-bit/mathgen'
|
|
34
|
+
const { version } = require('./package.json')
|
|
35
|
+
const RELEASE_BASE = `https://github.com/${OWNER_REPO}/releases/download/v${version}`
|
|
36
|
+
const WHEEL = `mathgen_backend-${version.replace(/-/g, '')}-py3-none-any.whl`
|
|
37
|
+
const ROOT = path.join(os.homedir(), '.mathgen')
|
|
38
|
+
const HOME = path.join(ROOT, `npm-${version}`)
|
|
39
|
+
const VENV = path.join(HOME, 'venv')
|
|
40
|
+
const PY = path.join(VENV, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python')
|
|
41
|
+
const MATHGEN = path.join(VENV, process.platform === 'win32' ? 'Scripts/mathgen.exe' : 'bin/mathgen')
|
|
42
|
+
const STAMP = path.join(HOME, '.installed')
|
|
43
|
+
|
|
44
|
+
const say = (text = '') => console.log(text)
|
|
45
|
+
|
|
46
|
+
function die(text, code = 1) {
|
|
47
|
+
console.error(`\n ${text}\n`)
|
|
48
|
+
process.exit(code)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function run(cmd, args, opts = {}) {
|
|
52
|
+
return spawnSync(cmd, args, { stdio: 'inherit', ...opts })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ------------------------------------------------------------------ 1. Python
|
|
56
|
+
|
|
57
|
+
function findPython() {
|
|
58
|
+
const candidates = [['py', ['-3.12']], ['python3.12', []], ['python3', []], ['python', []]]
|
|
59
|
+
for (const [exe, prefix] of candidates) {
|
|
60
|
+
// 这里必须 shell:false。Windows 上走 shell 会把 args 只做字符串拼接(Node 自己为此发 DEP0190
|
|
61
|
+
// 警告),`-c` 里的引号与分号会被 cmd.exe 吃掉 —— 实测 shell:true 时四个候选全部 status=1、
|
|
62
|
+
// 一个都探不到;shell:false 时 `python` 正常返回 "3.12"。参数按数组传给 CreateProcess,
|
|
63
|
+
// 不经过任何 shell,才不会被引号、分号或路径里的空格影响。
|
|
64
|
+
const probe = spawnSync(exe, [...prefix, '-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
shell: false,
|
|
67
|
+
})
|
|
68
|
+
if (probe.status === 0 && probe.stdout) {
|
|
69
|
+
const [major, minor] = probe.stdout.trim().split('.').map(Number)
|
|
70
|
+
if (major > 3 || (major === 3 && minor >= 12)) return { exe, prefix }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ------------------------------------------------------------------ 2. 下载
|
|
77
|
+
|
|
78
|
+
function download(url, dest) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
https
|
|
81
|
+
.get(url, { headers: { 'user-agent': `mathgen-npm/${version}` } }, (res) => {
|
|
82
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
83
|
+
res.resume()
|
|
84
|
+
return download(res.headers.location, dest).then(resolve, reject)
|
|
85
|
+
}
|
|
86
|
+
if (res.statusCode !== 200) {
|
|
87
|
+
res.resume()
|
|
88
|
+
return reject(new Error(`HTTP ${res.statusCode} — ${url}`))
|
|
89
|
+
}
|
|
90
|
+
const tmp = `${dest}.part`
|
|
91
|
+
const out = fs.createWriteStream(tmp)
|
|
92
|
+
res.pipe(out)
|
|
93
|
+
out.on('finish', () => {
|
|
94
|
+
out.close(() => {
|
|
95
|
+
fs.renameSync(tmp, dest)
|
|
96
|
+
resolve(dest)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
out.on('error', reject)
|
|
100
|
+
})
|
|
101
|
+
.on('error', reject)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ------------------------------------------------------------------ 3. 装配
|
|
106
|
+
|
|
107
|
+
async function ensureInstalled() {
|
|
108
|
+
if (fs.existsSync(STAMP)) return
|
|
109
|
+
|
|
110
|
+
say(`\n首次运行:把服务装到 ${HOME}`)
|
|
111
|
+
say('(只装一次,以后 npx mathgen-cli 直接起)\n')
|
|
112
|
+
|
|
113
|
+
const python = findPython()
|
|
114
|
+
if (!python) {
|
|
115
|
+
die(
|
|
116
|
+
'没有找到 Python 3.12 或更高版本。\n' +
|
|
117
|
+
' mathgen 的后端是 Python 写的,需要先装它:https://www.python.org/downloads/\n' +
|
|
118
|
+
' (macOS:brew install python@3.12 Debian/Ubuntu:sudo apt install python3.12 python3.12-venv)'
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
say(` [1/4] Python:${python.exe} ${python.prefix.join(' ')}`.trimEnd())
|
|
122
|
+
|
|
123
|
+
fs.mkdirSync(HOME, { recursive: true })
|
|
124
|
+
say(` [2/4] 建虚拟环境 ${VENV}`)
|
|
125
|
+
// 同样 shell:false:VENV 与 wheelPath 都是可能含空格的绝对路径(Windows 用户名里带空格很常见),
|
|
126
|
+
// 走 shell 时它们不会被加引号。
|
|
127
|
+
if (run(python.exe, [...python.prefix, '-m', 'venv', VENV], { shell: false }).status !== 0) {
|
|
128
|
+
die('创建虚拟环境失败。')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const wheelPath = path.join(HOME, WHEEL)
|
|
132
|
+
const local = process.env.MATHGEN_WHEEL_PATH
|
|
133
|
+
const override = process.env.MATHGEN_WHEEL_URL
|
|
134
|
+
if (local) {
|
|
135
|
+
say(` [3/4] 使用本地 wheel ${local}`)
|
|
136
|
+
if (!fs.existsSync(local)) die(`MATHGEN_WHEEL_PATH 指向的文件不存在:${local}`)
|
|
137
|
+
fs.copyFileSync(local, wheelPath)
|
|
138
|
+
} else {
|
|
139
|
+
const url = override || `${RELEASE_BASE}/${WHEEL}`
|
|
140
|
+
say(` [3/4] 下载后端 ${WHEEL}`)
|
|
141
|
+
try {
|
|
142
|
+
await download(url, wheelPath)
|
|
143
|
+
} catch (err) {
|
|
144
|
+
die(
|
|
145
|
+
`下载失败:${err.message}\n` +
|
|
146
|
+
` 可以手动下载后重试:${RELEASE_BASE}\n` +
|
|
147
|
+
` 或者改用源码方式:git clone https://github.com/${OWNER_REPO}.git\n` +
|
|
148
|
+
` (也可用 MATHGEN_WHEEL_URL / MATHGEN_WHEEL_PATH 指定别处的构建产物)`
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 刻意不跑 `pip install --upgrade pip`:在 Windows + 3.12 上实测它会长时间空转不下载
|
|
154
|
+
// (三次独立复现),而 venv 自带的 pip 处理 `pip install <wheel>` 完全够用。
|
|
155
|
+
// 要换源就设 PIP_INDEX_URL,pip 会从环境里读。
|
|
156
|
+
if (
|
|
157
|
+
run(PY, ['-m', 'pip', 'install', '--disable-pip-version-check', '--no-input', wheelPath]).status !== 0
|
|
158
|
+
) {
|
|
159
|
+
die(
|
|
160
|
+
'安装后端失败。常见原因:网络到不了 PyPI。\n' +
|
|
161
|
+
" 可先设镜像再重跑:set PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
say(' [4/4] 准备 Chromium(PDF 导出用,约 150 MB)')
|
|
166
|
+
if (run(PY, ['-c', 'import app.export.browser as b,sys; sys.exit(0 if b.is_available() else 1)'], {
|
|
167
|
+
stdio: 'ignore',
|
|
168
|
+
}).status !== 0) {
|
|
169
|
+
if (run(PY, ['-m', 'playwright', 'install', 'chromium']).status !== 0) {
|
|
170
|
+
say(' (下载失败:PDF 导出会返回 503,其余功能正常,可稍后重跑)')
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
say(' Chromium 已就绪')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fs.writeFileSync(STAMP, new Date().toISOString())
|
|
177
|
+
say('\n装好了。\n')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ------------------------------------------------------------------ 入口
|
|
181
|
+
|
|
182
|
+
async function main() {
|
|
183
|
+
await ensureInstalled()
|
|
184
|
+
const result = run(MATHGEN, process.argv.slice(2), { shell: false })
|
|
185
|
+
if (result.error) die(`启动失败:${result.error.message}`)
|
|
186
|
+
process.exit(result.status === null ? 1 : result.status)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main().catch((err) => die(err.stack || String(err)))
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mathgen-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-rc2",
|
|
4
|
+
"description": "小学四则运算练习题生成与批改系统 —— 一条命令在本机拉起老师端与学生端",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mathgen": "cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"cli.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"os": [
|
|
16
|
+
"win32",
|
|
17
|
+
"darwin",
|
|
18
|
+
"linux"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://github.com/EdwardXiao-bit/mathgen",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/EdwardXiao-bit/mathgen.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/EdwardXiao-bit/mathgen/issues"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"math",
|
|
31
|
+
"arithmetic",
|
|
32
|
+
"education",
|
|
33
|
+
"teacher",
|
|
34
|
+
"worksheet",
|
|
35
|
+
"primary-school",
|
|
36
|
+
"四则运算",
|
|
37
|
+
"口算"
|
|
38
|
+
]
|
|
39
|
+
}
|