@yugoo-design/dsh-design-knowledge-ladybug 0.1.0-beta.3 → 0.1.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/README.md +34 -0
- package/lib/{dsh-plugin-C0TG9tJI.js → dsh-plugin-BWO-rNYF.js} +67 -4
- package/lib/dsh-plugin.js +1 -2
- package/lib/embedding-diagnostic-CvIl3TWJ.js +104 -0
- package/lib/embedding-worker.js +12 -2
- package/lib/index.d.ts +7 -0
- package/lib/index.js +2 -2
- package/lib/semantic-index-CigpMVTO.js +599 -0
- package/lib/semantic-index.d.ts +1 -0
- package/lib/semantic-index.js +1 -366
- package/licenses/dsh-distribution.json +411 -9
- package/package.json +8 -3
package/lib/semantic-index.js
CHANGED
|
@@ -1,367 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isAbsolute, resolve } from "node:path";
|
|
3
|
-
import { SEMANTIC_TEXT_VERSION, normalizeVector, semanticDigest, semanticEdges } from "@yugoo-design/dsh-design-core";
|
|
4
|
-
import { fork } from "node:child_process";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
//#region src/semantic-index.ts
|
|
7
|
-
const localEmbeddingFailures = {
|
|
8
|
-
runtime: "本地 embedding 运行依赖不可用;请按当前操作系统安装依赖(包括原生模块),不要复用其他平台的 node_modules。",
|
|
9
|
-
model: "本地 embedding 模型加载失败;请检查固定版本模型缓存,离线模式需要先准备完整模型文件。",
|
|
10
|
-
inference: "本地 embedding 推理失败;请检查输入限制和本地内存、CPU 运行资源。"
|
|
11
|
-
};
|
|
12
|
-
/** Disposable, project-local cache. Never stores Knowledge/GRA facts. */
|
|
13
|
-
var MemorySemanticCache = class {
|
|
14
|
-
limit;
|
|
15
|
-
byteLimit;
|
|
16
|
-
values = /* @__PURE__ */ new Map();
|
|
17
|
-
bytes = 0;
|
|
18
|
-
constructor(limit = 300, byteLimit = 4e6) {
|
|
19
|
-
this.limit = limit;
|
|
20
|
-
this.byteLimit = byteLimit;
|
|
21
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || !Number.isSafeInteger(byteLimit) || byteLimit < 1) throw new Error("Invalid volatile cache bounds");
|
|
22
|
-
}
|
|
23
|
-
async readSemanticCache(key) {
|
|
24
|
-
return this.values.get(key);
|
|
25
|
-
}
|
|
26
|
-
async writeSemanticCache(key, value) {
|
|
27
|
-
const size = Buffer.byteLength(key) + Buffer.byteLength(value);
|
|
28
|
-
if (size > this.byteLimit) return;
|
|
29
|
-
const old = this.values.get(key);
|
|
30
|
-
if (old !== void 0) {
|
|
31
|
-
this.bytes -= Buffer.byteLength(key) + Buffer.byteLength(old);
|
|
32
|
-
this.values.delete(key);
|
|
33
|
-
}
|
|
34
|
-
while (this.values.size && (this.values.size >= this.limit || this.bytes + size > this.byteLimit)) {
|
|
35
|
-
const [id, text] = this.values.entries().next().value;
|
|
36
|
-
this.values.delete(id);
|
|
37
|
-
this.bytes -= Buffer.byteLength(id) + Buffer.byteLength(text);
|
|
38
|
-
}
|
|
39
|
-
this.values.set(key, value);
|
|
40
|
-
this.bytes += size;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
async function stopEmbeddingChild(worker) {
|
|
44
|
-
if (worker.exitCode !== null || worker.signalCode !== null) return;
|
|
45
|
-
await new Promise((resolveExit) => {
|
|
46
|
-
const timer = setTimeout(() => worker.kill("SIGKILL"), 2e3);
|
|
47
|
-
worker.once("exit", () => {
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
resolveExit();
|
|
50
|
-
});
|
|
51
|
-
worker.kill("SIGTERM");
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
var DesignSemanticIndex = class {
|
|
55
|
-
idleMs;
|
|
56
|
-
config;
|
|
57
|
-
identity;
|
|
58
|
-
get publicationIdentity() {
|
|
59
|
-
return semanticDigest(this.identity + ":similarity-v1:" + this.config.threshold);
|
|
60
|
-
}
|
|
61
|
-
jobs = /* @__PURE__ */ new Map();
|
|
62
|
-
tail = Promise.resolve();
|
|
63
|
-
closed = false;
|
|
64
|
-
constructor(config = {}, idleMs = 3e5) {
|
|
65
|
-
this.idleMs = idleMs;
|
|
66
|
-
if (!Number.isSafeInteger(idleMs) || idleMs < 1) throw new Error("Invalid semantic idle timeout");
|
|
67
|
-
this.config = {
|
|
68
|
-
provider: "local",
|
|
69
|
-
model: "Xenova/multilingual-e5-small",
|
|
70
|
-
revision: "761b726dd34fb83930e26aab4e9ac3899aa1fa78",
|
|
71
|
-
cacheDir: resolve(homedir(), ".cache/design-system/embeddings"),
|
|
72
|
-
localOnly: false,
|
|
73
|
-
prefix: "query: ",
|
|
74
|
-
threshold: .8,
|
|
75
|
-
...config
|
|
76
|
-
};
|
|
77
|
-
if (![
|
|
78
|
-
"local",
|
|
79
|
-
"remote",
|
|
80
|
-
"disabled"
|
|
81
|
-
].includes(this.config.provider)) throw new Error("Unknown embedding provider");
|
|
82
|
-
if (!this.config.model || !this.config.revision || !isAbsolute(this.config.cacheDir)) throw new Error("Invalid embedding configuration");
|
|
83
|
-
if (!(this.config.threshold >= 0 && this.config.threshold <= 1)) throw new Error("Invalid semantic threshold");
|
|
84
|
-
if (this.config.provider === "remote") {
|
|
85
|
-
if (!this.config.endpoint || !config.model || !config.revision) throw new Error("Remote embedding requires explicit endpoint, model and revision");
|
|
86
|
-
const url = new URL(this.config.endpoint);
|
|
87
|
-
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password) throw new Error("Invalid embedding endpoint");
|
|
88
|
-
if (url.protocol === "http:" && ![
|
|
89
|
-
"localhost",
|
|
90
|
-
"127.0.0.1",
|
|
91
|
-
"[::1]"
|
|
92
|
-
].includes(url.hostname)) throw new Error("Remote embedding requires HTTPS except loopback");
|
|
93
|
-
}
|
|
94
|
-
this.identity = semanticDigest(JSON.stringify({
|
|
95
|
-
provider: this.config.provider,
|
|
96
|
-
model: this.config.model,
|
|
97
|
-
revision: this.config.revision,
|
|
98
|
-
endpoint: this.config.endpoint,
|
|
99
|
-
prefix: this.config.prefix,
|
|
100
|
-
text: SEMANTIC_TEXT_VERSION,
|
|
101
|
-
pooling: "mean-token-windows-v1",
|
|
102
|
-
dtype: "q8",
|
|
103
|
-
engine: "transformers-3.8.1"
|
|
104
|
-
}));
|
|
105
|
-
}
|
|
106
|
-
read(project, graph, cache) {
|
|
107
|
-
const units = (graph.semanticUnits ?? []).map((unit) => ({
|
|
108
|
-
...unit,
|
|
109
|
-
fieldPointers: unit.fieldPointers?.slice()
|
|
110
|
-
}));
|
|
111
|
-
const snapshot = {
|
|
112
|
-
checkpoint: graph.checkpoint,
|
|
113
|
-
semanticUnits: units
|
|
114
|
-
};
|
|
115
|
-
const key = semanticDigest(graph.checkpoint + (graph.generation ?? "") + this.publicationIdentity + (cache ? "persistent" : "memory") + JSON.stringify(units.map((u) => [
|
|
116
|
-
u.id,
|
|
117
|
-
u.nodeId,
|
|
118
|
-
u.file,
|
|
119
|
-
u.pointer,
|
|
120
|
-
u.fieldPointers,
|
|
121
|
-
u.digest
|
|
122
|
-
])));
|
|
123
|
-
if (this.closed || this.config.provider === "disabled") return {
|
|
124
|
-
edges: [],
|
|
125
|
-
status: {
|
|
126
|
-
state: "disabled",
|
|
127
|
-
completed: 0,
|
|
128
|
-
total: units.length,
|
|
129
|
-
model: this.config.model
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
let job = this.jobs.get(project);
|
|
133
|
-
if (job?.key !== key) {
|
|
134
|
-
if (this.jobs.size >= 8 && !job) return {
|
|
135
|
-
edges: [],
|
|
136
|
-
status: {
|
|
137
|
-
state: "error",
|
|
138
|
-
completed: 0,
|
|
139
|
-
total: units.length,
|
|
140
|
-
model: this.config.model,
|
|
141
|
-
message: "语义索引同时最多处理8个项目。"
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
job?.abort.abort();
|
|
145
|
-
clearTimeout(job?.idle);
|
|
146
|
-
job = {
|
|
147
|
-
key,
|
|
148
|
-
status: {
|
|
149
|
-
state: "indexing",
|
|
150
|
-
completed: 0,
|
|
151
|
-
total: units.length,
|
|
152
|
-
model: this.config.model,
|
|
153
|
-
cache: cache ? "persistent" : "memory",
|
|
154
|
-
message: this.config.provider === "local" ? "本地索引准备中;首次可能下载模型,文本不上传" : "使用已配置远程 embedding 服务处理设计文本"
|
|
155
|
-
},
|
|
156
|
-
edges: [],
|
|
157
|
-
abort: new AbortController(),
|
|
158
|
-
memory: job?.memory ?? new MemorySemanticCache()
|
|
159
|
-
};
|
|
160
|
-
this.jobs.set(project, job);
|
|
161
|
-
const current = job;
|
|
162
|
-
this.tail = this.tail.catch(() => {}).then(async () => {
|
|
163
|
-
if (current.abort.signal.aborted) return;
|
|
164
|
-
try {
|
|
165
|
-
await this.build(snapshot, cache, current);
|
|
166
|
-
} catch (error) {
|
|
167
|
-
const phase = error?.embeddingFailure;
|
|
168
|
-
const detail = phase && Object.hasOwn(localEmbeddingFailures, phase) ? localEmbeddingFailures[phase] : "语义索引失败;请检查本地运行资源或已配置的 embedding 服务。";
|
|
169
|
-
if (!current.abort.signal.aborted) current.status = {
|
|
170
|
-
...current.status,
|
|
171
|
-
state: "error",
|
|
172
|
-
message: detail + " 保留结构图,不自动上传或重试。"
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
current.settled = this.tail;
|
|
177
|
-
}
|
|
178
|
-
clearTimeout(job.idle);
|
|
179
|
-
if (!job.waiters) {
|
|
180
|
-
job.idle = setTimeout(() => this.cancelProject(project), this.idleMs);
|
|
181
|
-
job.idle.unref();
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
edges: job.edges,
|
|
185
|
-
status: { ...job.status }
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
/** Await the existing inference job for background persistence; no polling and
|
|
189
|
-
* no project write lock held during model execution. Partial edges never escape. */
|
|
190
|
-
async compute(project, graph, cache, signal) {
|
|
191
|
-
signal?.throwIfAborted();
|
|
192
|
-
const status = this.read(project, graph, cache);
|
|
193
|
-
const job = this.jobs.get(project);
|
|
194
|
-
if (!job || status.status.state === "disabled" || status.status.state === "error") throw new Error(status.status.message ?? "Semantic indexing unavailable");
|
|
195
|
-
clearTimeout(job.idle);
|
|
196
|
-
job.waiters = (job.waiters ?? 0) + 1;
|
|
197
|
-
let rejectAbort;
|
|
198
|
-
const cancelled = new Promise((_resolve, reject) => {
|
|
199
|
-
rejectAbort = reject;
|
|
200
|
-
});
|
|
201
|
-
const abort = () => rejectAbort(signal?.reason ?? /* @__PURE__ */ new Error("Semantic consumer cancelled"));
|
|
202
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
203
|
-
try {
|
|
204
|
-
signal?.throwIfAborted();
|
|
205
|
-
await Promise.race([job.settled, cancelled]);
|
|
206
|
-
signal?.throwIfAborted();
|
|
207
|
-
job.abort.signal.throwIfAborted();
|
|
208
|
-
if (this.jobs.get(project) !== job || job.status.state !== "ready") throw new Error(job.status.message ?? "Semantic indexing did not complete");
|
|
209
|
-
return {
|
|
210
|
-
edges: structuredClone(job.edges),
|
|
211
|
-
status: { ...job.status }
|
|
212
|
-
};
|
|
213
|
-
} finally {
|
|
214
|
-
signal?.removeEventListener("abort", abort);
|
|
215
|
-
job.waiters--;
|
|
216
|
-
if (!job.waiters && signal?.aborted && job.status.state === "indexing") job.abort.abort(signal.reason);
|
|
217
|
-
if (this.jobs.get(project) === job && !job.waiters) {
|
|
218
|
-
job.idle = setTimeout(() => this.cancelProject(project), this.idleMs);
|
|
219
|
-
job.idle.unref();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
cancelProject(project) {
|
|
224
|
-
const job = this.jobs.get(project);
|
|
225
|
-
clearTimeout(job?.idle);
|
|
226
|
-
job?.abort.abort();
|
|
227
|
-
this.jobs.delete(project);
|
|
228
|
-
}
|
|
229
|
-
async close() {
|
|
230
|
-
this.closed = true;
|
|
231
|
-
for (const project of this.jobs.keys()) this.cancelProject(project);
|
|
232
|
-
await this.tail;
|
|
233
|
-
}
|
|
234
|
-
async build(graph, cache, job) {
|
|
235
|
-
const units = graph.semanticUnits ?? [], vectors = [], missing = [];
|
|
236
|
-
const signal = job.abort.signal;
|
|
237
|
-
for (let i = 0; i < units.length; i++) {
|
|
238
|
-
signal.throwIfAborted();
|
|
239
|
-
const key = semanticDigest(this.identity + units[i].digest);
|
|
240
|
-
let value;
|
|
241
|
-
if (cache && job.status.cache === "persistent") try {
|
|
242
|
-
value = await cache.readSemanticCache(key);
|
|
243
|
-
} catch {
|
|
244
|
-
job.status.cache = "memory";
|
|
245
|
-
}
|
|
246
|
-
value ??= await job.memory.readSemanticCache(key);
|
|
247
|
-
if (value) try {
|
|
248
|
-
vectors[i] = normalizeVector(JSON.parse(value));
|
|
249
|
-
job.status.completed++;
|
|
250
|
-
continue;
|
|
251
|
-
} catch {}
|
|
252
|
-
missing.push(i);
|
|
253
|
-
}
|
|
254
|
-
let worker;
|
|
255
|
-
const kill = () => {
|
|
256
|
-
worker?.kill("SIGTERM");
|
|
257
|
-
};
|
|
258
|
-
signal.addEventListener("abort", kill, { once: true });
|
|
259
|
-
try {
|
|
260
|
-
if (missing.length && this.config.provider === "local") {
|
|
261
|
-
const built = new URL("./embedding-worker.js", import.meta.url);
|
|
262
|
-
worker = fork(existsSync(built) ? built : new URL("./embedding-worker.mjs", import.meta.url), [], {
|
|
263
|
-
windowsHide: true,
|
|
264
|
-
execArgv: ["--max-old-space-size=512"],
|
|
265
|
-
stdio: [
|
|
266
|
-
"ignore",
|
|
267
|
-
"ignore",
|
|
268
|
-
"ignore",
|
|
269
|
-
"ipc"
|
|
270
|
-
]
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
for (let start = 0; start < missing.length; start += 4) {
|
|
274
|
-
signal.throwIfAborted();
|
|
275
|
-
const indices = missing.slice(start, start + 4), texts = indices.map((i) => units[i].text);
|
|
276
|
-
const values = worker ? await this.localBatch(worker, texts, job) : await this.remoteBatch(texts, signal);
|
|
277
|
-
if (!Array.isArray(values) || values.length !== texts.length) throw new Error("Embedding batch count mismatch");
|
|
278
|
-
for (let offset = 0; offset < indices.length; offset++) {
|
|
279
|
-
signal.throwIfAborted();
|
|
280
|
-
const i = indices[offset], vector = normalizeVector(values[offset]);
|
|
281
|
-
vectors[i] = vector;
|
|
282
|
-
const key = semanticDigest(this.identity + units[i].digest), value = JSON.stringify(vector);
|
|
283
|
-
await job.memory.writeSemanticCache(key, value);
|
|
284
|
-
if (cache && job.status.cache === "persistent") try {
|
|
285
|
-
await cache.writeSemanticCache(key, value);
|
|
286
|
-
} catch {
|
|
287
|
-
job.status.cache = "memory";
|
|
288
|
-
}
|
|
289
|
-
job.status.completed++;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
signal.throwIfAborted();
|
|
293
|
-
job.edges = semanticEdges(units, vectors, `${this.config.model}@${this.config.revision}/${this.identity.slice(0, 12)}`, graph.checkpoint, this.config.threshold);
|
|
294
|
-
job.status = {
|
|
295
|
-
...job.status,
|
|
296
|
-
state: "ready",
|
|
297
|
-
message: `${job.edges.length} 条跨文件语义候选;相似度不等于确认关系`
|
|
298
|
-
};
|
|
299
|
-
} finally {
|
|
300
|
-
signal.removeEventListener("abort", kill);
|
|
301
|
-
if (worker) await stopEmbeddingChild(worker);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
localBatch(worker, texts, job) {
|
|
305
|
-
return new Promise((resolveBatch, reject) => {
|
|
306
|
-
const cleanup = () => {
|
|
307
|
-
clearTimeout(timer);
|
|
308
|
-
worker.off("message", message);
|
|
309
|
-
worker.off("error", failure);
|
|
310
|
-
worker.off("exit", failure);
|
|
311
|
-
job.abort.signal.removeEventListener("abort", failure);
|
|
312
|
-
};
|
|
313
|
-
const failure = () => {
|
|
314
|
-
cleanup();
|
|
315
|
-
reject(/* @__PURE__ */ new Error("Embedding worker stopped"));
|
|
316
|
-
};
|
|
317
|
-
const message = (data) => {
|
|
318
|
-
if (data.progress) {
|
|
319
|
-
job.status.message = data.progress;
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
cleanup();
|
|
323
|
-
if (data.error || !data.vectors) reject(Object.assign(/* @__PURE__ */ new Error("Embedding worker failed"), { embeddingFailure: data.error }));
|
|
324
|
-
else resolveBatch(data.vectors);
|
|
325
|
-
};
|
|
326
|
-
const timer = setTimeout(() => {
|
|
327
|
-
failure();
|
|
328
|
-
worker.kill("SIGTERM");
|
|
329
|
-
}, 18e4);
|
|
330
|
-
worker.on("message", message);
|
|
331
|
-
worker.once("error", failure);
|
|
332
|
-
worker.once("exit", failure);
|
|
333
|
-
job.abort.signal.addEventListener("abort", failure, { once: true });
|
|
334
|
-
if (job.abort.signal.aborted) {
|
|
335
|
-
failure();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
worker.send({
|
|
339
|
-
texts,
|
|
340
|
-
config: this.config
|
|
341
|
-
}, (error) => {
|
|
342
|
-
if (error) failure();
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
async remoteBatch(texts, signal) {
|
|
347
|
-
const response = await fetch(this.config.endpoint, {
|
|
348
|
-
method: "POST",
|
|
349
|
-
redirect: "error",
|
|
350
|
-
headers: {
|
|
351
|
-
"Content-Type": "application/json",
|
|
352
|
-
...this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}
|
|
353
|
-
},
|
|
354
|
-
body: JSON.stringify({
|
|
355
|
-
model: this.config.model,
|
|
356
|
-
input: texts.map((t) => this.config.prefix + t)
|
|
357
|
-
}),
|
|
358
|
-
signal: AbortSignal.any([signal, AbortSignal.timeout(6e4)])
|
|
359
|
-
});
|
|
360
|
-
if (!response.ok) throw new Error("Remote embedding request failed");
|
|
361
|
-
const data = await response.json();
|
|
362
|
-
if (!Array.isArray(data.data) || data.data.length !== texts.length || new Set(data.data.map((d) => d.index)).size !== texts.length || data.data.some((d) => !Number.isInteger(d.index) || d.index < 0 || d.index >= texts.length)) throw new Error("Invalid remote embedding response");
|
|
363
|
-
return data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
//#endregion
|
|
1
|
+
import { n as MemorySemanticCache, r as stopEmbeddingChild, t as DesignSemanticIndex } from "./semantic-index-CigpMVTO.js";
|
|
367
2
|
export { DesignSemanticIndex, MemorySemanticCache, stopEmbeddingChild };
|