@dsh-libing/knowledge-base 0.1.0 → 0.1.4

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +12 -2
  3. package/lib/agent-access.d.ts +23 -0
  4. package/lib/agent-access.js +28 -0
  5. package/lib/chunks.js +27 -5
  6. package/lib/client-types/client/JobDetails.d.ts +16 -0
  7. package/lib/client-types/client/JobsPanel.d.ts +6 -1
  8. package/lib/client-types/client/QualityReport.d.ts +3 -2
  9. package/lib/client-types/client/api.d.ts +83 -52
  10. package/lib/client-types/client/locales.d.ts +2 -0
  11. package/lib/client-types/client/useKnowledge.d.ts +30 -15
  12. package/lib/client-types/contract.d.ts +83 -52
  13. package/lib/client-types/data.d.ts +11 -0
  14. package/lib/client-types/settings.d.ts +4 -0
  15. package/lib/client-types/shared.d.ts +17 -0
  16. package/lib/client.js +40 -40
  17. package/lib/config.js +9 -1
  18. package/lib/contract.d.ts +33 -2
  19. package/lib/contract.js +2 -1
  20. package/lib/data.d.ts +11 -0
  21. package/lib/data.js +2 -0
  22. package/lib/document/convert.js +52 -16
  23. package/lib/document/pdf-layout.js +45 -10
  24. package/lib/document/pdf-session.js +10 -4
  25. package/lib/document/pdf-vision.js +8 -3
  26. package/lib/document/pdf-worker.js +76 -21
  27. package/lib/document/pdf.js +39 -9
  28. package/lib/document/zip.js +14 -6
  29. package/lib/figures/image-client.d.ts +5 -0
  30. package/lib/figures/image-client.js +63 -0
  31. package/lib/figures/image-gate.d.ts +12 -0
  32. package/lib/figures/image-gate.js +39 -0
  33. package/lib/figures/image-protocol.d.ts +19 -0
  34. package/lib/figures/image-protocol.js +9 -0
  35. package/lib/figures/image-worker.d.ts +1 -0
  36. package/lib/figures/image-worker.js +69 -0
  37. package/lib/figures/process.d.ts +1 -1
  38. package/lib/figures/process.js +19 -38
  39. package/lib/index.js +6 -2
  40. package/lib/indexes.js +17 -6
  41. package/lib/job-storage.d.ts +264 -0
  42. package/lib/job-storage.js +118 -0
  43. package/lib/jobs.d.ts +782 -1489
  44. package/lib/jobs.js +64 -31
  45. package/lib/models.d.ts +1 -1
  46. package/lib/models.js +15 -9
  47. package/lib/retrieval.js +44 -13
  48. package/lib/revision-cache.d.ts +24 -0
  49. package/lib/revision-cache.js +100 -0
  50. package/lib/routes.js +66 -20
  51. package/lib/runtime.d.ts +4 -0
  52. package/lib/runtime.js +15 -4
  53. package/lib/settings.d.ts +4 -0
  54. package/lib/settings.js +5 -2
  55. package/lib/shared.d.ts +17 -0
  56. package/lib/shared.js +3 -0
  57. package/lib/skills.js +6 -3
  58. package/lib/store.d.ts +101 -5
  59. package/lib/store.js +78 -29
  60. package/lib/tools.js +12 -4
  61. package/lib/util.js +2 -1
  62. package/package.json +1 -1
package/lib/store.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { RevisionCache } from "./revision-cache.js";
1
2
  import { z } from 'zod';
2
3
  import { registrySchema, currentSchema, manifestSchema, uploadSchema, overlaySchema } from "./data.js";
3
4
  import { code, message, required } from "./util.js";
@@ -7,6 +8,7 @@ import { randomUUID } from 'node:crypto';
7
8
  import { atomicWrite, fileBytes, hash, id, now, readJson, safePath, text } from "./util.js";
8
9
  export const DEFAULT_SCHEMA = `# 知识库治理约定\n\n保留原文中的型号、管脚、单位、数值、极性和适用条件。图片描述与原文分别标注;不确定的信息保留待复核。摘要用于导航,回答依据原文和可核验图像。引用包含文档版本、章节、片段和图表编号。\n`;
