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,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 嵌入源状态层 + 活切换管理器。
|
|
3
|
+
*
|
|
4
|
+
* - 状态文件 embedding-source.json(写穿持久化:内存态 + tmp/rename 原子写 +
|
|
5
|
+
* 写队列串行化);无文件 = remote(与历史行为完全一致,老用户无感);
|
|
6
|
+
* - 生效 = 部署上限 AND 运行时选择(仓库铁律):远程档要求静态四件套配齐
|
|
7
|
+
* (运行时覆盖字段由 effectiveCfg 注入后同样计入),本地档受
|
|
8
|
+
* embedding.allowLocalModels 上限约束;
|
|
9
|
+
* - 活切换链(后台执行,RPC 立即返回 accepted,进度靠轮询):
|
|
10
|
+
* 安装运行时(首次本地)→ 预热加载 → 换服务 + swapProvider(维度变化 drop 向量表)
|
|
11
|
+
* → 后台全量重嵌(L1/L0 计数进度,可取消)→ 持久化状态。
|
|
12
|
+
* 失败语义:异常(安装失败/模型加载失败/db 拒绝)→ 状态不持久化,重启回到旧源;
|
|
13
|
+
* 重嵌取消/部分失败 → 已切换(物理表即新维度,meta 已同步),缺失向量由周期
|
|
14
|
+
* backfill 补齐——不回滚(回滚需要再 drop 一次表,得不偿失)。
|
|
15
|
+
*/
|
|
16
|
+
import { promises as fs } from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { NoopEmbeddingService, RemoteEmbeddingService } from './embedding.js';
|
|
19
|
+
import { catalogById, MODEL_CATALOG } from './model-catalog.js';
|
|
20
|
+
import { LocalEmbeddingService } from './local-embedding.js';
|
|
21
|
+
// ── 状态存储(写穿持久化) ──
|
|
22
|
+
export class EmbeddingSourceStore {
|
|
23
|
+
state = { source: 'remote', activeModel: null };
|
|
24
|
+
file;
|
|
25
|
+
writeQueue = Promise.resolve();
|
|
26
|
+
logger;
|
|
27
|
+
constructor(dataDir, logger) {
|
|
28
|
+
this.file = path.join(dataDir, 'embedding-source.json');
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
get() {
|
|
32
|
+
return { ...this.state };
|
|
33
|
+
}
|
|
34
|
+
async init() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await fs.readFile(this.file, 'utf8');
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if ((parsed.source === 'remote' || parsed.source === 'local' || parsed.source === 'off') &&
|
|
39
|
+
(parsed.activeModel === null || typeof parsed.activeModel === 'string')) {
|
|
40
|
+
this.state = { source: parsed.source, activeModel: parsed.activeModel };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.logger?.warn('[memory] 嵌入源状态文件损坏,按默认 remote 起步');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// 无文件 = 历史行为(跟随部署配置的远程嵌入)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async set(next) {
|
|
51
|
+
this.state = { source: next.source, activeModel: next.activeModel };
|
|
52
|
+
this.writeQueue = this.writeQueue.then(() => this.persist()).catch(() => { });
|
|
53
|
+
await this.writeQueue;
|
|
54
|
+
}
|
|
55
|
+
async persist() {
|
|
56
|
+
const tmp = this.file + '.tmp';
|
|
57
|
+
await fs.writeFile(tmp, JSON.stringify(this.state, null, 2), 'utf8');
|
|
58
|
+
await fs.rename(tmp, this.file);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** 远程档部署上限:baseUrl + model + 维度 + enabled。apiKey 可选(本地免 key 自托管
|
|
62
|
+
* /embeddings 也放行——与蒸馏 direct 通道的 key 可选语义一致)。cfg.embedding 缺失视为未就绪。 */
|
|
63
|
+
export function remoteCeiling(cfg) {
|
|
64
|
+
const e = cfg.embedding;
|
|
65
|
+
if (!e)
|
|
66
|
+
return false;
|
|
67
|
+
return e.enabled && !!e.baseUrl && !!e.model && e.dimensions > 0;
|
|
68
|
+
}
|
|
69
|
+
export async function resolveInitialEmbedding(cfg, sourceStore, downloader, makeLocal, logger) {
|
|
70
|
+
const state = sourceStore.get();
|
|
71
|
+
if (state.source === 'off') {
|
|
72
|
+
return { svc: new NoopEmbeddingService(), dims: 0 };
|
|
73
|
+
}
|
|
74
|
+
if (state.source === 'local') {
|
|
75
|
+
if (!cfg.embedding.allowLocalModels) {
|
|
76
|
+
logger?.warn('[memory] 嵌入源为 local 但部署已禁用本地模型(allowLocalModels=false),本次运行纯 FTS');
|
|
77
|
+
return { svc: new NoopEmbeddingService(), dims: 0, note: '部署配置已禁用本地嵌入模型' };
|
|
78
|
+
}
|
|
79
|
+
const entry = state.activeModel ? catalogById(state.activeModel) : undefined;
|
|
80
|
+
if (!entry) {
|
|
81
|
+
logger?.warn(`[memory] 嵌入源 local 的模型 ${state.activeModel} 不在目录,本次运行纯 FTS`);
|
|
82
|
+
return { svc: new NoopEmbeddingService(), dims: 0, note: '启用的模型不在内置目录' };
|
|
83
|
+
}
|
|
84
|
+
if (!(await downloader.isDownloaded(entry.id))) {
|
|
85
|
+
logger?.warn(`[memory] 本地模型 ${entry.id} 文件缺失(可能被清理),本次运行纯 FTS`);
|
|
86
|
+
return { svc: new NoopEmbeddingService(), dims: 0, note: '模型文件缺失,请重新下载' };
|
|
87
|
+
}
|
|
88
|
+
const svc = makeLocal(entry.id);
|
|
89
|
+
if (!svc)
|
|
90
|
+
return { svc: new NoopEmbeddingService(), dims: 0, note: '本地服务构造失败' };
|
|
91
|
+
return { svc, dims: entry.dims, providerInfo: { provider: 'local', model: entry.id, dimensions: entry.dims } };
|
|
92
|
+
}
|
|
93
|
+
// remote(默认)
|
|
94
|
+
if (!remoteCeiling(cfg)) {
|
|
95
|
+
return { svc: new NoopEmbeddingService(), dims: 0 };
|
|
96
|
+
}
|
|
97
|
+
const svc = new RemoteEmbeddingService({
|
|
98
|
+
baseUrl: cfg.embedding.baseUrl,
|
|
99
|
+
apiKey: cfg.embedding.apiKey,
|
|
100
|
+
model: cfg.embedding.model,
|
|
101
|
+
dimensions: cfg.embedding.dimensions,
|
|
102
|
+
maxInputChars: cfg.embedding.maxInputChars,
|
|
103
|
+
timeoutMs: cfg.embedding.timeoutMs,
|
|
104
|
+
logger,
|
|
105
|
+
});
|
|
106
|
+
return { svc, dims: cfg.embedding.dimensions, providerInfo: svc.getProviderInfo() };
|
|
107
|
+
}
|
|
108
|
+
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。
|
|
109
|
+
* 推理在 worker 线程(见 local-embedding.ts);此处只传 runtime 目录与模型目录。 */
|
|
110
|
+
export function makeLocalServiceFactory(installer, downloader, logger, maxInputChars) {
|
|
111
|
+
return (modelId) => {
|
|
112
|
+
const entry = catalogById(modelId);
|
|
113
|
+
if (!entry)
|
|
114
|
+
return null;
|
|
115
|
+
return new LocalEmbeddingService(entry, downloader.modelsDir(entry.id), {
|
|
116
|
+
runtimeDir: installer.runtimeDir,
|
|
117
|
+
logger,
|
|
118
|
+
maxInputChars,
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export class EmbeddingManager {
|
|
123
|
+
sourceStore;
|
|
124
|
+
installer;
|
|
125
|
+
downloader;
|
|
126
|
+
deps;
|
|
127
|
+
current;
|
|
128
|
+
localSvc = null;
|
|
129
|
+
applyPhase = 'idle';
|
|
130
|
+
applyMessage = '';
|
|
131
|
+
applyStartedAt = 0;
|
|
132
|
+
applyBusy = false;
|
|
133
|
+
reindex = {
|
|
134
|
+
running: false,
|
|
135
|
+
l1Done: 0,
|
|
136
|
+
l1Total: 0,
|
|
137
|
+
l0Done: 0,
|
|
138
|
+
l0Total: 0,
|
|
139
|
+
startedAt: 0,
|
|
140
|
+
cancelled: false,
|
|
141
|
+
};
|
|
142
|
+
reindexCancel = false;
|
|
143
|
+
/** 停机标志:dispose 后应用链不再推进(防卸载后的孤儿重嵌/安装)。 */
|
|
144
|
+
disposedFlag = false;
|
|
145
|
+
/** 当前生效目标的 providerInfo(backfill/启动链的 meta 写入用——杜绝陈旧闭包)。 */
|
|
146
|
+
currentInfo;
|
|
147
|
+
/** 初始解析的降级说明(活切换成功后清空,防过期提示常驻)。 */
|
|
148
|
+
activeNote;
|
|
149
|
+
constructor(deps) {
|
|
150
|
+
this.deps = deps;
|
|
151
|
+
this.sourceStore = deps.sourceStore;
|
|
152
|
+
this.installer = deps.installer;
|
|
153
|
+
this.downloader = deps.downloader;
|
|
154
|
+
this.current = deps.initial.svc;
|
|
155
|
+
this.currentInfo = deps.initial.providerInfo;
|
|
156
|
+
this.activeNote = deps.initial.note;
|
|
157
|
+
// 启动即本地档:initial.svc 已是绑定真实运行时 loader 的 LocalEmbeddingService。
|
|
158
|
+
// 立即后台预热(不阻塞启动)——否则 L1/L0.reindex 与 EmbedHelper.batch 的
|
|
159
|
+
// vectorReady 短路都不会触发懒加载,缺失向量要等到首次召回才补
|
|
160
|
+
if (deps.initial.providerInfo?.provider === 'local') {
|
|
161
|
+
this.localSvc = this.current;
|
|
162
|
+
this.localSvc.startWarmup();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** 当前目标的 providerInfo(index.ts 的启动重嵌链/周期 backfill 写 meta 用)。 */
|
|
166
|
+
currentProviderInfo() {
|
|
167
|
+
return this.currentInfo;
|
|
168
|
+
}
|
|
169
|
+
/** 当前生效配置(运行时覆盖优先;缺省回退静态 deps.cfg)。远程档 ceiling/换端点都读它。 */
|
|
170
|
+
eff() {
|
|
171
|
+
return this.deps.getEff ? this.deps.getEff() : this.deps.cfg;
|
|
172
|
+
}
|
|
173
|
+
/** 取消运行时安装(RPC:npm 卡死/用户主动放弃)。 */
|
|
174
|
+
cancelRuntimeInstall() {
|
|
175
|
+
return this.installer.cancel();
|
|
176
|
+
}
|
|
177
|
+
/** 当前生效服务(index.ts 初始建 store 用)。 */
|
|
178
|
+
getService() {
|
|
179
|
+
return this.current;
|
|
180
|
+
}
|
|
181
|
+
/** 构造绑定真实运行时 loader 的本地服务(deps.makeLocal 可注入,测试替换)。 */
|
|
182
|
+
makeLocalService(modelId) {
|
|
183
|
+
const factory = this.deps.makeLocal ?? makeLocalServiceFactory(this.installer, this.downloader, this.deps.logger, this.deps.cfg.embedding.maxInputChars);
|
|
184
|
+
return factory(modelId);
|
|
185
|
+
}
|
|
186
|
+
/** 活切换请求:验证通过即接受,后台执行应用链(进度轮询可见)。 */
|
|
187
|
+
requestSource(next) {
|
|
188
|
+
if (this.applyBusy)
|
|
189
|
+
return { accepted: false, error: '切换进行中,请等待完成' };
|
|
190
|
+
if (next.source === 'remote' && !remoteCeiling(this.eff())) {
|
|
191
|
+
return { accepted: false, error: '未配置远程嵌入(baseUrl/model/dimensions,可在设置页填写),远程档不可选' };
|
|
192
|
+
}
|
|
193
|
+
if (next.source === 'local') {
|
|
194
|
+
if (!this.deps.cfg.embedding.allowLocalModels) {
|
|
195
|
+
return { accepted: false, error: '部署已禁用本地嵌入模型(allowLocalModels=false)' };
|
|
196
|
+
}
|
|
197
|
+
if (!next.activeModel || !catalogById(next.activeModel)) {
|
|
198
|
+
return { accepted: false, error: '请选择内置目录中的模型' };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const state = {
|
|
202
|
+
source: next.source,
|
|
203
|
+
activeModel: next.source === 'local' ? next.activeModel : null,
|
|
204
|
+
};
|
|
205
|
+
this.applyBusy = true;
|
|
206
|
+
this.applyStartedAt = Date.now();
|
|
207
|
+
this.applyMessage = '';
|
|
208
|
+
void this.applyChain(state).finally(() => {
|
|
209
|
+
this.applyBusy = false;
|
|
210
|
+
});
|
|
211
|
+
return { accepted: true };
|
|
212
|
+
}
|
|
213
|
+
/** 下载启动(串行队列忙时拒绝);完成后自动做一次可加载性预热验证。 */
|
|
214
|
+
startDownload(modelId) {
|
|
215
|
+
if (!this.deps.cfg.embedding.allowLocalModels) {
|
|
216
|
+
return { ok: false, error: '部署已禁用本地嵌入模型' };
|
|
217
|
+
}
|
|
218
|
+
if (!catalogById(modelId))
|
|
219
|
+
return { ok: false, error: '未知模型' };
|
|
220
|
+
if (this.downloader.isBusy())
|
|
221
|
+
return { ok: false, error: '已有下载任务进行中' };
|
|
222
|
+
void this.downloader
|
|
223
|
+
.start(modelId)
|
|
224
|
+
.then(async (p) => {
|
|
225
|
+
if (p.phase !== 'done')
|
|
226
|
+
return;
|
|
227
|
+
// 下载完成自动预热验证:当前启用模型 → 直接 warmup;未启用的临时模型验完即释放
|
|
228
|
+
if (this.sourceStore.get().source === 'local' && this.sourceStore.get().activeModel === modelId) {
|
|
229
|
+
this.localSvc?.startWarmup();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const scratch = this.makeLocalService(modelId);
|
|
233
|
+
if (!scratch)
|
|
234
|
+
return;
|
|
235
|
+
if (await this.installer.isReady()) {
|
|
236
|
+
// 预热只为验证可加载性:完成后必须释放(bge-m3 ~550MB,不关就常驻泄漏,
|
|
237
|
+
// 且 onnxruntime 持文件句柄会卡住该模型的删除)
|
|
238
|
+
scratch.startWarmup();
|
|
239
|
+
void scratch.waitForReady().then(() => scratch.close(), () => scratch.close());
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
.catch(() => {
|
|
243
|
+
/* 失败态在 downloader 进度里 */
|
|
244
|
+
});
|
|
245
|
+
return { ok: true };
|
|
246
|
+
}
|
|
247
|
+
cancelDownload() {
|
|
248
|
+
return this.downloader.cancel();
|
|
249
|
+
}
|
|
250
|
+
async deleteModel(modelId) {
|
|
251
|
+
const state = this.sourceStore.get();
|
|
252
|
+
if (state.source === 'local' && state.activeModel === modelId) {
|
|
253
|
+
return { ok: false, error: '该模型正在使用中,请先切换嵌入源' };
|
|
254
|
+
}
|
|
255
|
+
return this.downloader.deleteModel(modelId);
|
|
256
|
+
}
|
|
257
|
+
cancelReindex() {
|
|
258
|
+
if (!this.reindex.running)
|
|
259
|
+
return false;
|
|
260
|
+
this.reindexCancel = true;
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
/** 应用链/后台任务是否在跑(backfill 并发门禁用)。 */
|
|
264
|
+
isBusy() {
|
|
265
|
+
return this.applyBusy || this.reindex.running || this.downloader.isBusy();
|
|
266
|
+
}
|
|
267
|
+
/** 停机钩子(插件 dispose):取消 npm 安装、下载与重嵌——不留后台孤儿任务。 */
|
|
268
|
+
dispose() {
|
|
269
|
+
this.disposedFlag = true;
|
|
270
|
+
this.installer.cancel();
|
|
271
|
+
this.downloader.cancel();
|
|
272
|
+
this.cancelReindex();
|
|
273
|
+
this.localSvc?.close();
|
|
274
|
+
}
|
|
275
|
+
async applyChain(next) {
|
|
276
|
+
try {
|
|
277
|
+
let svc;
|
|
278
|
+
let providerInfo;
|
|
279
|
+
if (next.source === 'off') {
|
|
280
|
+
this.applyPhase = 'switching';
|
|
281
|
+
svc = new NoopEmbeddingService();
|
|
282
|
+
providerInfo = undefined;
|
|
283
|
+
this.currentInfo = undefined;
|
|
284
|
+
}
|
|
285
|
+
else if (next.source === 'remote') {
|
|
286
|
+
this.applyPhase = 'switching';
|
|
287
|
+
const re = this.eff().embedding;
|
|
288
|
+
svc = new RemoteEmbeddingService({
|
|
289
|
+
baseUrl: re.baseUrl,
|
|
290
|
+
apiKey: re.apiKey,
|
|
291
|
+
model: re.model,
|
|
292
|
+
dimensions: re.dimensions,
|
|
293
|
+
maxInputChars: re.maxInputChars,
|
|
294
|
+
timeoutMs: re.timeoutMs,
|
|
295
|
+
logger: this.deps.logger,
|
|
296
|
+
});
|
|
297
|
+
providerInfo = svc.getProviderInfo();
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// local:下载完整性前置校验 → 运行时 → 预热 → 切换
|
|
301
|
+
if (!(await this.downloader.isDownloaded(next.activeModel))) {
|
|
302
|
+
throw new Error('模型文件不完整(未下载或已损坏),请先完成下载');
|
|
303
|
+
}
|
|
304
|
+
if (!(await this.installer.isReady())) {
|
|
305
|
+
this.applyPhase = 'installing-runtime';
|
|
306
|
+
const ok = await this.installer.ensure();
|
|
307
|
+
if (!ok) {
|
|
308
|
+
throw new Error(`运行时安装失败: ${this.installer.getProgress().error ?? '未知原因'}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (this.disposedFlag)
|
|
312
|
+
throw new Error('插件已卸载,切换中止');
|
|
313
|
+
this.applyPhase = 'warming';
|
|
314
|
+
const local = this.makeLocalService(next.activeModel);
|
|
315
|
+
if (!local)
|
|
316
|
+
throw new Error('模型不在目录');
|
|
317
|
+
await local.waitForReady();
|
|
318
|
+
svc = local;
|
|
319
|
+
providerInfo = local.getProviderInfo();
|
|
320
|
+
this.applyPhase = 'switching';
|
|
321
|
+
}
|
|
322
|
+
// 换服务 + 换表(providerInfo 变化 → drop 向量表按新维度重建)
|
|
323
|
+
let needsReindex = false;
|
|
324
|
+
if (providerInfo) {
|
|
325
|
+
const swap = this.deps.db.swapProvider(providerInfo);
|
|
326
|
+
if (!swap.ok)
|
|
327
|
+
throw new Error(swap.error ?? '切换向量引擎失败');
|
|
328
|
+
needsReindex = swap.needsReindex;
|
|
329
|
+
// 物理表在 swap 成功那一刻已是新维度——meta 立即跟上(即使后续重嵌被取消/
|
|
330
|
+
// 部分失败):meta 的语义是"物理表现状",缺失行由 backfill 按 missing 计数补,
|
|
331
|
+
// 不依赖 meta。拖着不写会把"meta=旧 provider"留给下次比对埋雷。
|
|
332
|
+
this.deps.db.markEmbeddingSynced(providerInfo);
|
|
333
|
+
this.currentInfo = providerInfo;
|
|
334
|
+
}
|
|
335
|
+
const oldLocal = this.localSvc;
|
|
336
|
+
this.localSvc = next.source === 'local' ? svc : null;
|
|
337
|
+
this.current = svc;
|
|
338
|
+
this.deps.l0.setEmbeddingService(svc);
|
|
339
|
+
this.deps.l1.setEmbeddingService(svc);
|
|
340
|
+
oldLocal?.close();
|
|
341
|
+
let pendingNote = '';
|
|
342
|
+
if (needsReindex) {
|
|
343
|
+
if (this.disposedFlag)
|
|
344
|
+
throw new Error('插件已卸载,重嵌入中止');
|
|
345
|
+
this.applyPhase = 'reindexing';
|
|
346
|
+
const result = await this.reindexNow();
|
|
347
|
+
if (result.cancelled) {
|
|
348
|
+
pendingNote = ';重嵌入已取消,缺失向量由周期任务补齐';
|
|
349
|
+
this.deps.logger.warn('[memory] 重嵌入已取消,缺失向量将由周期补齐(检索暂按关键词降级)');
|
|
350
|
+
}
|
|
351
|
+
else if (result.failedTotal > 0) {
|
|
352
|
+
pendingNote = `;${result.failedTotal} 条向量待补齐(周期任务会补)`;
|
|
353
|
+
}
|
|
354
|
+
if (result.error)
|
|
355
|
+
throw new Error(result.error);
|
|
356
|
+
}
|
|
357
|
+
await this.sourceStore.set(next);
|
|
358
|
+
this.activeNote = undefined;
|
|
359
|
+
this.applyPhase = 'done';
|
|
360
|
+
this.applyMessage =
|
|
361
|
+
next.source === 'off'
|
|
362
|
+
? '已切换为关键词检索'
|
|
363
|
+
: '切换完成' + pendingNote;
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
this.applyPhase = 'error';
|
|
367
|
+
this.applyMessage = err instanceof Error ? err.message : String(err);
|
|
368
|
+
this.deps.logger.warn(`[memory] 嵌入源切换失败(状态保持不变): ${this.applyMessage}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async reindexNow() {
|
|
372
|
+
this.reindexCancel = false;
|
|
373
|
+
this.reindex = { running: true, l1Done: 0, l1Total: 0, l0Done: 0, l0Total: 0, startedAt: Date.now(), cancelled: false };
|
|
374
|
+
let failedTotal = 0;
|
|
375
|
+
try {
|
|
376
|
+
const r1 = await this.deps.l1.reindex({
|
|
377
|
+
onProgress: (done, total) => {
|
|
378
|
+
this.reindex.l1Done = done;
|
|
379
|
+
this.reindex.l1Total = total;
|
|
380
|
+
},
|
|
381
|
+
shouldCancel: () => this.reindexCancel,
|
|
382
|
+
});
|
|
383
|
+
failedTotal += r1.failed;
|
|
384
|
+
const r0 = await this.deps.l0.reindex({
|
|
385
|
+
onProgress: (done, total) => {
|
|
386
|
+
this.reindex.l0Done = done;
|
|
387
|
+
this.reindex.l0Total = total;
|
|
388
|
+
},
|
|
389
|
+
shouldCancel: () => this.reindexCancel,
|
|
390
|
+
});
|
|
391
|
+
failedTotal += r0.failed;
|
|
392
|
+
const cancelled = !!(r1.cancelled || r0.cancelled);
|
|
393
|
+
this.reindex.cancelled = cancelled;
|
|
394
|
+
this.reindex.running = false;
|
|
395
|
+
return { cancelled, failedTotal };
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
399
|
+
this.reindex.running = false;
|
|
400
|
+
this.reindex.error = message;
|
|
401
|
+
return { cancelled: false, failedTotal, error: `重嵌入失败: ${message}` };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/** RPC 快照(设置页嵌入区块数据源;client 忙时 1s 轮询)。 */
|
|
405
|
+
async snapshot() {
|
|
406
|
+
const status = await this.downloader.listStatus();
|
|
407
|
+
const models = MODEL_CATALOG.map((entry) => {
|
|
408
|
+
const s = status.find((x) => x.id === entry.id);
|
|
409
|
+
return {
|
|
410
|
+
id: entry.id,
|
|
411
|
+
name: entry.name,
|
|
412
|
+
dims: entry.dims,
|
|
413
|
+
contextTokens: entry.contextTokens,
|
|
414
|
+
tags: entry.tags,
|
|
415
|
+
description: entry.description,
|
|
416
|
+
totalBytes: s?.totalBytes ?? 0,
|
|
417
|
+
bytesOnDisk: s?.bytesOnDisk ?? 0,
|
|
418
|
+
state: s?.state ?? 'none',
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
const state = this.sourceStore.get();
|
|
422
|
+
const effE = this.eff().embedding;
|
|
423
|
+
return {
|
|
424
|
+
source: state.source,
|
|
425
|
+
activeModel: state.activeModel,
|
|
426
|
+
ceilings: { remote: remoteCeiling(this.eff()), local: this.deps.cfg.embedding.allowLocalModels },
|
|
427
|
+
remote: {
|
|
428
|
+
baseURL: effE.baseUrl ?? '',
|
|
429
|
+
model: effE.model ?? '',
|
|
430
|
+
dimensions: effE.dimensions ?? 0,
|
|
431
|
+
// API key:生效(运行时覆盖 ? 静态配置)非空即视为已配置——明文不回传
|
|
432
|
+
apiKeySet: Boolean(effE.apiKey),
|
|
433
|
+
},
|
|
434
|
+
runtime: this.installer.getProgress(),
|
|
435
|
+
models,
|
|
436
|
+
download: this.downloader.getProgress(),
|
|
437
|
+
apply: { phase: this.applyPhase, message: this.applyMessage, startedAt: this.applyStartedAt, busy: this.applyBusy },
|
|
438
|
+
local: this.localSvc ? { state: this.localSvc.getState(), error: this.localSvc.getLoadError() } : null,
|
|
439
|
+
reindex: { ...this.reindex },
|
|
440
|
+
activeNote: this.activeNote,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding 服务基座:类型、远程 OpenAI 兼容实现、降级助手。
|
|
3
|
+
*
|
|
4
|
+
* dsh 的 ctx.llm 只有 chat 流式接口、无 embeddings 端点,因此向量能力
|
|
5
|
+
* 需要用户自备任意 OpenAI 兼容 /embeddings 服务(OpenAI/SiliconFlow/Jina/自托管 BGE 等)。
|
|
6
|
+
* 默认关闭(Noop)——纯 FTS 模式即可运行,这是官方 provider="none" 的同款语义。
|
|
7
|
+
*
|
|
8
|
+
* 重写版远程服务内建两层此前缺失的可靠性纪律:
|
|
9
|
+
* - 重试退避:5xx/网络错误指数退避带抖动重试(默认 2 次),4xx(请求/维度错误)不重试;
|
|
10
|
+
* - 并发信号量:全局嵌入调用并发受限(默认 4),防 L1 去重候选一路 Promise.all 打爆上游。
|
|
11
|
+
* (嵌入源三态活切换、本地 worker、模型下载/运行时安装在嵌入子系统提交中落地。)
|
|
12
|
+
*/
|
|
13
|
+
import type { MemoryLogger } from '../types.js';
|
|
14
|
+
export interface EmbeddingProviderInfo {
|
|
15
|
+
provider: string;
|
|
16
|
+
model: string;
|
|
17
|
+
dimensions: number;
|
|
18
|
+
}
|
|
19
|
+
/** 单次嵌入调用的可选参数:timeoutMs 只允许缩短服务配置的超时(内层钳制),
|
|
20
|
+
* 永不放大——召回路径用它给 FTS 降级留时间(规格 A 节)。本地实现经 worker
|
|
21
|
+
* 代理以 Promise.race 钳制(迟到回复丢弃,推理在 worker 线程无法真正取消)。 */
|
|
22
|
+
export interface EmbedCallOptions {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface EmbeddingService {
|
|
26
|
+
embed(text: string, callOpts?: EmbedCallOptions): Promise<Float32Array>;
|
|
27
|
+
embedBatch(texts: string[], callOpts?: EmbedCallOptions): Promise<Float32Array[]>;
|
|
28
|
+
getDimensions(): number;
|
|
29
|
+
getProviderInfo(): EmbeddingProviderInfo;
|
|
30
|
+
isReady(): boolean;
|
|
31
|
+
close?(): void;
|
|
32
|
+
}
|
|
33
|
+
/** 空实现:向量能力关闭/不可用时使用,一切调用安全返回。 */
|
|
34
|
+
export declare class NoopEmbeddingService implements EmbeddingService {
|
|
35
|
+
getDimensions(): number;
|
|
36
|
+
getProviderInfo(): EmbeddingProviderInfo;
|
|
37
|
+
isReady(): boolean;
|
|
38
|
+
embed(): Promise<Float32Array>;
|
|
39
|
+
embedBatch(texts: string[]): Promise<Float32Array[]>;
|
|
40
|
+
}
|
|
41
|
+
export interface RemoteEmbeddingOptions {
|
|
42
|
+
baseUrl: string;
|
|
43
|
+
apiKey: string;
|
|
44
|
+
model: string;
|
|
45
|
+
/** 必填且须与模型输出一致(vec0 建表需要固定维度)。 */
|
|
46
|
+
dimensions: number;
|
|
47
|
+
maxInputChars?: number;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
logger?: MemoryLogger;
|
|
50
|
+
/** 5xx/网络错误的最大重试次数(默认 2;4xx 不重试)。 */
|
|
51
|
+
maxRetries?: number;
|
|
52
|
+
/** 全局并发信号量上限(默认 4)。 */
|
|
53
|
+
concurrency?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* OpenAI 兼容远程 embedding:POST {baseUrl}/embeddings。
|
|
57
|
+
* 向量在客户端做 L2 归一化(与余弦度量配套)。
|
|
58
|
+
*/
|
|
59
|
+
export declare class RemoteEmbeddingService implements EmbeddingService {
|
|
60
|
+
private readonly opts;
|
|
61
|
+
private readonly sem;
|
|
62
|
+
constructor(opts: RemoteEmbeddingOptions);
|
|
63
|
+
getDimensions(): number;
|
|
64
|
+
getProviderInfo(): EmbeddingProviderInfo;
|
|
65
|
+
isReady(): boolean;
|
|
66
|
+
embed(text: string, callOpts?: EmbedCallOptions): Promise<Float32Array>;
|
|
67
|
+
embedBatch(texts: string[], callOpts?: EmbedCallOptions): Promise<Float32Array[]>;
|
|
68
|
+
private callWithRetry;
|
|
69
|
+
private callOnce;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 嵌入调用降级助手(L0/L1 Store 共用):查询向量失败 → undefined(调用方降级 FTS);
|
|
73
|
+
* 批量嵌入失败 → 全 undefined(跳过向量写入,由周期性 backfill 补齐)。
|
|
74
|
+
* 同类失败只告警一次,避免刷屏。
|
|
75
|
+
*/
|
|
76
|
+
export declare class EmbedHelper {
|
|
77
|
+
private embed;
|
|
78
|
+
private readonly logger?;
|
|
79
|
+
private warned;
|
|
80
|
+
constructor(embed: EmbeddingService, logger?: MemoryLogger | undefined);
|
|
81
|
+
/** 活切换嵌入源:换掉底层服务并复位一次性告警(新服务重新获得告警机会)。 */
|
|
82
|
+
setService(svc: EmbeddingService): void;
|
|
83
|
+
vectorReady(): boolean;
|
|
84
|
+
/** 查询向量;失败或空向量返回 undefined(调用方降级 FTS)。
|
|
85
|
+
* timeoutMs 为内层钳制(仅缩短服务超时),召回路径使用。 */
|
|
86
|
+
query(text: string, timeoutMs?: number): Promise<Float32Array | undefined>;
|
|
87
|
+
/** 批量嵌入;服务未就绪或失败时返回全 undefined(不阻断元数据/FTS 写入)。 */
|
|
88
|
+
batch(texts: string[]): Promise<Array<Float32Array | undefined>>;
|
|
89
|
+
private warn;
|
|
90
|
+
}
|