@kira905/dsh-memory-toolkit 0.1.3 → 0.2.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.md +92 -0
- package/README.en.md +225 -2
- package/README.md +180 -2
- package/lib/deploy-guard.mjs +463 -275
- package/lib/guard-http.mjs +59 -62
- package/lib/pitfall-form-map.mjs +94 -84
- package/lib/pitfall-score.mjs +262 -185
- package/lib/session-file-name.mjs +116 -100
- package/lib/zstd-frames.mjs +88 -67
- package/memory-db-tools/README.md +29 -1
- package/memory-db-tools/audit-analyze.mjs +1 -1
- package/memory-db-tools/audit-export.mjs +60 -16
- package/memory-db-tools/audit-struct-probe.mjs +29 -8
- package/memory-db-tools/lib/known-devices.mjs +137 -0
- package/memory-engine/README.md +58 -4
- package/memory-engine/bench/bench.mjs +1 -1
- package/memory-engine/cli/band-sentinel.mjs +164 -0
- package/memory-engine/cli/distill-rollback.mjs +112 -44
- package/memory-engine/cli/distill-sessions.mjs +237 -0
- package/memory-engine/cli/distill.mjs +196 -95
- package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
- package/memory-engine/cli/doc-index.mjs +47 -38
- package/memory-engine/cli/domain-tags.mjs +246 -162
- package/memory-engine/cli/layer-migrate.mjs +176 -0
- package/memory-engine/cli/layer-move.mjs +189 -0
- package/memory-engine/cli/pitfall-hook.mjs +136 -70
- package/memory-engine/cli/recall.mjs +243 -110
- package/memory-engine/cli/selfcheck.mjs +93 -44
- package/memory-engine/cli/serve-start.mjs +221 -140
- package/memory-engine/cli/sweep.mjs +83 -44
- package/memory-engine/cli/sync.mjs +93 -57
- package/memory-engine/config/audit-devices.json +6 -0
- package/memory-engine/config/bands.json +7 -0
- package/memory-engine/config/layers.json +7 -0
- package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
- package/memory-engine/dict/lexicon-config.json +11 -0
- package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
- package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
- package/memory-engine/lib/band-sentinel.mjs +417 -0
- package/memory-engine/lib/cli-strict.mjs +128 -0
- package/memory-engine/lib/cli-util.mjs +147 -125
- package/memory-engine/lib/config.mjs +83 -96
- package/memory-engine/lib/dirty-ledger.mjs +127 -94
- package/memory-engine/lib/distill-rollback.mjs +173 -133
- package/memory-engine/lib/distill.mjs +878 -585
- package/memory-engine/lib/doc-index-store.mjs +208 -235
- package/memory-engine/lib/doc-index.mjs +351 -350
- package/memory-engine/lib/doc-scan.mjs +178 -156
- package/memory-engine/lib/doc-summarize.mjs +220 -205
- package/memory-engine/lib/domain-tags.mjs +358 -277
- package/memory-engine/lib/embedder.mjs +175 -194
- package/memory-engine/lib/hashes.mjs +22 -7
- package/memory-engine/lib/layer-migrate.mjs +330 -0
- package/memory-engine/lib/layer-move.mjs +307 -0
- package/memory-engine/lib/layers.mjs +191 -0
- package/memory-engine/lib/lexicon-recall.mjs +473 -0
- package/memory-engine/lib/memory-source.mjs +78 -80
- package/memory-engine/lib/pitfall-source.mjs +115 -114
- package/memory-engine/lib/policy-source.mjs +279 -261
- package/memory-engine/lib/recall-log.mjs +285 -0
- package/memory-engine/lib/recall.mjs +565 -240
- package/memory-engine/lib/selfcheck.mjs +142 -106
- package/memory-engine/lib/session-distill.mjs +603 -0
- package/memory-engine/lib/session-source.mjs +421 -0
- package/memory-engine/lib/sweep.mjs +107 -130
- package/memory-engine/lib/sync.mjs +110 -140
- package/memory-engine/lib/tick-hook.mjs +123 -148
- package/memory-engine/lib/vectors-store.mjs +213 -188
- package/memory-engine/test/12-layering.test.mjs +539 -0
- package/memory-engine/test/13-session-distill.test.mjs +616 -0
- package/memory-engine/tools/bridge-smoke.mjs +94 -55
- package/memory-engine/tools/fetch-model.mjs +167 -87
- package/memory-engine/tools/smoke-recall.mjs +94 -38
- package/memory-engine/tools/verify-recall-chain.mjs +413 -0
- package/package.json +1 -1
|
@@ -1,48 +1,77 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 独立向量库(方案 §3.1)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// 为什么单独一个 SQLite 文件:主记忆库跑 DELETE 日志模式、busy_timeout 是 0,
|
|
4
|
+
// 再挂一个写者就可能把主人的 memory_write 撞成失败。向量本身可由正文重算,是纯派生物。
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { DatabaseSync } from 'node:sqlite';
|
|
8
8
|
import { SCHEMA_VERSION } from './config.mjs';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
source
|
|
13
|
-
ref_id
|
|
14
|
-
model
|
|
15
|
-
dim
|
|
16
|
-
vec
|
|
17
|
-
text_hash
|
|
18
|
-
updated_at INTEGER NOT NULL,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
CREATE
|
|
10
|
+
// 建表改成「列定义表 → 拼 SQL」:以后加列只动这张表,不再改 SQL 正文
|
|
11
|
+
const VECTOR_COLUMN_SPEC = [
|
|
12
|
+
['source', 'TEXT NOT NULL'],
|
|
13
|
+
['ref_id', 'TEXT NOT NULL'],
|
|
14
|
+
['model', 'TEXT NOT NULL'],
|
|
15
|
+
['dim', 'INTEGER NOT NULL'],
|
|
16
|
+
['vec', 'BLOB NOT NULL'],
|
|
17
|
+
['text_hash', 'TEXT NOT NULL'],
|
|
18
|
+
['updated_at', 'INTEGER NOT NULL'],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const VECTOR_COLUMN_SQL = VECTOR_COLUMN_SPEC.map(([col, type]) => ` ${col} ${type},`).join('\n');
|
|
22
|
+
|
|
23
|
+
const DDL = [
|
|
24
|
+
`CREATE TABLE IF NOT EXISTS vectors (\n${VECTOR_COLUMN_SQL}\n PRIMARY KEY (source, ref_id)\n);`,
|
|
25
|
+
'CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);',
|
|
26
|
+
'CREATE INDEX IF NOT EXISTS vectors_source_model ON vectors (source, model);',
|
|
27
|
+
].join('\n');
|
|
28
|
+
|
|
29
|
+
const SQL_META_READ = 'SELECT v FROM meta WHERE k = ?';
|
|
30
|
+
const SQL_META_UPSERT = `
|
|
31
|
+
INSERT INTO meta (k, v) VALUES (?, ?)
|
|
32
|
+
ON CONFLICT(k) DO UPDATE SET v = excluded.v
|
|
26
33
|
`;
|
|
34
|
+
const SQL_META_DUMP = 'SELECT k, v FROM meta ORDER BY k';
|
|
35
|
+
const SQL_TABLE_PROBE = 'SELECT name FROM sqlite_master WHERE type = ? AND name = ?';
|
|
36
|
+
const SQL_VECTOR_UPSERT = `
|
|
37
|
+
INSERT INTO vectors (source, ref_id, model, dim, vec, text_hash, updated_at)
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
39
|
+
ON CONFLICT(source, ref_id) DO UPDATE SET
|
|
40
|
+
model = excluded.model,
|
|
41
|
+
dim = excluded.dim,
|
|
42
|
+
vec = excluded.vec,
|
|
43
|
+
text_hash = excluded.text_hash,
|
|
44
|
+
updated_at = excluded.updated_at
|
|
45
|
+
`;
|
|
46
|
+
const SQL_HASH_READ = 'SELECT text_hash FROM vectors WHERE source = ? AND ref_id = ?';
|
|
47
|
+
const SQL_COUNT_ALL = 'SELECT COUNT(*) AS n FROM vectors';
|
|
48
|
+
const SQL_COUNT_BY_SOURCE = 'SELECT COUNT(*) AS n FROM vectors WHERE source = ?';
|
|
49
|
+
const SQL_COUNT_BY_MODEL = 'SELECT COUNT(*) AS n FROM vectors WHERE source = ? AND model = ?';
|
|
50
|
+
const SQL_SOURCE_DELETE = 'DELETE FROM vectors WHERE source = ?';
|
|
51
|
+
const SQL_MATRIX_BASE = 'SELECT ref_id, model, dim, vec, text_hash FROM vectors WHERE source = ?';
|
|
52
|
+
const SQL_MATRIX_ORDER = 'ORDER BY CAST(ref_id AS INTEGER)';
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
const META_KEY_NAMES = [
|
|
55
|
+
'schemaVersion',
|
|
56
|
+
'hostId',
|
|
57
|
+
'modelId',
|
|
58
|
+
'dim',
|
|
59
|
+
'lastIncrementalSyncAt',
|
|
60
|
+
'lastFullHashSweepAt',
|
|
61
|
+
'hashSweepCursor',
|
|
62
|
+
'lastSyncStats',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** meta 键名(方案 §3.1 要的前 6 项;后 2 项是兜底轮转的运行状态) */
|
|
66
|
+
export const META_KEYS = META_KEY_NAMES.reduce((bag, name) => {
|
|
67
|
+
bag[name] = name;
|
|
68
|
+
return bag;
|
|
69
|
+
}, {});
|
|
39
70
|
|
|
40
71
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 两者的水位/游标/模型键若同名会互相覆盖 ⇒ 表现为「记忆增量漏编码且不报错」(方案 §1.3-③.1 / §A.6 首要风险)。
|
|
45
|
-
* ⇒ 这四个键(+ 两个运行状态键)一律写成 `<key>:<source>`。
|
|
72
|
+
* 需要按来源分命名空间的键(记忆 v2 §3 A.4)。
|
|
73
|
+
* 坑库与记忆共用一个 vectors.db,同名键会互相覆盖 —— 症状是「记忆增量漏编码却不报错」。
|
|
74
|
+
* 因此这些键一律存成 `<key>:<source>`。
|
|
46
75
|
*/
|
|
47
76
|
export const SOURCE_SCOPED_META_KEYS = [
|
|
48
77
|
'modelId',
|
|
@@ -53,62 +82,69 @@ export const SOURCE_SCOPED_META_KEYS = [
|
|
|
53
82
|
'lastSyncStats',
|
|
54
83
|
];
|
|
55
84
|
|
|
56
|
-
/**
|
|
85
|
+
/** 内部:读一个键(没有就 null) */
|
|
86
|
+
function metaRead(db, key) {
|
|
87
|
+
const row = db.prepare(SQL_META_READ).get(key);
|
|
88
|
+
return row ? row.v : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 内部:写一个键(覆盖式) */
|
|
92
|
+
function metaWrite(db, key, value) {
|
|
93
|
+
db.prepare(SQL_META_UPSERT).run(String(key), String(value));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `<baseKey>:<source>` 形式的键名 */
|
|
57
97
|
export function metaKeyFor(baseKey, source) {
|
|
58
98
|
return `${baseKey}:${source}`;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
102
|
+
* 按来源读 meta:先认命名空间键;落空且来源是 memory 时兼容读无后缀旧键
|
|
103
|
+
* (老库还没迁移的那一次,语义与迁移后一致)。
|
|
64
104
|
*/
|
|
65
105
|
export function getMetaFor(db, baseKey, source) {
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
return null;
|
|
106
|
+
const scoped = metaRead(db, metaKeyFor(baseKey, source));
|
|
107
|
+
if (scoped != null) return scoped;
|
|
108
|
+
return String(source) === 'memory' ? metaRead(db, baseKey) : null;
|
|
70
109
|
}
|
|
71
110
|
|
|
72
111
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* 不会走「首次全量」分支把整库 DELETE 重建(626 条 ≈ 102 s 的一次数据面事件)。
|
|
112
|
+
* 按来源写 meta,并**冗余**写一份无后缀旧键(方案 §A.4 的 C4 选项①)。
|
|
113
|
+
* 目的:回滚到旧代码时水位仍然读得到,不会走「首次全量」把整库 DELETE 重建。
|
|
76
114
|
*/
|
|
77
115
|
export function setMetaFor(db, baseKey, source, value) {
|
|
78
|
-
|
|
79
|
-
if (String(source) === 'memory')
|
|
116
|
+
metaWrite(db, metaKeyFor(baseKey, source), value);
|
|
117
|
+
if (String(source) === 'memory') metaWrite(db, baseKey, value);
|
|
80
118
|
}
|
|
81
119
|
|
|
82
120
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* @returns {string[]}
|
|
121
|
+
* 一次性命名空间迁移(幂等):把旧键现值播种到 `<key>:memory`。
|
|
122
|
+
* 触发条件 = 旧键在、且新键缺;跑几次都只迁一次,旧键保留不算删。
|
|
123
|
+
* @returns {string[]} 本次真正迁过的键名
|
|
86
124
|
*/
|
|
87
125
|
export function migrateMetaNamespace(db) {
|
|
88
|
-
const
|
|
89
|
-
for (const
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
const touched = [];
|
|
127
|
+
for (const base of SOURCE_SCOPED_META_KEYS) {
|
|
128
|
+
const legacyValue = metaRead(db, base);
|
|
129
|
+
if (legacyValue == null) continue;
|
|
130
|
+
const scopedKey = metaKeyFor(base, 'memory');
|
|
131
|
+
if (metaRead(db, scopedKey) != null) continue;
|
|
132
|
+
metaWrite(db, scopedKey, legacyValue);
|
|
133
|
+
touched.push(base);
|
|
95
134
|
}
|
|
96
|
-
return
|
|
135
|
+
return touched;
|
|
97
136
|
}
|
|
98
137
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return Boolean(r);
|
|
138
|
+
/** 内部:某张表在不在 */
|
|
139
|
+
function hasTable(db, name) {
|
|
140
|
+
const probe = db.prepare(SQL_TABLE_PROBE).get('table', name);
|
|
141
|
+
return probe !== undefined && probe !== null;
|
|
104
142
|
}
|
|
105
143
|
|
|
106
144
|
export function ensureSchema(db) {
|
|
107
|
-
const
|
|
108
|
-
if (!tableExists(db, 'vectors')) created.push('vectors');
|
|
109
|
-
if (!tableExists(db, 'meta')) created.push('meta');
|
|
145
|
+
const missing = ['vectors', 'meta'].filter((name) => !hasTable(db, name));
|
|
110
146
|
db.exec(DDL);
|
|
111
|
-
return { created, tables: { vectors:
|
|
147
|
+
return { created: missing, tables: { vectors: hasTable(db, 'vectors'), meta: hasTable(db, 'meta') } };
|
|
112
148
|
}
|
|
113
149
|
|
|
114
150
|
/**
|
|
@@ -117,176 +153,165 @@ export function ensureSchema(db) {
|
|
|
117
153
|
* @param {{create?: boolean}} [opts]
|
|
118
154
|
*/
|
|
119
155
|
export function openVectorsStore(dbPath, opts = {}) {
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
}
|
|
156
|
+
const mayCreate = opts.create !== false;
|
|
157
|
+
if (mayCreate) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
158
|
+
else if (!fs.existsSync(dbPath)) throw new Error(`向量库不存在: ${dbPath}`);
|
|
159
|
+
|
|
125
160
|
const db = new DatabaseSync(dbPath);
|
|
126
161
|
db.exec('PRAGMA busy_timeout = 5000;');
|
|
127
162
|
try {
|
|
128
163
|
db.exec('PRAGMA journal_mode = WAL;');
|
|
129
164
|
} catch {
|
|
130
|
-
|
|
165
|
+
// 只读介质上 WAL 可能起不来:退回默认日志模式即可,不影响读写语义
|
|
131
166
|
}
|
|
167
|
+
|
|
132
168
|
const schema = ensureSchema(db);
|
|
133
|
-
//
|
|
169
|
+
// 命名空间迁移放在 schemaVersion 之前:迁移发生过就顺手把版本号推上去(表结构没变,只是键位变宽)
|
|
134
170
|
const migrated = migrateMetaNamespace(db);
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// 迁移实际发生 ⇒ 升版本号(表结构未变,只是键位扩了命名空间)
|
|
140
|
-
setMeta(db, META_KEYS.schemaVersion, SCHEMA_VERSION);
|
|
141
|
-
}
|
|
171
|
+
const storedVersion = metaRead(db, META_KEYS.schemaVersion);
|
|
172
|
+
const shouldStamp = storedVersion == null || (migrated.length > 0 && storedVersion !== SCHEMA_VERSION);
|
|
173
|
+
if (shouldStamp) metaWrite(db, META_KEYS.schemaVersion, SCHEMA_VERSION);
|
|
174
|
+
|
|
142
175
|
return { db, dbPath, schema, migrated };
|
|
143
176
|
}
|
|
144
177
|
|
|
145
|
-
export
|
|
146
|
-
const r = db.prepare('SELECT v FROM meta WHERE k = ?').get(k);
|
|
147
|
-
return r ? r.v : null;
|
|
148
|
-
}
|
|
178
|
+
export const getMeta = (db, key) => metaRead(db, key);
|
|
149
179
|
|
|
150
|
-
export
|
|
151
|
-
db.prepare(
|
|
152
|
-
'INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v',
|
|
153
|
-
).run(String(k), String(v));
|
|
154
|
-
}
|
|
180
|
+
export const setMeta = (db, key, value) => metaWrite(db, key, value);
|
|
155
181
|
|
|
156
|
-
export
|
|
157
|
-
const
|
|
158
|
-
for (const
|
|
159
|
-
return
|
|
160
|
-
}
|
|
182
|
+
export const getMetaAll = (db) => {
|
|
183
|
+
const bag = {};
|
|
184
|
+
for (const row of db.prepare(SQL_META_DUMP).all()) bag[row.k] = row.v;
|
|
185
|
+
return bag;
|
|
186
|
+
};
|
|
161
187
|
|
|
162
188
|
export function vecToBlob(vec) {
|
|
163
|
-
const
|
|
164
|
-
return Buffer.from(
|
|
189
|
+
const floats = vec instanceof Float32Array ? vec : Float32Array.from(vec);
|
|
190
|
+
return Buffer.from(floats.buffer, floats.byteOffset, floats.byteLength);
|
|
165
191
|
}
|
|
166
192
|
|
|
167
193
|
export function blobToVec(blob) {
|
|
168
|
-
//
|
|
169
|
-
const
|
|
170
|
-
return new Float32Array(
|
|
194
|
+
// sqlite 回来的 Buffer 可能是池化分配的(byteOffset 不保证 4 字节对齐)⇒ 先按窗口切一份再建视图
|
|
195
|
+
const { buffer, byteOffset, byteLength } = blob;
|
|
196
|
+
return new Float32Array(buffer.slice(byteOffset, byteOffset + byteLength));
|
|
171
197
|
}
|
|
172
198
|
|
|
173
|
-
/**
|
|
174
|
-
|
|
175
|
-
if (!items.length) return 0;
|
|
176
|
-
const stmt = db.prepare(`
|
|
177
|
-
INSERT INTO vectors (source, ref_id, model, dim, vec, text_hash, updated_at)
|
|
178
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
179
|
-
ON CONFLICT(source, ref_id) DO UPDATE SET
|
|
180
|
-
model = excluded.model,
|
|
181
|
-
dim = excluded.dim,
|
|
182
|
-
vec = excluded.vec,
|
|
183
|
-
text_hash = excluded.text_hash,
|
|
184
|
-
updated_at = excluded.updated_at
|
|
185
|
-
`);
|
|
199
|
+
/** 内部:开事务跑一小段 body(异常先回滚再原样抛出) */
|
|
200
|
+
function inTransaction(db, body) {
|
|
186
201
|
db.exec('BEGIN');
|
|
187
202
|
try {
|
|
188
|
-
|
|
189
|
-
stmt.run(
|
|
190
|
-
it.source,
|
|
191
|
-
String(it.refId),
|
|
192
|
-
it.model,
|
|
193
|
-
it.dim,
|
|
194
|
-
vecToBlob(it.vec),
|
|
195
|
-
it.textHash,
|
|
196
|
-
it.updatedAt,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
203
|
+
const outcome = body();
|
|
199
204
|
db.exec('COMMIT');
|
|
200
|
-
|
|
205
|
+
return outcome;
|
|
206
|
+
} catch (err) {
|
|
201
207
|
try {
|
|
202
208
|
db.exec('ROLLBACK');
|
|
203
209
|
} catch {
|
|
204
|
-
|
|
210
|
+
// 回滚自己失败也不掩盖原始错误
|
|
205
211
|
}
|
|
206
|
-
throw
|
|
212
|
+
throw err;
|
|
207
213
|
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 批量 upsert(单事务) */
|
|
217
|
+
export function upsertVectors(db, items) {
|
|
218
|
+
if (items.length === 0) return 0;
|
|
219
|
+
const put = db.prepare(SQL_VECTOR_UPSERT);
|
|
220
|
+
inTransaction(db, () => {
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
put.run(
|
|
223
|
+
item.source,
|
|
224
|
+
String(item.refId),
|
|
225
|
+
item.model,
|
|
226
|
+
item.dim,
|
|
227
|
+
vecToBlob(item.vec),
|
|
228
|
+
item.textHash,
|
|
229
|
+
item.updatedAt,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
208
233
|
return items.length;
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
/** 取指定 ref_id
|
|
236
|
+
/** 取指定 ref_id 的已有哈希(增量同步据此跳过没变的条目) */
|
|
212
237
|
export function getHashes(db, source, refIds) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
215
|
-
const
|
|
216
|
-
for (const
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
238
|
+
const found = new Map();
|
|
239
|
+
if (refIds.length === 0) return found;
|
|
240
|
+
const probe = db.prepare(SQL_HASH_READ);
|
|
241
|
+
for (const refId of refIds) {
|
|
242
|
+
const row = probe.get(source, String(refId));
|
|
243
|
+
if (row) found.set(String(refId), row.text_hash);
|
|
219
244
|
}
|
|
220
|
-
return
|
|
245
|
+
return found;
|
|
221
246
|
}
|
|
222
247
|
|
|
223
248
|
export function countVectors(db, source) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
: db.prepare('SELECT COUNT(*) AS n FROM vectors').get();
|
|
227
|
-
return r.n;
|
|
249
|
+
const row = source ? db.prepare(SQL_COUNT_BY_SOURCE).get(source) : db.prepare(SQL_COUNT_ALL).get();
|
|
250
|
+
return row.n;
|
|
228
251
|
}
|
|
229
252
|
|
|
230
253
|
export function countByModel(db, source, model) {
|
|
231
|
-
return db
|
|
232
|
-
.prepare('SELECT COUNT(*) AS n FROM vectors WHERE source = ? AND model = ?')
|
|
233
|
-
.get(source, model).n;
|
|
254
|
+
return db.prepare(SQL_COUNT_BY_MODEL).get(source, model).n;
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
export function deleteSource(db, source) {
|
|
237
|
-
const
|
|
238
|
-
return Number(
|
|
258
|
+
const res = db.prepare(SQL_SOURCE_DELETE).run(source);
|
|
259
|
+
return Number(res.changes || 0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 空矩阵(形状与正常返回一致,调用方不用判空) */
|
|
263
|
+
function emptyMatrix() {
|
|
264
|
+
return {
|
|
265
|
+
refIds: [],
|
|
266
|
+
refIdsNum: [],
|
|
267
|
+
dim: 0,
|
|
268
|
+
data: new Float32Array(0),
|
|
269
|
+
count: 0,
|
|
270
|
+
textHashes: [],
|
|
271
|
+
models: [],
|
|
272
|
+
};
|
|
239
273
|
}
|
|
240
274
|
|
|
241
275
|
/**
|
|
242
|
-
*
|
|
243
|
-
* @returns {{refIds: string[], refIdsNum: number[], dim: number, data: Float32Array, count: number,
|
|
276
|
+
* 载入整个矩阵(召回用;万级候选直接内存暴力扫,方案 §3.3)。
|
|
277
|
+
* @returns {{refIds: string[], refIdsNum: number[], dim: number, data: Float32Array, count: number,
|
|
278
|
+
* textHashes: string[], models: string[]}}
|
|
244
279
|
*/
|
|
245
280
|
export function loadMatrix(db, source, model = null) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
count: 0,
|
|
264
|
-
textHashes: [],
|
|
265
|
-
models: [],
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
const dim = rows[0].dim;
|
|
269
|
-
const data = new Float32Array(rows.length * dim);
|
|
270
|
-
rows.forEach((r, i) => {
|
|
271
|
-
if (r.dim !== dim) {
|
|
281
|
+
const filterByModel = Boolean(model);
|
|
282
|
+
const sql = `${filterByModel ? `${SQL_MATRIX_BASE} AND model = ?` : SQL_MATRIX_BASE} ${SQL_MATRIX_ORDER}`;
|
|
283
|
+
const recs = filterByModel ? db.prepare(sql).all(source, model) : db.prepare(sql).all(source);
|
|
284
|
+
if (recs.length === 0) return emptyMatrix();
|
|
285
|
+
|
|
286
|
+
const dim = recs[0].dim;
|
|
287
|
+
const count = recs.length;
|
|
288
|
+
const data = new Float32Array(count * dim);
|
|
289
|
+
const refIds = [];
|
|
290
|
+
const refIdsNum = [];
|
|
291
|
+
const textHashes = [];
|
|
292
|
+
const models = [];
|
|
293
|
+
const seenModel = new Set();
|
|
294
|
+
|
|
295
|
+
for (let i = 0; i < count; i += 1) {
|
|
296
|
+
const rec = recs[i];
|
|
297
|
+
if (rec.dim !== dim) {
|
|
272
298
|
throw new Error(
|
|
273
|
-
`向量库维度不一致:ref_id=${
|
|
299
|
+
`向量库维度不一致:ref_id=${rec.ref_id} dim=${rec.dim},首个为 ${dim}(应全量重建)`,
|
|
274
300
|
);
|
|
275
301
|
}
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
throw new Error(`向量 blob 长度异常:ref_id=${
|
|
302
|
+
const vec = blobToVec(rec.vec);
|
|
303
|
+
if (vec.length !== dim) {
|
|
304
|
+
throw new Error(`向量 blob 长度异常:ref_id=${rec.ref_id} len=${vec.length} 期望 ${dim}`);
|
|
279
305
|
}
|
|
280
|
-
data.set(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
};
|
|
306
|
+
data.set(vec, i * dim);
|
|
307
|
+
refIds.push(String(rec.ref_id));
|
|
308
|
+
refIdsNum.push(Number(rec.ref_id));
|
|
309
|
+
textHashes.push(rec.text_hash);
|
|
310
|
+
if (!seenModel.has(rec.model)) {
|
|
311
|
+
seenModel.add(rec.model);
|
|
312
|
+
models.push(rec.model);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { refIds, refIdsNum, dim, data, count, textHashes, models };
|
|
292
317
|
}
|