9
10
  export class Store {
11
+ revisionCache = new RevisionCache();
10
12
  config;
11
13
  root;
12
14
  registryTail;
@@ -30,11 +32,13 @@ export class Store {
30
32
  }
31
33
  async trash(kbId, expectedName) {
32
34
  return this.serialized(async () => {
33
- const registry = await readJson(join(this.root, 'registry.json'), registrySchema), kb = registry.kbs.find(k => k.kbId === id(kbId) && !k.deletedAt);
35
+ const registry = await readJson(join(this.root, 'registry.json'), registrySchema);
36
+ const kb = registry.kbs.find(k => k.kbId === id(kbId) && !k.deletedAt);
34
37
  if (!kb)
35
38
  throw new Error('知识库不存在,请刷新');
36
39
  if (kb.name !== expectedName)
37
40
  throw new Error('知识库名称已变化,请刷新后确认删除');
41
+ this.revisionCache.clear();
38
42
  kb.deletedAt = now();
39
43
  this.record(registry, 'trash', kb);
40
44
  await atomicWrite(join(this.root, 'registry.json'), registry);
@@ -43,13 +47,15 @@ export class Store {
43
47
  }
44
48
  async restore(kbId) {
45
49
  return this.serialized(async () => {
46
- const registry = await readJson(join(this.root, 'registry.json'), registrySchema), kb = registry.kbs.find(k => k.kbId === id(kbId) && k.deletedAt);
50
+ const registry = await readJson(join(this.root, 'registry.json'), registrySchema);
51
+ const kb = registry.kbs.find(k => k.kbId === id(kbId) && k.deletedAt);
47
52
  if (!kb)
48
53
  throw new Error('知识库不在回收站,请刷新');
49
54
  if (kb.purge)
50
55
  throw new Error('知识库已开始彻底删除,不能恢复;请继续彻底删除');
51
56
  if (registry.kbs.some(k => !k.deletedAt && k.name === kb.name))
52
57
  throw new Error('同名知识库已存在,不能恢复;请先删除同名库');
58
+ this.revisionCache.clear();
53
59
  delete kb.deletedAt;
54
60
  this.record(registry, 'restore', kb);
55
61
  await atomicWrite(join(this.root, 'registry.json'), registry);
@@ -72,16 +78,19 @@ export class Store {
72
78
  }
73
79
  async removePurgeTree(path) { await rm(path, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 }); }
74
80
  async purge(kbId, expectedName, confirmed, jobIds = []) {
81
+ this.revisionCache.clear();
75
82
  if (confirmed !== true)
76
83
  throw new Error('必须明确确认彻底删除不可恢复');
77
84
  return this.serialized(async () => {
78
- const registry = await readJson(join(this.root, 'registry.json'), registrySchema), kb = registry.kbs.find(k => k.kbId === id(kbId) && k.deletedAt);
85
+ const registry = await readJson(join(this.root, 'registry.json'), registrySchema);
86
+ const kb = registry.kbs.find(k => k.kbId === id(kbId) && k.deletedAt);
79
87
  if (!kb)
80
88
  throw new Error('只能彻底删除回收站中的知识库');
81
89
  if (kb.name !== expectedName)
82
90
  throw new Error('知识库名称已变化,请重新确认彻底删除');
83
91
  const targets = [...new Set([...(kb.purge?.jobIds || []), ...jobIds])];
84
- const kbPath = await this.purgeTarget('kbs', kb.kbId), jobPaths = await Promise.all(targets.map(key => this.purgeTarget('jobs', key)));
92
+ const kbPath = await this.purgeTarget('kbs', kb.kbId);
93
+ const jobPaths = await Promise.all(targets.map(key => this.purgeTarget('jobs', key)));
85
94
  // Persist intent before removing files: a crash or partial failure must not allow restoration.
86
95
  kb.purge = { startedAt: kb.purge?.startedAt || now(), jobIds: targets };
87
96
  await atomicWrite(join(this.root, 'registry.json'), registry);
@@ -105,7 +114,8 @@ export class Store {
105
114
  });
106
115
  }
107
116
  async kb(selector) {
108
- const kbs = await this.list(), matches = kbs.filter(k => k.kbId === selector || k.name === selector);
117
+ const kbs = await this.list();
118
+ const matches = kbs.filter(k => k.kbId === selector || k.name === selector);
109
119
  if (matches.length !== 1)
110
120
  throw new Error(matches.length ? '知识库名称重复,请使用 kbId' : '知识库不存在,请先创建或选择');
111
121
  await safePath(this.root, join('kbs', id(required(matches[0]).kbId)), true);
@@ -129,13 +139,21 @@ export class Store {
129
139
  });
130
140
  }
131
141
  kbRoot(kbId) { return join(this.root, 'kbs', id(kbId, 'kbId')); }
132
- async inspect(kbId, revision) {
133
- const kb = await this.kb(kbId), kbPath = await safePath(this.root, join('kbs', kb.kbId));
142
+ async inspect(kbId, revision) { return this.inspectKnown(await this.kb(kbId), revision); }
143
+ async inspectKnown(kb, revision) {
144
+ const kbPath = await safePath(this.root, join('kbs', kb.kbId));
134
145
  const current = await this.current(kb.kbId);
135
146
  if (!revision && !current)
136
147
  return { location: { kbPath, revision: null, wikiPath: null, indexPath: null }, summary: { revision: null, documentCount: 0, chunkCount: 0, coverageIncomplete: false } };
137
- const snapshot = await this.revision(kb.kbId, revision || required(current).revision);
138
- return { location: { kbPath, revision: snapshot.revision, wikiPath: await safePath(snapshot.root, 'wiki'), indexPath: await safePath(snapshot.root, 'index.md') }, summary: { revision: snapshot.revision, documentCount: snapshot.manifest.documents.length, chunkCount: snapshot.manifest.documents.reduce((sum, d) => sum + d.chunks.length, 0), coverageIncomplete: Boolean(snapshot.manifest.coverageIncomplete) } };
148
+ const snapshot = await this.revisionKnown(kb, revision || required(current).revision);
149
+ return { location: await this.snapshotLocation(kb.kbId, snapshot), summary: { revision: snapshot.revision, documentCount: snapshot.manifest.documents.length, chunkCount: snapshot.manifest.documents.reduce((sum, d) => sum + d.chunks.length, 0), coverageIncomplete: Boolean(snapshot.manifest.coverageIncomplete) } };
150
+ }
151
+ async overview() {
152
+ const registry = await readJson(join(this.root, 'registry.json'), registrySchema);
153
+ return { kbs: await Promise.all(registry.kbs.filter(k => !k.deletedAt).map(async (kb) => ({ ...kb, ...await this.inspectKnown(kb) }))), deletedKbs: registry.kbs.filter(k => k.deletedAt), activities: registry.activities || [] };
154
+ }
155
+ async snapshotLocation(kbId, snapshot) {
156
+ return { kbPath: await safePath(this.root, join('kbs', id(kbId))), revision: snapshot.revision, wikiPath: await safePath(snapshot.root, 'wiki'), indexPath: await safePath(snapshot.root, 'index.md') };
139
157
  }
140
158
  async location(kbId, revision) { return (await this.inspect(kbId, revision)).location; }
141
159
  async schema(kbId) { return readFile(await safePath(this.kbRoot(kbId), 'schema.md'), 'utf8'); }
@@ -154,7 +172,8 @@ export class Store {
154
172
  filename = text(basename(filename), '文件名', 200);
155
173
  if (!/\.(?:docx?|pdf)$/i.test(filename) || data.length > this.config.maxInputBytes)
156
174
  throw new Error('只支持大小限制内的 .docx / .doc / .pdf 文件');
157
- const receiptId = `u-${randomUUID()}`, file = join(this.root, 'uploads', receiptId + '.bin');
175
+ const receiptId = `u-${randomUUID()}`;
176
+ const file = join(this.root, 'uploads', receiptId + '.bin');
158
177
  await atomicWrite(file, data);
159
178
  await atomicWrite(join(this.root, 'uploads', receiptId + '.json'), { receiptId, filename, createdAt: now(), bytes: data.length, hash: hash(data) });
160
179
  return { receiptId, filename, bytes: data.length };
@@ -167,15 +186,17 @@ export class Store {
167
186
  throw new Error('上传内容已变化');
168
187
  return { ...receipt, data };
169
188
  }
170
- async current(kbId) { return readJson(join(this.kbRoot(kbId), 'current.json'), currentSchema, null); }
171
- async revision(kbId, requested) {
172
- await this.kb(kbId);
173
- const current = await this.current(kbId), revision = requested || current?.revision;
189
+ async current(kbId) { return readJson(await safePath(this.root, join('kbs', id(kbId), 'current.json'), false), currentSchema, null); }
190
+ async open(selector, requested) { const kb = await this.kb(selector); return { kb, snapshot: await this.revisionKnown(kb, requested) }; }
191
+ async revision(kbId, requested) { return (await this.open(kbId, requested)).snapshot; }
192
+ async revisionKnown(kb, requested) {
193
+ const current = requested ? null : await this.current(kb.kbId);
194
+ const revision = requested || current?.revision;
174
195
  if (!revision)
175
196
  throw new Error('知识库尚无已发布版本');
176
197
  id(revision, 'revision');
177
- const root = await safePath(this.kbRoot(kbId), join('revisions', revision));
178
- const manifest = await readJson(join(root, 'manifest.json'), manifestSchema);
198
+ const root = await safePath(this.root, join('kbs', kb.kbId, 'revisions', revision));
199
+ const manifest = await this.revisionCache.manifest(await safePath(root, 'manifest.json'));
179
200
  if (manifest.revision !== revision)
180
201
  throw new Error('版本清单不一致');
181
202
  return { root, manifest, revision };
@@ -188,21 +209,21 @@ export class Store {
188
209
  const content = await this.chunkContent(snapshot, chunk);
189
210
  return { ...snapshot, chunk, content };
190
211
  }
191
- async chunkContent(snapshot, chunk) {
192
- const content = await readFile(await safePath(snapshot.root, chunk.path), 'utf8');
193
- if (hash(content) !== chunk.contentHash)
194
- throw new Error('片段内容与清单不一致,请重建');
195
- return content;
212
+ async chunkContent(snapshot, chunk) { return (await this.chunkView(snapshot, chunk, true)).content; }
213
+ async chunkView(snapshot, chunk, fresh = false) {
214
+ return this.revisionCache.chunk(await safePath(snapshot.root, chunk.path), chunk.contentHash, fresh);
196
215
  }
197
216
  async asset(kbId, revision, docId, assetId) {
198
- const snapshot = await this.revision(kbId, revision), doc = snapshot.manifest.documents.find(d => d.docId === docId);
217
+ const snapshot = await this.revision(kbId, revision);
218
+ const doc = snapshot.manifest.documents.find(d => d.docId === docId);
199
219
  const asset = doc?.assets.find(a => a.assetId === assetId);
200
220
  if (!asset)
201
221
  throw new Error('图片不在所选文档版本中');
202
222
  const file = await safePath(snapshot.root, join('sources', id(docId), asset.path));
203
- if (`a-${hash(await fileBytes(file, this.config.maxUncompressedBytes)).slice(0, 20)}` !== asset.assetId)
223
+ const data = await fileBytes(file, this.config.maxUncompressedBytes);
224
+ if (`a-${hash(data).slice(0, 20)}` !== asset.assetId)
204
225
  throw new Error('图片内容与清单不一致');
205
- return { ...snapshot, asset, file, doc: required(doc) };
226
+ return { ...snapshot, asset, file, data, doc: required(doc) };
206
227
  }
207
228
  async rawSnapshot(kbId, docId, sourceHash, data, info) {
208
229
  const root = join(this.kbRoot(kbId), 'raw', id(docId), id(sourceHash));
@@ -233,7 +254,8 @@ export class Store {
233
254
  return [];
234
255
  const { root, manifest } = await this.revision(kbId, current.revision);
235
256
  const docs = [];
236
- for (const document of manifest.documents.filter(d => d.docId !== replaceDocId)) {
257
+ for (const saved of manifest.documents.filter(d => d.docId !== replaceDocId)) {
258
+ const document = structuredClone(saved);
237
259
  const chunks = [];
238
260
  for (const chunk of document.chunks) {
239
261
  const content = await this.chunkContent({ root, manifest, revision: manifest.revision }, chunk);
@@ -254,7 +276,8 @@ export class Store {
254
276
  return docs;
255
277
  }
256
278
  async publish(kbId, revision, stage, manifest, operation) {
257
- const kbRoot = this.kbRoot(kbId), target = join(kbRoot, 'revisions', id(revision));
279
+ const kbRoot = this.kbRoot(kbId);
280
+ const target = join(kbRoot, 'revisions', id(revision));
258
281
  await atomicWrite(join(stage, 'manifest.json'), manifest);
259
282
  await mkdir(join(kbRoot, 'revisions'), { recursive: true });
260
283
  try {
@@ -267,10 +290,13 @@ export class Store {
267
290
  if (existing.buildKey !== manifest.buildKey)
268
291
  throw new Error('版本发布冲突');
269
292
  }
293
+ this.revisionCache.clear();
270
294
  // The pointer owns visibility. Replaying the same job after a crash is idempotent.
271
295
  await atomicWrite(join(kbRoot, 'current.json'), { revision, publishedAt: now(), buildKey: manifest.buildKey, coverageIncomplete: manifest.coverageIncomplete });
272
296
  await this.serialized(async () => {
273
- const logFile = join(kbRoot, 'log.md'), old = await readFile(logFile, 'utf8'), line = `- ${operation.jobId} ${now()} ${operation.kind} ${revision} ← ${operation.docId}\n`;
297
+ const logFile = join(kbRoot, 'log.md');
298
+ const old = await readFile(logFile, 'utf8');
299
+ const line = `- ${operation.jobId} ${now()} ${operation.kind} ${revision} ← ${operation.docId}\n`;
274
300
  if (!old.includes(`- ${operation.jobId} `))
275
301
  await atomicWrite(logFile, old + line);
276
302
  });
@@ -290,9 +316,32 @@ export class Store {
290
316
  return overlays[key];
291
317
  });
292
318
  }
293
- async jobDirectories() { return (await readdir(join(this.root, 'jobs'), { withFileTypes: true })).filter(d => d.isDirectory()).map(d => join(this.root, 'jobs', d.name)); }
319
+ /** Cleanup runs with the sole owner before accepting uploads or starting jobs. Inputs are independent copies. */
320
+ async cleanUploads(at = Date.now()) {
321
+ const root = await safePath(this.root, 'uploads');
322
+ const cutoff = at - this.config.uploadRetentionDays * 86400000;
323
+ for (const entry of await readdir(root, { withFileTypes: true })) {
324
+ if (!entry.isFile() || !/^u-[a-f0-9-]{36}\.json$/u.test(entry.name))
325
+ continue;
326
+ const file = await safePath(root, entry.name);
327
+ const receipt = await readJson(file, uploadSchema);
328
+ if (entry.name !== receipt.receiptId + '.json')
329
+ throw new Error('上传收据身份不一致');
330
+ const time = Date.parse(receipt.createdAt);
331
+ if (!Number.isFinite(time))
332
+ throw new Error('上传收据日期无效');
333
+ if (time >= cutoff)
334
+ continue;
335
+ const binary = await safePath(root, receipt.receiptId + '.bin', false);
336
+ // Remove the binary first. If interrupted, the validated receipt makes cleanup retryable.
337
+ await rm(binary, { force: true });
338
+ await rm(file);
339
+ }
340
+ }
341
+ async jobDirectories() { return (await readdir(await safePath(this.root, 'jobs'), { withFileTypes: true })).filter(d => d.isDirectory()).map(d => join(this.root, 'jobs', d.name)); }
294
342
  async acquireOwner() {
295
- const dir = join(this.root, '.owner.lock'), nonce = randomUUID();
343
+ const dir = join(this.root, '.owner.lock');
344
+ const nonce = randomUUID();
296
345
  try {
297
346
  await mkdir(dir);
298
347
  }
package/lib/tools.js CHANGED
@@ -4,10 +4,12 @@ import { z } from 'zod';
4
4
  import { prepareImage } from "./figures/process.js";
5
5
  import { buildSchema, jobActionSchema, catalogQuerySchema, readQuerySchema, searchSchema, assetQuerySchema } from "./shared.js";
6
6
  import { checkAbort, message } from "./util.js";
7
+ import { selectedKnowledge } from "./agent-access.js";
7
8
  const str = (description) => ({ type: 'string', description });
8
9
  const bool = (description) => ({ type: 'boolean', description });
9
10
  const int = (description) => ({ type: 'integer', description });
10
- const selector = str('已登记知识库 ID 或唯一库名'), revision = str('固定的知识库版本;首次检索可省略');
11
+ const selector = str('已登记知识库 ID 或唯一库名');
12
+ const revision = str('固定的知识库版本;首次检索可省略');
11
13
  const canonical = (value) => z.json().parse(JSON.parse(JSON.stringify(value)));
12
14
  function jobView(job) {
13
15
  const { jobId, kbId, docId, revision, state, error, outputPath, progress, routes, usage, report, failures } = job;
@@ -20,7 +22,10 @@ export function tools(runtime) {
20
22
  const define = (name, title, description, parameters, execute, render) => defineTool({
21
23
  name: `libing_${name}`, description, parameters, output: { schema: { type: 'json' }, render: render ?? ((_args, value) => [{ type: 'text', text: JSON.stringify(value) }]) },
22
24
  presentCall: args => ({ card: 'generic', title, kind: 'other', rawInput: args }),
23
- execute: async (args, exec) => { await ready; checkAbort(exec.signal); return runtime.run(async (signal) => canonical(await execute(args, { ...exec, signal })), exec.signal); },
25
+ execute: async (args, exec) => { await ready; checkAbort(exec.signal); return runtime.run(async (signal) => { const policy = await selectedKnowledge(runtime, exec.agent, name, args); const scoped = { ...args, ...policy.kbId ? { kbId: policy.kbId } : {} }; const result = await execute(scoped, { ...exec, signal }); if (policy.allowed !== undefined && name === 'kb_catalog' && policy.kbId === undefined) {
26
+ const value = z.object({ knowledgeBases: z.array(z.object({ kbId: z.string() }).passthrough()), truncated: z.boolean() }).parse(result);
27
+ return canonical({ ...value, knowledgeBases: value.knowledgeBases.filter(kb => policy.allowed?.includes(kb.kbId)) });
28
+ } return canonical(result); }, exec.signal); },
24
29
  });
25
30
  return [
26
31
  define('kb_build', '构建知识库', '将指定 Word / PDF 构建为知识库;返回后台 jobId。仅明确允许时建库、跳过图片或接受缺口。', {
@@ -55,8 +60,11 @@ export function tools(runtime) {
55
60
  define('kb_read_image', '核对知识库原图', '读取所选文档版本原图及裁片。当前模型须声明图片输入能力;忽略的图片不可作为证据。', {
56
61
  kbId: { ...selector, required: true }, revision, docId: { ...str('文档ID'), required: true }, assetId: { ...str('图片ID'), required: true },
57
62
  }, async (raw, exec) => {
58
- const args = assetQuerySchema.parse(raw), kb = await store.kb(args.kbId), asset = await store.asset(kb.kbId, args.revision, args.docId, args.assetId);
59
- const figures = asset.doc.figures.filter(f => f.assetId === args.assetId && f.status !== 'ignored'), region = asset.asset.source && asset.doc.sourceFormat === 'pdf';
63
+ const args = assetQuerySchema.parse(raw);
64
+ const kb = await store.kb(args.kbId);
65
+ const asset = await store.asset(kb.kbId, args.revision, args.docId, args.assetId);
66
+ const figures = asset.doc.figures.filter(f => f.assetId === args.assetId && f.status !== 'ignored');
67
+ const region = asset.asset.source && asset.doc.sourceFormat === 'pdf';
60
68
  if (!figures.length && (!region || asset.doc.figures.some(f => f.assetId === args.assetId && f.status === 'ignored')))
61
69
  throw new Error('此版本的图片已忽略,请在图片复核恢复导入');
62
70
  const route = exec.agent?.session.requestHeader()?.config ?? exec.agent?.options;
package/lib/util.js CHANGED
@@ -48,7 +48,8 @@ export async function atomicWrite(file, value) {
48
48
  export async function safePath(root, rel = '.', existing = true) {
49
49
  if (typeof rel !== 'string' || rel.includes('\0') || isAbsolute(rel))
50
50
  throw new Error('无效相对路径');
51
- const base = await realpath(root), target = resolve(base, rel);
51
+ const base = await realpath(root);
52
+ const target = resolve(base, rel);
52
53
  const inside = (value) => { const r = relative(base, value); return r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r); };
53
54
  if (!inside(target))
54
55
  throw new Error('路径越界');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-libing/knowledge-base",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "知识治理:Word/PDF 导入、图表复核、可恢复任务与固定版本证据检索",
5
5
  "type": "module",
6
6
  "icon": "icon.svg",