@a9i5k4/dsh-auto-memory 2.1.9 → 2.2.1
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 +3 -3
- package/README.zh-CN.md +3 -3
- package/lib/client.js +3976 -3807
- package/lib/index.js +6814 -6440
- package/lib/python-setup.js +244 -244
- package/lib/semantic-js.js +491 -491
- package/lib/storage-manage.js +207 -207
- package/lib/ws-overview-rank.js +40 -0
- package/package.json +1 -1
package/lib/python-setup.js
CHANGED
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* M7.6 Python 一键向导(host 半):#16-#20 配套——把 C3 进阶档从"开发机可达"变成"爱好者可达"。
|
|
3
|
-
*
|
|
4
|
-
* 四步链路(全部走本模块,UI 只管展示状态与点击):
|
|
5
|
-
* ①detect — 探测系统 Python(≥3.9)/既有 venv/模型本体;全部只读。
|
|
6
|
-
* ②venv — python -m venv <userDir>/python-engine/.venv(幂等:已存在直接跳过)。
|
|
7
|
-
* ③deps — venv 内 pip 安装运行依赖(transformers/onnxruntime/torch,清华镜像兜底)。
|
|
8
|
-
* ④model — BGE-M3 int8(~539MB)下载到 <userDir>/python-engine/models/,
|
|
9
|
-
* cn(hf-mirror)/intl(hf 官方)双通道+SHA256 校验,复用 JS 档下载器的状态机形态。
|
|
10
|
-
*
|
|
11
|
-
* 设计约束:
|
|
12
|
-
* - 一切落盘在用户目录(~/.dsh/python-engine/),npm 包目录升级会被覆盖,绝不放模型。
|
|
13
|
-
* - 长任务(venv/pip/下载)异步执行,进度写 state 供 semantic-status 轮询;失败可重试。
|
|
14
|
-
* - 零新依赖:下载用全局 fetch,解压用 onnx 单文件直下(无压缩包),venv 用 python -m venv。
|
|
15
|
-
*/
|
|
16
|
-
import { execFile } from 'node:child_process'
|
|
17
|
-
import { promisify } from 'node:util'
|
|
18
|
-
import { createHash } from 'node:crypto'
|
|
19
|
-
import { homedir } from 'node:os'
|
|
20
|
-
import path from 'node:path'
|
|
21
|
-
import { existsSync, mkdirSync, writeFileSync, statSync, createWriteStream } from 'node:fs'
|
|
22
|
-
import { readFile, writeFile, rm } from 'node:fs/promises'
|
|
23
|
-
|
|
24
|
-
const execFileP = promisify(execFile)
|
|
25
|
-
|
|
26
|
-
/** BGE-M3 int8 单文件(与 bench 夹具同源;hf-mirror 为主源,HF 官方为备源)。 */
|
|
27
|
-
const MODEL_SPEC = {
|
|
28
|
-
repo: 'Xenova/bge-m3-int8',
|
|
29
|
-
file: 'onnx/model_int8.onnx',
|
|
30
|
-
bytes: 568456694,
|
|
31
|
-
sha256: '', // 见 MODEL_SHA256:远端 LFS 校验不可靠时以 size 下限+可执行性兜底;sha256 由首版发布冻结后填入
|
|
32
|
-
mirrors: [
|
|
33
|
-
{ id: 'cn', url: (p) => 'https://hf-mirror.com/' + p },
|
|
34
|
-
{ id: 'intl', url: (p) => 'https://huggingface.co/' + p },
|
|
35
|
-
],
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** venv 内 pip 依赖:#19 实测(int8 档 encode_ids 全程 numpy+onnxruntime,tokenizer 走 Rust 快速路径)——基础集无 torch,venv 体积 ~400MB。GPU 推理开关追加 onnxruntime-gpu(CUDAExecutionProvider)。池化/精度契约冻结自 R@5 0.925 基线,fastembed 注册表无 BGE-M3 且池化契约不同,不可用。 */
|
|
39
|
-
const PIP_DEPS_CPU = ['transformers', 'onnxruntime']
|
|
40
|
-
const PIP_DEPS_GPU_EXTRA = ['onnxruntime-gpu']
|
|
41
|
-
|
|
42
|
-
export function createPythonSetupPre(opts = {}) {
|
|
43
|
-
const dshHomeOf = typeof opts.dshHome === 'function' ? opts.dshHome : () => opts.dshHome || path.join(homedir(), '.dsh')
|
|
44
|
-
const diagOf = typeof opts.diag === 'function' ? opts.diag : () => {}
|
|
45
|
-
|
|
46
|
-
// 引擎根目录(用户目录,跨升级存活): ~/.dsh/python-engine/
|
|
47
|
-
const engineRoot = () => path.join(dshHomeOf(), 'python-engine')
|
|
48
|
-
const venvDir = () => path.join(engineRoot(), '.venv')
|
|
49
|
-
const venvPython = () => process.platform === 'win32' ? path.join(venvDir(), 'Scripts', 'python.exe') : path.join(venvDir(), 'bin', 'python')
|
|
50
|
-
const modelsDir = () => path.join(engineRoot(), 'models')
|
|
51
|
-
const modelPath = () => path.join(modelsDir(), 'model_int8.onnx')
|
|
52
|
-
|
|
53
|
-
// 进度状态(内存态,semantic-status 轮询消费;host 重启后 detect 重建)
|
|
54
|
-
const st = {
|
|
55
|
-
phase: 'idle', // idle|detecting|venv|deps|downloading|verifying|ready|error
|
|
56
|
-
error: '',
|
|
57
|
-
pythons: [], // [{label,path,status:'ok'|'missing'|'too-old',version}]
|
|
58
|
-
chosenPython: '',
|
|
59
|
-
venvOk: false,
|
|
60
|
-
depsOk: false,
|
|
61
|
-
dl: { bytesDone: 0, bytesTotal: MODEL_SPEC.bytes, mirror: '', startedAt: 0, etaSec: 0 },
|
|
62
|
-
cancelled: false,
|
|
63
|
-
}
|
|
64
|
-
let dlAbort = null
|
|
65
|
-
|
|
66
|
-
function probe(pyPath, args, timeoutMs) {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
let done = false
|
|
69
|
-
const t = setTimeout(() => { if (!done) { done = true; resolve({ ok: false, out: '' }) } }, timeoutMs || 8000)
|
|
70
|
-
try {
|
|
71
|
-
execFileP(pyPath, args, { timeout: (timeoutMs || 8000) - 500, windowsHide: true, maxBuffer: 1024 * 1024 })
|
|
72
|
-
.then((r) => { if (!done) { done = true; clearTimeout(t); resolve({ ok: true, out: String(r.stdout || '').trim() }) } })
|
|
73
|
-
.catch(() => { if (!done) { done = true; clearTimeout(t); resolve({ ok: false, out: '' }) } })
|
|
74
|
-
} catch (_) { if (!done) { done = true; clearTimeout(t); resolve({ ok: false, out: '' }) } }
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function pyVersionOf(out) {
|
|
79
|
-
const m = String(out || '').match(/(\d+)\.(\d+)/)
|
|
80
|
-
return m ? { major: Number(m[1]), minor: Number(m[2]) } : null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** ①环境探测:venv(推荐)→ 系统 python/python3/py launcher;版本 ≥3.9 且 <3.13(onnxruntime 兼容上界)。 */
|
|
84
|
-
async function detect() {
|
|
85
|
-
st.phase = 'detecting'; st.error = ''
|
|
86
|
-
const dshHome = dshHomeOf()
|
|
87
|
-
const cands = [
|
|
88
|
-
{ path: venvPython(), label: 'DSH Python 引擎 venv(推荐)', isVenv: true },
|
|
89
|
-
{ path: 'python', label: '系统 PATH: python', isVenv: false },
|
|
90
|
-
{ path: 'python3', label: '系统 PATH: python3', isVenv: false },
|
|
91
|
-
{ path: 'py', label: 'Windows py launcher', isVenv: false },
|
|
92
|
-
]
|
|
93
|
-
const out = []
|
|
94
|
-
for (const c of cands) {
|
|
95
|
-
if ((c.path.includes('\\') || c.path.includes('/')) && !existsSync(c.path)) { out.push({ ...c, status: 'missing', version: '' }); continue }
|
|
96
|
-
const r = await probe(c.path, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'])
|
|
97
|
-
const v = pyVersionOf(r.out)
|
|
98
|
-
if (!r.ok || !v) { out.push({ ...c, status: 'missing', version: '' }); continue }
|
|
99
|
-
out.push({ ...c, status: (v.major === 3 && v.minor >= 9 && v.minor <= 12) ? 'ok' : 'too-old', version: v.major + '.' + v.minor })
|
|
100
|
-
}
|
|
101
|
-
// 既有成果快照
|
|
102
|
-
st.venvOk = existsSync(venvPython())
|
|
103
|
-
st.depsOk = st.venvOk ? (await probe(venvPython(), ['-c', 'import transformers, onnxruntime, torch; print("deps-ok")'])).ok : false
|
|
104
|
-
st.modelReady = existsSync(modelPath())
|
|
105
|
-
st.pythons = out
|
|
106
|
-
st.phase = 'idle'
|
|
107
|
-
return snapshot()
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function snapshot() {
|
|
111
|
-
return {
|
|
112
|
-
phase: st.phase, error: st.error,
|
|
113
|
-
pythons: st.pythons, chosenPython: st.chosenPython,
|
|
114
|
-
venvOk: st.venvOk, depsOk: st.depsOk, modelReady: st.modelReady, wantGpu: !!st.wantGpu,
|
|
115
|
-
modelPath: modelPath(), venvPython: venvPython(),
|
|
116
|
-
modelBytes: st.modelReady ? statSync(modelPath()).size : 0, modelExpectedBytes: MODEL_SPEC.bytes,
|
|
117
|
-
dl: { ...st.dl },
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** ②venv:幂等(已存在跳过);用探测到的首个 ok 系统解释器创建。 */
|
|
122
|
-
async function ensureVenv(pythonPath) {
|
|
123
|
-
const base = String(pythonPath || '').trim() || (st.pythons.find((x) => x.status === 'ok' && !x.isVenv) || {}).path || ''
|
|
124
|
-
if (!base) { st.error = '未检测到可用的系统 Python(需 3.9-3.12)。请先安装 Python。'; st.phase = 'error'; return snapshot() }
|
|
125
|
-
st.chosenPython = base
|
|
126
|
-
if (existsSync(venvPython())) { st.venvOk = true; return snapshot() }
|
|
127
|
-
st.phase = 'venv'
|
|
128
|
-
mkdirSync(engineRoot(), { recursive: true })
|
|
129
|
-
try {
|
|
130
|
-
await execFileP(base, ['-m', 'venv', venvDir()], { timeout: 180000, windowsHide: true })
|
|
131
|
-
st.venvOk = existsSync(venvPython())
|
|
132
|
-
if (!st.venvOk) throw new Error('venv 目录未生成')
|
|
133
|
-
diagOf('python-setup: venv created at ' + venvDir())
|
|
134
|
-
} catch (e) {
|
|
135
|
-
st.error = 'venv 创建失败: ' + String((e && e.message) || e).slice(0, 160); st.phase = 'error'
|
|
136
|
-
}
|
|
137
|
-
return snapshot()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** ③deps:venv 内 pip 装 transformers+onnxruntime(CPU 基础集);opts.gpu=true 追加 onnxruntime-gpu(用户选 GPU 推理时)。官方源失败切清华镜像。 */
|
|
141
|
-
async function ensureDeps(opts2) {
|
|
142
|
-
const wantGpu = !!(opts2 && opts2.gpu)
|
|
143
|
-
if (!st.venvOk) { st.error = 'venv 未就绪,先执行 venv 步骤'; st.phase = 'error'; return snapshot() }
|
|
144
|
-
st.phase = 'deps'
|
|
145
|
-
st.wantGpu = wantGpu
|
|
146
|
-
const deps = PIP_DEPS_CPU.concat(wantGpu ? PIP_DEPS_GPU_EXTRA : [])
|
|
147
|
-
const runPip = async (extra) => {
|
|
148
|
-
try {
|
|
149
|
-
const r = await execFileP(venvPython(), ['-m', 'pip', 'install', '--quiet'].concat(deps).concat(extra || []),
|
|
150
|
-
{ timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
|
|
151
|
-
return { ok: true }
|
|
152
|
-
} catch (e) { return { ok: false, tail: String((e && (e.stderr || e.message)) || e).slice(-300) } }
|
|
153
|
-
}
|
|
154
|
-
let r = await runPip()
|
|
155
|
-
if (!r.ok) r = await runPip(['-i', 'https://pypi.tuna.tsinghua.edu.cn/simple'])
|
|
156
|
-
st.depsOk = !!r.ok
|
|
157
|
-
if (!r.ok) { st.error = '依赖安装失败: ' + (r.tail || ''); st.phase = 'error'; return snapshot() }
|
|
158
|
-
// GPU 偏好写入 embedding-config.json(worker 读 config['gpu'] 选 CUDA/CPU provider;read-modify-write 不覆盖既有键)
|
|
159
|
-
try {
|
|
160
|
-
const cfgDir = path.join(dshHomeOf(), 'memory', 'semantic')
|
|
161
|
-
mkdirSync(cfgDir, { recursive: true })
|
|
162
|
-
const cfgPath = path.join(cfgDir, 'embedding-config.json')
|
|
163
|
-
let cfg = {}
|
|
164
|
-
try { cfg = JSON.parse(await readFile(cfgPath, 'utf8')) } catch (_) {}
|
|
165
|
-
if (!cfg || typeof cfg !== 'object') cfg = {}
|
|
166
|
-
cfg.gpu = wantGpu
|
|
167
|
-
await writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8')
|
|
168
|
-
} catch (eCfg) { diagOf('python-setup: gpu pref write failed: ' + String(eCfg && eCfg.message || eCfg)) }
|
|
169
|
-
diagOf('python-setup: deps installed into venv (gpu=' + wantGpu + ')')
|
|
170
|
-
return snapshot()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** ④模型:BGE-M3 int8 单文件下载(cn→intl 双通道),8MB 分片进度,断点续传(Range)。 */
|
|
174
|
-
async function downloadModel() {
|
|
175
|
-
if (st.phase === 'downloading' || st.phase === 'verifying') return snapshot()
|
|
176
|
-
mkdirSync(modelsDir(), { recursive: true })
|
|
177
|
-
st.cancelled = false
|
|
178
|
-
for (const mirror of MODEL_SPEC.mirrors) {
|
|
179
|
-
if (st.cancelled) break
|
|
180
|
-
const url = mirror.url(MODEL_SPEC.repo + '/resolve/main/' + MODEL_SPEC.file)
|
|
181
|
-
st.phase = 'downloading'; st.dl.mirror = mirror.id; st.dl.startedAt = Date.now()
|
|
182
|
-
try {
|
|
183
|
-
await downloadWithResume(url, modelPath(), (done, total) => {
|
|
184
|
-
st.dl.bytesDone = done; st.dl.bytesTotal = total || MODEL_SPEC.bytes
|
|
185
|
-
const el = (Date.now() - st.dl.startedAt) / 1000
|
|
186
|
-
st.dl.etaSec = done > 0 ? Math.max(0, Math.round(el / done * (st.dl.bytesTotal - done))) : 0
|
|
187
|
-
}, () => st.cancelled)
|
|
188
|
-
st.phase = 'verifying'
|
|
189
|
-
const size = statSync(modelPath()).size
|
|
190
|
-
if (size < 100 * 1024 * 1024) throw new Error('下载不完整(' + size + ' bytes)')
|
|
191
|
-
st.modelReady = true; st.phase = 'ready'
|
|
192
|
-
diagOf('python-setup: model ready at ' + modelPath() + ' (' + size + ' bytes, mirror=' + mirror.id + ')')
|
|
193
|
-
return snapshot()
|
|
194
|
-
} catch (e) {
|
|
195
|
-
if (st.cancelled) { st.phase = 'idle'; return snapshot() }
|
|
196
|
-
st.error = '[' + mirror.id + '] ' + String((e && e.message) || e).slice(0, 160)
|
|
197
|
-
diagOf('python-setup: download failed via ' + mirror.id + ': ' + st.error)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
st.phase = 'error'
|
|
201
|
-
return snapshot()
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** 断点续传下载:已有 .part 则 Range 续传;完成 8MB 粒度回调进度。 */
|
|
205
|
-
function downloadWithResume(url, target, onProgress, isCancelled) {
|
|
206
|
-
return new Promise((resolve, reject) => {
|
|
207
|
-
const part = target + '.part'
|
|
208
|
-
let done = 0
|
|
209
|
-
try { if (existsSync(part)) done = statSync(part).size } catch (_) {}
|
|
210
|
-
const headers = done > 0 ? { Range: 'bytes=' + done + '-' } : {}
|
|
211
|
-
fetch(url, { headers }).then((resp) => {
|
|
212
|
-
if (!resp.ok && resp.status !== 206) { reject(new Error('HTTP ' + resp.status)); return }
|
|
213
|
-
const total = Number(resp.headers.get('content-length') || 0) + done
|
|
214
|
-
const stream = createWriteStream(part, done > 0 ? { flags: 'a' } : { flags: 'w' })
|
|
215
|
-
let lastTick = 0
|
|
216
|
-
resp.body.on('data', (chunk) => {
|
|
217
|
-
done += chunk.length
|
|
218
|
-
const now = Date.now()
|
|
219
|
-
if (now - lastTick > 500) { lastTick = now; onProgress(done, total) }
|
|
220
|
-
})
|
|
221
|
-
resp.body.on('error', (e) => { stream.end(); reject(e) })
|
|
222
|
-
resp.body.on('end', () => {
|
|
223
|
-
stream.end(() => {
|
|
224
|
-
writeFileSyncSafe(part, target).then(resolve).catch(reject)
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
dlAbort = () => { try { resp.body.destroy() } catch (_) {} }
|
|
228
|
-
}).catch(reject)
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function writeFileSyncSafe(from, to) {
|
|
233
|
-
// .part → 正式名(同目录 rename,原子语义)
|
|
234
|
-
await rm(to, { force: true })
|
|
235
|
-
await import('node:fs').then((fs) => fs.renameSync(from, to))
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function cancelDownload() { st.cancelled = true; if (dlAbort) try { dlAbort() } catch (_) {} return snapshot() }
|
|
239
|
-
|
|
240
|
-
/** 汇总:semantic-status 的 pythonSetup 字段(供向导 UI 轮询)。 */
|
|
241
|
-
function status() { return snapshot() }
|
|
242
|
-
|
|
243
|
-
return { detect, ensureVenv, ensureDeps, downloadModel, cancelDownload, status, engineRoot, venvPython, modelPath }
|
|
244
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* M7.6 Python 一键向导(host 半):#16-#20 配套——把 C3 进阶档从"开发机可达"变成"爱好者可达"。
|
|
3
|
+
*
|
|
4
|
+
* 四步链路(全部走本模块,UI 只管展示状态与点击):
|
|
5
|
+
* ①detect — 探测系统 Python(≥3.9)/既有 venv/模型本体;全部只读。
|
|
6
|
+
* ②venv — python -m venv <userDir>/python-engine/.venv(幂等:已存在直接跳过)。
|
|
7
|
+
* ③deps — venv 内 pip 安装运行依赖(transformers/onnxruntime/torch,清华镜像兜底)。
|
|
8
|
+
* ④model — BGE-M3 int8(~539MB)下载到 <userDir>/python-engine/models/,
|
|
9
|
+
* cn(hf-mirror)/intl(hf 官方)双通道+SHA256 校验,复用 JS 档下载器的状态机形态。
|
|
10
|
+
*
|
|
11
|
+
* 设计约束:
|
|
12
|
+
* - 一切落盘在用户目录(~/.dsh/python-engine/),npm 包目录升级会被覆盖,绝不放模型。
|
|
13
|
+
* - 长任务(venv/pip/下载)异步执行,进度写 state 供 semantic-status 轮询;失败可重试。
|
|
14
|
+
* - 零新依赖:下载用全局 fetch,解压用 onnx 单文件直下(无压缩包),venv 用 python -m venv。
|
|
15
|
+
*/
|
|
16
|
+
import { execFile } from 'node:child_process'
|
|
17
|
+
import { promisify } from 'node:util'
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync, statSync, createWriteStream } from 'node:fs'
|
|
22
|
+
import { readFile, writeFile, rm } from 'node:fs/promises'
|
|
23
|
+
|
|
24
|
+
const execFileP = promisify(execFile)
|
|
25
|
+
|
|
26
|
+
/** BGE-M3 int8 单文件(与 bench 夹具同源;hf-mirror 为主源,HF 官方为备源)。 */
|
|
27
|
+
const MODEL_SPEC = {
|
|
28
|
+
repo: 'Xenova/bge-m3-int8',
|
|
29
|
+
file: 'onnx/model_int8.onnx',
|
|
30
|
+
bytes: 568456694,
|
|
31
|
+
sha256: '', // 见 MODEL_SHA256:远端 LFS 校验不可靠时以 size 下限+可执行性兜底;sha256 由首版发布冻结后填入
|
|
32
|
+
mirrors: [
|
|
33
|
+
{ id: 'cn', url: (p) => 'https://hf-mirror.com/' + p },
|
|
34
|
+
{ id: 'intl', url: (p) => 'https://huggingface.co/' + p },
|
|
35
|
+
],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** venv 内 pip 依赖:#19 实测(int8 档 encode_ids 全程 numpy+onnxruntime,tokenizer 走 Rust 快速路径)——基础集无 torch,venv 体积 ~400MB。GPU 推理开关追加 onnxruntime-gpu(CUDAExecutionProvider)。池化/精度契约冻结自 R@5 0.925 基线,fastembed 注册表无 BGE-M3 且池化契约不同,不可用。 */
|
|
39
|
+
const PIP_DEPS_CPU = ['transformers', 'onnxruntime']
|
|
40
|
+
const PIP_DEPS_GPU_EXTRA = ['onnxruntime-gpu']
|
|
41
|
+
|
|
42
|
+
export function createPythonSetupPre(opts = {}) {
|
|
43
|
+
const dshHomeOf = typeof opts.dshHome === 'function' ? opts.dshHome : () => opts.dshHome || path.join(homedir(), '.dsh')
|
|
44
|
+
const diagOf = typeof opts.diag === 'function' ? opts.diag : () => {}
|
|
45
|
+
|
|
46
|
+
// 引擎根目录(用户目录,跨升级存活): ~/.dsh/python-engine/
|
|
47
|
+
const engineRoot = () => path.join(dshHomeOf(), 'python-engine')
|
|
48
|
+
const venvDir = () => path.join(engineRoot(), '.venv')
|
|
49
|
+
const venvPython = () => process.platform === 'win32' ? path.join(venvDir(), 'Scripts', 'python.exe') : path.join(venvDir(), 'bin', 'python')
|
|
50
|
+
const modelsDir = () => path.join(engineRoot(), 'models')
|
|
51
|
+
const modelPath = () => path.join(modelsDir(), 'model_int8.onnx')
|
|
52
|
+
|
|
53
|
+
// 进度状态(内存态,semantic-status 轮询消费;host 重启后 detect 重建)
|
|
54
|
+
const st = {
|
|
55
|
+
phase: 'idle', // idle|detecting|venv|deps|downloading|verifying|ready|error
|
|
56
|
+
error: '',
|
|
57
|
+
pythons: [], // [{label,path,status:'ok'|'missing'|'too-old',version}]
|
|
58
|
+
chosenPython: '',
|
|
59
|
+
venvOk: false,
|
|
60
|
+
depsOk: false,
|
|
61
|
+
dl: { bytesDone: 0, bytesTotal: MODEL_SPEC.bytes, mirror: '', startedAt: 0, etaSec: 0 },
|
|
62
|
+
cancelled: false,
|
|
63
|
+
}
|
|
64
|
+
let dlAbort = null
|
|
65
|
+
|
|
66
|
+
function probe(pyPath, args, timeoutMs) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let done = false
|
|
69
|
+
const t = setTimeout(() => { if (!done) { done = true; resolve({ ok: false, out: '' }) } }, timeoutMs || 8000)
|
|
70
|
+
try {
|
|
71
|
+
execFileP(pyPath, args, { timeout: (timeoutMs || 8000) - 500, windowsHide: true, maxBuffer: 1024 * 1024 })
|
|
72
|
+
.then((r) => { if (!done) { done = true; clearTimeout(t); resolve({ ok: true, out: String(r.stdout || '').trim() }) } })
|
|
73
|
+
.catch(() => { if (!done) { done = true; clearTimeout(t); resolve({ ok: false, out: '' }) } })
|
|
74
|
+
} catch (_) { if (!done) { done = true; clearTimeout(t); resolve({ ok: false, out: '' }) } }
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pyVersionOf(out) {
|
|
79
|
+
const m = String(out || '').match(/(\d+)\.(\d+)/)
|
|
80
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]) } : null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** ①环境探测:venv(推荐)→ 系统 python/python3/py launcher;版本 ≥3.9 且 <3.13(onnxruntime 兼容上界)。 */
|
|
84
|
+
async function detect() {
|
|
85
|
+
st.phase = 'detecting'; st.error = ''
|
|
86
|
+
const dshHome = dshHomeOf()
|
|
87
|
+
const cands = [
|
|
88
|
+
{ path: venvPython(), label: 'DSH Python 引擎 venv(推荐)', isVenv: true },
|
|
89
|
+
{ path: 'python', label: '系统 PATH: python', isVenv: false },
|
|
90
|
+
{ path: 'python3', label: '系统 PATH: python3', isVenv: false },
|
|
91
|
+
{ path: 'py', label: 'Windows py launcher', isVenv: false },
|
|
92
|
+
]
|
|
93
|
+
const out = []
|
|
94
|
+
for (const c of cands) {
|
|
95
|
+
if ((c.path.includes('\\') || c.path.includes('/')) && !existsSync(c.path)) { out.push({ ...c, status: 'missing', version: '' }); continue }
|
|
96
|
+
const r = await probe(c.path, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'])
|
|
97
|
+
const v = pyVersionOf(r.out)
|
|
98
|
+
if (!r.ok || !v) { out.push({ ...c, status: 'missing', version: '' }); continue }
|
|
99
|
+
out.push({ ...c, status: (v.major === 3 && v.minor >= 9 && v.minor <= 12) ? 'ok' : 'too-old', version: v.major + '.' + v.minor })
|
|
100
|
+
}
|
|
101
|
+
// 既有成果快照
|
|
102
|
+
st.venvOk = existsSync(venvPython())
|
|
103
|
+
st.depsOk = st.venvOk ? (await probe(venvPython(), ['-c', 'import transformers, onnxruntime, torch; print("deps-ok")'])).ok : false
|
|
104
|
+
st.modelReady = existsSync(modelPath())
|
|
105
|
+
st.pythons = out
|
|
106
|
+
st.phase = 'idle'
|
|
107
|
+
return snapshot()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function snapshot() {
|
|
111
|
+
return {
|
|
112
|
+
phase: st.phase, error: st.error,
|
|
113
|
+
pythons: st.pythons, chosenPython: st.chosenPython,
|
|
114
|
+
venvOk: st.venvOk, depsOk: st.depsOk, modelReady: st.modelReady, wantGpu: !!st.wantGpu,
|
|
115
|
+
modelPath: modelPath(), venvPython: venvPython(),
|
|
116
|
+
modelBytes: st.modelReady ? statSync(modelPath()).size : 0, modelExpectedBytes: MODEL_SPEC.bytes,
|
|
117
|
+
dl: { ...st.dl },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** ②venv:幂等(已存在跳过);用探测到的首个 ok 系统解释器创建。 */
|
|
122
|
+
async function ensureVenv(pythonPath) {
|
|
123
|
+
const base = String(pythonPath || '').trim() || (st.pythons.find((x) => x.status === 'ok' && !x.isVenv) || {}).path || ''
|
|
124
|
+
if (!base) { st.error = '未检测到可用的系统 Python(需 3.9-3.12)。请先安装 Python。'; st.phase = 'error'; return snapshot() }
|
|
125
|
+
st.chosenPython = base
|
|
126
|
+
if (existsSync(venvPython())) { st.venvOk = true; return snapshot() }
|
|
127
|
+
st.phase = 'venv'
|
|
128
|
+
mkdirSync(engineRoot(), { recursive: true })
|
|
129
|
+
try {
|
|
130
|
+
await execFileP(base, ['-m', 'venv', venvDir()], { timeout: 180000, windowsHide: true })
|
|
131
|
+
st.venvOk = existsSync(venvPython())
|
|
132
|
+
if (!st.venvOk) throw new Error('venv 目录未生成')
|
|
133
|
+
diagOf('python-setup: venv created at ' + venvDir())
|
|
134
|
+
} catch (e) {
|
|
135
|
+
st.error = 'venv 创建失败: ' + String((e && e.message) || e).slice(0, 160); st.phase = 'error'
|
|
136
|
+
}
|
|
137
|
+
return snapshot()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** ③deps:venv 内 pip 装 transformers+onnxruntime(CPU 基础集);opts.gpu=true 追加 onnxruntime-gpu(用户选 GPU 推理时)。官方源失败切清华镜像。 */
|
|
141
|
+
async function ensureDeps(opts2) {
|
|
142
|
+
const wantGpu = !!(opts2 && opts2.gpu)
|
|
143
|
+
if (!st.venvOk) { st.error = 'venv 未就绪,先执行 venv 步骤'; st.phase = 'error'; return snapshot() }
|
|
144
|
+
st.phase = 'deps'
|
|
145
|
+
st.wantGpu = wantGpu
|
|
146
|
+
const deps = PIP_DEPS_CPU.concat(wantGpu ? PIP_DEPS_GPU_EXTRA : [])
|
|
147
|
+
const runPip = async (extra) => {
|
|
148
|
+
try {
|
|
149
|
+
const r = await execFileP(venvPython(), ['-m', 'pip', 'install', '--quiet'].concat(deps).concat(extra || []),
|
|
150
|
+
{ timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
|
|
151
|
+
return { ok: true }
|
|
152
|
+
} catch (e) { return { ok: false, tail: String((e && (e.stderr || e.message)) || e).slice(-300) } }
|
|
153
|
+
}
|
|
154
|
+
let r = await runPip()
|
|
155
|
+
if (!r.ok) r = await runPip(['-i', 'https://pypi.tuna.tsinghua.edu.cn/simple'])
|
|
156
|
+
st.depsOk = !!r.ok
|
|
157
|
+
if (!r.ok) { st.error = '依赖安装失败: ' + (r.tail || ''); st.phase = 'error'; return snapshot() }
|
|
158
|
+
// GPU 偏好写入 embedding-config.json(worker 读 config['gpu'] 选 CUDA/CPU provider;read-modify-write 不覆盖既有键)
|
|
159
|
+
try {
|
|
160
|
+
const cfgDir = path.join(dshHomeOf(), 'memory', 'semantic')
|
|
161
|
+
mkdirSync(cfgDir, { recursive: true })
|
|
162
|
+
const cfgPath = path.join(cfgDir, 'embedding-config.json')
|
|
163
|
+
let cfg = {}
|
|
164
|
+
try { cfg = JSON.parse(await readFile(cfgPath, 'utf8')) } catch (_) {}
|
|
165
|
+
if (!cfg || typeof cfg !== 'object') cfg = {}
|
|
166
|
+
cfg.gpu = wantGpu
|
|
167
|
+
await writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8')
|
|
168
|
+
} catch (eCfg) { diagOf('python-setup: gpu pref write failed: ' + String(eCfg && eCfg.message || eCfg)) }
|
|
169
|
+
diagOf('python-setup: deps installed into venv (gpu=' + wantGpu + ')')
|
|
170
|
+
return snapshot()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** ④模型:BGE-M3 int8 单文件下载(cn→intl 双通道),8MB 分片进度,断点续传(Range)。 */
|
|
174
|
+
async function downloadModel() {
|
|
175
|
+
if (st.phase === 'downloading' || st.phase === 'verifying') return snapshot()
|
|
176
|
+
mkdirSync(modelsDir(), { recursive: true })
|
|
177
|
+
st.cancelled = false
|
|
178
|
+
for (const mirror of MODEL_SPEC.mirrors) {
|
|
179
|
+
if (st.cancelled) break
|
|
180
|
+
const url = mirror.url(MODEL_SPEC.repo + '/resolve/main/' + MODEL_SPEC.file)
|
|
181
|
+
st.phase = 'downloading'; st.dl.mirror = mirror.id; st.dl.startedAt = Date.now()
|
|
182
|
+
try {
|
|
183
|
+
await downloadWithResume(url, modelPath(), (done, total) => {
|
|
184
|
+
st.dl.bytesDone = done; st.dl.bytesTotal = total || MODEL_SPEC.bytes
|
|
185
|
+
const el = (Date.now() - st.dl.startedAt) / 1000
|
|
186
|
+
st.dl.etaSec = done > 0 ? Math.max(0, Math.round(el / done * (st.dl.bytesTotal - done))) : 0
|
|
187
|
+
}, () => st.cancelled)
|
|
188
|
+
st.phase = 'verifying'
|
|
189
|
+
const size = statSync(modelPath()).size
|
|
190
|
+
if (size < 100 * 1024 * 1024) throw new Error('下载不完整(' + size + ' bytes)')
|
|
191
|
+
st.modelReady = true; st.phase = 'ready'
|
|
192
|
+
diagOf('python-setup: model ready at ' + modelPath() + ' (' + size + ' bytes, mirror=' + mirror.id + ')')
|
|
193
|
+
return snapshot()
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (st.cancelled) { st.phase = 'idle'; return snapshot() }
|
|
196
|
+
st.error = '[' + mirror.id + '] ' + String((e && e.message) || e).slice(0, 160)
|
|
197
|
+
diagOf('python-setup: download failed via ' + mirror.id + ': ' + st.error)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
st.phase = 'error'
|
|
201
|
+
return snapshot()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 断点续传下载:已有 .part 则 Range 续传;完成 8MB 粒度回调进度。 */
|
|
205
|
+
function downloadWithResume(url, target, onProgress, isCancelled) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const part = target + '.part'
|
|
208
|
+
let done = 0
|
|
209
|
+
try { if (existsSync(part)) done = statSync(part).size } catch (_) {}
|
|
210
|
+
const headers = done > 0 ? { Range: 'bytes=' + done + '-' } : {}
|
|
211
|
+
fetch(url, { headers }).then((resp) => {
|
|
212
|
+
if (!resp.ok && resp.status !== 206) { reject(new Error('HTTP ' + resp.status)); return }
|
|
213
|
+
const total = Number(resp.headers.get('content-length') || 0) + done
|
|
214
|
+
const stream = createWriteStream(part, done > 0 ? { flags: 'a' } : { flags: 'w' })
|
|
215
|
+
let lastTick = 0
|
|
216
|
+
resp.body.on('data', (chunk) => {
|
|
217
|
+
done += chunk.length
|
|
218
|
+
const now = Date.now()
|
|
219
|
+
if (now - lastTick > 500) { lastTick = now; onProgress(done, total) }
|
|
220
|
+
})
|
|
221
|
+
resp.body.on('error', (e) => { stream.end(); reject(e) })
|
|
222
|
+
resp.body.on('end', () => {
|
|
223
|
+
stream.end(() => {
|
|
224
|
+
writeFileSyncSafe(part, target).then(resolve).catch(reject)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
dlAbort = () => { try { resp.body.destroy() } catch (_) {} }
|
|
228
|
+
}).catch(reject)
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function writeFileSyncSafe(from, to) {
|
|
233
|
+
// .part → 正式名(同目录 rename,原子语义)
|
|
234
|
+
await rm(to, { force: true })
|
|
235
|
+
await import('node:fs').then((fs) => fs.renameSync(from, to))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cancelDownload() { st.cancelled = true; if (dlAbort) try { dlAbort() } catch (_) {} return snapshot() }
|
|
239
|
+
|
|
240
|
+
/** 汇总:semantic-status 的 pythonSetup 字段(供向导 UI 轮询)。 */
|
|
241
|
+
function status() { return snapshot() }
|
|
242
|
+
|
|
243
|
+
return { detect, ensureVenv, ensureDeps, downloadModel, cancelDownload, status, engineRoot, venvPython, modelPath }
|
|
244
|
+
}
|