@puwenhui/dsh-rag-kb 0.0.0-stage → 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/README.md +85 -2
- package/USAGE.zh.md +110 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +443 -0
- package/lib/plugin.mjs +793 -0
- package/package.json +55 -4
package/lib/plugin.mjs
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, mkdirSync, statSync, watch } from "node:fs";
|
|
3
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
4
|
+
import z from "@deepseek-ai/schemastery";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __exportAll = (all, no_symbols) => {
|
|
10
|
+
let target = {};
|
|
11
|
+
for (var name in all) __defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/config.ts
|
|
21
|
+
/**
|
|
22
|
+
* RAG 知识库插件配置:schemastery schema,全字段 volatile(0.2.0 设置体系要求)。
|
|
23
|
+
* 由 build 脚本编译打包为 lib/plugin.mjs。
|
|
24
|
+
*/
|
|
25
|
+
/** 用户可见配置(设置面板「知识库」标签页) */
|
|
26
|
+
const PublicConfig = z.object({
|
|
27
|
+
watchDir: z.string().default("").description("本地文档目录(放入即自动索引)"),
|
|
28
|
+
topK: z.number().default(5).description("混合检索召回条数(默认 5)"),
|
|
29
|
+
minScore: z.number().default(.3).description("相似度阈值(0~1,默认 0.3)"),
|
|
30
|
+
description: z.string().default("").description("知识库描述(agent 检索前会阅读)"),
|
|
31
|
+
enableReranker: z.boolean().default(false).description("启用 reranker 重排序(首次下载约 500MB 模型)"),
|
|
32
|
+
rerankTopN: z.number().default(3).description("重排后返回条数(默认 3)")
|
|
33
|
+
});
|
|
34
|
+
/** 完整配置(含高级项) */
|
|
35
|
+
const Config = z.object({
|
|
36
|
+
watchDir: z.string().default("").description("本地文档目录"),
|
|
37
|
+
topK: z.number().default(5).description("混合检索召回条数"),
|
|
38
|
+
minScore: z.number().default(.3).description("相似度阈值"),
|
|
39
|
+
description: z.string().default("").description("知识库描述"),
|
|
40
|
+
embeddingModel: z.string().default("Xenova/bge-small-zh-v1.5").description("embedding 模型"),
|
|
41
|
+
batchSize: z.number().default(32).description("批量推理大小"),
|
|
42
|
+
enableReranker: z.boolean().default(false).description("启用 reranker 重排序"),
|
|
43
|
+
rerankTopN: z.number().default(3).description("重排后返回条数")
|
|
44
|
+
});
|
|
45
|
+
/** 全字段 volatile 注入(npm 版 schemastery 无 .volatile(),0.2.0 设置面板只收录 volatile 字段) */
|
|
46
|
+
for (const schema of [PublicConfig, Config]) for (const child of Object.values(schema.dict ?? {})) child.meta.volatile = true;
|
|
47
|
+
/** 解析后的配置快照 */
|
|
48
|
+
function resolveKbConfig(c = {}) {
|
|
49
|
+
return {
|
|
50
|
+
watchDir: c.watchDir ?? "",
|
|
51
|
+
topK: Math.max(1, Math.min(50, c.topK ?? 5)),
|
|
52
|
+
minScore: Math.max(0, Math.min(1, c.minScore ?? .3)),
|
|
53
|
+
description: c.description ?? "",
|
|
54
|
+
embeddingModel: c.embeddingModel ?? "Xenova/bge-small-zh-v1.5",
|
|
55
|
+
batchSize: Math.max(1, Math.min(128, c.batchSize ?? 32)),
|
|
56
|
+
enableReranker: c.enableReranker === true,
|
|
57
|
+
rerankTopN: Math.max(1, Math.min(20, c.rerankTopN ?? 3))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/store.ts
|
|
62
|
+
/**
|
|
63
|
+
* 向量索引库:node:sqlite(DSH 宿主先例模式)。
|
|
64
|
+
* 表结构:documents(文档元数据)+ chunks(切块 + 向量 blob)。
|
|
65
|
+
* 检索:向量全量载入内存 → 余弦相似 top-K(万级 chunk 毫秒级)。
|
|
66
|
+
*/
|
|
67
|
+
/** 库归属标识(PRAGMA application_id,抄宿主 session-query-sqlite 惯例) */
|
|
68
|
+
const APP_ID = 1380009794;
|
|
69
|
+
const SCHEMA_VERSION = 1;
|
|
70
|
+
var KbStore = class {
|
|
71
|
+
db;
|
|
72
|
+
vectors = [];
|
|
73
|
+
vectorRows = [];
|
|
74
|
+
loaded = false;
|
|
75
|
+
constructor(dbPath) {
|
|
76
|
+
const { DatabaseSync } = __require("node:sqlite");
|
|
77
|
+
mkdirSync(join(dbPath, ".."), { recursive: true });
|
|
78
|
+
this.db = new DatabaseSync(dbPath);
|
|
79
|
+
this.db.exec("PRAGMA journal_mode=WAL;");
|
|
80
|
+
const appId = this.db.prepare("PRAGMA application_id").get().application_id;
|
|
81
|
+
if (appId !== 0 && appId !== APP_ID) throw new Error(`rag-kb: ${dbPath} belongs to another application`);
|
|
82
|
+
this.db.exec(`PRAGMA application_id = ${APP_ID}; PRAGMA user_version = ${SCHEMA_VERSION};`);
|
|
83
|
+
this.db.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS documents(
|
|
85
|
+
doc_id TEXT PRIMARY KEY,
|
|
86
|
+
name TEXT NOT NULL,
|
|
87
|
+
source TEXT NOT NULL DEFAULT 'upload',
|
|
88
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
91
|
+
error TEXT NOT NULL DEFAULT '',
|
|
92
|
+
created_at TEXT NOT NULL DEFAULT '',
|
|
93
|
+
updated_at TEXT NOT NULL DEFAULT ''
|
|
94
|
+
);
|
|
95
|
+
CREATE TABLE IF NOT EXISTS chunks(
|
|
96
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
|
+
doc_id TEXT NOT NULL,
|
|
98
|
+
seq INTEGER NOT NULL,
|
|
99
|
+
text TEXT NOT NULL,
|
|
100
|
+
embedding BLOB NOT NULL,
|
|
101
|
+
FOREIGN KEY(doc_id) REFERENCES documents(doc_id) ON DELETE CASCADE
|
|
102
|
+
);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_doc ON chunks(doc_id);
|
|
104
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunk_fts USING fts5(
|
|
105
|
+
text,
|
|
106
|
+
tokenize='trigram'
|
|
107
|
+
);
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
/** 文档内容 hash 作 doc_id(同名不同内容=不同文档) */
|
|
111
|
+
static docId(content) {
|
|
112
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
113
|
+
}
|
|
114
|
+
hasDoc(docId) {
|
|
115
|
+
return this.db.prepare("SELECT 1 FROM documents WHERE doc_id=?").get(docId) !== void 0;
|
|
116
|
+
}
|
|
117
|
+
upsertDoc(docId, name, source, bytes, status, error = "") {
|
|
118
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
119
|
+
this.db.prepare(`
|
|
120
|
+
INSERT INTO documents(doc_id, name, source, bytes, chunk_count, status, error, created_at, updated_at)
|
|
121
|
+
VALUES(?,?,?,?,0,?,?,?,?)
|
|
122
|
+
ON CONFLICT(doc_id) DO UPDATE SET name=excluded.name, bytes=excluded.bytes, status=excluded.status, error=excluded.error, updated_at=excluded.updated_at
|
|
123
|
+
`).run(docId, name, source, bytes, status, error, now, now);
|
|
124
|
+
this.loaded = false;
|
|
125
|
+
}
|
|
126
|
+
setDocStatus(docId, status, chunkCount, error = "") {
|
|
127
|
+
this.db.prepare("UPDATE documents SET status=?, chunk_count=?, error=?, updated_at=? WHERE doc_id=?").run(status, chunkCount, error, (/* @__PURE__ */ new Date()).toISOString(), docId);
|
|
128
|
+
this.loaded = false;
|
|
129
|
+
}
|
|
130
|
+
deleteDoc(docId) {
|
|
131
|
+
const ids = this.db.prepare("SELECT id FROM chunks WHERE doc_id=?").all(docId);
|
|
132
|
+
const ftsDel = this.db.prepare("DELETE FROM chunk_fts WHERE rowid=?");
|
|
133
|
+
for (const { id } of ids) ftsDel.run(id);
|
|
134
|
+
this.db.prepare("DELETE FROM chunks WHERE doc_id=?").run(docId);
|
|
135
|
+
this.db.prepare("DELETE FROM documents WHERE doc_id=?").run(docId);
|
|
136
|
+
this.loaded = false;
|
|
137
|
+
}
|
|
138
|
+
listDocs() {
|
|
139
|
+
return this.db.prepare("SELECT * FROM documents ORDER BY updated_at DESC").all();
|
|
140
|
+
}
|
|
141
|
+
getDoc(docId) {
|
|
142
|
+
return this.db.prepare("SELECT * FROM documents WHERE doc_id=?").get(docId);
|
|
143
|
+
}
|
|
144
|
+
/** 批量写入切块+向量+FTS 索引(一个事务,失败回滚) */
|
|
145
|
+
insertChunks(docId, chunks) {
|
|
146
|
+
this.db.exec("BEGIN");
|
|
147
|
+
try {
|
|
148
|
+
this.db.prepare("DELETE FROM chunks WHERE doc_id=?").run(docId);
|
|
149
|
+
const oldIds = this.db.prepare("SELECT id FROM chunks WHERE doc_id=?").all(docId);
|
|
150
|
+
const ftsDel = this.db.prepare("DELETE FROM chunk_fts WHERE rowid=?");
|
|
151
|
+
for (const { id } of oldIds) ftsDel.run(id);
|
|
152
|
+
const stmt = this.db.prepare("INSERT INTO chunks(doc_id, seq, text, embedding) VALUES(?,?,?,?)");
|
|
153
|
+
const ftsIns = this.db.prepare("INSERT INTO chunk_fts(rowid, text) VALUES(?,?)");
|
|
154
|
+
for (const c of chunks) {
|
|
155
|
+
const buf = Buffer.from(c.embedding.buffer, c.embedding.byteOffset, c.embedding.byteLength);
|
|
156
|
+
const info = stmt.run(docId, c.seq, c.text, buf);
|
|
157
|
+
const rowid = Number(info.lastInsertRowid);
|
|
158
|
+
ftsIns.run(rowid, c.text);
|
|
159
|
+
}
|
|
160
|
+
this.db.exec("COMMIT");
|
|
161
|
+
} catch (e) {
|
|
162
|
+
this.db.exec("ROLLBACK");
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
165
|
+
this.loaded = false;
|
|
166
|
+
}
|
|
167
|
+
/** 惰性加载全部向量到内存(首次或写后失效) */
|
|
168
|
+
ensureLoaded() {
|
|
169
|
+
if (this.loaded) return;
|
|
170
|
+
this.vectors = [];
|
|
171
|
+
this.vectorRows = [];
|
|
172
|
+
const rows = this.db.prepare("SELECT c.doc_id, c.seq, c.embedding FROM chunks c JOIN documents d ON d.doc_id=c.doc_id WHERE d.status='ready'").all();
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
this.vectors.push(new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4));
|
|
175
|
+
this.vectorRows.push({
|
|
176
|
+
docId: row.doc_id,
|
|
177
|
+
seq: row.seq
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
this.loaded = true;
|
|
181
|
+
}
|
|
182
|
+
/** 混合检索:向量余弦 + FTS5 关键词双路合并(加权排序) */
|
|
183
|
+
hybridSearch(queryVec, queryText, topK, minScore) {
|
|
184
|
+
this.ensureLoaded();
|
|
185
|
+
const vectorScores = /* @__PURE__ */ new Map();
|
|
186
|
+
for (let i = 0; i < this.vectors.length; i++) {
|
|
187
|
+
const v = this.vectors[i];
|
|
188
|
+
let dot = 0;
|
|
189
|
+
for (let j = 0; j < queryVec.length; j++) dot += queryVec[j] * v[j];
|
|
190
|
+
if (dot >= minScore) {
|
|
191
|
+
const key = `${this.vectorRows[i].docId}:${this.vectorRows[i].seq}`;
|
|
192
|
+
vectorScores.set(key, {
|
|
193
|
+
docId: this.vectorRows[i].docId,
|
|
194
|
+
seq: this.vectorRows[i].seq,
|
|
195
|
+
score: dot
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const keywordScores = /* @__PURE__ */ new Map();
|
|
200
|
+
if (queryText.trim().length >= 3) try {
|
|
201
|
+
const safe = queryText.replace(/["'*()\-:]/g, " ").trim().slice(0, 50);
|
|
202
|
+
if (safe.length >= 3) {
|
|
203
|
+
const rows = this.db.prepare(`
|
|
204
|
+
SELECT c.doc_id, c.seq, bm25(chunk_fts) AS rank
|
|
205
|
+
FROM chunk_fts f
|
|
206
|
+
JOIN chunks c ON c.id = f.rowid
|
|
207
|
+
WHERE chunk_fts MATCH ?
|
|
208
|
+
ORDER BY rank
|
|
209
|
+
LIMIT ?
|
|
210
|
+
`).all(`"${safe}"`, topK * 2);
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
const key = `${row.doc_id}:${row.seq}`;
|
|
213
|
+
const kwScore = 1 / (1 + Math.abs(row.rank));
|
|
214
|
+
keywordScores.set(key, {
|
|
215
|
+
docId: row.doc_id,
|
|
216
|
+
seq: row.seq,
|
|
217
|
+
score: kwScore
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
const merged = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const [key, v] of vectorScores) merged.set(key, {
|
|
224
|
+
...v,
|
|
225
|
+
score: v.score,
|
|
226
|
+
via: keywordScores.has(key) ? "both" : "vector"
|
|
227
|
+
});
|
|
228
|
+
for (const [key, k] of keywordScores) if (merged.has(key)) {
|
|
229
|
+
const existing = merged.get(key);
|
|
230
|
+
existing.score = existing.score + k.score * .3;
|
|
231
|
+
existing.via = "both";
|
|
232
|
+
} else merged.set(key, {
|
|
233
|
+
...k,
|
|
234
|
+
via: "keyword"
|
|
235
|
+
});
|
|
236
|
+
return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, topK);
|
|
237
|
+
}
|
|
238
|
+
/** 向后兼容:纯向量检索(内部调 hybridSearch 空 queryText) */
|
|
239
|
+
search(queryVec, topK, minScore) {
|
|
240
|
+
return this.hybridSearch(queryVec, "", topK, minScore).map((r) => ({
|
|
241
|
+
docId: r.docId,
|
|
242
|
+
seq: r.seq,
|
|
243
|
+
score: r.score
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
/** 按 doc_id + seq 取 chunk 原文 */
|
|
247
|
+
chunkText(docId, seq) {
|
|
248
|
+
return this.db.prepare("SELECT text FROM chunks WHERE doc_id=? AND seq=?").get(docId, seq)?.text ?? "";
|
|
249
|
+
}
|
|
250
|
+
close() {
|
|
251
|
+
this.db.close();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/indexer.ts
|
|
256
|
+
/**
|
|
257
|
+
* 文档管线:解析(pdf/docx/md/txt)→ 切块(标题感知 + 固定长度 fallback)→ 批量向量化。
|
|
258
|
+
* Embedding: transformers.js(GPU 加速优先,ONNX Runtime 自动检测 CUDA/WebGPU/CPU)。
|
|
259
|
+
*/
|
|
260
|
+
var indexer_exports = /* @__PURE__ */ __exportAll({
|
|
261
|
+
chunkText: () => chunkText,
|
|
262
|
+
extractText: () => extractText,
|
|
263
|
+
getEmbedder: () => getEmbedder,
|
|
264
|
+
indexFile: () => indexFile,
|
|
265
|
+
rerank: () => rerank
|
|
266
|
+
});
|
|
267
|
+
/** 中文友好的切块参数 */
|
|
268
|
+
const CHUNK_TOKENS = 400;
|
|
269
|
+
const OVERLAP_RATIO = .1;
|
|
270
|
+
async function extractText(filePath) {
|
|
271
|
+
const ext = extname(filePath).toLowerCase();
|
|
272
|
+
const buf = await readFile(filePath);
|
|
273
|
+
if (ext === ".pdf") {
|
|
274
|
+
const { extractText: pdfExtract, getDocumentProxy } = await import("unpdf");
|
|
275
|
+
const { text, totalPages } = await pdfExtract(await getDocumentProxy(new Uint8Array(buf)), { mergePages: true });
|
|
276
|
+
return {
|
|
277
|
+
text,
|
|
278
|
+
pages: totalPages
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (ext === ".docx") {
|
|
282
|
+
const { value: html } = await (await import("mammoth")).extractRawText({ buffer: buf });
|
|
283
|
+
return { text: html };
|
|
284
|
+
}
|
|
285
|
+
if (ext === ".md" || ext === ".txt" || ext === ".log" || ext === ".csv" || ext === ".json") return { text: buf.toString("utf8") };
|
|
286
|
+
throw new Error(`不支持的文件类型 ${ext}(支持 pdf/docx/md/txt/csv/json)`);
|
|
287
|
+
}
|
|
288
|
+
/** 标题感知切块:按标题/空行分段,过长段再固定长度切 */
|
|
289
|
+
function chunkText(text) {
|
|
290
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
291
|
+
if (normalized === "") return [];
|
|
292
|
+
const paragraphs = normalized.split(/\n(?=#{1,4} )|\n\n+/).map((p) => p.trim()).filter((p) => p !== "");
|
|
293
|
+
const chunks = [];
|
|
294
|
+
let current = "";
|
|
295
|
+
const maxChars = CHUNK_TOKENS * 1.5;
|
|
296
|
+
const overlap = Math.floor(maxChars * OVERLAP_RATIO);
|
|
297
|
+
for (const para of paragraphs) {
|
|
298
|
+
if (current !== "" && (current + "\n" + para).length > maxChars) {
|
|
299
|
+
chunks.push(current);
|
|
300
|
+
current = current.length > overlap ? current.slice(-60) : "";
|
|
301
|
+
}
|
|
302
|
+
if (para.length > maxChars * 2) {
|
|
303
|
+
if (current !== "") {
|
|
304
|
+
chunks.push(current);
|
|
305
|
+
current = "";
|
|
306
|
+
}
|
|
307
|
+
for (let i = 0; i < para.length; i += 540) chunks.push(para.slice(i, i + maxChars));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
current = current === "" ? para : current + "\n" + para;
|
|
311
|
+
}
|
|
312
|
+
if (current !== "") chunks.push(current);
|
|
313
|
+
return chunks.filter((c) => c.trim().length >= 10);
|
|
314
|
+
}
|
|
315
|
+
let gpuPipeline;
|
|
316
|
+
let cpuPipeline;
|
|
317
|
+
async function loadPipeline(modelName, device) {
|
|
318
|
+
const { pipeline, env } = await import("@huggingface/transformers");
|
|
319
|
+
const { dshHomePath } = await import("@deepseek-ai/dsh-home-paths");
|
|
320
|
+
env.cacheDir = dshHomePath("rag-kb", "hf-cache");
|
|
321
|
+
try {
|
|
322
|
+
return await pipeline("feature-extraction", modelName, { device });
|
|
323
|
+
} catch (e) {
|
|
324
|
+
if (device === "dml") {
|
|
325
|
+
console.log("[rag-kb] DML GPU 不可用,回退 CPU");
|
|
326
|
+
return await pipeline("feature-extraction", modelName, { device: "cpu" });
|
|
327
|
+
}
|
|
328
|
+
throw e;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function getEmbedder(modelName) {
|
|
332
|
+
return {
|
|
333
|
+
/** 单条向量化(检索查询用,CPU 单条比 GPU 快) */
|
|
334
|
+
async embed(text) {
|
|
335
|
+
cpuPipeline ??= await loadPipeline(modelName, "cpu");
|
|
336
|
+
const out = await cpuPipeline(text, {
|
|
337
|
+
pooling: "cls",
|
|
338
|
+
normalize: true
|
|
339
|
+
});
|
|
340
|
+
return new Float32Array(out.data);
|
|
341
|
+
},
|
|
342
|
+
/** 批量向量化(索引用,GPU/DML 批量快 4 倍,不可用自动回退 CPU) */
|
|
343
|
+
async embedBatch(texts, batchSize) {
|
|
344
|
+
gpuPipeline ??= await loadPipeline(modelName, "dml");
|
|
345
|
+
const results = [];
|
|
346
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
347
|
+
const batch = texts.slice(i, i + batchSize);
|
|
348
|
+
const out = await gpuPipeline(batch, {
|
|
349
|
+
pooling: "cls",
|
|
350
|
+
normalize: true
|
|
351
|
+
});
|
|
352
|
+
const dim = out.data.length / batch.length;
|
|
353
|
+
for (let j = 0; j < batch.length; j++) results.push(new Float32Array(out.data.slice(j * dim, (j + 1) * dim)));
|
|
354
|
+
}
|
|
355
|
+
return results;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
let rerankerPipeline;
|
|
360
|
+
async function loadReranker() {
|
|
361
|
+
const { pipeline, env } = await import("@huggingface/transformers");
|
|
362
|
+
const { dshHomePath } = await import("@deepseek-ai/dsh-home-paths");
|
|
363
|
+
env.cacheDir = dshHomePath("rag-kb", "hf-cache");
|
|
364
|
+
return pipeline("text-classification", "Xenova/bge-reranker-base");
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* 对混合检索结果做 cross-encoder 精排。
|
|
368
|
+
* 输入:query + 候选列表(含原文),输出按相关性重排序。
|
|
369
|
+
*/
|
|
370
|
+
async function rerank(query, candidates) {
|
|
371
|
+
rerankerPipeline ??= await loadReranker();
|
|
372
|
+
const pairs = candidates.map((c) => ({
|
|
373
|
+
text: query,
|
|
374
|
+
text_pair: c.text
|
|
375
|
+
}));
|
|
376
|
+
const scores = await rerankerPipeline(pairs);
|
|
377
|
+
const scored = candidates.map((c, i) => ({
|
|
378
|
+
...c,
|
|
379
|
+
rerankScore: scores[i].score
|
|
380
|
+
}));
|
|
381
|
+
scored.sort((a, b) => b.rerankScore - a.rerankScore);
|
|
382
|
+
return scored;
|
|
383
|
+
}
|
|
384
|
+
/** 完整索引管线:文件路径 → 切块+向量数组 */
|
|
385
|
+
async function indexFile(filePath, embedder, batchSize, onProgress) {
|
|
386
|
+
const { text } = await extractText(filePath);
|
|
387
|
+
const pieces = chunkText(text);
|
|
388
|
+
if (pieces.length === 0) return [];
|
|
389
|
+
const embeddings = await embedder.embedBatch(pieces, batchSize);
|
|
390
|
+
if (onProgress) onProgress(pieces.length, pieces.length);
|
|
391
|
+
return pieces.map((piece, i) => ({
|
|
392
|
+
seq: i,
|
|
393
|
+
text: piece,
|
|
394
|
+
embedding: embeddings[i]
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/plugin.ts
|
|
399
|
+
/**
|
|
400
|
+
* DSH RAG 知识库插件宿主半边入口。
|
|
401
|
+
* - 模型工具:knowledge_search / knowledge_status / knowledge_manage(会话语义管理)
|
|
402
|
+
* - systemPrompt 注入:agent 自动知道知识库
|
|
403
|
+
* - SSE 通道:索引进度推送(浏览器面板用)
|
|
404
|
+
* - 目录监视:watchDir 下的文档自动索引(V2 能力,V1 已生效)
|
|
405
|
+
*/
|
|
406
|
+
const name = "rag-kb";
|
|
407
|
+
const inject = [
|
|
408
|
+
"tools",
|
|
409
|
+
"systemPrompt",
|
|
410
|
+
"settings"
|
|
411
|
+
];
|
|
412
|
+
let store;
|
|
413
|
+
let cfg;
|
|
414
|
+
const progressListeners = /* @__PURE__ */ new Set();
|
|
415
|
+
function broadcastProgress(msg) {
|
|
416
|
+
for (const write of progressListeners) try {
|
|
417
|
+
write(`event: progress\ndata: ${JSON.stringify(msg)}\n\n`);
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
async function ensureStore() {
|
|
421
|
+
if (store !== void 0) return store;
|
|
422
|
+
const { dshHomePath } = await import("@deepseek-ai/dsh-home-paths");
|
|
423
|
+
store = new KbStore(dshHomePath("rag-kb", "index.db"));
|
|
424
|
+
return store;
|
|
425
|
+
}
|
|
426
|
+
/** 索引一个文件路径(upload 或 watch 共用) */
|
|
427
|
+
async function indexOne(ctx, filePath, source) {
|
|
428
|
+
const s = await ensureStore();
|
|
429
|
+
const name = basename(filePath);
|
|
430
|
+
const bytes = statSync(filePath).size;
|
|
431
|
+
const content = await (await import("node:fs/promises")).readFile(filePath);
|
|
432
|
+
const docId = KbStore.docId(content);
|
|
433
|
+
if (s.hasDoc(docId) && s.getDoc(docId)?.status === "ready") return {
|
|
434
|
+
ok: true,
|
|
435
|
+
message: `${name} 已索引(内容未变化,跳过)`
|
|
436
|
+
};
|
|
437
|
+
s.upsertDoc(docId, name, source, bytes, "indexing");
|
|
438
|
+
broadcastProgress(`正在索引 ${name}`);
|
|
439
|
+
try {
|
|
440
|
+
const chunks = await indexFile(filePath, await getEmbedder(cfg?.embeddingModel ?? "Xenova/bge-small-zh-v1.5"), cfg?.batchSize ?? 32);
|
|
441
|
+
s.insertChunks(docId, chunks.map((c) => ({
|
|
442
|
+
seq: c.seq,
|
|
443
|
+
text: c.text,
|
|
444
|
+
embedding: c.embedding
|
|
445
|
+
})));
|
|
446
|
+
s.setDocStatus(docId, "ready", chunks.length);
|
|
447
|
+
broadcastProgress(`${name} 索引完成(${chunks.length} 块)`);
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
message: `${name} 索引完成,共 ${chunks.length} 个知识块`
|
|
451
|
+
};
|
|
452
|
+
} catch (e) {
|
|
453
|
+
const msg = String(e.message ?? e);
|
|
454
|
+
s.setDocStatus(docId, "failed", 0, msg);
|
|
455
|
+
broadcastProgress(`${name} 索引失败:${msg}`);
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
message: `${name} 索引失败:${msg}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/** 混合语义检索(向量+关键词)+ 可选 reranker 精排 + 引用元数据 */
|
|
463
|
+
async function search(query, topK) {
|
|
464
|
+
const s = await ensureStore();
|
|
465
|
+
const queryVec = await (await getEmbedder(cfg?.embeddingModel ?? "Xenova/bge-small-zh-v1.5")).embed(query);
|
|
466
|
+
const effectiveTopK = topK ?? cfg?.topK ?? 5;
|
|
467
|
+
const recallK = cfg?.enableReranker === true ? effectiveTopK * 3 : effectiveTopK;
|
|
468
|
+
let results = s.hybridSearch(queryVec, query, recallK, cfg?.minScore ?? .3).map((h) => {
|
|
469
|
+
const docName = s.getDoc(h.docId)?.name ?? h.docId;
|
|
470
|
+
const fullText = s.chunkText(h.docId, h.seq);
|
|
471
|
+
return {
|
|
472
|
+
docId: h.docId,
|
|
473
|
+
doc: docName,
|
|
474
|
+
seq: h.seq,
|
|
475
|
+
score: Number(h.score.toFixed(3)),
|
|
476
|
+
via: h.via,
|
|
477
|
+
text: fullText,
|
|
478
|
+
citation: `${docName} 第${h.seq + 1}段`,
|
|
479
|
+
position: `文档「${docName}」第 ${h.seq + 1} 段`
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
if (cfg?.enableReranker === true && results.length > 0) try {
|
|
483
|
+
const reranked = await rerank(query, results);
|
|
484
|
+
const topN = cfg?.rerankTopN ?? 3;
|
|
485
|
+
results = reranked.slice(0, topN).map((r) => ({
|
|
486
|
+
...r,
|
|
487
|
+
text: r.text.slice(0, 800),
|
|
488
|
+
rerankScore: Number(r.rerankScore.toFixed(4))
|
|
489
|
+
}));
|
|
490
|
+
} catch (e) {
|
|
491
|
+
console.warn("[rag-kb] reranker 失败,使用混合检索结果:", String(e.message ?? e));
|
|
492
|
+
results = results.slice(0, effectiveTopK).map((r) => ({
|
|
493
|
+
...r,
|
|
494
|
+
text: r.text.slice(0, 800)
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
else results = results.slice(0, effectiveTopK).map((r) => ({
|
|
498
|
+
...r,
|
|
499
|
+
text: r.text.slice(0, 800)
|
|
500
|
+
}));
|
|
501
|
+
return results;
|
|
502
|
+
}
|
|
503
|
+
function apply(ctx, config = {}) {
|
|
504
|
+
const unwrap = (v) => v !== null && typeof v === "object" && "get" in v ? v.get() : v;
|
|
505
|
+
const raw = {};
|
|
506
|
+
for (const [k, v] of Object.entries(config)) raw[k] = unwrap(v);
|
|
507
|
+
cfg = resolveKbConfig(raw);
|
|
508
|
+
ctx.inject(["webServer"], (hostCtx) => {
|
|
509
|
+
hostCtx.webServer.register({
|
|
510
|
+
kind: "exact",
|
|
511
|
+
path: "/rag-kb/progress",
|
|
512
|
+
handler: (req, res) => {
|
|
513
|
+
res.writeHead(200, {
|
|
514
|
+
"content-type": "text/event-stream",
|
|
515
|
+
"cache-control": "no-store",
|
|
516
|
+
connection: "keep-alive"
|
|
517
|
+
});
|
|
518
|
+
res.write("retry: 3000\n\n");
|
|
519
|
+
const write = (chunk) => res.write(chunk);
|
|
520
|
+
progressListeners.add(write);
|
|
521
|
+
const heartbeat = setInterval(() => {
|
|
522
|
+
try {
|
|
523
|
+
res.write(": ping\n\n");
|
|
524
|
+
} catch {}
|
|
525
|
+
}, 25e3);
|
|
526
|
+
req.on("close", () => {
|
|
527
|
+
clearInterval(heartbeat);
|
|
528
|
+
progressListeners.delete(write);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
if (cfg.watchDir !== "" && existsSync(cfg.watchDir)) {
|
|
534
|
+
const dir = resolve(cfg.watchDir);
|
|
535
|
+
try {
|
|
536
|
+
const watcher = watch(dir, { persistent: false }, (_ev, filename) => {
|
|
537
|
+
if (filename === null || filename === void 0) return;
|
|
538
|
+
const fname = String(filename);
|
|
539
|
+
if (!/\.(pdf|docx|md|txt|csv|json|log)$/i.test(fname)) return;
|
|
540
|
+
const f = join(dir, fname);
|
|
541
|
+
setTimeout(() => {
|
|
542
|
+
if (existsSync(f)) indexOne(ctx, f, "watch");
|
|
543
|
+
}, 2e3);
|
|
544
|
+
});
|
|
545
|
+
ctx.effect(() => {
|
|
546
|
+
watcher.close();
|
|
547
|
+
}, "rag-kb: dir watcher");
|
|
548
|
+
} catch (e) {
|
|
549
|
+
console.warn(`[rag-kb] 目录监视失败 ${dir}: ${String(e.message)}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const compileParams = (spec) => {
|
|
553
|
+
const properties = {};
|
|
554
|
+
const required = [];
|
|
555
|
+
for (const [key, def] of Object.entries(spec)) {
|
|
556
|
+
const { required: req, ...rest } = def;
|
|
557
|
+
properties[key] = rest;
|
|
558
|
+
if (req === true) required.push(key);
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties,
|
|
563
|
+
...required.length > 0 ? { required } : {}
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
const disposers = [
|
|
567
|
+
ctx.tools.register({
|
|
568
|
+
name: "knowledge_search",
|
|
569
|
+
description: "在本地知识库中混合检索(语义向量 + 关键词)。返回最相关的知识块,每条带 citation 引用标注。回答知识库相关问题时:先检索 → 组织回答 → 在答案中标注来源 citation。query 支持自然语言和精确关键词(错误码/型号/人名等)。",
|
|
570
|
+
parameters: compileParams({
|
|
571
|
+
query: {
|
|
572
|
+
type: "string",
|
|
573
|
+
required: true,
|
|
574
|
+
description: "检索问题(自然语言或精确关键词)"
|
|
575
|
+
},
|
|
576
|
+
top_k: {
|
|
577
|
+
type: "number",
|
|
578
|
+
description: "返回条数(默认 5)"
|
|
579
|
+
}
|
|
580
|
+
}),
|
|
581
|
+
output: {
|
|
582
|
+
schema: {},
|
|
583
|
+
render: (_a, v) => [{
|
|
584
|
+
type: "text",
|
|
585
|
+
text: JSON.stringify(v)
|
|
586
|
+
}]
|
|
587
|
+
},
|
|
588
|
+
async execute(args) {
|
|
589
|
+
const a = args;
|
|
590
|
+
const hits = await search(a.query, a.top_k);
|
|
591
|
+
return {
|
|
592
|
+
ok: true,
|
|
593
|
+
count: hits.length,
|
|
594
|
+
results: hits
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}),
|
|
598
|
+
ctx.tools.register({
|
|
599
|
+
name: "knowledge_status",
|
|
600
|
+
description: "查看知识库状态:已索引文档列表(名称/大小/块数/状态)、配置参数、知识库描述。",
|
|
601
|
+
parameters: compileParams({}),
|
|
602
|
+
output: {
|
|
603
|
+
schema: {},
|
|
604
|
+
render: (_a, v) => [{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: JSON.stringify(v)
|
|
607
|
+
}]
|
|
608
|
+
},
|
|
609
|
+
async execute() {
|
|
610
|
+
const s = await ensureStore();
|
|
611
|
+
return {
|
|
612
|
+
ok: true,
|
|
613
|
+
description: cfg?.description ?? "",
|
|
614
|
+
topK: cfg?.topK,
|
|
615
|
+
minScore: cfg?.minScore,
|
|
616
|
+
documents: s.listDocs().map((d) => ({
|
|
617
|
+
name: d.name,
|
|
618
|
+
source: d.source,
|
|
619
|
+
bytes: d.bytes,
|
|
620
|
+
chunks: d.chunk_count,
|
|
621
|
+
status: d.status,
|
|
622
|
+
...d.error !== "" ? { error: d.error } : {}
|
|
623
|
+
}))
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}),
|
|
627
|
+
ctx.tools.register({
|
|
628
|
+
name: "knowledge_manage",
|
|
629
|
+
description: "知识库管理操作(用户在对话中用自然语言请求时调用)。支持:save(把对话中的内容直接保存入库,给 title + text)、add(索引本地文件路径或目录)、remove(按文档名删除)、reindex(重建全部索引)、describe(设置知识库描述)。用户说「存到知识库」「把这个记下来」时用 save。",
|
|
630
|
+
parameters: compileParams({
|
|
631
|
+
action: {
|
|
632
|
+
type: "string",
|
|
633
|
+
required: true,
|
|
634
|
+
description: "add | remove | reindex | describe"
|
|
635
|
+
},
|
|
636
|
+
path: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "add:本地文件或目录路径"
|
|
639
|
+
},
|
|
640
|
+
title: {
|
|
641
|
+
type: "string",
|
|
642
|
+
description: "save:文档标题(会成为知识库里的文档名)"
|
|
643
|
+
},
|
|
644
|
+
text: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "save:要保存的文本内容(直接从对话中提取,无需先写文件)"
|
|
647
|
+
},
|
|
648
|
+
name: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "remove:要删除的文档名"
|
|
651
|
+
},
|
|
652
|
+
description: {
|
|
653
|
+
type: "string",
|
|
654
|
+
description: "describe:知识库描述文本"
|
|
655
|
+
}
|
|
656
|
+
}),
|
|
657
|
+
output: {
|
|
658
|
+
schema: {},
|
|
659
|
+
render: (_a, v) => [{
|
|
660
|
+
type: "text",
|
|
661
|
+
text: JSON.stringify(v)
|
|
662
|
+
}]
|
|
663
|
+
},
|
|
664
|
+
async execute(args) {
|
|
665
|
+
const a = args;
|
|
666
|
+
const s = await ensureStore();
|
|
667
|
+
if (a.action === "save" && a.text !== void 0 && a.text !== "") {
|
|
668
|
+
const title = a.title !== void 0 && a.title !== "" ? a.title : `对话保存 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`;
|
|
669
|
+
const content = `# ${title}\n\n${a.text}`;
|
|
670
|
+
const docId = KbStore.docId(content);
|
|
671
|
+
if (s.hasDoc(docId) && s.getDoc(docId)?.status === "ready") return {
|
|
672
|
+
ok: true,
|
|
673
|
+
message: `${title} 已在知识库中(内容相同,跳过)`
|
|
674
|
+
};
|
|
675
|
+
s.upsertDoc(docId, title, "save", content.length, "indexing");
|
|
676
|
+
broadcastProgress(`正在索引对话内容「${title}」`);
|
|
677
|
+
try {
|
|
678
|
+
const embedder = await getEmbedder(cfg?.embeddingModel ?? "Xenova/bge-small-zh-v1.5");
|
|
679
|
+
const { chunkText } = await Promise.resolve().then(() => indexer_exports);
|
|
680
|
+
const pieces = chunkText(content);
|
|
681
|
+
if (pieces.length === 0) return {
|
|
682
|
+
ok: false,
|
|
683
|
+
error: "内容太短或为空,无法切块"
|
|
684
|
+
};
|
|
685
|
+
const embeddings = await embedder.embedBatch(pieces, cfg?.batchSize ?? 32);
|
|
686
|
+
s.insertChunks(docId, pieces.map((piece, i) => ({
|
|
687
|
+
seq: i,
|
|
688
|
+
text: piece,
|
|
689
|
+
embedding: embeddings[i]
|
|
690
|
+
})));
|
|
691
|
+
s.setDocStatus(docId, "ready", pieces.length);
|
|
692
|
+
broadcastProgress(`「${title}」已入库(${pieces.length} 块)`);
|
|
693
|
+
return {
|
|
694
|
+
ok: true,
|
|
695
|
+
message: `已保存「${title}」到知识库,共 ${pieces.length} 个知识块`
|
|
696
|
+
};
|
|
697
|
+
} catch (e) {
|
|
698
|
+
const msg = String(e.message ?? e);
|
|
699
|
+
s.setDocStatus(docId, "failed", 0, msg);
|
|
700
|
+
return {
|
|
701
|
+
ok: false,
|
|
702
|
+
error: `索引失败:${msg}`
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (a.action === "add" && a.path) {
|
|
707
|
+
const p = resolve(a.path);
|
|
708
|
+
if (!existsSync(p)) return {
|
|
709
|
+
ok: false,
|
|
710
|
+
error: `路径不存在:${p}`
|
|
711
|
+
};
|
|
712
|
+
if (statSync(p).isFile()) {
|
|
713
|
+
const r = await indexOne(ctx, p, "upload");
|
|
714
|
+
return {
|
|
715
|
+
ok: r.ok,
|
|
716
|
+
message: r.message
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const { readdir } = await import("node:fs/promises");
|
|
720
|
+
const files = (await readdir(p)).filter((f) => /\.(pdf|docx|md|txt|csv|json|log)$/i.test(f)).map((f) => join(p, f));
|
|
721
|
+
const results = [];
|
|
722
|
+
for (const f of files) {
|
|
723
|
+
const r = await indexOne(ctx, f, "upload");
|
|
724
|
+
results.push(r.message);
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
ok: true,
|
|
728
|
+
message: `批量索引 ${files.length} 个文件`,
|
|
729
|
+
detail: results
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (a.action === "remove" && a.name) {
|
|
733
|
+
const docs = s.listDocs().filter((d) => d.name.includes(a.name));
|
|
734
|
+
if (docs.length === 0) return {
|
|
735
|
+
ok: false,
|
|
736
|
+
error: `未找到包含「${a.name}」的文档`
|
|
737
|
+
};
|
|
738
|
+
for (const d of docs) s.deleteDoc(d.doc_id);
|
|
739
|
+
return {
|
|
740
|
+
ok: true,
|
|
741
|
+
message: `已删除 ${docs.length} 个文档:${docs.map((d) => d.name).join(", ")}`
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (a.action === "reindex") {
|
|
745
|
+
const docs = s.listDocs();
|
|
746
|
+
for (const d of docs) s.setDocStatus(d.doc_id, "pending", 0);
|
|
747
|
+
return {
|
|
748
|
+
ok: true,
|
|
749
|
+
message: `已重置 ${docs.length} 个文档为待索引。请用户通过设置面板或重新 add 触发重建(文件源路径不再保留,需重新添加)。`
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (a.action === "describe" && a.description !== void 0) return {
|
|
753
|
+
ok: true,
|
|
754
|
+
message: `知识库描述已记录(当前会话生效)。持久化请通过设置面板「知识库描述」字段。`,
|
|
755
|
+
description: a.description
|
|
756
|
+
};
|
|
757
|
+
return {
|
|
758
|
+
ok: false,
|
|
759
|
+
error: `未知操作 ${a.action}(支持 save/add/remove/reindex/describe)`
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
];
|
|
764
|
+
ctx.effect(() => () => {
|
|
765
|
+
for (const d of disposers) d();
|
|
766
|
+
}, "rag-kb: tools");
|
|
767
|
+
const promptText = () => {
|
|
768
|
+
const desc = cfg?.description ?? "";
|
|
769
|
+
const docs = store?.listDocs().filter((d) => d.status === "ready").length ?? 0;
|
|
770
|
+
return [
|
|
771
|
+
"You have a local knowledge base accessible via these tools:",
|
|
772
|
+
"- knowledge_search: hybrid search (semantic + keyword). Use BEFORE answering questions about KB content.",
|
|
773
|
+
"- knowledge_status: list documents and config",
|
|
774
|
+
"- knowledge_manage: add/save/remove/reindex documents",
|
|
775
|
+
"IMPORTANT: When the user says 搜一下/查一下/搜索, FIRST try knowledge_search on the local KB. Only do a web search if KB has no relevant results AND the topic is clearly not about local documents. Do NOT search both simultaneously — this confuses the answer.",
|
|
776
|
+
desc !== "" ? `KB description: ${desc}` : "",
|
|
777
|
+
`Currently ${docs} document(s) indexed.`,
|
|
778
|
+
"CITATION STYLE (footnote format): In the body of your answer, mark sources with superscript numbers like ¹ ² ³ (Unicode superscripts). At the END, add a \"参考来源\" section. IMPORTANT: MERGE citations from the same document into ONE entry — e.g. if citing 文档A 第1段/第2段/第3段, list as \"1. 文档A 第1-3段\" (not 3 separate lines). Only list each document once with all its paragraph numbers combined. Keep the answer body clean.",
|
|
779
|
+
"When the user says \"save this to the knowledge base\" (保存到知识库/记下来), extract the valuable content from the conversation and call knowledge_manage with action=\"save\", providing a concise title and the text.",
|
|
780
|
+
"For file/directory indexing use action=\"add\" with a path."
|
|
781
|
+
].filter(Boolean).join("\n");
|
|
782
|
+
};
|
|
783
|
+
ctx.systemPrompt.section({
|
|
784
|
+
name: "tool:rag-kb",
|
|
785
|
+
order: 2350,
|
|
786
|
+
text: promptText,
|
|
787
|
+
interpolate: false
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
791
|
+
export { Config, KbStore, apply, chunkText, extractText, getEmbedder, indexFile, inject, name, rerank, resolveKbConfig };
|
|
792
|
+
|
|
793
|
+
//# sourceMappingURL=plugin.mjs.map
|