@a9i5k4/dsh-auto-memory 0.1.29 → 0.1.30
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 +352 -266
- package/README.zh-CN.md +370 -264
- package/cordis.patch.yml +9 -9
- package/lib/activation-host.js +455 -0
- package/lib/activation-inbox-state.js +261 -0
- package/lib/activation-inbox.js +426 -0
- package/lib/client.js +1379 -50
- package/lib/context-bridge.js +619 -0
- package/lib/context-host.js +712 -0
- package/lib/context-sink-python.js +90 -0
- package/lib/episodic-store.js +316 -0
- package/lib/evidence-store.js +272 -0
- package/lib/fact-store.js +418 -0
- package/lib/index-sync.js +160 -0
- package/lib/index.js +2357 -102
- package/lib/intent-clean.js +74 -0
- package/lib/m4-corpus.js +169 -0
- package/lib/m7-index-sync-host.js +194 -0
- package/lib/m7-wire.js +268 -0
- package/lib/memory-anchor.js +451 -0
- package/lib/memory-hub.js +259 -0
- package/lib/memory-index.js +145 -0
- package/lib/memory-writer.js +391 -0
- package/lib/policies/activation_policy_v2.json +88 -0
- package/lib/policies/recall_intent_lr_v1.json +1 -0
- package/lib/procedure-store.js +406 -0
- package/lib/python-sidecar-client.js +326 -0
- package/lib/semantic-decide.js +265 -0
- package/lib/semantic-js.js +381 -0
- package/lib/shadow-host.js +361 -0
- package/lib/shadow-retrieval.js +673 -0
- package/lib/storage-manage.js +203 -0
- package/package.json +2 -2
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C2 内置语义引擎宿主(js_semantic_engine_v1)+ 资产下载器(js_semantic_dl_v1)。
|
|
3
|
+
* 2026-08-26 用户裁定:C2 是默认主路径,本模块把 js-semantic-trial 试验模块接入生产。
|
|
4
|
+
*
|
|
5
|
+
* 组成:
|
|
6
|
+
* 1) fuseD6Pre —— 与 Python sidecar 同一契约的 minmax 加权融合(dense 0.7 / lexical 0.3,D6);
|
|
7
|
+
* 2) createJsSemanticEnginePre —— 懒加载 e5-small q8(peer 多候选路径解析)、miv 键索引缓存
|
|
8
|
+
* (trial 版每次全库重嵌,此处修复)、cosine 排名;任何失败 → degraded + null,调用方回退词法;
|
|
9
|
+
* 3) createSemanticDownloaderPre —— 五文件资产清单(SHA256 冻结)双源下载
|
|
10
|
+
* (cn=hf-mirror 国内 / intl=huggingface 国际;auto=国内优先),流式进度 + 哈希校验 + 原子落位;
|
|
11
|
+
*
|
|
12
|
+
* 边界(与 M7-CLOSED-LOOP-WIRING.md 一致):本模块只做检索排序,绝不做激活决策;
|
|
13
|
+
* 「要不要打断」仍属两车道策略(Python sidecar 在场时)。全部函数对非法输入 fail closed。
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, renameSync, unlinkSync, writeFileSync, rmSync } from 'node:fs'
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
export const JS_SEMANTIC_ENGINE_VERSION = 'js_semantic_engine_v1'
|
|
21
|
+
export const JS_SEMANTIC_DL_VERSION = 'js_semantic_dl_v1'
|
|
22
|
+
/** D6 冻结融合权重(与 worker DEFAULT_SEARCH_POLICY 一致)。 */
|
|
23
|
+
export const D6_FUSION_WEIGHTS_V1 = Object.freeze({ dense: 0.7, lexical: 0.3 })
|
|
24
|
+
|
|
25
|
+
// ========== 1) D6 minmax 融合(纯函数) ==========
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* pairs: [{memoryId, dense:number|null, lex:number|null}]
|
|
29
|
+
* 两臂各自在非空值域上 minmax 归一(单值/零极差 → 该臂非空值记 0.5);缺失记 0。
|
|
30
|
+
* 返回 [{memoryId, fused, denseN, lexN}] 按 fused 降序、平局 memoryId 升序(确定性)。
|
|
31
|
+
*/
|
|
32
|
+
export function fuseD6Pre(pairs) {
|
|
33
|
+
const list = Array.isArray(pairs) ? pairs.filter((p) => p && typeof p.memoryId === 'string') : []
|
|
34
|
+
const normArm = (key) => {
|
|
35
|
+
const vals = list.map((p) => (typeof p[key] === 'number' && Number.isFinite(p[key]) ? p[key] : null)).filter((v) => v !== null)
|
|
36
|
+
if (!vals.length) return { lo: 0, hi: -1 } // 空臂:全部归 0(下方 missing 分支覆盖)
|
|
37
|
+
const lo = Math.min(...vals)
|
|
38
|
+
const hi = Math.max(...vals)
|
|
39
|
+
return hi > lo ? { lo, hi } : { lo, hi: lo, flat: true }
|
|
40
|
+
}
|
|
41
|
+
const dn = normArm('dense')
|
|
42
|
+
const ln = normArm('lex')
|
|
43
|
+
const normOf = (v, arm) => {
|
|
44
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) return 0
|
|
45
|
+
const a = arm === 'dense' ? dn : ln
|
|
46
|
+
if (a.hi < a.lo) return 0
|
|
47
|
+
if (a.flat) return 0.5
|
|
48
|
+
return (v - a.lo) / (a.hi - a.lo)
|
|
49
|
+
}
|
|
50
|
+
return list
|
|
51
|
+
.map((p) => {
|
|
52
|
+
const denseN = normOf(p.dense, 'dense')
|
|
53
|
+
const lexN = normOf(p.lex, 'lex')
|
|
54
|
+
return {
|
|
55
|
+
memoryId: p.memoryId,
|
|
56
|
+
denseN,
|
|
57
|
+
lexN,
|
|
58
|
+
fused: D6_FUSION_WEIGHTS_V1.dense * denseN + D6_FUSION_WEIGHTS_V1.lexical * lexN,
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.sort((x, y) => (y.fused !== x.fused ? y.fused - x.fused : (x.memoryId < y.memoryId ? -1 : 1)))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ========== 2) 引擎宿主 ==========
|
|
65
|
+
|
|
66
|
+
const E5_MODELS_SUBDIR = 'multilingual-e5-small'
|
|
67
|
+
|
|
68
|
+
function defaultModelsDirCandidates(pluginDir) {
|
|
69
|
+
// 发行包(lib/models)优先,其次开发树 artifacts/js-semantic-trial/models
|
|
70
|
+
return [
|
|
71
|
+
path.join(pluginDir, 'models'),
|
|
72
|
+
path.join(pluginDir, '..', 'artifacts', 'm7-live-pre', 'js-semantic-trial', 'models'),
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function defaultPeerDirCandidates(pluginDir) {
|
|
77
|
+
return [
|
|
78
|
+
path.join(pluginDir, 'node_modules', '@huggingface', 'transformers'),
|
|
79
|
+
path.join(pluginDir, '..', 'artifacts', 'm7-live-pre', 'js-semantic-trial', 'node_modules', '@huggingface', 'transformers'),
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function importPeerTransformers(peerDirs) {
|
|
84
|
+
try {
|
|
85
|
+
return await import('@huggingface/transformers') // 发行包:peer 邻接安装
|
|
86
|
+
} catch (_) { /* 开发树:按路径解析 */ }
|
|
87
|
+
for (const dir of peerDirs || []) {
|
|
88
|
+
try {
|
|
89
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
90
|
+
if (!existsSync(pkgPath)) continue
|
|
91
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
92
|
+
const mainFile = String(pkg.main || 'dist/transformers.js')
|
|
93
|
+
const entry = path.join(dir, mainFile)
|
|
94
|
+
if (!existsSync(entry)) continue
|
|
95
|
+
return await import(pathToFileURL(entry).href)
|
|
96
|
+
} catch (_) { /* 尝试下一候选 */ }
|
|
97
|
+
}
|
|
98
|
+
const e = new Error('js-semantic-engine: optional peer @huggingface/transformers not installed; tier stays disabled')
|
|
99
|
+
e.code = 'JS_SEMANTIC_PEER_MISSING'
|
|
100
|
+
throw e
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* opts: { pluginDir, modelsDirCandidates?, peerDirCandidates?, injectEmbedder? }
|
|
105
|
+
* injectEmbedder: 测试注入 {embedQuery(text)→Float32Array, embedPassages(texts)→Float32Array[]};
|
|
106
|
+
* 注入时跳过真实模型加载(冒烟测试离线跑全逻辑)。
|
|
107
|
+
*/
|
|
108
|
+
export function createJsSemanticEnginePre(opts = {}) {
|
|
109
|
+
const pluginDir = opts.pluginDir
|
|
110
|
+
const modelDirCands = opts.modelsDirCandidates || defaultModelsDirCandidates(pluginDir)
|
|
111
|
+
const peerDirCands = opts.peerDirCandidates || defaultPeerDirCandidates(pluginDir)
|
|
112
|
+
let tier = null // {embedQuery, embedPassages, model}
|
|
113
|
+
let tierPromise = null
|
|
114
|
+
let degraded = '' // 非空 = 初始化失败原因(调用方回退词法)
|
|
115
|
+
let lastRankError = '' // 运行期排名失败原因(诊断用;rank 恒返回 null 由调用方回退)
|
|
116
|
+
let idx = { miv: null, entries: [] } // entries [{memoryId, vec}]
|
|
117
|
+
let rebuilding = null // 单飞行重建 promise
|
|
118
|
+
|
|
119
|
+
function modelsDir() {
|
|
120
|
+
return (modelDirCands.find((c) => existsSync(path.join(c, E5_MODELS_SUBDIR))) || modelDirCands[0])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function ensureTier() {
|
|
124
|
+
if (tier) return tier
|
|
125
|
+
if (degraded) throw new Error(degraded)
|
|
126
|
+
if (!tierPromise) {
|
|
127
|
+
tierPromise = (async () => {
|
|
128
|
+
if (opts.injectEmbedder) return opts.injectEmbedder
|
|
129
|
+
const md = modelsDir()
|
|
130
|
+
if (!existsSync(path.join(md, E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'))) {
|
|
131
|
+
throw new Error('js-semantic-engine: model asset missing under ' + md)
|
|
132
|
+
}
|
|
133
|
+
const mod = await importPeerTransformers(peerDirCands)
|
|
134
|
+
const { pipeline, env } = mod
|
|
135
|
+
env.allowRemoteModels = false // 全离线:记忆不出电脑
|
|
136
|
+
env.localModelPath = md
|
|
137
|
+
const extractor = await pipeline('feature-extraction', E5_MODELS_SUBDIR, { dtype: 'q8' })
|
|
138
|
+
const built = {
|
|
139
|
+
model: E5_MODELS_SUBDIR + '/q8',
|
|
140
|
+
async embedQuery(text) {
|
|
141
|
+
const out = await extractor('query: ' + clean(String(text || '')), { pooling: 'mean', normalize: true, truncation: true })
|
|
142
|
+
return Float32Array.from(out.data)
|
|
143
|
+
},
|
|
144
|
+
async embedPassages(texts) {
|
|
145
|
+
const out = []
|
|
146
|
+
for (const t of texts) {
|
|
147
|
+
const o = await extractor('passage: ' + clean(String(t || '')), { pooling: 'mean', normalize: true, truncation: true })
|
|
148
|
+
out.push(Float32Array.from(o.data))
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
// 规范 F3 最小推理自检(RELEASE-SEMANTIC-OPTION.md):加载后立即一次真实编码,
|
|
154
|
+
// 校验维度(384)与非退化输出(模长≈1 且非全零)。失败=资产损坏 → 整体 degraded,
|
|
155
|
+
// 调用方回退 C1,不给下游喂坏向量。
|
|
156
|
+
const probe = await built.embedQuery('semantic self-test 探针')
|
|
157
|
+
if (!(probe.length === 384)) throw new Error('js-semantic-engine: self-test dimension mismatch: ' + probe.length)
|
|
158
|
+
let sum = 0
|
|
159
|
+
for (let i = 0; i < probe.length; i++) sum += probe[i] * probe[i]
|
|
160
|
+
const norm = Math.sqrt(sum)
|
|
161
|
+
if (!(norm > 0.9 && norm < 1.1)) throw new Error('js-semantic-engine: self-test degenerate output (norm=' + norm.toFixed(3) + ')')
|
|
162
|
+
return built
|
|
163
|
+
})()
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
tier = await tierPromise
|
|
167
|
+
return tier
|
|
168
|
+
} catch (e) {
|
|
169
|
+
degraded = String(e && e.message || e).slice(0, 160)
|
|
170
|
+
tierPromise = null
|
|
171
|
+
throw e
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function buildIndexIfStale(miv, records) {
|
|
176
|
+
if (idx.miv === miv) return idx
|
|
177
|
+
if (rebuilding) return rebuilding
|
|
178
|
+
rebuilding = (async () => {
|
|
179
|
+
const t = await ensureTier()
|
|
180
|
+
const seen = new Set()
|
|
181
|
+
const items = []
|
|
182
|
+
for (const r of Array.isArray(records) ? records : []) {
|
|
183
|
+
if (!r || typeof r.memoryId !== 'string' || seen.has(r.memoryId)) continue
|
|
184
|
+
seen.add(r.memoryId)
|
|
185
|
+
items.push({ memoryId: r.memoryId, text: String(r.text || '') })
|
|
186
|
+
}
|
|
187
|
+
const vecs = await t.embedPassages(items.map((i) => i.text.slice(0, 1200)))
|
|
188
|
+
idx = { miv: String(miv || ''), entries: items.map((it, i) => ({ memoryId: it.memoryId, vec: vecs[i] })) }
|
|
189
|
+
return idx
|
|
190
|
+
})()
|
|
191
|
+
try {
|
|
192
|
+
return await rebuilding
|
|
193
|
+
} finally {
|
|
194
|
+
rebuilding = null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
version: JS_SEMANTIC_ENGINE_VERSION,
|
|
200
|
+
/** 探测状态(不触发加载):ready 仅表示「可尝试」,真实可用性以 rank 成功为准。 */
|
|
201
|
+
status() {
|
|
202
|
+
return {
|
|
203
|
+
version: JS_SEMANTIC_ENGINE_VERSION,
|
|
204
|
+
assetPresent: existsSync(path.join(modelsDir(), E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx')),
|
|
205
|
+
ready: !!tier,
|
|
206
|
+
degraded,
|
|
207
|
+
lastRankError,
|
|
208
|
+
model: tier ? tier.model : null,
|
|
209
|
+
indexedRecords: idx.entries.length,
|
|
210
|
+
miv: idx.miv,
|
|
211
|
+
embedding: !!rebuilding,
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* 对语料做稠密排名。返回 {miv, scores:Map(memoryId→cosine)};失败返回 null(调用方回退词法)。
|
|
216
|
+
*/
|
|
217
|
+
async rank(corpusSnap, queryText) {
|
|
218
|
+
try {
|
|
219
|
+
const miv = String((corpusSnap && corpusSnap.memoryIndexVersion) || '')
|
|
220
|
+
if (!miv || !/^idx_[0-9a-f]{32}$/.test(miv)) return null
|
|
221
|
+
const t = await ensureTier()
|
|
222
|
+
const built = await buildIndexIfStale(miv, corpusSnap && corpusSnap.records)
|
|
223
|
+
if (!built.entries.length) return { miv, scores: new Map() }
|
|
224
|
+
const qv = await t.embedQuery(String(queryText || '').slice(0, 2000))
|
|
225
|
+
const scores = new Map()
|
|
226
|
+
for (const en of built.entries) {
|
|
227
|
+
if (!en.vec || en.vec.length !== qv.length) continue
|
|
228
|
+
let d = 0
|
|
229
|
+
for (let i = 0; i < qv.length; i++) d += qv[i] * en.vec[i]
|
|
230
|
+
scores.set(en.memoryId, d)
|
|
231
|
+
}
|
|
232
|
+
return { miv, scores }
|
|
233
|
+
} catch (e) {
|
|
234
|
+
lastRankError = String(e && e.message || e).slice(0, 160)
|
|
235
|
+
return null // fail closed:词法回退由调用方自然发生
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
_resetForTest() { tier = null; tierPromise = null; degraded = ''; idx = { miv: null, entries: [] }; rebuilding = null },
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function clean(t) {
|
|
243
|
+
return String(t || '').replace(/\s+/g, ' ').trim().slice(0, 1200)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ========== 3) 资产下载器 ==========
|
|
247
|
+
|
|
248
|
+
/** 资产清单:SHA256 于 2026-08-26 从已验证开发树副本冻结(Xenova/multilingual-e5-small q8)。 */
|
|
249
|
+
const E5_SMALL_Q8_FILES_V1 = Object.freeze([
|
|
250
|
+
Object.freeze({ rel: 'multilingual-e5-small/onnx/model_quantized.onnx', bytes: 118308185, sha256: 'f80102d3f2a1229f387d3c81909990d8945513e347b0eab049f7de3c6f98c193' }),
|
|
251
|
+
Object.freeze({ rel: 'multilingual-e5-small/tokenizer.json', bytes: 17082730, sha256: '0b44a9d7b51c3c62626640cda0e2c2f70fdacdc25bbbd68038369d14ebdf4c39' }),
|
|
252
|
+
Object.freeze({ rel: 'multilingual-e5-small/config.json', bytes: 658, sha256: 'cb99455288675345e1a4f411438d5d0adbba5fbd3a67ea4fb03c015433b996c1' }),
|
|
253
|
+
Object.freeze({ rel: 'multilingual-e5-small/special_tokens_map.json', bytes: 167, sha256: 'd05497f1da52c5e09554c0cd874037a083e1dc1b9cfd48034d1c717f1afc07a7' }),
|
|
254
|
+
Object.freeze({ rel: 'multilingual-e5-small/tokenizer_config.json', bytes: 443, sha256: 'a1d6bc8734a6f635dc158508bef000f8e2e5a759c7d92f984b2c86e5ff53425b' }),
|
|
255
|
+
])
|
|
256
|
+
export const E5_SMALL_Q8_MANIFEST_V1 = Object.freeze({
|
|
257
|
+
model: E5_MODELS_SUBDIR,
|
|
258
|
+
dtype: 'q8',
|
|
259
|
+
files: E5_SMALL_Q8_FILES_V1,
|
|
260
|
+
totalBytes: E5_SMALL_Q8_FILES_V1.reduce((s, f) => s + f.bytes, 0),
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
/** 双通道:cn=hf-mirror(国内可达)/ intl=huggingface 官方。auto=国内优先(用户群主体)。 */
|
|
264
|
+
export const SEMANTIC_DL_MIRRORS_V1 = Object.freeze({
|
|
265
|
+
cn: 'https://hf-mirror.com/Xenova/multilingual-e5-small/resolve/main/',
|
|
266
|
+
intl: 'https://huggingface.co/Xenova/multilingual-e5-small/resolve/main/',
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* opts: { modelsRoot(下载落位根目录), manifest?, mirrors?, fetchImpl?, chunkBytes? }
|
|
271
|
+
* 状态机: idle → downloading → verifying → done | error | cancelled;单飞行。
|
|
272
|
+
*/
|
|
273
|
+
export function createSemanticDownloaderPre(opts = {}) {
|
|
274
|
+
const modelsRoot = String(opts.modelsRoot || '')
|
|
275
|
+
const manifest = opts.manifest || E5_SMALL_Q8_MANIFEST_V1
|
|
276
|
+
const doFetch = opts.fetchImpl || ((u, o) => fetch(u, o))
|
|
277
|
+
let st = { phase: 'idle', file: '', bytesDone: 0, bytesTotal: manifest.totalBytes, mirrorUsed: '', error: '', startedAt: 0 }
|
|
278
|
+
let cancelFlag = false
|
|
279
|
+
|
|
280
|
+
function setState(patch) { st = Object.assign({}, st, patch) }
|
|
281
|
+
|
|
282
|
+
function tmpDir() { return path.join(modelsRoot, '.tmp-dl') }
|
|
283
|
+
|
|
284
|
+
async function fetchToFile(fileRec, base) {
|
|
285
|
+
const res = await doFetch(base + fileRelUrl(fileRec.rel), { redirect: 'follow' })
|
|
286
|
+
if (!res.ok) throw new Error('http-' + res.status)
|
|
287
|
+
if (!res.body) throw new Error('no-body')
|
|
288
|
+
const total = Number(res.headers.get('content-length')) || fileRec.bytes
|
|
289
|
+
mkdirSync(tmpDir(), { recursive: true })
|
|
290
|
+
const dst = path.join(tmpDir(), path.basename(fileRec.rel))
|
|
291
|
+
const hash = createHash('sha256')
|
|
292
|
+
const reader = res.body.getReader()
|
|
293
|
+
const fd = writeFileSync // 占位避免未用告警;真正写入走手动缓冲
|
|
294
|
+
let buf = Buffer.alloc(0)
|
|
295
|
+
let n = 0
|
|
296
|
+
for (;;) {
|
|
297
|
+
if (cancelFlag) throw new Error('cancelled')
|
|
298
|
+
const { done, value } = await reader.read()
|
|
299
|
+
if (done) break
|
|
300
|
+
buf = Buffer.concat([buf, Buffer.from(value)])
|
|
301
|
+
n += value.byteLength
|
|
302
|
+
hash.update(Buffer.from(value))
|
|
303
|
+
setState({ file: fileRec.rel, bytesDone: st.bytesDoneBase + n, bytesTotal: st.bytesTotalBase + Math.max(0, total - fileRec.bytes) })
|
|
304
|
+
if (buf.length >= (opts.chunkBytes || 1 << 20)) {
|
|
305
|
+
appendChunk(dst, buf); buf = Buffer.alloc(0)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (buf.length) appendChunk(dst, buf)
|
|
309
|
+
void fd
|
|
310
|
+
const hex = hash.digest('hex')
|
|
311
|
+
if (hex !== fileRec.sha256) {
|
|
312
|
+
try { unlinkSync(dst) } catch (_) {}
|
|
313
|
+
const e = new Error('sha256-mismatch:' + hex.slice(0, 12))
|
|
314
|
+
e.code = 'SHA256_MISMATCH'
|
|
315
|
+
throw e
|
|
316
|
+
}
|
|
317
|
+
return { dst, got: n }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function appendChunk(dst, buf) {
|
|
321
|
+
// writeFileSync flag 'a':追加;首块前由调用方确保不存在
|
|
322
|
+
writeFileSync(dst, buf, { flag: 'a' })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function fileRelUrl(rel) { return rel.split('\\').join('/') }
|
|
326
|
+
|
|
327
|
+
async function run(order) {
|
|
328
|
+
setState({ phase: 'downloading', bytesDone: 0, error: '', startedAt: Date.now() })
|
|
329
|
+
rmSync(tmpDir(), { recursive: true, force: true })
|
|
330
|
+
let cum = 0
|
|
331
|
+
for (const f of manifest.files) {
|
|
332
|
+
st.bytesDoneBase = cum
|
|
333
|
+
st.bytesTotalBase = cum + f.bytes
|
|
334
|
+
let ok = false
|
|
335
|
+
let lastErr = ''
|
|
336
|
+
for (const m of order) {
|
|
337
|
+
if (cancelFlag) break
|
|
338
|
+
try {
|
|
339
|
+
setState({ mirrorUsed: m })
|
|
340
|
+
const r = await fetchToFile(f, SEMANTIC_DL_MIRRORS_V1[m])
|
|
341
|
+
const final = path.join(modelsRoot, f.rel)
|
|
342
|
+
mkdirSync(path.dirname(final), { recursive: true })
|
|
343
|
+
try { unlinkSync(final) } catch (_) {}
|
|
344
|
+
renameSync(r.dst, final)
|
|
345
|
+
ok = true
|
|
346
|
+
break
|
|
347
|
+
} catch (e) {
|
|
348
|
+
lastErr = (e && e.code === 'SHA256_MISMATCH' ? e.message : String(e && e.message || e)).slice(0, 140)
|
|
349
|
+
if (e && e.message === 'cancelled') break
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (cancelFlag) { setState({ phase: 'cancelled', error: 'user-cancelled', file: f.rel }); return }
|
|
353
|
+
if (!ok) { setState({ phase: 'error', error: (f.rel + ' ← ' + lastErr) }); return }
|
|
354
|
+
setState({ phase: 'verifying' })
|
|
355
|
+
cum += f.bytes
|
|
356
|
+
setState({ bytesDone: cum, bytesTotalBase: cum, bytesDoneBase: cum })
|
|
357
|
+
}
|
|
358
|
+
rmSync(tmpDir(), { recursive: true, force: true })
|
|
359
|
+
setState({ phase: 'done', mirrorUsed: st.mirrorUsed, error: '' })
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
version: JS_SEMANTIC_DL_VERSION,
|
|
364
|
+
start(mirror) {
|
|
365
|
+
const m = String(mirror || 'auto')
|
|
366
|
+
if (!['auto', 'cn', 'intl'].includes(m)) return { ok: false, reason: 'bad-mirror' }
|
|
367
|
+
if (st.phase === 'downloading' || st.phase === 'verifying') return { ok: false, reason: 'already-running' }
|
|
368
|
+
if (!modelsRoot) return { ok: false, reason: 'no-models-root' }
|
|
369
|
+
cancelFlag = false
|
|
370
|
+
const order = m === 'auto' ? ['cn', 'intl'] : [m]
|
|
371
|
+
void run(order).catch((e) => setState({ phase: 'error', error: String(e && e.message || e).slice(0, 160) }))
|
|
372
|
+
return { ok: true }
|
|
373
|
+
},
|
|
374
|
+
cancel() {
|
|
375
|
+
if (st.phase !== 'downloading' && st.phase !== 'verifying') return { ok: false, reason: 'not-running' }
|
|
376
|
+
cancelFlag = true
|
|
377
|
+
return { ok: true }
|
|
378
|
+
},
|
|
379
|
+
state() { return Object.assign({}, st) },
|
|
380
|
+
}
|
|
381
|
+
}
|