@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,242 +1,223 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// 设计要点:同步/召回核心一律依赖本接口,不直接 import 模型运行时
|
|
9
|
-
// ⇒ 模型拉不动时,骨架与单测仍可完整跑通(本卡 §二.6 的退路)。
|
|
10
|
-
|
|
11
|
-
import crypto from 'node:crypto';
|
|
1
|
+
// 编码器层:同一个接口的三种实现,调用方只认 encode / encodeBatch / health / describe。
|
|
2
|
+
// · stub —— 确定性哈希,零依赖,只给骨架与单测用(没有半点语义质量)
|
|
3
|
+
// · http —— 回环编码桥的客户端(超时默认 250ms)
|
|
4
|
+
// · local —— 本地 ONNX 推理(bge 小模型,CPU 上跑)
|
|
5
|
+
// 模型运行时只在 local 里动态 import ⇒ 权重拉不下来时,其余实现与测试照样能跑。
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
12
8
|
import fs from 'node:fs';
|
|
13
9
|
import path from 'node:path';
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const STUB_MAX_N = 3;
|
|
12
|
+
const HEALTH_TIMEOUT_MS = 2000;
|
|
13
|
+
const BATCH_TIMEOUT_FLOOR_MS = 30000;
|
|
14
|
+
const LOCAL_PROBE_TEXT = '维度自检';
|
|
15
|
+
|
|
16
|
+
const errorText = (e) => (e && e.message ? e.message : String(e));
|
|
17
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
18
|
+
|
|
19
|
+
// 原地单位化;零向量按长度 1 处理,免得除零把整条变成 NaN。
|
|
20
|
+
function unitize(vec) {
|
|
21
|
+
let squareSum = 0;
|
|
22
|
+
for (let i = 0; i < vec.length; i += 1) squareSum += vec[i] * vec[i];
|
|
23
|
+
const length = Math.sqrt(squareSum) || 1;
|
|
24
|
+
for (let i = 0; i < vec.length; i += 1) vec[i] /= length;
|
|
25
|
+
return vec;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 连着的 n 元切片,n = 1..maxN;先统一小写再切。
|
|
29
|
+
function ngramsOf(text, maxN) {
|
|
30
|
+
const body = text.toLowerCase();
|
|
31
|
+
const grams = [];
|
|
32
|
+
for (let n = 1; n <= maxN; n += 1) {
|
|
33
|
+
for (let at = 0; at + n <= body.length; at += 1) grams.push(body.slice(at, at + n));
|
|
34
|
+
}
|
|
35
|
+
return grams;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 测试骨架用的确定性编码器:每个 n 元算一次 md5,头两字节定槽位、第三字节定正负。 */
|
|
39
|
+
export function createStubEmbedder({ dim = 512, sleepMs = 0, modelId = 'stub-ngram' } = {}) {
|
|
17
40
|
let encodeCount = 0;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (const g of grams) {
|
|
26
|
-
const h = crypto.createHash('md5').update(g).digest();
|
|
27
|
-
const idx = ((h[0] << 8) | h[1]) % dim;
|
|
28
|
-
const sign = h[2] % 2 === 0 ? 1 : -1;
|
|
29
|
-
v[idx] += sign;
|
|
41
|
+
|
|
42
|
+
const hashToVector = (text) => {
|
|
43
|
+
const vec = new Float32Array(dim);
|
|
44
|
+
for (const gram of ngramsOf(String(text ?? ''), STUB_MAX_N)) {
|
|
45
|
+
const digest = createHash('md5').update(gram).digest();
|
|
46
|
+
const slot = ((digest[0] << 8) | digest[1]) % dim;
|
|
47
|
+
vec[slot] += digest[2] % 2 === 0 ? 1 : -1;
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
for (let i = 0; i < dim; i += 1) norm += v[i] * v[i];
|
|
33
|
-
norm = Math.sqrt(norm) || 1;
|
|
34
|
-
for (let i = 0; i < dim; i += 1) v[i] /= norm;
|
|
35
|
-
return v;
|
|
49
|
+
return unitize(vec);
|
|
36
50
|
};
|
|
51
|
+
|
|
52
|
+
// 每次编码都过这里:模拟耗时 + 计数(两者都是测试要观测的)
|
|
53
|
+
const encodeOnce = async (text) => {
|
|
54
|
+
if (sleepMs) await sleep(sleepMs);
|
|
55
|
+
encodeCount = encodeCount + 1;
|
|
56
|
+
return hashToVector(text);
|
|
57
|
+
};
|
|
58
|
+
|
|
37
59
|
return {
|
|
38
|
-
kind: 'stub',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
kind: 'stub', modelId, dim,
|
|
61
|
+
describe: () => ({
|
|
62
|
+
kind: 'stub',
|
|
63
|
+
modelId,
|
|
64
|
+
dim,
|
|
65
|
+
note: '确定性哈希编码器,仅用于骨架自测',
|
|
66
|
+
}),
|
|
67
|
+
health: async () => ({ ready: true, modelLoading: false, model: modelId, dim }),
|
|
68
|
+
encode: (text) => encodeOnce(text),
|
|
69
|
+
encodeBatch: async (texts) => {
|
|
70
|
+
const vectors = [];
|
|
71
|
+
for (const text of texts) vectors.push(await encodeOnce(text));
|
|
72
|
+
return vectors;
|
|
49
73
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const t of texts) {
|
|
53
|
-
if (sleepMs) await new Promise((r) => setTimeout(r, sleepMs));
|
|
54
|
-
encodeCount += 1;
|
|
55
|
-
out.push(encodeOne(t));
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
74
|
+
stats() {
|
|
75
|
+
return { encodeCount };
|
|
58
76
|
},
|
|
59
|
-
stats: () => ({ encodeCount }),
|
|
60
77
|
};
|
|
61
78
|
}
|
|
62
79
|
|
|
63
|
-
/**
|
|
64
|
-
export function createHttpEmbedder({
|
|
65
|
-
host = '127.0.0.1',
|
|
66
|
-
port = 8201,
|
|
67
|
-
timeoutMs = 250,
|
|
68
|
-
modelId = null,
|
|
69
|
-
dim = null,
|
|
70
|
-
} = {}) {
|
|
80
|
+
/** 回环编码桥的客户端:只发 JSON,一切都带超时,桥不在就如实报错而不是挂着。 */
|
|
81
|
+
export function createHttpEmbedder({ host = '127.0.0.1', port = 8201, timeoutMs = 250, modelId = null, dim = null } = {}) {
|
|
71
82
|
const base = `http://${host}:${port}`;
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
let lastHealth = null;
|
|
84
|
+
|
|
85
|
+
// payload 为 null ⇒ 走 GET;否则按 JSON POST 发出去。
|
|
86
|
+
const callJson = async (url, payload, ms) => {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
76
89
|
try {
|
|
77
|
-
const
|
|
90
|
+
const init = payload
|
|
91
|
+
? {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body: JSON.stringify(payload),
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
}
|
|
97
|
+
: { signal: controller.signal };
|
|
98
|
+
const res = await fetch(url, init);
|
|
78
99
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
79
100
|
return await res.json();
|
|
80
101
|
} finally {
|
|
81
|
-
clearTimeout(
|
|
102
|
+
clearTimeout(timer);
|
|
82
103
|
}
|
|
83
104
|
};
|
|
105
|
+
|
|
84
106
|
return {
|
|
85
107
|
kind: 'http',
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
get dim() {
|
|
90
|
-
return dim || (healthCache && healthCache.dim) || 0;
|
|
91
|
-
},
|
|
108
|
+
// 显式给了就用手上的;没给就跟着最近一次探活的结果走(读那一瞬才算)
|
|
109
|
+
get modelId() { return modelId || (lastHealth && lastHealth.model) || 'unknown'; },
|
|
110
|
+
get dim() { return dim || (lastHealth && lastHealth.dim) || 0; },
|
|
92
111
|
describe: () => ({ kind: 'http', base, modelId, dim, timeoutMs }),
|
|
93
|
-
async
|
|
112
|
+
health: async () => {
|
|
94
113
|
try {
|
|
95
|
-
|
|
96
|
-
return { ...
|
|
114
|
+
lastHealth = await callJson(`${base}/health`, null, HEALTH_TIMEOUT_MS);
|
|
115
|
+
return { ...lastHealth, bridge: base };
|
|
97
116
|
} catch (e) {
|
|
98
|
-
return { ready: false, modelLoading: false, error:
|
|
117
|
+
return { ready: false, modelLoading: false, error: errorText(e), bridge: base };
|
|
99
118
|
}
|
|
100
119
|
},
|
|
101
|
-
async
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
method: 'POST',
|
|
106
|
-
headers: { 'content-type': 'application/json' },
|
|
107
|
-
body: JSON.stringify({ texts: [String(text ?? '')] }),
|
|
108
|
-
},
|
|
109
|
-
timeoutMs,
|
|
110
|
-
);
|
|
111
|
-
return Float32Array.from(out.vectors[0]);
|
|
120
|
+
encode: async (text) => {
|
|
121
|
+
const answer = await callJson(`${base}/encode`, { texts: [String(text ?? '')] }, timeoutMs);
|
|
122
|
+
return Float32Array.from(answer.vectors[0]);
|
|
112
123
|
},
|
|
113
|
-
async
|
|
114
|
-
if (!texts.length)
|
|
115
|
-
|
|
124
|
+
encodeBatch: async (texts) => {
|
|
125
|
+
if (!texts.length) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const answer = await callJson(
|
|
116
129
|
`${base}/encode`,
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
headers: { 'content-type': 'application/json' },
|
|
120
|
-
body: JSON.stringify({ texts: texts.map((t) => String(t ?? '')) }),
|
|
121
|
-
},
|
|
122
|
-
Math.max(timeoutMs, 30000),
|
|
130
|
+
{ texts: texts.map((text) => String(text ?? '')) },
|
|
131
|
+
Math.max(timeoutMs, BATCH_TIMEOUT_FLOOR_MS),
|
|
123
132
|
);
|
|
124
|
-
return
|
|
133
|
+
return answer.vectors.map((row) => Float32Array.from(row));
|
|
134
|
+
},
|
|
135
|
+
stats() {
|
|
136
|
+
return { encodeCount: -1 };
|
|
125
137
|
},
|
|
126
|
-
stats: () => ({ encodeCount: -1 }),
|
|
127
138
|
};
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
// 权重目录有两种摆法:要么 config.json 就在这一层,要么下面还套一层以模型名命名的子目录。
|
|
142
|
+
// 推理库要的是"根目录 + 子目录名",所以先探一下再决定怎么喂。
|
|
143
|
+
function resolveWeightLayout(modelDir, modelId) {
|
|
144
|
+
if (!fs.existsSync(path.join(modelDir, 'config.json'))) {
|
|
145
|
+
return { root: modelDir, id: modelId };
|
|
146
|
+
}
|
|
147
|
+
return { root: path.dirname(modelDir), id: path.basename(modelDir) };
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
/**
|
|
131
|
-
* 本地 ONNX
|
|
132
|
-
*
|
|
151
|
+
* 本地 ONNX 编码器:默认取 cls 位做向量,也可以按注意力掩码做均值池化。
|
|
152
|
+
* 维度以**实测**为准(模型真吐几维就是几维),配置里那个数只作比对。
|
|
133
153
|
*/
|
|
134
|
-
export async function createLocalEmbedder({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
: {};
|
|
164
|
-
const tokenizer = await AutoTokenizer.from_pretrained(localId, { local_files_only: true });
|
|
165
|
-
const model = await AutoModel.from_pretrained(localId, { local_files_only: true, ...loadOpts });
|
|
166
|
-
|
|
167
|
-
const encodeOne = async (text) => {
|
|
168
|
-
const input = String(text ?? '');
|
|
169
|
-
const inputs = await tokenizer(input, { padding: true, truncation: true, max_length: maxLength });
|
|
170
|
-
const out = await model(inputs);
|
|
171
|
-
const hidden = out.last_hidden_state ?? out[Object.keys(out)[0]];
|
|
172
|
-
const [, seq, d] = hidden.dims;
|
|
173
|
-
const data = hidden.data;
|
|
174
|
-
const v = new Float32Array(d);
|
|
154
|
+
export async function createLocalEmbedder({ modelDir, modelId = 'bge-small-zh-v1.5', dim = 512, quantized = false, dtype = null, maxLength = 512, pooling = 'cls', queryPrefix = '' } = {}) {
|
|
155
|
+
const runtime = await import('@huggingface/transformers');
|
|
156
|
+
const { env, AutoTokenizer, AutoModel } = runtime;
|
|
157
|
+
const layout = resolveWeightLayout(modelDir, modelId);
|
|
158
|
+
|
|
159
|
+
// 全离线:只认本地目录,不碰远端、不占浏览器缓存
|
|
160
|
+
Object.assign(env, {
|
|
161
|
+
allowRemoteModels: false,
|
|
162
|
+
allowLocalModels: true,
|
|
163
|
+
localModelPath: layout.root,
|
|
164
|
+
useBrowserCache: false,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const loadOptions = { local_files_only: true };
|
|
168
|
+
if (dtype) loadOptions.dtype = dtype;
|
|
169
|
+
else if (quantized) loadOptions.dtype = 'q8';
|
|
170
|
+
const tokenizer = await AutoTokenizer.from_pretrained(layout.id, { local_files_only: true });
|
|
171
|
+
const model = await AutoModel.from_pretrained(layout.id, loadOptions);
|
|
172
|
+
|
|
173
|
+
const embedOnce = async (text) => {
|
|
174
|
+
const inputs = await tokenizer(String(text ?? ''), {
|
|
175
|
+
padding: true,
|
|
176
|
+
truncation: true,
|
|
177
|
+
max_length: maxLength,
|
|
178
|
+
});
|
|
179
|
+
const output = await model(inputs);
|
|
180
|
+
const hidden = output.last_hidden_state ?? output[Object.keys(output)[0]];
|
|
181
|
+
const [, tokenCount, width] = hidden.dims;
|
|
182
|
+
const vec = new Float32Array(width);
|
|
175
183
|
if (pooling === 'cls') {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
n = n || 1;
|
|
188
|
-
for (let i = 0; i < d; i += 1) v[i] /= n;
|
|
184
|
+
vec.set(hidden.data.subarray(0, width));
|
|
185
|
+
return unitize(vec);
|
|
186
|
+
}
|
|
187
|
+
// 均值池化:被掩码盖掉的位不参与平均
|
|
188
|
+
const mask = inputs.attention_mask?.data;
|
|
189
|
+
let kept = 0;
|
|
190
|
+
for (let token = 0; token < tokenCount; token += 1) {
|
|
191
|
+
if (mask && !Number(mask[token])) continue;
|
|
192
|
+
kept += 1;
|
|
193
|
+
for (let i = 0; i < width; i += 1) vec[i] += hidden.data[token * width + i];
|
|
189
194
|
}
|
|
190
|
-
|
|
191
|
-
for (let i = 0; i <
|
|
192
|
-
|
|
193
|
-
for (let i = 0; i < d; i += 1) v[i] /= norm;
|
|
194
|
-
return v;
|
|
195
|
+
const divisor = kept || 1;
|
|
196
|
+
for (let i = 0; i < width; i += 1) vec[i] /= divisor;
|
|
197
|
+
return unitize(vec);
|
|
195
198
|
};
|
|
196
199
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return probe.length;
|
|
200
|
-
})();
|
|
200
|
+
// 用一句固定探针量真维度(此时不加查询前缀)
|
|
201
|
+
const measuredDim = (await embedOnce(LOCAL_PROBE_TEXT)).length;
|
|
201
202
|
|
|
202
203
|
return {
|
|
203
|
-
kind: 'local',
|
|
204
|
-
modelId,
|
|
205
|
-
dim:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
dim: actualDim,
|
|
212
|
-
pooling,
|
|
213
|
-
quantized,
|
|
214
|
-
dtype: dtype || (quantized ? 'q8' : 'fp32'),
|
|
215
|
-
queryPrefix,
|
|
216
|
-
}),
|
|
217
|
-
async health() {
|
|
218
|
-
return {
|
|
219
|
-
ready: true,
|
|
220
|
-
modelLoading: false,
|
|
221
|
-
model: modelId,
|
|
222
|
-
dim: actualDim,
|
|
223
|
-
modelDir,
|
|
224
|
-
};
|
|
225
|
-
},
|
|
226
|
-
async encode(text) {
|
|
227
|
-
return encodeOne(String(queryPrefix || '') + String(text ?? ''));
|
|
204
|
+
kind: 'local', modelId, dim: measuredDim, configuredDim: dim,
|
|
205
|
+
describe: () => ({ kind: 'local', modelId, modelDir, dim: measuredDim, pooling, quantized, dtype: dtype || (quantized ? 'q8' : 'fp32'), queryPrefix }),
|
|
206
|
+
health: async () => ({ ready: true, modelLoading: false, model: modelId, dim: measuredDim, modelDir }),
|
|
207
|
+
encode: (text) => embedOnce(String(queryPrefix || '') + String(text ?? '')),
|
|
208
|
+
encodeBatch: async (texts) => {
|
|
209
|
+
const vectors = [];
|
|
210
|
+
for (const text of texts) vectors.push(await embedOnce(text));
|
|
211
|
+
return vectors;
|
|
228
212
|
},
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const t of texts) out.push(await encodeOne(t));
|
|
232
|
-
return out;
|
|
213
|
+
stats() {
|
|
214
|
+
return { encodeCount: -1 };
|
|
233
215
|
},
|
|
234
|
-
stats: () => ({ encodeCount: -1 }),
|
|
235
216
|
};
|
|
236
217
|
}
|
|
237
218
|
|
|
238
|
-
/**
|
|
219
|
+
/** 按偏好挑一个:要 http 就走桥,其余一律走本地。 */
|
|
239
220
|
export async function createAutoEmbedder({ prefer = 'local', ...opts } = {}) {
|
|
240
|
-
|
|
241
|
-
return
|
|
221
|
+
const build = prefer === 'http' ? createHttpEmbedder : createLocalEmbedder;
|
|
222
|
+
return build(opts);
|
|
242
223
|
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// 正文与标识的摘要工具。
|
|
2
|
+
//
|
|
3
|
+
// 全仓只用一种算法:sha256 的十六进制形态。选它的理由是**跨机可复算** ——
|
|
4
|
+
// 日志、台账、去重键都拿这个值对齐,换机器重算必须得到同一串。
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
const ALGORITHM = 'sha256';
|
|
8
|
+
const SHORT_LEN = 12;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 正文指纹。
|
|
12
|
+
* 入参先归一到字符串(null / undefined 折成空串),
|
|
13
|
+
* 免得同一份正文因为传参形态不同算出两个哈希。
|
|
14
|
+
*/
|
|
5
15
|
export function textHash(text) {
|
|
6
|
-
|
|
16
|
+
const source = text == null ? '' : String(text);
|
|
17
|
+
return createHash(ALGORITHM).update(source, 'utf8').digest('hex');
|
|
7
18
|
}
|
|
8
19
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
/**
|
|
21
|
+
* 截短显示(日志与报告里用)。
|
|
22
|
+
* 哈希本身是 ASCII 十六进制,按码元切与按码点切结果相同,这里沿用 String#slice。
|
|
23
|
+
*/
|
|
24
|
+
export function shortHash(h, n = SHORT_LEN) {
|
|
25
|
+
const value = h || '';
|
|
26
|
+
return String(value).slice(0, n);
|
|
12
27
|
}
|