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
package/dist/store/l0.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0 原始对话存储(双写架构):
|
|
3
|
+
* - conversations/YYYY-MM-DD.jsonl:追加式事实源;
|
|
4
|
+
* - MemoryDb(SQLite):主检索引擎——检索不再按天扫文件、现建内存索引。
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { EmbedHelper, NoopEmbeddingService } from './embedding.js';
|
|
9
|
+
import { appendJsonl, dayKey, ensureDir, nowIso, readJsonl } from '../util/io.js';
|
|
10
|
+
import { rrfMerge } from './search-utils.js';
|
|
11
|
+
import { isZeroVector } from './sqlite.js';
|
|
12
|
+
/** 官方过度召回倍数(conversation-search:limit × 3)。 */
|
|
13
|
+
const CANDIDATE_MULTIPLIER = 3;
|
|
14
|
+
export class L0Store {
|
|
15
|
+
db;
|
|
16
|
+
dir;
|
|
17
|
+
legacyDir;
|
|
18
|
+
helper;
|
|
19
|
+
embedSvc;
|
|
20
|
+
logger;
|
|
21
|
+
constructor(dataDir, db, embed = new NoopEmbeddingService(), logger) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
this.dir = path.join(dataDir, 'conversations');
|
|
24
|
+
this.legacyDir = path.join(dataDir, 'l0');
|
|
25
|
+
this.embedSvc = embed;
|
|
26
|
+
this.helper = new EmbedHelper(embed, logger);
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
}
|
|
29
|
+
async init() {
|
|
30
|
+
await ensureDir(this.dir);
|
|
31
|
+
await this.importLegacy();
|
|
32
|
+
}
|
|
33
|
+
/** 旧版 l0/*.jsonl 一次性导入检索库,成功后目录改名 l0.imported/。 */
|
|
34
|
+
async importLegacy() {
|
|
35
|
+
if (!existsSync(this.legacyDir))
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const files = await fs.readdir(this.legacyDir).catch(() => []);
|
|
39
|
+
let imported = 0;
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (const f of files.sort()) {
|
|
42
|
+
if (!f.endsWith('.jsonl'))
|
|
43
|
+
continue;
|
|
44
|
+
const records = await readJsonl(path.join(this.legacyDir, f));
|
|
45
|
+
// 最小有效性门(同 L1 的 importLegacy):坏行读取时丢弃,按 valid 数判迁移完成
|
|
46
|
+
const valid = records.filter((r) => r && typeof r.id === 'string' && r.content);
|
|
47
|
+
const badCount = records.length - valid.length;
|
|
48
|
+
if (badCount > 0) {
|
|
49
|
+
this.logger?.warn(`[memory] 旧版 L0 文件 ${f} 丢弃 ${badCount} 条坏行(缺 id/content)`);
|
|
50
|
+
}
|
|
51
|
+
if (valid.length > 0) {
|
|
52
|
+
total += valid.length;
|
|
53
|
+
if (this.db.upsertL0Batch(valid))
|
|
54
|
+
imported += valid.length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 只有全部批次入库成功(或目录为空)才改名,避免数据被改名带走
|
|
58
|
+
if (imported === total) {
|
|
59
|
+
const renamed = await fs
|
|
60
|
+
.rename(this.legacyDir, `${this.legacyDir}.imported`)
|
|
61
|
+
.then(() => true, () => false);
|
|
62
|
+
if (renamed) {
|
|
63
|
+
this.logger?.info(`[memory] 旧版 L0 数据已导入检索库 ${imported} 条(l0/ → l0.imported/)`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.logger?.warn('[memory] 旧版 L0 导入完成但改名失败(l0.imported/ 已存在?),下次启动会重复导入(幂等,无害)');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.logger?.warn(`[memory] 旧版 L0 导入不完整(${imported}/${total}),保留原目录下次重试`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
this.logger?.warn(`[memory] 旧版 L0 数据导入失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async append(sessionId, messages) {
|
|
78
|
+
if (messages.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
const records = messages.map((m) => ({
|
|
81
|
+
sessionId,
|
|
82
|
+
recordedAt: nowIso(),
|
|
83
|
+
id: m.id,
|
|
84
|
+
role: m.role,
|
|
85
|
+
content: m.content,
|
|
86
|
+
timestamp: m.timestamp,
|
|
87
|
+
}));
|
|
88
|
+
// 事实源:按天追加
|
|
89
|
+
const byDay = new Map();
|
|
90
|
+
for (const r of records) {
|
|
91
|
+
const k = dayKey(r.timestamp);
|
|
92
|
+
const arr = byDay.get(k) ?? [];
|
|
93
|
+
arr.push(r);
|
|
94
|
+
byDay.set(k, arr);
|
|
95
|
+
}
|
|
96
|
+
for (const [day, list] of byDay) {
|
|
97
|
+
await appendJsonl(path.join(this.dir, `${day}.jsonl`), list);
|
|
98
|
+
}
|
|
99
|
+
// 检索引擎:DB + 向量(嵌入失败只跳过向量,不影响元数据/FTS,backfill 补齐)。
|
|
100
|
+
// 双写失败闭环:JSONL 事实源已先行追加,DB 缺行 = 这些消息检索不可见
|
|
101
|
+
// (conversation_search / 蒸馏背景参考都查不到)——升 error 并给自愈指引。
|
|
102
|
+
const vecs = await this.helper.batch(records.map((r) => r.content));
|
|
103
|
+
if (!this.db.upsertL0Batch(records, vecs)) {
|
|
104
|
+
this.logger?.error(`[memory] L0 检索库批量写入失败(${records.length} 条,JSONL 事实源完好),` +
|
|
105
|
+
'这些消息暂不可检索;可在设置页运行「重建记忆」修复');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** 今日已捕获消息数(SQL 计数,不再读整文件)。 */
|
|
109
|
+
async countToday() {
|
|
110
|
+
const d = new Date();
|
|
111
|
+
return this.db.countL0Since(new Date(d.getFullYear(), d.getMonth(), d.getDate()).toISOString());
|
|
112
|
+
}
|
|
113
|
+
/** 该会话累计已捕获消息数(session-stats 数据源;索引 COUNT)。 */
|
|
114
|
+
async countBySession(sessionId) {
|
|
115
|
+
return this.db.countL0BySession(sessionId);
|
|
116
|
+
}
|
|
117
|
+
/** 该会话最近 n 条消息(时间升序;蒸馏背景参考用,按会话现查——ADR-0003)。 */
|
|
118
|
+
async recentBySession(sessionId, limit) {
|
|
119
|
+
return this.db.recentL0BySession(sessionId, limit);
|
|
120
|
+
}
|
|
121
|
+
/** 检索:FTS + 向量 hybrid(RRF 融合),返回按相关性排序的消息。 */
|
|
122
|
+
async search(query, limit) {
|
|
123
|
+
const caps = this.db.getCapabilities();
|
|
124
|
+
if (!caps.ftsSearch && !caps.vectorSearch)
|
|
125
|
+
return [];
|
|
126
|
+
const candidateK = limit * CANDIDATE_MULTIPLIER;
|
|
127
|
+
if (caps.vectorSearch && this.helper.vectorReady()) {
|
|
128
|
+
const [ftsRaw, vec] = await Promise.all([
|
|
129
|
+
Promise.resolve(this.db.searchL0Fts(query, candidateK)),
|
|
130
|
+
this.helper.query(query),
|
|
131
|
+
]);
|
|
132
|
+
const vecList = vec ? this.db.searchL0Vector(vec, candidateK) : [];
|
|
133
|
+
const merged = rrfMerge([ftsRaw, vecList], (h) => h.id);
|
|
134
|
+
return merged.map(({ rrfScore: _rrf, ...r }) => r).slice(0, limit);
|
|
135
|
+
}
|
|
136
|
+
return this.db.searchL0Fts(query, limit).map(({ score: _score, ...r }) => r);
|
|
137
|
+
}
|
|
138
|
+
/** 活切换嵌入源:同步换底层服务(嵌入源三态切换用)。 */
|
|
139
|
+
setEmbeddingService(svc) {
|
|
140
|
+
this.embedSvc = svc;
|
|
141
|
+
this.helper.setService(svc);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 增量重嵌入(同 L1Store.reindex:只补缺失向量,零向量记 skipped 并入 skip 集,
|
|
145
|
+
* 不算失败、不阻塞同步标记——保证补齐判据收敛)。onProgress/shouldCancel
|
|
146
|
+
* 供活切换的进度展示与取消。
|
|
147
|
+
*/
|
|
148
|
+
async reindex(opts) {
|
|
149
|
+
if (!this.helper.vectorReady())
|
|
150
|
+
return { written: 0, failed: 0, skipped: 0 };
|
|
151
|
+
const items = this.db.getL0ForReindex(this.db.getVecSkipSet('l0'));
|
|
152
|
+
const total = items.length;
|
|
153
|
+
let done = 0;
|
|
154
|
+
let written = 0;
|
|
155
|
+
let failed = 0;
|
|
156
|
+
let skipped = 0;
|
|
157
|
+
let cancelled = false;
|
|
158
|
+
const skippedNow = [];
|
|
159
|
+
const CHUNK = 32;
|
|
160
|
+
for (let i = 0; i < items.length; i += CHUNK) {
|
|
161
|
+
if (opts?.shouldCancel?.()) {
|
|
162
|
+
cancelled = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
const chunk = items.slice(i, i + CHUNK);
|
|
166
|
+
let vecs;
|
|
167
|
+
try {
|
|
168
|
+
vecs = await this.embedSvc.embedBatch(chunk.map((c) => c.text));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
failed += chunk.length;
|
|
172
|
+
done += chunk.length;
|
|
173
|
+
opts?.onProgress?.(done, total);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const pending = [];
|
|
177
|
+
chunk.forEach((c, j) => {
|
|
178
|
+
if (isZeroVector(vecs[j])) {
|
|
179
|
+
skipped++;
|
|
180
|
+
skippedNow.push(c.id);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
pending.push({ id: c.id, embedding: vecs[j] });
|
|
184
|
+
});
|
|
185
|
+
if (pending.length > 0) {
|
|
186
|
+
const ok = this.db.updateL0VecBatch(pending, '');
|
|
187
|
+
written += ok;
|
|
188
|
+
failed += pending.length - ok;
|
|
189
|
+
}
|
|
190
|
+
done += chunk.length;
|
|
191
|
+
opts?.onProgress?.(done, total);
|
|
192
|
+
}
|
|
193
|
+
if (skippedNow.length > 0)
|
|
194
|
+
this.db.addVecSkippedIds('l0', skippedNow);
|
|
195
|
+
return { written, failed, skipped, cancelled };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { L1Hit, MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
|
|
2
|
+
import { type EmbeddingService } from './embedding.js';
|
|
3
|
+
import { type MemoryDb } from './sqlite.js';
|
|
4
|
+
export type RecallStrategy = 'keyword' | 'embedding' | 'hybrid';
|
|
5
|
+
export interface L1SearchOptions {
|
|
6
|
+
/** 按记忆类型精确过滤(后置过滤,官方做法)。 */
|
|
7
|
+
type?: string;
|
|
8
|
+
/** 按族过滤(undefined = 不过滤,即 auto 档与浏览路径;检索唯一缝的族语义)。 */
|
|
9
|
+
family?: MemoryFamily;
|
|
10
|
+
/** 分数阈值(仅召回路径传;keyword/embedding 策略生效,FTS 含小语料例外;
|
|
11
|
+
* hybrid 按官方语义在 RRF 融合前不过滤)。 */
|
|
12
|
+
scoreThreshold?: number;
|
|
13
|
+
/** 嵌入查询内层钳制(ms,只缩短不放大;召回路径传入给 FTS 降级留时间)。 */
|
|
14
|
+
embeddingTimeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class L1Store {
|
|
17
|
+
private readonly db;
|
|
18
|
+
private readonly strategy;
|
|
19
|
+
private readonly recordsDir;
|
|
20
|
+
private readonly legacyFile;
|
|
21
|
+
private readonly helper;
|
|
22
|
+
private embedSvc;
|
|
23
|
+
private readonly logger?;
|
|
24
|
+
/** 时效衰减半衰期(天;0=关)。 */
|
|
25
|
+
private readonly decayHalfLifeDays;
|
|
26
|
+
constructor(dataDir: string, db: MemoryDb, embed?: EmbeddingService, strategy?: RecallStrategy, logger?: MemoryLogger,
|
|
27
|
+
/** 时效衰减半衰期(天;0=关)。缺省 30 与 config 默认一致。 */
|
|
28
|
+
decayHalfLifeDays?: number);
|
|
29
|
+
init(): Promise<void>;
|
|
30
|
+
/** 旧版单文件 records.jsonl 一次性导入检索库,成功后改名 .imported。 */
|
|
31
|
+
private importLegacy;
|
|
32
|
+
get size(): number;
|
|
33
|
+
/** 全量读取(调试/迁移用;检索请走 search)。 */
|
|
34
|
+
all(): MemoryRecord[];
|
|
35
|
+
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
36
|
+
getByIds(ids: string[]): MemoryRecord[];
|
|
37
|
+
/** 新记忆落盘:JSONL 按天追加(事实源)+ 检索库 upsert + 向量。 */
|
|
38
|
+
appendNew(records: MemoryRecord[]): Promise<void>;
|
|
39
|
+
/** 去重 update/merge 产出的记录:只更新检索库(JSONL 事实源不改写,官方语义)。 */
|
|
40
|
+
upsert(record: MemoryRecord): Promise<void>;
|
|
41
|
+
/** 活切换嵌入源:同步换底层服务(嵌入源三态切换用)。 */
|
|
42
|
+
setEmbeddingService(svc: EmbeddingService): void;
|
|
43
|
+
deleteBatch(ids: string[]): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* 三策略检索(自动召回与 memory_search 工具共用接缝)。
|
|
46
|
+
* embedding 不可用时自动降级 keyword;type 后置过滤;
|
|
47
|
+
* scoreThreshold 仅对 keyword/embedding 单路策略生效——hybrid 按官方语义
|
|
48
|
+
* 融合完整列表(融合分已归一化 0~1,可直接用于展示/过滤)。
|
|
49
|
+
*/
|
|
50
|
+
search(query: string, limit: number, opts?: L1SearchOptions): Promise<L1Hit[]>;
|
|
51
|
+
/**
|
|
52
|
+
* 时效衰减加权(#29):三路共用的读路径后处理——阈值过滤之后、截断之前
|
|
53
|
+
* (才能轮转名额,而不只是重排已截断的集合)。updated_at 经主表批量点查
|
|
54
|
+
* 回填(FTS 表无该列;候选池 ≤ limit×3 条主键查询,微秒级)。关闭时零开销。
|
|
55
|
+
*/
|
|
56
|
+
private applyDecay;
|
|
57
|
+
/** 浏览列表(UI 用):无关键词时按更新时间倒序分页,支持 Hall 过滤。 */
|
|
58
|
+
list(opts: {
|
|
59
|
+
type?: string;
|
|
60
|
+
scene?: string;
|
|
61
|
+
family?: string;
|
|
62
|
+
hall?: string;
|
|
63
|
+
limit: number;
|
|
64
|
+
offset: number;
|
|
65
|
+
}): {
|
|
66
|
+
items: MemoryRecord[];
|
|
67
|
+
total: number;
|
|
68
|
+
};
|
|
69
|
+
/** 场景名去重列表(UI 筛选器数据源)。 */
|
|
70
|
+
distinctScenes(): string[];
|
|
71
|
+
/**
|
|
72
|
+
* 去重候选召回(官方 3 级):空库跳过 → 向量优先 → FTS 兜底。
|
|
73
|
+
* 传入 family 时只在同族记录里召回(去重永不跨族)。
|
|
74
|
+
*/
|
|
75
|
+
searchCandidates(query: string, limit: number, family?: MemoryFamily): Promise<MemoryRecord[]>;
|
|
76
|
+
/**
|
|
77
|
+
* 增量重嵌入(embedding 配置变化 / 周期性补齐用):只处理缺失向量的记录,
|
|
78
|
+
* 排除已判定"当前 provider 不可嵌入"的 skip 集。返回写入/失败/跳过数——
|
|
79
|
+
* failed > 0 时调用方不应标记 meta 同步完成;skipped(零向量)不算失败、
|
|
80
|
+
* 不阻塞同步标记(否则补齐判据永不收敛,周期性全量重嵌死循环)。
|
|
81
|
+
* onProgress/shouldCancel 供活切换的进度展示与取消。
|
|
82
|
+
*/
|
|
83
|
+
reindex(opts?: {
|
|
84
|
+
onProgress?: (done: number, total: number) => void;
|
|
85
|
+
shouldCancel?: () => boolean;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
written: number;
|
|
88
|
+
failed: number;
|
|
89
|
+
skipped: number;
|
|
90
|
+
cancelled?: boolean;
|
|
91
|
+
}>;
|
|
92
|
+
private postProcess;
|
|
93
|
+
}
|
package/dist/store/l1.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 原子记忆存储(双写架构):
|
|
3
|
+
* - records/YYYY-MM-DD.jsonl:追加式事实源(只增不改,备份/恢复用);
|
|
4
|
+
* - MemoryDb(SQLite):主检索引擎,upsert/delete 只动这里;
|
|
5
|
+
* - 检索三策略:keyword(FTS5 BM25)/ embedding(vec0 余弦)/ hybrid(双路 + RRF k=60)。
|
|
6
|
+
*
|
|
7
|
+
* 去重/合并的更新记录走 upsert(新 record id + 版本递增),不再全量重写文件。
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { familyForType } from '../types.js';
|
|
12
|
+
import { EmbedHelper, NoopEmbeddingService } from './embedding.js';
|
|
13
|
+
import { appendJsonl, dayKey, ensureDir, readJsonl } from '../util/io.js';
|
|
14
|
+
import { applyDecayWeight, RRF_K, rrfMerge } from './search-utils.js';
|
|
15
|
+
import { isZeroVector } from './sqlite.js';
|
|
16
|
+
/** 官方过度召回倍数:候选池 = limit × 3(官方 tool 路径同款)。 */
|
|
17
|
+
const CANDIDATE_MULTIPLIER = 3;
|
|
18
|
+
export class L1Store {
|
|
19
|
+
db;
|
|
20
|
+
strategy;
|
|
21
|
+
recordsDir;
|
|
22
|
+
legacyFile;
|
|
23
|
+
helper;
|
|
24
|
+
embedSvc;
|
|
25
|
+
logger;
|
|
26
|
+
/** 时效衰减半衰期(天;0=关)。 */
|
|
27
|
+
decayHalfLifeDays;
|
|
28
|
+
constructor(dataDir, db, embed = new NoopEmbeddingService(), strategy = 'hybrid', logger,
|
|
29
|
+
/** 时效衰减半衰期(天;0=关)。缺省 30 与 config 默认一致。 */
|
|
30
|
+
decayHalfLifeDays) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
this.strategy = strategy;
|
|
33
|
+
this.recordsDir = path.join(dataDir, 'records');
|
|
34
|
+
this.legacyFile = path.join(dataDir, 'l1', 'records.jsonl');
|
|
35
|
+
this.embedSvc = embed;
|
|
36
|
+
this.helper = new EmbedHelper(embed, logger);
|
|
37
|
+
this.logger = logger;
|
|
38
|
+
this.decayHalfLifeDays = decayHalfLifeDays ?? 30;
|
|
39
|
+
}
|
|
40
|
+
async init() {
|
|
41
|
+
await ensureDir(this.recordsDir);
|
|
42
|
+
await this.importLegacy();
|
|
43
|
+
}
|
|
44
|
+
/** 旧版单文件 records.jsonl 一次性导入检索库,成功后改名 .imported。 */
|
|
45
|
+
async importLegacy() {
|
|
46
|
+
if (!existsSync(this.legacyFile))
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
const records = await readJsonl(this.legacyFile);
|
|
50
|
+
const valid = records.filter((r) => r && typeof r.id === 'string' && r.content);
|
|
51
|
+
const badCount = records.length - valid.length;
|
|
52
|
+
let n = 0;
|
|
53
|
+
if (valid.length > 0 && this.db.upsertL1Batch(valid))
|
|
54
|
+
n = valid.length;
|
|
55
|
+
// 只有确实导入成功才改名,避免把未入库的数据改名带走;判据按 valid 数——
|
|
56
|
+
// 坏行已在读取时过滤,按 records.length 判会让混入坏行的文件迁移永不完成
|
|
57
|
+
if (n === valid.length) {
|
|
58
|
+
const renamed = await fs
|
|
59
|
+
.rename(this.legacyFile, `${this.legacyFile}.imported`)
|
|
60
|
+
.then(() => true, () => false);
|
|
61
|
+
if (renamed) {
|
|
62
|
+
this.logger?.info(`[memory] 旧版 L1 数据已导入检索库 ${n} 条${badCount > 0 ? `(另丢弃 ${badCount} 条坏行)` : ''}(l1/records.jsonl → .imported)`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.logger?.warn('[memory] 旧版 L1 导入完成但改名失败,下次启动会重复导入(upsert 幂等,无害)');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.logger?.warn(`[memory] 旧版 L1 导入不完整(${n}/${valid.length}),保留原文件下次重试`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.logger?.warn(`[memory] 旧版 L1 数据导入失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
get size() {
|
|
77
|
+
return this.db.countL1();
|
|
78
|
+
}
|
|
79
|
+
/** 全量读取(调试/迁移用;检索请走 search)。 */
|
|
80
|
+
all() {
|
|
81
|
+
return this.db.getAllL1();
|
|
82
|
+
}
|
|
83
|
+
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
84
|
+
getByIds(ids) {
|
|
85
|
+
return this.db.getL1ByIds(ids);
|
|
86
|
+
}
|
|
87
|
+
/** 新记忆落盘:JSONL 按天追加(事实源)+ 检索库 upsert + 向量。 */
|
|
88
|
+
async appendNew(records) {
|
|
89
|
+
if (records.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
for (const r of records) {
|
|
92
|
+
if (!r.family)
|
|
93
|
+
r.family = familyForType(r.type);
|
|
94
|
+
}
|
|
95
|
+
const byDay = new Map();
|
|
96
|
+
for (const r of records) {
|
|
97
|
+
const k = dayKey(r.createdAt || Date.now());
|
|
98
|
+
const arr = byDay.get(k) ?? [];
|
|
99
|
+
arr.push(r);
|
|
100
|
+
byDay.set(k, arr);
|
|
101
|
+
}
|
|
102
|
+
for (const [day, list] of byDay) {
|
|
103
|
+
await appendJsonl(path.join(this.recordsDir, `${day}.jsonl`), list);
|
|
104
|
+
}
|
|
105
|
+
const vecs = await this.helper.batch(records.map((r) => r.content));
|
|
106
|
+
// 单事务批量写:逐条开事务在 WAL FULL 下每条一次 fsync。
|
|
107
|
+
// 双写失败闭环:JSONL 事实源已先行追加,DB 缺行 = 这批记忆检索不可见、
|
|
108
|
+
// 去重候选缺失(重复记忆会累积)。upsert 内部已有逐条 warn,这里升 error
|
|
109
|
+
// 并给出自愈指引——检索库可由「重建记忆」从事实源全量重导修复。
|
|
110
|
+
if (!this.db.upsertL1Batch(records, vecs)) {
|
|
111
|
+
this.logger?.error(`[memory] L1 检索库批量写入失败(${records.length} 条,JSONL 事实源完好),` +
|
|
112
|
+
'这批记忆暂不可检索;可在设置页运行「重建记忆」修复');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** 去重 update/merge 产出的记录:只更新检索库(JSONL 事实源不改写,官方语义)。 */
|
|
116
|
+
async upsert(record) {
|
|
117
|
+
if (!record.family)
|
|
118
|
+
record.family = familyForType(record.type);
|
|
119
|
+
const vec = (await this.helper.batch([record.content]))[0];
|
|
120
|
+
if (!this.db.upsertL1(record, vec)) {
|
|
121
|
+
this.logger?.error(`[memory] L1 检索库写入失败 id=${record.id}(JSONL 事实源完好),该记忆暂不可检索,重建可修复`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** 活切换嵌入源:同步换底层服务(嵌入源三态切换用)。 */
|
|
125
|
+
setEmbeddingService(svc) {
|
|
126
|
+
this.embedSvc = svc;
|
|
127
|
+
this.helper.setService(svc);
|
|
128
|
+
}
|
|
129
|
+
async deleteBatch(ids) {
|
|
130
|
+
this.db.deleteL1Batch(ids);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 三策略检索(自动召回与 memory_search 工具共用接缝)。
|
|
134
|
+
* embedding 不可用时自动降级 keyword;type 后置过滤;
|
|
135
|
+
* scoreThreshold 仅对 keyword/embedding 单路策略生效——hybrid 按官方语义
|
|
136
|
+
* 融合完整列表(融合分已归一化 0~1,可直接用于展示/过滤)。
|
|
137
|
+
*/
|
|
138
|
+
async search(query, limit, opts) {
|
|
139
|
+
const caps = this.db.getCapabilities();
|
|
140
|
+
const canVec = caps.vectorSearch && this.helper.vectorReady();
|
|
141
|
+
let strategy = this.strategy;
|
|
142
|
+
if (strategy !== 'keyword' && !canVec)
|
|
143
|
+
strategy = caps.ftsSearch ? 'keyword' : 'none';
|
|
144
|
+
const candidateK = limit * CANDIDATE_MULTIPLIER;
|
|
145
|
+
const threshold = opts?.scoreThreshold ?? 0;
|
|
146
|
+
if (strategy === 'none')
|
|
147
|
+
return [];
|
|
148
|
+
if (strategy === 'keyword') {
|
|
149
|
+
const fts = this.db.searchL1Fts(query, candidateK, opts?.family);
|
|
150
|
+
return this.postProcess(this.applyDecay(applyFtsThreshold(fts, threshold, limit)), opts?.type, limit);
|
|
151
|
+
}
|
|
152
|
+
if (strategy === 'embedding') {
|
|
153
|
+
const vec = await this.helper.query(query, opts?.embeddingTimeoutMs);
|
|
154
|
+
if (!vec) {
|
|
155
|
+
// embedding 调用失败:降级 FTS,不阻断
|
|
156
|
+
const fts = this.db.searchL1Fts(query, candidateK, opts?.family);
|
|
157
|
+
return this.postProcess(this.applyDecay(applyFtsThreshold(fts, threshold, limit)), opts?.type, limit);
|
|
158
|
+
}
|
|
159
|
+
const vecHits = this.db.searchL1Vector(vec, candidateK, opts?.family);
|
|
160
|
+
return this.postProcess(this.applyDecay(filterScore(vecHits, threshold)), opts?.type, limit);
|
|
161
|
+
}
|
|
162
|
+
// hybrid(官方语义):双路并行 → 完整列表 RRF 融合(融合前不过滤阈值)
|
|
163
|
+
// → 融合分归一化:rank1 双列表命中 = 1.0,单列表命中 ≤ 0.5,保持 0~1 语义
|
|
164
|
+
const [ftsList, vecRaw] = await Promise.all([
|
|
165
|
+
Promise.resolve(this.db.searchL1Fts(query, candidateK, opts?.family)),
|
|
166
|
+
this.helper.query(query, opts?.embeddingTimeoutMs),
|
|
167
|
+
]);
|
|
168
|
+
const vecList = vecRaw ? this.db.searchL1Vector(vecRaw, candidateK, opts?.family) : [];
|
|
169
|
+
const merged = rrfMerge([ftsList, vecList], (h) => h.id);
|
|
170
|
+
return this.postProcess(this.applyDecay(merged.map(({ rrfScore, ...h }) => ({ ...h, score: normalizeRrf(rrfScore) }))), opts?.type, limit);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 时效衰减加权(#29):三路共用的读路径后处理——阈值过滤之后、截断之前
|
|
174
|
+
* (才能轮转名额,而不只是重排已截断的集合)。updated_at 经主表批量点查
|
|
175
|
+
* 回填(FTS 表无该列;候选池 ≤ limit×3 条主键查询,微秒级)。关闭时零开销。
|
|
176
|
+
*/
|
|
177
|
+
applyDecay(hits) {
|
|
178
|
+
if (!(this.decayHalfLifeDays > 0) || hits.length === 0)
|
|
179
|
+
return hits;
|
|
180
|
+
const updatedAtById = new Map();
|
|
181
|
+
for (const r of this.db.getL1ByIds(hits.map((h) => h.id))) {
|
|
182
|
+
if (Number.isFinite(r.updatedAt))
|
|
183
|
+
updatedAtById.set(r.id, r.updatedAt);
|
|
184
|
+
}
|
|
185
|
+
return applyDecayWeight(hits, this.decayHalfLifeDays, (h) => updatedAtById.get(h.id));
|
|
186
|
+
}
|
|
187
|
+
/** 浏览列表(UI 用):无关键词时按更新时间倒序分页,支持 Hall 过滤。 */
|
|
188
|
+
list(opts) {
|
|
189
|
+
return this.db.listL1(opts);
|
|
190
|
+
}
|
|
191
|
+
/** 场景名去重列表(UI 筛选器数据源)。 */
|
|
192
|
+
distinctScenes() {
|
|
193
|
+
return this.db.distinctL1Scenes();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 去重候选召回(官方 3 级):空库跳过 → 向量优先 → FTS 兜底。
|
|
197
|
+
* 传入 family 时只在同族记录里召回(去重永不跨族)。
|
|
198
|
+
*/
|
|
199
|
+
async searchCandidates(query, limit, family) {
|
|
200
|
+
if (this.db.countL1() === 0)
|
|
201
|
+
return [];
|
|
202
|
+
const caps = this.db.getCapabilities();
|
|
203
|
+
if (caps.vectorSearch && this.helper.vectorReady()) {
|
|
204
|
+
try {
|
|
205
|
+
const vec = await this.helper.query(query);
|
|
206
|
+
if (vec) {
|
|
207
|
+
const hits = this.db.searchL1Vector(vec, limit, family);
|
|
208
|
+
if (hits.length > 0)
|
|
209
|
+
return this.db.getL1ByIds(hits.map((h) => h.id));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
this.logger?.warn(`[memory] 向量候选召回失败,降级 FTS: ${err instanceof Error ? err.message : String(err)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const fts = this.db.searchL1Fts(query, limit * 2, family);
|
|
217
|
+
return this.db.getL1ByIds(fts.map((h) => h.id));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 增量重嵌入(embedding 配置变化 / 周期性补齐用):只处理缺失向量的记录,
|
|
221
|
+
* 排除已判定"当前 provider 不可嵌入"的 skip 集。返回写入/失败/跳过数——
|
|
222
|
+
* failed > 0 时调用方不应标记 meta 同步完成;skipped(零向量)不算失败、
|
|
223
|
+
* 不阻塞同步标记(否则补齐判据永不收敛,周期性全量重嵌死循环)。
|
|
224
|
+
* onProgress/shouldCancel 供活切换的进度展示与取消。
|
|
225
|
+
*/
|
|
226
|
+
async reindex(opts) {
|
|
227
|
+
if (!this.helper.vectorReady())
|
|
228
|
+
return { written: 0, failed: 0, skipped: 0 };
|
|
229
|
+
const items = this.db.getL1ForReindex(this.db.getVecSkipSet('l1'));
|
|
230
|
+
const total = items.length;
|
|
231
|
+
let done = 0;
|
|
232
|
+
let written = 0;
|
|
233
|
+
let failed = 0;
|
|
234
|
+
let skipped = 0;
|
|
235
|
+
let cancelled = false;
|
|
236
|
+
const skippedNow = [];
|
|
237
|
+
const CHUNK = 16;
|
|
238
|
+
for (let i = 0; i < items.length; i += CHUNK) {
|
|
239
|
+
if (opts?.shouldCancel?.()) {
|
|
240
|
+
cancelled = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
const chunk = items.slice(i, i + CHUNK);
|
|
244
|
+
let vecs;
|
|
245
|
+
try {
|
|
246
|
+
vecs = await this.embedSvc.embedBatch(chunk.map((c) => c.content));
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
failed += chunk.length;
|
|
250
|
+
done += chunk.length;
|
|
251
|
+
opts?.onProgress?.(done, total);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const pending = [];
|
|
255
|
+
chunk.forEach((c, j) => {
|
|
256
|
+
if (isZeroVector(vecs[j])) {
|
|
257
|
+
skipped++;
|
|
258
|
+
skippedNow.push(c.id);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
pending.push({ id: c.id, embedding: vecs[j] });
|
|
262
|
+
});
|
|
263
|
+
if (pending.length > 0) {
|
|
264
|
+
const ok = this.db.updateL1VecBatch(pending);
|
|
265
|
+
written += ok;
|
|
266
|
+
failed += pending.length - ok;
|
|
267
|
+
}
|
|
268
|
+
done += chunk.length;
|
|
269
|
+
opts?.onProgress?.(done, total);
|
|
270
|
+
}
|
|
271
|
+
if (skippedNow.length > 0)
|
|
272
|
+
this.db.addVecSkippedIds('l1', skippedNow);
|
|
273
|
+
return { written, failed, skipped, cancelled };
|
|
274
|
+
}
|
|
275
|
+
postProcess(hits, type, limit) {
|
|
276
|
+
const filtered = type ? hits.filter((h) => h.type === type) : hits;
|
|
277
|
+
return filtered.slice(0, limit);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** RRF 原始分归一化到 0~1:双列表 rank1 命中 = 2/(k+1) → 1.0。 */
|
|
281
|
+
function normalizeRrf(rrfScore) {
|
|
282
|
+
return (rrfScore * (RRF_K + 1)) / 2;
|
|
283
|
+
}
|
|
284
|
+
/** FTS 阈值过滤(含官方小语料例外:全部低于阈值但结果数 ≤ maxResults 时保留)。 */
|
|
285
|
+
function applyFtsThreshold(hits, threshold, maxResults) {
|
|
286
|
+
if (threshold <= 0)
|
|
287
|
+
return hits;
|
|
288
|
+
const filtered = hits.filter((h) => h.score >= threshold);
|
|
289
|
+
if (filtered.length === 0 && hits.length > 0 && hits.length <= maxResults)
|
|
290
|
+
return hits;
|
|
291
|
+
return filtered;
|
|
292
|
+
}
|
|
293
|
+
function filterScore(hits, threshold) {
|
|
294
|
+
if (threshold <= 0)
|
|
295
|
+
return hits;
|
|
296
|
+
return hits.filter((h) => h.score >= threshold);
|
|
297
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { MemoryLogger } from '../types.js';
|
|
2
|
+
import type { CatalogEntry } from './model-catalog.js';
|
|
3
|
+
import type { EmbedCallOptions, EmbeddingProviderInfo, EmbeddingService } from './embedding.js';
|
|
4
|
+
type LocalState = 'idle' | 'loading' | 'ready' | 'failed' | 'terminated';
|
|
5
|
+
/** worker 启动参数(workerData;构造后不可变)。 */
|
|
6
|
+
export interface EmbedWorkerData {
|
|
7
|
+
runtimeDir: string;
|
|
8
|
+
modelDir: string;
|
|
9
|
+
pooling: 'cls' | 'mean';
|
|
10
|
+
dtype: string;
|
|
11
|
+
maxInputChars: number;
|
|
12
|
+
}
|
|
13
|
+
/** 主线程 → worker 的调用(id 由通道分配)。 */
|
|
14
|
+
export type EmbedWorkerCall = {
|
|
15
|
+
type: 'ping';
|
|
16
|
+
} | {
|
|
17
|
+
type: 'warmup';
|
|
18
|
+
} | {
|
|
19
|
+
type: 'embed';
|
|
20
|
+
texts: string[];
|
|
21
|
+
priority: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** worker → 主线程的应答。 */
|
|
24
|
+
export type EmbedWorkerReply = {
|
|
25
|
+
id: number;
|
|
26
|
+
ok: true;
|
|
27
|
+
type: 'pong';
|
|
28
|
+
} | {
|
|
29
|
+
id: number;
|
|
30
|
+
ok: true;
|
|
31
|
+
type: 'ready';
|
|
32
|
+
} | {
|
|
33
|
+
id: number;
|
|
34
|
+
ok: true;
|
|
35
|
+
type: 'embedded';
|
|
36
|
+
vectors: Float32Array[];
|
|
37
|
+
} | {
|
|
38
|
+
id: number;
|
|
39
|
+
ok: false;
|
|
40
|
+
stage: 'load' | 'infer';
|
|
41
|
+
error: string;
|
|
42
|
+
};
|
|
43
|
+
/** worker 通道抽象(测试缝:注入假实现验证协议与状态机)。 */
|
|
44
|
+
export interface EmbedWorkerChannel {
|
|
45
|
+
request(call: EmbedWorkerCall): Promise<EmbedWorkerReply>;
|
|
46
|
+
/** 立即终止 worker 并拒绝全部未决请求(close 语义);幂等。 */
|
|
47
|
+
terminate(): void;
|
|
48
|
+
/** worker 意外崩溃通知(此后所有未决请求已被通道拒绝)。 */
|
|
49
|
+
setOnCrash(cb: (error: string) => void): void;
|
|
50
|
+
}
|
|
51
|
+
export interface LocalEmbeddingOptions {
|
|
52
|
+
/** 数据目录 runtime/(worker 据此 createRequire 加载 transformers)。 */
|
|
53
|
+
runtimeDir: string;
|
|
54
|
+
/** worker 资产路径(默认 dist/embedding-worker.cjs;测试可显式指定)。 */
|
|
55
|
+
workerPath?: string;
|
|
56
|
+
/** 通道注入缝(测试用;缺省 spawn 真实 worker)。 */
|
|
57
|
+
channel?: EmbedWorkerChannel;
|
|
58
|
+
logger?: MemoryLogger;
|
|
59
|
+
maxInputChars?: number;
|
|
60
|
+
}
|
|
61
|
+
export declare class LocalEmbeddingService implements EmbeddingService {
|
|
62
|
+
private state;
|
|
63
|
+
private loadError;
|
|
64
|
+
private readonly channel;
|
|
65
|
+
private readonly entry;
|
|
66
|
+
private readonly logger?;
|
|
67
|
+
constructor(entry: CatalogEntry, modelDir: string, opts: LocalEmbeddingOptions);
|
|
68
|
+
getDimensions(): number;
|
|
69
|
+
getProviderInfo(): EmbeddingProviderInfo;
|
|
70
|
+
isReady(): boolean;
|
|
71
|
+
/** 状态(进度展示用)。 */
|
|
72
|
+
getState(): LocalState;
|
|
73
|
+
getLoadError(): string | null;
|
|
74
|
+
/** 后台预热:启动后让 worker 立即加载模型(幂等;失败态可重试)。 */
|
|
75
|
+
startWarmup(): void;
|
|
76
|
+
/** 等待模型就绪(warmup 协议;applyChain 的 warming 阶段与测试用)。 */
|
|
77
|
+
waitForReady(): Promise<void>;
|
|
78
|
+
embed(text: string, callOpts?: EmbedCallOptions): Promise<Float32Array>;
|
|
79
|
+
embedBatch(texts: string[], callOpts?: EmbedCallOptions): Promise<Float32Array[]>;
|
|
80
|
+
/** 释放 worker 线程与模型(嵌入源切走/关闭时调用;幂等)。terminated 后不可
|
|
81
|
+
* 再复用——防止插件卸载/切走后残留的重嵌循环把模型重新加载常驻(内存泄漏)。 */
|
|
82
|
+
close(): void;
|
|
83
|
+
/** 内层钳制(仅缩短):超时放弃等待(迟到回复由通道按 id 丢弃),调用方降级。 */
|
|
84
|
+
private requestWithTimeout;
|
|
85
|
+
/** loading → ready 一次性日志(memory.log 时序可读性:启动到模型就绪的间隔)。 */
|
|
86
|
+
private markReady;
|
|
87
|
+
private applyLoadFailure;
|
|
88
|
+
}
|
|
89
|
+
export {};
|