dsh-tool-folder 0.3.0 → 0.3.2

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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * static-embed.js — 离线静态嵌入(零推理 / 零网络 / 零依赖)
3
+ *
4
+ * 为什么需要:语义腿原有两条路都不可靠——
5
+ * Ark doubao-embedding:订阅过期即 400,熔断 5 分钟
6
+ * Ollama bge-m3:本机未安装(11434 返回 000)
7
+ * 实测(eval/queries-holdout.json,29 条留出查询)纯词法 + 意图词表只到
8
+ * recall@3 0.448 —— 剩下 55% 是词表覆盖不到的改写,必须有语义。
9
+ *
10
+ * 做法(model2vec 思路,MinishLab):把 sentence transformer 蒸馏成一张
11
+ * 「token → 向量」查找表,运行期只做 查表 + 均值池化 + 余弦。
12
+ * 无推理图、无 onnx、无 WASM、无网络。
13
+ *
14
+ * 构建:tools/build-static-embed.py(离线,读本地 safetensors)
15
+ * 产物三件套,写入 lib/static-embed/:
16
+ * vocab.json {"dim":256,"dtype":"int8","count":N,"tokens":{token:rowIndex}}
17
+ * vectors.bin int8 行优先,行已 L2 归一化后再量化
18
+ * scales.bin float32 每行独立 scale
19
+ * meta.json 构建信息
20
+ *
21
+ * 分词口径必须与 Python 侧 tokenize_mixed() 逐字一致,
22
+ * 否则构建期建的词和运行期查的词对不上。
23
+ */
24
+
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+
28
+ const CJK_CHAR = /[\u4e00-\u9fff]/;
29
+ const PIECE = /[\u4e00-\u9fff]|[A-Za-z0-9]+/g;
30
+
31
+ let _cache = null;
32
+
33
+ /**
34
+ * 与 Python tokenize_mixed() 完全同构:
35
+ * 1) camelCase / PascalCase 在 lowercase 之前拆开
36
+ * 2) CJK 单字成 token
37
+ * 3) 拉丁/数字按连续段切,lowercase,长度 ≥ 2 才收
38
+ */
39
+ export function tokenizeMixed(text) {
40
+ let s = String(text == null ? "" : text);
41
+ s = s
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
43
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
44
+
45
+ const out = [];
46
+ PIECE.lastIndex = 0;
47
+ let m;
48
+ while ((m = PIECE.exec(s)) !== null) {
49
+ const part = m[0];
50
+ if (part.length === 1 && CJK_CHAR.test(part)) {
51
+ out.push(part);
52
+ } else {
53
+ const w = part.toLowerCase();
54
+ if (w.length >= 2) out.push(w);
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /** 读 meta/vocab/vectors/scales,任何一步失败都返回 null(调用方退回词法)。 */
61
+ function loadFrom(dir) {
62
+ try {
63
+ const p = (f) => path.join(dir, f);
64
+ const files = [p("meta.json"), p("vocab.json"), p("vectors.bin")];
65
+ for (const f of files) if (!fs.existsSync(f)) return null;
66
+
67
+ const mtime = fs.statSync(p("vectors.bin")).mtimeMs;
68
+ if (_cache && _cache.dir === dir && _cache.mtime === mtime) return _cache;
69
+
70
+ const meta = JSON.parse(fs.readFileSync(p("meta.json"), "utf8"));
71
+ const vj = JSON.parse(fs.readFileSync(p("vocab.json"), "utf8"));
72
+ const dim = Number(meta.dim || vj.dim);
73
+ const count = Number(meta.count || vj.count);
74
+ if (!dim || !count) return null;
75
+
76
+ const vbuf = fs.readFileSync(p("vectors.bin"));
77
+ if (vbuf.length < count * dim) return null;
78
+ const vectors = new Int8Array(vbuf.buffer, vbuf.byteOffset, count * dim);
79
+
80
+ // 每行独立 scale;退化为全局 1 时不报错
81
+ let scales = new Float32Array(count).fill(1);
82
+ if (fs.existsSync(p("scales.bin"))) {
83
+ const sbuf = fs.readFileSync(p("scales.bin"));
84
+ if (sbuf.length >= count * 4) {
85
+ const dv = new DataView(sbuf.buffer, sbuf.byteOffset, count * 4);
86
+ for (let i = 0; i < count; i++) scales[i] = dv.getFloat32(i * 4, true);
87
+ }
88
+ }
89
+
90
+ const tokens = vj.tokens || {};
91
+ const isArray = Array.isArray(tokens);
92
+ const vocab = new Map();
93
+ if (isArray) {
94
+ for (let i = 0; i < tokens.length; i++) vocab.set(tokens[i], i);
95
+ } else {
96
+ for (const [k, v] of Object.entries(tokens)) vocab.set(k, v | 0);
97
+ }
98
+
99
+ _cache = {
100
+ dir,
101
+ mtime,
102
+ vocab,
103
+ vectors,
104
+ scales,
105
+ dim,
106
+ count,
107
+ model: meta.source || meta.model || "",
108
+ bytes: vbuf.length + count * 4,
109
+ };
110
+ return _cache;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /** token 序列 → 行下标(丢掉词表外的 token)。 */
117
+ export function toIds(text, st) {
118
+ const ids = [];
119
+ for (const t of tokenizeMixed(text)) {
120
+ const id = st.vocab.get(t);
121
+ if (id !== undefined) ids.push(id);
122
+ }
123
+ return ids;
124
+ }
125
+
126
+ /** 均值池化(含每行反量化)→ L2 归一化 Float32Array。空文本返回 null。 */
127
+ export function pool(ids, st) {
128
+ if (!ids.length) return null;
129
+ const { dim, vectors, scales } = st;
130
+ const v = new Float32Array(dim);
131
+ for (let k = 0; k < ids.length; k++) {
132
+ const id = ids[k];
133
+ const base = id * dim;
134
+ const sc = scales[id];
135
+ for (let d = 0; d < dim; d++) v[d] += vectors[base + d] * sc;
136
+ }
137
+ let norm = 0;
138
+ for (let d = 0; d < dim; d++) norm += v[d] * v[d];
139
+ norm = Math.sqrt(norm);
140
+ if (norm > 1e-12) {
141
+ for (let d = 0; d < dim; d++) v[d] /= norm;
142
+ } else {
143
+ return null;
144
+ }
145
+ return v;
146
+ }
147
+
148
+ /** 余弦(两边已归一化时等价点积)。 */
149
+ export function cosine(a, b) {
150
+ if (!a || !b) return 0;
151
+ const n = Math.min(a.length, b.length);
152
+ let s = 0;
153
+ for (let i = 0; i < n; i++) s += a[i] * b[i];
154
+ return s;
155
+ }
156
+
157
+ /**
158
+ * 打开静态嵌入目录。
159
+ * @param {string} dir
160
+ * @returns {{available:boolean, dim:number, count:number, model:string, bytes:number,
161
+ * embed:(t:string)=>Float32Array|null,
162
+ * embedMany:(ts:string[])=>Array<Float32Array|null>}}
163
+ */
164
+ export function openStaticEmbed(dir) {
165
+ const st = dir ? loadFrom(dir) : null;
166
+ if (!st) {
167
+ return {
168
+ available: false, dim: 0, count: 0, model: "", bytes: 0,
169
+ embed: () => null,
170
+ embedMany: (n) => n.map(() => null),
171
+ };
172
+ }
173
+ return {
174
+ available: true,
175
+ dim: st.dim,
176
+ count: st.count,
177
+ model: st.model,
178
+ bytes: st.bytes,
179
+ embed: (t) => pool(toIds(t, st), st),
180
+ embedMany: (ts) => ts.map((t) => pool(toIds(t, st), st)),
181
+ };
182
+ }
183
+
184
+ /** 在候选向量里按余弦排序,返回 [{index, score}]。 */
185
+ export function rankByVector(queryVec, vectors, topK = 10) {
186
+ if (!queryVec) return [];
187
+ const out = [];
188
+ for (let i = 0; i < vectors.length; i++) {
189
+ const v = vectors[i];
190
+ if (!v) continue;
191
+ out.push([i, cosine(queryVec, v)]);
192
+ }
193
+ out.sort((a, b) => b[1] - a[1]);
194
+ return out.slice(0, topK).map(([i, s]) => ({ index: i, score: s }));
195
+ }
package/package.json CHANGED
@@ -1,56 +1,56 @@
1
- {
2
- "name": "dsh-tool-folder",
3
- "version": "0.3.0",
4
- "description": "Fold the DSH tool surface per request + ChainGuard firewall. v0.3.0: remote-first semantic retrieval (Ark doubao-embedding / Ollama fallback), weighted RRF with adaptive BM25 downweight, batched incremental index (partial results cached), body-level rerank (opt-in), duplicate-id fusion fix.",
5
- "keywords": [
6
- "dsh-plugin",
7
- "dsh",
8
- "deepseek-harness",
9
- "cordis",
10
- "tool-selection",
11
- "token-optimization",
12
- "bm25",
13
- "semantic-search",
14
- "security",
15
- "firewall",
16
- "obfuscation"
17
- ],
18
- "type": "module",
19
- "main": "lib/index.js",
20
- "files": [
21
- "lib",
22
- "cordis.patch.yml",
23
- "README.md"
24
- ],
25
- "dsh": {
26
- "bundle": {
27
- "patch": "./cordis.patch.yml"
28
- }
29
- },
30
- "license": "MIT",
31
- "engines": {
32
- "node": ">=18"
33
- },
34
- "dependencies": {
35
- "@deepseek-ai/schemastery": "^3.18.1"
36
- },
37
- "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
40
- },
41
- "scripts": {
42
- "test": "node --test \"test/*.test.js\""
43
- },
44
- "exports": {
45
- ".": "./lib/index.js",
46
- "./lib/*": "./lib/*"
47
- },
48
- "peerDependenciesMeta": {
49
- "@deepseek-ai/cordis": {
50
- "optional": true
51
- },
52
- "@deepseek-ai/dsh-tools": {
53
- "optional": true
54
- }
55
- }
56
- }
1
+ {
2
+ "name": "dsh-tool-folder",
3
+ "version": "0.3.2",
4
+ "description": "v0.3.2: offline static embedding leg (7760x256 int8, 2MB, built from potion-multilingual — replaces dead Ark/Ollama paths), staticWeight 0.15 tuned by sweep, empty-query guard. Fold the DSH tool surface per request + ChainGuard firewall. v0.3.1: intent thesaurus expansion (CN/EN bidirectional + reverse aliases), camelCase name splitting, CJK stop-bigram filtering, body-rerank weight tuned 0.35->0.45, RRF fusion option. v0.3.0: remote-first semantic retrieval (Ark doubao-embedding / Ollama fallback), weighted RRF with adaptive BM25 downweight, batched incremental index (partial results cached), body-level rerank (opt-in), duplicate-id fusion fix.",
5
+ "keywords": [
6
+ "dsh-plugin",
7
+ "dsh",
8
+ "deepseek-harness",
9
+ "cordis",
10
+ "tool-selection",
11
+ "token-optimization",
12
+ "bm25",
13
+ "semantic-search",
14
+ "security",
15
+ "firewall",
16
+ "obfuscation"
17
+ ],
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "files": [
21
+ "lib",
22
+ "cordis.patch.yml",
23
+ "README.md"
24
+ ],
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ }
29
+ },
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.1"
36
+ },
37
+ "peerDependencies": {
38
+ "@deepseek-ai/cordis": "^4.0.1",
39
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
40
+ },
41
+ "scripts": {
42
+ "test": "node --test \"test/*.test.js\""
43
+ },
44
+ "exports": {
45
+ ".": "./lib/index.js",
46
+ "./lib/*": "./lib/*"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@deepseek-ai/cordis": {
50
+ "optional": true
51
+ },
52
+ "@deepseek-ai/dsh-tools": {
53
+ "optional": true
54
+ }
55
+ }
56
+ }