dsh-prime-memory 0.11.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/CHANGELOG.en.md +28 -0
- package/CHANGELOG.ja.md +30 -0
- package/CHANGELOG.ko.md +30 -0
- package/CHANGELOG.md +1220 -0
- package/ENGINEERING-NOTES.md +452 -0
- package/INSTALL.en.md +92 -0
- package/INSTALL.ja.md +92 -0
- package/INSTALL.ko.md +92 -0
- package/INSTALL.md +92 -0
- package/LICENSE +21 -0
- package/README.en.md +458 -0
- package/README.ja.md +306 -0
- package/README.ko.md +306 -0
- package/README.md +425 -0
- package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
- package/assets/changelog/0.8.9/01-panel.png +0 -0
- package/assets/changelog/0.8.9/02-halo.png +0 -0
- package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
- package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
- package/assets/img/EmbeddingSource.png +0 -0
- package/assets/img/Hero.png +0 -0
- package/assets/img/Layers.png +0 -0
- package/assets/img/MemoryTools.png +0 -0
- package/assets/img/Modes.png +0 -0
- package/assets/img/ToolTrajectory.png +0 -0
- package/assets/img/ui-dark.jpg +0 -0
- package/assets/img/ui-light.jpg +0 -0
- package/assets/readme/bench-dialog.svg +70 -0
- package/assets/readme/bench-workflow.svg +79 -0
- package/assets/readme/flow.svg +189 -0
- package/assets/readme/storage.svg +115 -0
- package/cordis.patch.yml +16 -0
- package/dist/bench-control.d.ts +34 -0
- package/dist/bench-control.js +16 -0
- package/dist/client.js +4293 -0
- package/dist/config.d.ts +683 -0
- package/dist/config.js +129 -0
- package/dist/contract.d.ts +820 -0
- package/dist/contract.js +1 -0
- package/dist/embedding-worker.cjs +176 -0
- package/dist/graph/apply.d.ts +37 -0
- package/dist/graph/apply.js +270 -0
- package/dist/graph/constraints.d.ts +47 -0
- package/dist/graph/constraints.js +38 -0
- package/dist/graph/search.d.ts +16 -0
- package/dist/graph/search.js +115 -0
- package/dist/graph/types.d.ts +142 -0
- package/dist/graph/types.js +14 -0
- package/dist/hooks/capture.d.ts +32 -0
- package/dist/hooks/capture.js +194 -0
- package/dist/hooks/recall.d.ts +63 -0
- package/dist/hooks/recall.js +429 -0
- package/dist/index.d.ts +534 -0
- package/dist/index.js +344 -0
- package/dist/llm-usage.d.ts +26 -0
- package/dist/llm-usage.js +37 -0
- package/dist/llm.d.ts +153 -0
- package/dist/llm.js +530 -0
- package/dist/pipeline/graph.d.ts +35 -0
- package/dist/pipeline/graph.js +104 -0
- package/dist/pipeline/l1.d.ts +19 -0
- package/dist/pipeline/l1.js +271 -0
- package/dist/pipeline/l2.d.ts +13 -0
- package/dist/pipeline/l2.js +83 -0
- package/dist/pipeline/l3.d.ts +15 -0
- package/dist/pipeline/l3.js +78 -0
- package/dist/pipeline/rebuild.d.ts +61 -0
- package/dist/pipeline/rebuild.js +307 -0
- package/dist/pipeline/ruminate.d.ts +89 -0
- package/dist/pipeline/ruminate.js +298 -0
- package/dist/pipeline/runner.d.ts +167 -0
- package/dist/pipeline/runner.js +638 -0
- package/dist/pipeline/trigger.d.ts +40 -0
- package/dist/pipeline/trigger.js +75 -0
- package/dist/prompts/graph-projection.d.ts +70 -0
- package/dist/prompts/graph-projection.js +167 -0
- package/dist/prompts/l1-dedup.d.ts +22 -0
- package/dist/prompts/l1-dedup.js +251 -0
- package/dist/prompts/l1-extraction.d.ts +22 -0
- package/dist/prompts/l1-extraction.js +457 -0
- package/dist/prompts/persona.d.ts +23 -0
- package/dist/prompts/persona.js +240 -0
- package/dist/prompts/scene.d.ts +32 -0
- package/dist/prompts/scene.js +414 -0
- package/dist/runtime-package-lock.json +982 -0
- package/dist/settings.d.ts +50 -0
- package/dist/settings.js +355 -0
- package/dist/stats.d.ts +109 -0
- package/dist/stats.js +929 -0
- package/dist/store/bm25.d.ts +19 -0
- package/dist/store/bm25.js +63 -0
- package/dist/store/cost-ledger.d.ts +75 -0
- package/dist/store/cost-ledger.js +171 -0
- package/dist/store/download-queue.d.ts +79 -0
- package/dist/store/download-queue.js +424 -0
- package/dist/store/embedding-source.d.ts +118 -0
- package/dist/store/embedding-source.js +443 -0
- package/dist/store/embedding.d.ts +90 -0
- package/dist/store/embedding.js +206 -0
- package/dist/store/graph-store.d.ts +94 -0
- package/dist/store/graph-store.js +641 -0
- package/dist/store/l0.d.ts +40 -0
- package/dist/store/l0.js +197 -0
- package/dist/store/l1.d.ts +93 -0
- package/dist/store/l1.js +297 -0
- package/dist/store/local-embedding.d.ts +89 -0
- package/dist/store/local-embedding.js +227 -0
- package/dist/store/model-catalog.d.ts +48 -0
- package/dist/store/model-catalog.js +81 -0
- package/dist/store/occupancy.d.ts +30 -0
- package/dist/store/occupancy.js +134 -0
- package/dist/store/pending.d.ts +36 -0
- package/dist/store/pending.js +103 -0
- package/dist/store/persona.d.ts +15 -0
- package/dist/store/persona.js +60 -0
- package/dist/store/recall-dedupe.d.ts +26 -0
- package/dist/store/recall-dedupe.js +138 -0
- package/dist/store/runtime-installer.d.ts +59 -0
- package/dist/store/runtime-installer.js +243 -0
- package/dist/store/scenes.d.ts +24 -0
- package/dist/store/scenes.js +160 -0
- package/dist/store/search-utils.d.ts +38 -0
- package/dist/store/search-utils.js +100 -0
- package/dist/store/session-modes.d.ts +35 -0
- package/dist/store/session-modes.js +144 -0
- package/dist/store/sqlite.d.ts +246 -0
- package/dist/store/sqlite.js +1491 -0
- package/dist/store/state.d.ts +41 -0
- package/dist/store/state.js +72 -0
- package/dist/token-cost.d.ts +23 -0
- package/dist/token-cost.js +185 -0
- package/dist/tools/index.d.ts +34 -0
- package/dist/tools/index.js +758 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +38 -0
- package/dist/util/context-occupancy.d.ts +68 -0
- package/dist/util/context-occupancy.js +92 -0
- package/dist/util/filelog.d.ts +6 -0
- package/dist/util/filelog.js +108 -0
- package/dist/util/io.d.ts +18 -0
- package/dist/util/io.js +97 -0
- package/dist/util/recall-budget.d.ts +31 -0
- package/dist/util/recall-budget.js +84 -0
- package/dist/util/sanitize.d.ts +11 -0
- package/dist/util/sanitize.js +67 -0
- package/dist/util/text.d.ts +16 -0
- package/dist/util/text.js +61 -0
- package/dist/util/tokenizer.d.ts +9 -0
- package/dist/util/tokenizer.js +50 -0
- package/dsh.plugin.json +22 -0
- package/package.json +118 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型下载器:镜像直连 + Range 断点续传 + sha256 校验 + 串行队列。
|
|
3
|
+
*
|
|
4
|
+
* - 进度是用户硬性要求(不能傻等):字节级实时进度(文件 i/N、已收/总量、EMA 速度),
|
|
5
|
+
* 进度对象由 RPC 轮询读取(client 1s 拉一次);
|
|
6
|
+
* - 断点续传:写 .part 旁车文件,重试从断点 Range 续传;服务器不支持 Range(回 200)
|
|
7
|
+
* 则从头重写;取消保留断点;
|
|
8
|
+
* - 完整性:每文件下满后流式哈希整文件比对目录 sha256(续传无法增量哈希,落盘后
|
|
9
|
+
* 单遍校验最简单且正确);失配删文件整体重下;单文件失败自动重试(默认 2 次,
|
|
10
|
+
* 吸收镜像瞬态污染——sha 失配从零重下、网络类错误保留断点续传);
|
|
11
|
+
* - 缓存键绕开:镜像 CDN 存在缓存对象污染窗口——同一时间窗内同一 URL 确定性拿到
|
|
12
|
+
* 错误字节,普通重试会全打同一污染缓存;每次重试追加 `?dshmem-retry=N` 另取对象;
|
|
13
|
+
* - 磁盘门禁:下载前检查数据目录所在卷剩余空间 ≥ 模型体积 × 1.2(statfs 不可用时跳过);
|
|
14
|
+
* - 同一时刻只跑一个下载任务(串行队列),后续请求直接拒绝并说明;
|
|
15
|
+
* - 代理三态:''(默认)= 探测代理环境变量;'none' = 强制直连;其他 = 代理 URL。
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { promises as fs } from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fetch as undiciFetch, ProxyAgent } from 'undici';
|
|
21
|
+
import { catalogById, catalogTotalBytes, MODEL_CATALOG } from './model-catalog.js';
|
|
22
|
+
const DISK_HEADROOM = 1.2;
|
|
23
|
+
export class ModelDownloadQueue {
|
|
24
|
+
dataDir;
|
|
25
|
+
opts;
|
|
26
|
+
progress = null;
|
|
27
|
+
busy = false;
|
|
28
|
+
abort = null;
|
|
29
|
+
/** 代理 dispatcher(按需创建;dispose 关闭连接池)。 */
|
|
30
|
+
agent;
|
|
31
|
+
/** 默认 fetch:undici(可挂代理 dispatcher);测试注入优先。 */
|
|
32
|
+
defaultFetch;
|
|
33
|
+
constructor(dataDir, opts) {
|
|
34
|
+
this.dataDir = dataDir;
|
|
35
|
+
this.opts = opts;
|
|
36
|
+
// 畸形 mirror(无 scheme 等)只跳过代理解析,不炸构造器(下载本身还会报清晰的 URL 错)
|
|
37
|
+
let host = '';
|
|
38
|
+
try {
|
|
39
|
+
host = new URL(this.mirrorUrl()).host;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
const proxy = resolveProxyUrl(opts.proxy, host);
|
|
45
|
+
if (proxy) {
|
|
46
|
+
try {
|
|
47
|
+
this.agent = new ProxyAgent(proxy);
|
|
48
|
+
opts.logger?.info(`[memory] 模型下载走代理 ${maskProxyUrl(proxy)}(镜像直连在国内网络间歇不可达)`);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// 畸形代理(无 scheme 等常见笔误)只降级直连,绝不炸构造器——本构造发生在
|
|
52
|
+
// apply 装配链上,抛错会拖垮宿主启动
|
|
53
|
+
opts.logger?.warn(`[memory] 代理配置无效,已忽略并直连(${maskProxyUrl(proxy)}): ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.defaultFetch = ((u, init) => {
|
|
57
|
+
const dispatch = this.agent;
|
|
58
|
+
// undici fetch 的 init 接受 dispatcher;RequestInit 类型无此字段,断言透传
|
|
59
|
+
return undiciFetch(u, { ...init, ...(dispatch ? { dispatcher: dispatch } : {}) });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** 镜像根(无尾斜杠)。 */
|
|
63
|
+
mirrorUrl() {
|
|
64
|
+
return this.opts.mirror.replace(/\/+$/, '');
|
|
65
|
+
}
|
|
66
|
+
/** 释放代理连接池(插件 dispose 链调用;无代理时幂等无操作)。 */
|
|
67
|
+
dispose() {
|
|
68
|
+
void this.agent?.close().catch(() => { });
|
|
69
|
+
this.agent = undefined;
|
|
70
|
+
}
|
|
71
|
+
/** 当前进度快照(无任务时 null)。 */
|
|
72
|
+
getProgress() {
|
|
73
|
+
return this.progress ? { ...this.progress } : null;
|
|
74
|
+
}
|
|
75
|
+
/** 是否有任务在跑(含校验阶段)。 */
|
|
76
|
+
isBusy() {
|
|
77
|
+
return this.busy;
|
|
78
|
+
}
|
|
79
|
+
modelsDir(id) {
|
|
80
|
+
return path.join(this.dataDir, 'models', id);
|
|
81
|
+
}
|
|
82
|
+
/** 全目录状态扫描(设置页模型卡数据源)。 */
|
|
83
|
+
async listStatus() {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const entry of MODEL_CATALOG) {
|
|
86
|
+
const dir = this.modelsDir(entry.id);
|
|
87
|
+
let bytes = 0;
|
|
88
|
+
let complete = true;
|
|
89
|
+
let anyFile = false;
|
|
90
|
+
for (const f of entry.files) {
|
|
91
|
+
const size = await fileSize(path.join(dir, f.path));
|
|
92
|
+
const partSize = await fileSize(path.join(dir, f.path + '.part'));
|
|
93
|
+
if (size === f.size) {
|
|
94
|
+
bytes += size;
|
|
95
|
+
anyFile = true;
|
|
96
|
+
}
|
|
97
|
+
else if (partSize !== null) {
|
|
98
|
+
bytes += partSize;
|
|
99
|
+
complete = false;
|
|
100
|
+
anyFile = true;
|
|
101
|
+
}
|
|
102
|
+
else if (size !== null) {
|
|
103
|
+
// 尺寸不吻合的残留文件按 partial 记
|
|
104
|
+
bytes += size;
|
|
105
|
+
complete = false;
|
|
106
|
+
anyFile = true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
complete = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
out.push({
|
|
113
|
+
id: entry.id,
|
|
114
|
+
state: complete && anyFile ? 'downloaded' : anyFile ? 'partial' : 'none',
|
|
115
|
+
bytesOnDisk: bytes,
|
|
116
|
+
totalBytes: catalogTotalBytes(entry),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** 单模型是否已完整下载(尺寸口径,不做哈希复验——下载完成时已验过)。 */
|
|
122
|
+
async isDownloaded(id) {
|
|
123
|
+
return (await this.listStatus()).find((s) => s.id === id)?.state === 'downloaded';
|
|
124
|
+
}
|
|
125
|
+
/** 删除已下载模型(切走后释放磁盘;正在使用/下载中的拒绝)。 */
|
|
126
|
+
async deleteModel(id) {
|
|
127
|
+
const entry = catalogById(id);
|
|
128
|
+
if (!entry)
|
|
129
|
+
return { ok: false, error: '未知模型' };
|
|
130
|
+
if (this.busy && this.progress?.modelId === id)
|
|
131
|
+
return { ok: false, error: '该模型正在下载' };
|
|
132
|
+
try {
|
|
133
|
+
await fs.rm(this.modelsDir(id), { recursive: true, force: true });
|
|
134
|
+
return { ok: true };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** 启动下载(串行队列:忙时直接拒绝)。resolve 在任务终态(done/error/cancelled)。 */
|
|
141
|
+
async start(id) {
|
|
142
|
+
const entry = catalogById(id);
|
|
143
|
+
if (!entry)
|
|
144
|
+
throw new Error(`未知模型: ${id}`);
|
|
145
|
+
return this.startEntry(entry);
|
|
146
|
+
}
|
|
147
|
+
/** 按给定目录项启动(测试缝:合成目录项驱动状态机,不触网)。 */
|
|
148
|
+
async startEntry(entry) {
|
|
149
|
+
if (this.busy)
|
|
150
|
+
throw new Error('已有下载任务进行中(串行队列,请等待或取消)');
|
|
151
|
+
this.busy = true;
|
|
152
|
+
this.abort = new AbortController();
|
|
153
|
+
const totalBytes = catalogTotalBytes(entry);
|
|
154
|
+
this.progress = {
|
|
155
|
+
modelId: entry.id,
|
|
156
|
+
phase: 'downloading',
|
|
157
|
+
fileIndex: 0,
|
|
158
|
+
fileCount: entry.files.length,
|
|
159
|
+
fileReceived: 0,
|
|
160
|
+
fileTotal: 0,
|
|
161
|
+
overallReceived: 0,
|
|
162
|
+
overallTotal: totalBytes,
|
|
163
|
+
speedBps: 0,
|
|
164
|
+
startedAt: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
await this.run(entry);
|
|
168
|
+
this.progress.phase = 'done';
|
|
169
|
+
this.opts.logger?.info(`[memory] 模型 ${entry.id} 下载校验完成(${totalBytes} 字节)`);
|
|
170
|
+
return { ...this.progress };
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
const cancelled = this.progress.phase === 'cancelled';
|
|
174
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
175
|
+
if (!cancelled) {
|
|
176
|
+
this.progress.phase = 'error';
|
|
177
|
+
this.progress.error = message;
|
|
178
|
+
this.opts.logger?.warn(`[memory] 模型 ${entry.id} 下载失败: ${message}`);
|
|
179
|
+
}
|
|
180
|
+
return { ...this.progress };
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this.busy = false;
|
|
184
|
+
this.abort = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** 取消当前任务:中断 fetch,保留 .part 断点。 */
|
|
188
|
+
cancel() {
|
|
189
|
+
if (!this.busy || !this.abort)
|
|
190
|
+
return false;
|
|
191
|
+
if (this.progress)
|
|
192
|
+
this.progress.phase = 'cancelled';
|
|
193
|
+
this.abort.abort();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
async run(entry) {
|
|
197
|
+
const prog = this.progress; // startEntry 已置位,本方法存活期内非空
|
|
198
|
+
// 磁盘门禁:剩余空间 ≥ 体积 × 1.2(statfs 不可用则跳过检查)
|
|
199
|
+
const free = await this.freeBytes();
|
|
200
|
+
if (free !== null) {
|
|
201
|
+
const need = Math.ceil(catalogTotalBytes(entry) * DISK_HEADROOM);
|
|
202
|
+
if (free < need) {
|
|
203
|
+
const fmt = (n) => (n >= 1e6 ? `${Math.round(n / 1e6)}MB` : `${Math.max(1, Math.round(n / 1e3))}KB`);
|
|
204
|
+
throw new Error(`磁盘剩余空间不足:需要约 ${fmt(need)}(含 20% 余量),当前 ${fmt(free)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const dir = this.modelsDir(entry.id);
|
|
208
|
+
await fs.mkdir(path.join(dir, 'onnx'), { recursive: true });
|
|
209
|
+
let overall = 0;
|
|
210
|
+
// 之前已完整就位的文件计入整体进度(重试/断点续传场景分母口径一致)
|
|
211
|
+
for (const f of entry.files) {
|
|
212
|
+
if ((await fileSize(path.join(dir, f.path))) === f.size)
|
|
213
|
+
overall += f.size;
|
|
214
|
+
}
|
|
215
|
+
for (let i = 0; i < entry.files.length; i++) {
|
|
216
|
+
if (prog.phase === 'cancelled')
|
|
217
|
+
throw new Error('已取消');
|
|
218
|
+
const f = entry.files[i];
|
|
219
|
+
prog.fileIndex = i + 1;
|
|
220
|
+
prog.fileTotal = f.size;
|
|
221
|
+
const alreadyOk = (await fileSize(path.join(dir, f.path))) === f.size;
|
|
222
|
+
if (alreadyOk) {
|
|
223
|
+
prog.fileReceived = f.size;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
overall += await this.downloadFile(entry, f, dir, (received) => {
|
|
227
|
+
prog.fileReceived = received;
|
|
228
|
+
prog.overallReceived = overall + received;
|
|
229
|
+
});
|
|
230
|
+
prog.overallReceived = overall;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** 下载单文件到最终路径(含续传与校验),返回该文件贡献的字节数。
|
|
234
|
+
* 单文件失败自动重试(默认 2 次)+ 重试换缓存键(见模块头注释):
|
|
235
|
+
* - sha256 失配:downloadFileOnce 已删除断点 → 从零重下;
|
|
236
|
+
* - 数量不吻合/网络错误:断点保留 → Range 续传重试;
|
|
237
|
+
* - 取消:立即上抛不重试。 */
|
|
238
|
+
async downloadFile(entry, f, dir, onBytes) {
|
|
239
|
+
const delays = this.opts.retryDelaysMs ?? [1000, 3000];
|
|
240
|
+
for (let attempt = 0;; attempt++) {
|
|
241
|
+
if (this.progress?.phase === 'cancelled')
|
|
242
|
+
throw new Error('已取消');
|
|
243
|
+
try {
|
|
244
|
+
return await this.downloadFileOnce(entry, f, dir, attempt, onBytes);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
// 断言绕开 CFA 窄化——await 期间 cancel() 可能已改写 phase(同 downloadFileOnce 末段)
|
|
248
|
+
if (this.progress?.phase === 'cancelled')
|
|
249
|
+
throw err;
|
|
250
|
+
if (attempt >= delays.length)
|
|
251
|
+
throw err;
|
|
252
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
253
|
+
this.opts.logger?.warn(`[memory] 文件 ${f.path} 第 ${attempt + 1} 次尝试失败(${msg}),${delays[attempt]}ms 后自动重试(换缓存键)`);
|
|
254
|
+
await new Promise((r) => setTimeout(r, delays[attempt]));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** 单次尝试:续传探测 → fetch(attempt>0 追加缓存键参数)→ 落盘 → 尺寸与 sha256 校验 → rename。 */
|
|
259
|
+
async downloadFileOnce(entry, f, dir, attempt, onBytes) {
|
|
260
|
+
const finalPath = path.join(dir, f.path);
|
|
261
|
+
const partPath = finalPath + '.part';
|
|
262
|
+
const base = this.mirrorUrl();
|
|
263
|
+
// 重试追加缓存键参数:绕开镜像 CDN 的污染缓存对象(同窗口普通重试全打同一对象)
|
|
264
|
+
const cacheBust = attempt > 0 ? `?dshmem-retry=${attempt}` : '';
|
|
265
|
+
const url = `${base}/${entry.repo}/resolve/${entry.revision}/${f.path}${cacheBust}`;
|
|
266
|
+
const fetchImpl = this.opts.fetchImpl ?? this.defaultFetch;
|
|
267
|
+
const prog = this.progress;
|
|
268
|
+
let resumeFrom = 0;
|
|
269
|
+
const partSize = await fileSize(partPath);
|
|
270
|
+
if (partSize !== null && partSize < f.size)
|
|
271
|
+
resumeFrom = partSize;
|
|
272
|
+
else if (partSize === f.size) {
|
|
273
|
+
// 断点已写满但尚未 rename(进程在最后一字节与改名间被杀):直接校验收编,
|
|
274
|
+
// 避免发 bytes=<size>- 吃 416 死循环
|
|
275
|
+
const pre = await sha256File(partPath);
|
|
276
|
+
if (pre === f.sha256) {
|
|
277
|
+
await fs.rename(partPath, finalPath);
|
|
278
|
+
return f.size;
|
|
279
|
+
}
|
|
280
|
+
await fs.rm(partPath, { force: true });
|
|
281
|
+
}
|
|
282
|
+
else if (partSize !== null) {
|
|
283
|
+
await fs.rm(partPath, { force: true });
|
|
284
|
+
}
|
|
285
|
+
const headers = {};
|
|
286
|
+
if (resumeFrom > 0)
|
|
287
|
+
headers.range = `bytes=${resumeFrom}-`;
|
|
288
|
+
let res = await fetchImpl(url, { headers, signal: this.abort?.signal });
|
|
289
|
+
if (res.status === 416 && resumeFrom > 0) {
|
|
290
|
+
// 服务器对已满 Range 回 416:删断点从零重来(一次性)
|
|
291
|
+
await fs.rm(partPath, { force: true });
|
|
292
|
+
resumeFrom = 0;
|
|
293
|
+
delete headers.range;
|
|
294
|
+
res = await fetchImpl(url, { headers, signal: this.abort?.signal });
|
|
295
|
+
}
|
|
296
|
+
if (!res.ok)
|
|
297
|
+
throw new Error(`HTTP ${res.status}(${f.path})`);
|
|
298
|
+
const appending = res.status === 206 && resumeFrom > 0;
|
|
299
|
+
if (!appending)
|
|
300
|
+
resumeFrom = 0;
|
|
301
|
+
// 服务器未给 content-length(chunked)时按目录尺寸兜底展示
|
|
302
|
+
const declared = Number(res.headers.get('content-length') ?? 0);
|
|
303
|
+
const expectBytes = appending ? resumeFrom + declared : declared || f.size;
|
|
304
|
+
const handle = await fs.open(partPath, appending ? 'a' : 'w');
|
|
305
|
+
let received = resumeFrom;
|
|
306
|
+
let lastTick = Date.now();
|
|
307
|
+
let lastBytes = received;
|
|
308
|
+
try {
|
|
309
|
+
if (!res.body)
|
|
310
|
+
throw new Error('响应无 body');
|
|
311
|
+
const reader = res.body.getReader();
|
|
312
|
+
for (;;) {
|
|
313
|
+
const { done, value } = await reader.read();
|
|
314
|
+
if (done)
|
|
315
|
+
break;
|
|
316
|
+
if (prog.phase === 'cancelled')
|
|
317
|
+
throw new Error('已取消');
|
|
318
|
+
await handle.write(value);
|
|
319
|
+
received += value.byteLength;
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
if (now - lastTick > 200) {
|
|
322
|
+
const inst = ((received - lastBytes) / (now - lastTick)) * 1000;
|
|
323
|
+
prog.speedBps = prog.speedBps * 0.6 + inst * 0.4;
|
|
324
|
+
lastTick = now;
|
|
325
|
+
lastBytes = received;
|
|
326
|
+
}
|
|
327
|
+
onBytes(Math.min(received, f.size));
|
|
328
|
+
prog.fileTotal = expectBytes || f.size;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
await handle.close();
|
|
333
|
+
}
|
|
334
|
+
if (received !== f.size) {
|
|
335
|
+
throw new Error(`下载数量不吻合:期望 ${f.size},收到 ${received}(${f.path})`);
|
|
336
|
+
}
|
|
337
|
+
// 校验(含续传):落盘后单遍流式哈希
|
|
338
|
+
prog.phase = 'verifying';
|
|
339
|
+
const sha = await sha256File(partPath);
|
|
340
|
+
if (sha !== f.sha256) {
|
|
341
|
+
await fs.rm(partPath, { force: true });
|
|
342
|
+
throw new Error(`sha256 校验失败(${f.path}),已删除断点,请重试`);
|
|
343
|
+
}
|
|
344
|
+
// 校验期间取消(cancel 把 phase 置 cancelled):不得被下面覆写回 downloading。
|
|
345
|
+
if (prog.phase === 'cancelled')
|
|
346
|
+
throw new Error('已取消');
|
|
347
|
+
prog.phase = 'downloading';
|
|
348
|
+
await fs.rename(partPath, finalPath);
|
|
349
|
+
return f.size;
|
|
350
|
+
}
|
|
351
|
+
async freeBytes() {
|
|
352
|
+
if (this.opts.freeBytes)
|
|
353
|
+
return this.opts.freeBytes();
|
|
354
|
+
try {
|
|
355
|
+
const statfs = (await import('node:fs/promises')).statfs;
|
|
356
|
+
if (typeof statfs !== 'function')
|
|
357
|
+
return null;
|
|
358
|
+
const s = await statfs(this.dataDir);
|
|
359
|
+
return Number(BigInt(s.bavail) * BigInt(s.bsize));
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function fileSize(p) {
|
|
367
|
+
try {
|
|
368
|
+
const s = await fs.stat(p);
|
|
369
|
+
return s.isFile() ? s.size : null;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 解析下载代理(三态):`''`(默认)= 探测代理环境变量(HTTPS_PROXY > ALL_PROXY >
|
|
377
|
+
* HTTP_PROXY,大小写双形态,尊重 NO_PROXY);`'none'` = 禁用代理强制直连;
|
|
378
|
+
* 其他值 = 显式代理 URL。与 curl/npm 同语义。
|
|
379
|
+
*/
|
|
380
|
+
export function resolveProxyUrl(setting, host) {
|
|
381
|
+
const value = (setting ?? '').trim();
|
|
382
|
+
if (value.toLowerCase() === 'none')
|
|
383
|
+
return '';
|
|
384
|
+
if (value)
|
|
385
|
+
return value;
|
|
386
|
+
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? '';
|
|
387
|
+
if (noProxy.trim() === '*')
|
|
388
|
+
return '';
|
|
389
|
+
if (noProxy) {
|
|
390
|
+
for (const raw of noProxy.split(',')) {
|
|
391
|
+
const entry = raw.trim().replace(/^\./, '').toLowerCase();
|
|
392
|
+
if (entry && (host.toLowerCase() === entry || host.toLowerCase().endsWith(`.${entry}`)))
|
|
393
|
+
return '';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const candidates = [
|
|
397
|
+
process.env.HTTPS_PROXY, process.env.https_proxy,
|
|
398
|
+
process.env.ALL_PROXY, process.env.all_proxy,
|
|
399
|
+
process.env.HTTP_PROXY, process.env.http_proxy,
|
|
400
|
+
];
|
|
401
|
+
for (const c of candidates) {
|
|
402
|
+
if (c && c.trim())
|
|
403
|
+
return c.trim();
|
|
404
|
+
}
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
/** 代理 URL 日志脱敏:剥掉 userinfo(内网代理常带 user:pass 凭据),只留 scheme//host。 */
|
|
408
|
+
export function maskProxyUrl(proxy) {
|
|
409
|
+
try {
|
|
410
|
+
const u = new URL(proxy);
|
|
411
|
+
return `${u.protocol}//${u.host}`;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return '<invalid-url>';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function sha256File(p) {
|
|
418
|
+
const { createReadStream } = await import('node:fs');
|
|
419
|
+
const hash = createHash('sha256');
|
|
420
|
+
const stream = createReadStream(p);
|
|
421
|
+
for await (const chunk of stream)
|
|
422
|
+
hash.update(chunk);
|
|
423
|
+
return hash.digest('hex');
|
|
424
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { MemoryConfig } from '../config.js';
|
|
2
|
+
import type { MemoryLogger } from '../types.js';
|
|
3
|
+
import type { EmbeddingProviderInfo, EmbeddingService } from './embedding.js';
|
|
4
|
+
import type { L0Store } from './l0.js';
|
|
5
|
+
import type { L1Store } from './l1.js';
|
|
6
|
+
import { LocalEmbeddingService } from './local-embedding.js';
|
|
7
|
+
import { ModelDownloadQueue } from './download-queue.js';
|
|
8
|
+
import { RuntimeInstaller } from './runtime-installer.js';
|
|
9
|
+
import type { MemoryDb } from './sqlite.js';
|
|
10
|
+
import type { EmbeddingSourceKind, EmbeddingStateView } from '../contract.js';
|
|
11
|
+
export type { ApplyPhase, EmbeddingSourceKind, EmbeddingStateView, ReindexProgressState } from '../contract.js';
|
|
12
|
+
export interface EmbeddingSourceState {
|
|
13
|
+
source: EmbeddingSourceKind;
|
|
14
|
+
/** source=local 时启用的目录模型 id。 */
|
|
15
|
+
activeModel: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare class EmbeddingSourceStore {
|
|
18
|
+
private state;
|
|
19
|
+
private readonly file;
|
|
20
|
+
private writeQueue;
|
|
21
|
+
private readonly logger?;
|
|
22
|
+
constructor(dataDir: string, logger?: MemoryLogger);
|
|
23
|
+
get(): EmbeddingSourceState;
|
|
24
|
+
init(): Promise<void>;
|
|
25
|
+
set(next: EmbeddingSourceState): Promise<void>;
|
|
26
|
+
private persist;
|
|
27
|
+
}
|
|
28
|
+
export interface InitialEmbedding {
|
|
29
|
+
svc: EmbeddingService;
|
|
30
|
+
dims: number;
|
|
31
|
+
/** 传给 db.init 的 providerInfo(触发既有配置比对 → drop → needsReindex 链)。 */
|
|
32
|
+
providerInfo?: EmbeddingProviderInfo;
|
|
33
|
+
/** 解析降级原因(UI 展示)。 */
|
|
34
|
+
note?: string;
|
|
35
|
+
}
|
|
36
|
+
/** 远程档部署上限:baseUrl + model + 维度 + enabled。apiKey 可选(本地免 key 自托管
|
|
37
|
+
* /embeddings 也放行——与蒸馏 direct 通道的 key 可选语义一致)。cfg.embedding 缺失视为未就绪。 */
|
|
38
|
+
export declare function remoteCeiling(cfg: MemoryConfig): boolean;
|
|
39
|
+
export declare function resolveInitialEmbedding(cfg: MemoryConfig, sourceStore: EmbeddingSourceStore, downloader: ModelDownloadQueue, makeLocal: (modelId: string) => LocalEmbeddingService | null, logger?: MemoryLogger): Promise<InitialEmbedding>;
|
|
40
|
+
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。
|
|
41
|
+
* 推理在 worker 线程(见 local-embedding.ts);此处只传 runtime 目录与模型目录。 */
|
|
42
|
+
export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger, maxInputChars?: number): (modelId: string) => LocalEmbeddingService | null;
|
|
43
|
+
export interface EmbeddingManagerDeps {
|
|
44
|
+
dataDir: string;
|
|
45
|
+
cfg: MemoryConfig;
|
|
46
|
+
/** 取当前生效配置(含运行时覆盖:远程嵌入 baseURL/apiKey/model/dimensions 在 UI 可编辑)。
|
|
47
|
+
* 缺省回退 deps.cfg(静态配置)。远程档的 ceiling/换端点在运行期都读它,而非静态 cfg,
|
|
48
|
+
* 否则设置页里的编辑即时生效不到活切换。 */
|
|
49
|
+
getEff?: () => MemoryConfig;
|
|
50
|
+
db: MemoryDb;
|
|
51
|
+
l0: L0Store;
|
|
52
|
+
l1: L1Store;
|
|
53
|
+
sourceStore: EmbeddingSourceStore;
|
|
54
|
+
installer: RuntimeInstaller;
|
|
55
|
+
downloader: ModelDownloadQueue;
|
|
56
|
+
initial: InitialEmbedding;
|
|
57
|
+
logger: MemoryLogger;
|
|
58
|
+
/** 本地服务构造(默认用 makeLocalServiceFactory(installer, downloader)。 */
|
|
59
|
+
makeLocal?: (modelId: string) => LocalEmbeddingService | null;
|
|
60
|
+
}
|
|
61
|
+
export declare class EmbeddingManager {
|
|
62
|
+
readonly sourceStore: EmbeddingSourceStore;
|
|
63
|
+
readonly installer: RuntimeInstaller;
|
|
64
|
+
readonly downloader: ModelDownloadQueue;
|
|
65
|
+
private readonly deps;
|
|
66
|
+
private current;
|
|
67
|
+
private localSvc;
|
|
68
|
+
private applyPhase;
|
|
69
|
+
private applyMessage;
|
|
70
|
+
private applyStartedAt;
|
|
71
|
+
private applyBusy;
|
|
72
|
+
private reindex;
|
|
73
|
+
private reindexCancel;
|
|
74
|
+
/** 停机标志:dispose 后应用链不再推进(防卸载后的孤儿重嵌/安装)。 */
|
|
75
|
+
private disposedFlag;
|
|
76
|
+
/** 当前生效目标的 providerInfo(backfill/启动链的 meta 写入用——杜绝陈旧闭包)。 */
|
|
77
|
+
private currentInfo;
|
|
78
|
+
/** 初始解析的降级说明(活切换成功后清空,防过期提示常驻)。 */
|
|
79
|
+
private activeNote;
|
|
80
|
+
constructor(deps: EmbeddingManagerDeps);
|
|
81
|
+
/** 当前目标的 providerInfo(index.ts 的启动重嵌链/周期 backfill 写 meta 用)。 */
|
|
82
|
+
currentProviderInfo(): EmbeddingProviderInfo | undefined;
|
|
83
|
+
/** 当前生效配置(运行时覆盖优先;缺省回退静态 deps.cfg)。远程档 ceiling/换端点都读它。 */
|
|
84
|
+
private eff;
|
|
85
|
+
/** 取消运行时安装(RPC:npm 卡死/用户主动放弃)。 */
|
|
86
|
+
cancelRuntimeInstall(): boolean;
|
|
87
|
+
/** 当前生效服务(index.ts 初始建 store 用)。 */
|
|
88
|
+
getService(): EmbeddingService;
|
|
89
|
+
/** 构造绑定真实运行时 loader 的本地服务(deps.makeLocal 可注入,测试替换)。 */
|
|
90
|
+
private makeLocalService;
|
|
91
|
+
/** 活切换请求:验证通过即接受,后台执行应用链(进度轮询可见)。 */
|
|
92
|
+
requestSource(next: {
|
|
93
|
+
source: EmbeddingSourceKind;
|
|
94
|
+
activeModel?: string | null;
|
|
95
|
+
}): {
|
|
96
|
+
accepted: boolean;
|
|
97
|
+
error?: string;
|
|
98
|
+
};
|
|
99
|
+
/** 下载启动(串行队列忙时拒绝);完成后自动做一次可加载性预热验证。 */
|
|
100
|
+
startDownload(modelId: string): {
|
|
101
|
+
ok: boolean;
|
|
102
|
+
error?: string;
|
|
103
|
+
};
|
|
104
|
+
cancelDownload(): boolean;
|
|
105
|
+
deleteModel(modelId: string): Promise<{
|
|
106
|
+
ok: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
}>;
|
|
109
|
+
cancelReindex(): boolean;
|
|
110
|
+
/** 应用链/后台任务是否在跑(backfill 并发门禁用)。 */
|
|
111
|
+
isBusy(): boolean;
|
|
112
|
+
/** 停机钩子(插件 dispose):取消 npm 安装、下载与重嵌——不留后台孤儿任务。 */
|
|
113
|
+
dispose(): void;
|
|
114
|
+
private applyChain;
|
|
115
|
+
private reindexNow;
|
|
116
|
+
/** RPC 快照(设置页嵌入区块数据源;client 忙时 1s 轮询)。 */
|
|
117
|
+
snapshot(): Promise<EmbeddingStateView>;
|
|
118
|
+
}
|