@dsh-libing/knowledge-base 0.1.3 → 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.
@@ -12,17 +12,24 @@ export function unpackDocx(buffer, config) {
12
12
  }
13
13
  if (end < 0)
14
14
  throw new Error('无效 DOCX ZIP 文件');
15
- const count = buffer.readUInt16LE(end + 10), size = buffer.readUInt32LE(end + 12), start = buffer.readUInt32LE(end + 16);
15
+ const count = buffer.readUInt16LE(end + 10);
16
+ const size = buffer.readUInt32LE(end + 12);
17
+ const start = buffer.readUInt32LE(end + 16);
16
18
  if (count === 65535 || count > config.maxArchiveEntries || start + size > end || buffer.readUInt16LE(end + 4) !== 0)
17
19
  throw new Error('不支持的分卷/ZIP64 或过多 ZIP 条目');
18
- let pos = start, total = 0;
20
+ let pos = start;
21
+ let total = 0;
19
22
  const entries = new Map();
20
23
  for (let n = 0; n < count; n++) {
21
24
  if (pos + 46 > end || buffer.readUInt32LE(pos) !== 0x02014b50)
22
25
  throw new Error('损坏的 ZIP 目录');
23
- const flags = buffer.readUInt16LE(pos + 8), method = buffer.readUInt16LE(pos + 10);
24
- const compressed = buffer.readUInt32LE(pos + 20), uncompressed = buffer.readUInt32LE(pos + 24);
25
- const nameLength = buffer.readUInt16LE(pos + 28), extraLength = buffer.readUInt16LE(pos + 30), commentLength = buffer.readUInt16LE(pos + 32);
26
+ const flags = buffer.readUInt16LE(pos + 8);
27
+ const method = buffer.readUInt16LE(pos + 10);
28
+ const compressed = buffer.readUInt32LE(pos + 20);
29
+ const uncompressed = buffer.readUInt32LE(pos + 24);
30
+ const nameLength = buffer.readUInt16LE(pos + 28);
31
+ const extraLength = buffer.readUInt16LE(pos + 30);
32
+ const commentLength = buffer.readUInt16LE(pos + 32);
26
33
  const next = pos + 46 + nameLength + extraLength + commentLength;
27
34
  if (next > end || uncompressed === 0xffffffff || compressed === 0xffffffff)
28
35
  throw new Error('损坏或不支持的 ZIP 条目');
@@ -34,7 +41,8 @@ export function unpackDocx(buffer, config) {
34
41
  const local = buffer.readUInt32LE(pos + 42);
35
42
  if (local + 30 > start || buffer.readUInt32LE(local) !== 0x04034b50)
36
43
  throw new Error('损坏的 ZIP 本地条目');
37
- const localNameLength = buffer.readUInt16LE(local + 26), localExtra = buffer.readUInt16LE(local + 28);
44
+ const localNameLength = buffer.readUInt16LE(local + 26);
45
+ const localExtra = buffer.readUInt16LE(local + 28);
38
46
  if (buffer.subarray(local + 30, local + 30 + localNameLength).toString('utf8') !== name || local + 30 + localNameLength + localExtra + compressed > start)
39
47
  throw new Error('ZIP 条目不一致');
40
48
  total += uncompressed;
@@ -7,12 +7,17 @@ import { imageMessageSchema, imageOptionsSchema } from "./image-protocol.js";
7
7
  export async function runImageWorker(file, config, save, signal) {
8
8
  const options = imageOptionsSchema.parse({ visionMaxEdge: config.visionMaxEdge, visionTileSize: config.visionTileSize, visionMaxTiles: config.visionMaxTiles });
9
9
  const release = await imageGate.acquire(signal);
10
- let worker, saving;
11
- const result = deferred(), abort = () => result.reject(signal?.reason ?? new Error('图片预处理已取消'));
12
- let timer, complete = false, next = 1;
10
+ let worker;
11
+ let saving;
12
+ const result = deferred();
13
+ const abort = () => result.reject(signal?.reason ?? new Error('图片预处理已取消'));
14
+ let timer;
15
+ let complete = false;
16
+ let next = 1;
13
17
  try {
14
18
  checkAbort(signal);
15
- const js = new URL('./image-worker.js', import.meta.url), source = !existsSync(fileURLToPath(js));
19
+ const js = new URL('./image-worker.js', import.meta.url);
20
+ const source = !existsSync(fileURLToPath(js));
16
21
  worker = new Worker(source ? new URL('./image-worker.ts', import.meta.url) : js, { workerData: { file, options }, execArgv: source ? ['--import', 'tsx/esm'] : [], resourceLimits: { maxOldGenerationSizeMb: 512 } });
17
22
  const current = worker;
18
23
  timer = setTimeout(() => result.reject(new Error('图片预处理超时')), Math.min(config.imageTimeoutMs, 120000));
@@ -10,16 +10,21 @@ async function run() {
10
10
  const { file, options } = z.object({ file: z.string(), options: imageOptionsSchema }).parse(workerData);
11
11
  if ((await stat(file)).size > 32 * 1024 * 1024)
12
12
  throw new Error('单张图片超过 32 MiB,请提供较小的位图');
13
- const data = await readFile(file), dimensions = imageSize(data);
13
+ const data = await readFile(file);
14
+ const dimensions = imageSize(data);
14
15
  if (data.length > 32 * 1024 * 1024)
15
16
  throw new Error('单张图片超过 32 MiB,请提供较小的位图');
16
17
  if (!dimensions.width || !dimensions.height || dimensions.width * dimensions.height > 50000000)
17
18
  throw new Error('图片像素过大,请提供分辨率较低的版本');
18
- const image = await Jimp.read(data), { width, height } = image.bitmap;
19
+ const image = await Jimp.read(data);
20
+ const { width, height } = image.bitmap;
19
21
  if (width * height > 50000000)
20
22
  throw new Error('图片像素过大,请提供分辨率较低的版本');
21
- const tile = options.visionTileSize, step = Math.max(1, Math.floor(tile * .85)), tiled = Math.max(width, height) > options.visionMaxEdge;
22
- const columns = Math.max(1, Math.ceil((width - tile) / step) + 1), rows = Math.max(1, Math.ceil((height - tile) / step) + 1);
23
+ const tile = options.visionTileSize;
24
+ const step = Math.max(1, Math.floor(tile * .85));
25
+ const tiled = Math.max(width, height) > options.visionMaxEdge;
26
+ const columns = Math.max(1, Math.ceil((width - tile) / step) + 1);
27
+ const rows = Math.max(1, Math.ceil((height - tile) / step) + 1);
23
28
  if (tiled && columns * rows > options.visionMaxTiles)
24
29
  throw new Error(`大图需要 ${columns * rows} 个裁片,超过 visionMaxTiles;请拆分图片或提高上限`);
25
30
  let id = 0;
@@ -27,7 +32,8 @@ async function run() {
27
32
  const bytes = await bitmap.getBuffer('image/png');
28
33
  if (bytes.length > 8 * 1024 * 1024)
29
34
  throw new Error('图像转换后超过单张 8 MiB,请降低 visionTileSize');
30
- const packet = new Uint8Array(bytes), current = ++id;
35
+ const packet = new Uint8Array(bytes);
36
+ const current = ++id;
31
37
  const acknowledged = new Promise((resolve, reject) => port.once('message', raw => {
32
38
  const ack = z.object({ ack: z.literal(current) }).safeParse(raw);
33
39
  if (ack.success)
@@ -48,7 +54,10 @@ async function run() {
48
54
  if (tiled)
49
55
  for (let row = 0; row < rows; row++)
50
56
  for (let col = 0; col < columns; col++) {
51
- const x = Math.min(col * step, Math.max(0, width - tile)), y = Math.min(row * step, Math.max(0, height - tile)), w = Math.min(tile, width), h = Math.min(tile, height);
57
+ const x = Math.min(col * step, Math.max(0, width - tile));
58
+ const y = Math.min(row * step, Math.max(0, height - tile));
59
+ const w = Math.min(tile, width);
60
+ const h = Math.min(tile, height);
52
61
  // Copy only the requested region; never clone the full source for a tile.
53
62
  const bitmap = new Jimp({ width: w, height: h });
54
63
  for (let line = 0; line < h; line++)
@@ -45,8 +45,10 @@ export async function processFigures(document, sourceDir, job, models, signal, o
45
45
  checkAbort(signal);
46
46
  job.progress.inflightFigures = [...(job.progress.inflightFigures || []), { occurrenceId: block.occurrenceId, caption: block.caption || block.occurrenceId }];
47
47
  await job.checkpoint();
48
- const asset = document.assets.find(x => x.assetId === block.assetId), resultPath = join(sourceDir, 'figures', block.occurrenceId + '.json');
49
- const overlayKey = hash({ assetId: block.assetId, context }), overlay = overlays[overlayKey];
48
+ const asset = document.assets.find(x => x.assetId === block.assetId);
49
+ const resultPath = join(sourceDir, 'figures', block.occurrenceId + '.json');
50
+ const overlayKey = hash({ assetId: block.assetId, context });
51
+ const overlay = overlays[overlayKey];
50
52
  const cacheKey = hash({ assetId: block.assetId, context, route: job.routes.vision, prompt: PROMPT_VERSION, params: { maxEdge: job.config.visionMaxEdge, tile: job.config.visionTileSize, maxChars: job.config.figureMaxChars } });
51
53
  let result = await readJson(resultPath, figureSchema, null);
52
54
  const overlayHash = hash(overlay || null);
@@ -60,7 +62,15 @@ export async function processFigures(document, sourceDir, job, models, signal, o
60
62
  if (!asset)
61
63
  throw new Error('未提取原图');
62
64
  if (overlay)
63
- result = figureSchema.parse({ ...overlay, cacheKey, overlayKey, overlayHash, assetId: block.assetId, occurrenceId: block.occurrenceId, caption: block.caption, status: overlay.ignore ? 'ignored' : overlay.skip ? 'decoration' : 'reviewed', humanReviewed: true });
65
+ result = figureSchema.parse({ ...overlay, cacheKey, overlayKey, overlayHash, assetId: block.assetId, occurrenceId: block.occurrenceId, caption: block.caption, status: (() => {
66
+ if (overlay.ignore) {
67
+ return 'ignored';
68
+ }
69
+ if (overlay.skip) {
70
+ return 'decoration';
71
+ }
72
+ return 'reviewed';
73
+ })(), humanReviewed: true });
64
74
  else if (job.config.skipImages)
65
75
  result = figureSchema.parse({ status: 'skipped', cacheKey, overlayKey, overlayHash, assetId: block.assetId, occurrenceId: block.occurrenceId, caption: block.caption, description: '本次明确跳过图片理解', needsReview: true, uncertainties: ['未分析图片'] });
66
76
  else {
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ import { createRuntime } from "./runtime.js";
3
3
  import { routes } from "./routes.js";
4
4
  import { tools } from "./tools.js";
5
5
  import { skillProvider } from "./skills.js";
6
+ import { registerKnowledgeCatalog } from "./agent-access.js";
6
7
  export { Config } from "./config.js";
7
8
  export { createRuntime } from "./runtime.js";
8
9
  export const name = 'libing-knowledge-base';
@@ -10,7 +11,8 @@ export const inject = ['connection', 'tools', 'skills', 'llm', 'attachments'];
10
11
  const lifetimes = new Map();
11
12
  /** Context owns all registrations and drains durable work before releasing the data root. */
12
13
  export async function apply(ctx, config = {}) {
13
- const resolved = resolveConfig(config), previous = lifetimes.get(resolved.dataRoot);
14
+ const resolved = resolveConfig(config);
15
+ const previous = lifetimes.get(resolved.dataRoot);
14
16
  const scope = { disposed: false };
15
17
  // Own cancellation before awaiting a previous generation; disposal during that wait must not leak a newly created runtime.
16
18
  ctx.effect(() => () => {
@@ -28,13 +30,15 @@ export async function apply(ctx, config = {}) {
28
30
  await previous.closing;
29
31
  if (scope.disposed)
30
32
  return;
31
- const runtime = createRuntime(ctx, resolved), lifetime = { runtime };
33
+ const runtime = createRuntime(ctx, resolved);
34
+ const lifetime = { runtime };
32
35
  scope.owned = lifetime;
33
36
  if (!lifetimes.has(resolved.dataRoot))
34
37
  lifetimes.set(resolved.dataRoot, lifetime);
35
38
  await runtime.ready;
36
39
  if (scope.disposed)
37
40
  return;
41
+ registerKnowledgeCatalog(ctx, runtime);
38
42
  for (const definition of tools(runtime))
39
43
  ctx.tools.register(definition);
40
44
  ctx.effect(() => ctx.skills.registerProvider(() => skillProvider()), 'libing-knowledge-base: bundled skills');
package/lib/indexes.js CHANGED
@@ -12,7 +12,8 @@ export async function summarizeChunks(chunks, job, models, signal) {
12
12
  for (const chunk of chunks) {
13
13
  job.progress.currentChunk = { title: chunk.title, headingPath: chunk.headingPath, chunkId: chunk.chunkId };
14
14
  await job.checkpoint();
15
- let summary, fallback = false;
15
+ let summary;
16
+ let fallback = false;
16
17
  if (job.routes.summary) {
17
18
  try {
18
19
  const key = hash({ content: chunk.contentHash, route: job.routes.summary, max: job.config.summaryMaxChars, schema: job.schemaHash, prompt: 1 });
@@ -43,11 +44,13 @@ export async function summarizeChunks(chunks, job, models, signal) {
43
44
  }
44
45
  /** Every node is a bounded navigation page; overflow is split into linked pages. */
45
46
  export async function writeIndexes(dir, documents, config, revision) {
46
- const catalogs = {}, entities = {};
47
+ const catalogs = {};
48
+ const entities = {};
47
49
  const writeCatalog = async (catalogId, path, title, entries) => {
48
50
  const intro = `# ${mdText(title)}\n\n版本:${revision}\n\n`;
49
51
  const pageEntries = [];
50
- let group = [], length = chars(intro);
52
+ let group = [];
53
+ let length = chars(intro);
51
54
  for (const entry of entries) {
52
55
  const line = entry.line || `- [${mdText(entry.title)}](${posix.relative(posix.dirname(path), required(entry.path))}) — ${mdText(entry.summary)};docId=${entry.docId}${entry.nodeId ? '; nodeId=' + entry.nodeId : ''}${entry.chunkId ? '; chunkId=' + entry.chunkId : ''};${entry.fullChars || 0} 字符;${mdText((entry.keywords || []).slice(0, 12).join('、'))};${entry.reviewStatus || ''}${entry.pdfScope ? ';' + mdText(entry.pdfScope) : ''}${entry.pageRange ? ';PDF第' + entry.pageRange.join('–') + '页' : ''}\n`;
53
56
  if (group.length && length + chars(line) > config.catalogMaxChars - 300) {
@@ -61,7 +64,8 @@ export async function writeIndexes(dir, documents, config, revision) {
61
64
  pageEntries.push(group);
62
65
  catalogs[catalogId] = { catalogId, path, title, pages: [] };
63
66
  for (let i = 0; i < pageEntries.length; i++) {
64
- const pagePath = i === 0 ? path : path.replace(/\.md$/, `-${i + 1}.md`), next = i + 1 < pageEntries.length ? path.replace(/\.md$/, `-${i + 2}.md`) : null;
67
+ const pagePath = i === 0 ? path : path.replace(/\.md$/, `-${i + 1}.md`);
68
+ const next = i + 1 < pageEntries.length ? path.replace(/\.md$/, `-${i + 2}.md`) : null;
65
69
  const content = intro + required(pageEntries[i]).map(x => x.line).join('') + (next ? `\n[下一页索引](${posix.basename(next)})\n` : '');
66
70
  if (chars(content) > config.catalogMaxChars)
67
71
  throw new Error('单个索引条目超过导航预算,请提高 catalogMaxChars 或缩短标题');
@@ -32,7 +32,8 @@ export async function loadJob(directory) {
32
32
  if (record.jobId !== basename(directory))
33
33
  throw new Error('任务目录与身份不一致');
34
34
  if (['queued', 'preflight', 'converting', 'describing', 'splitting', 'indexing', 'validating', 'publishing'].includes(record.state)) {
35
- let total = 0, reported = 0;
35
+ let total = 0;
36
+ let reported = 0;
36
37
  for (let number = 1; number <= record.usage.calls; number++) {
37
38
  const receipt = await readJson(await safePath(directory, join('calls', `${String(number).padStart(6, '0')}.json`), false), receiptSchema, null);
38
39
  if (receipt?.usage) {
@@ -96,7 +97,8 @@ export async function readReport(job) {
96
97
  export async function usagePage(job, offset, limit, signal) {
97
98
  if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 50)
98
99
  throw new Error('用量分页参数无效');
99
- const total = job.legacyUsageCount + Math.max(0, job.usage.calls - job.legacyCallCount), records = [];
100
+ const total = job.legacyUsageCount + Math.max(0, job.usage.calls - job.legacyCallCount);
101
+ const records = [];
100
102
  const end = Math.min(total, offset + limit);
101
103
  let legacy;
102
104
  if (offset < job.legacyUsageCount)
@@ -108,7 +110,8 @@ export async function usagePage(job, offset, limit, signal) {
108
110
  continue;
109
111
  }
110
112
  const number = job.legacyCallCount + index - job.legacyUsageCount + 1;
111
- const file = await safePath(job.directory, join('calls', `${String(number).padStart(6, '0')}.json`), false), receipt = await readJson(file, receiptSchema, null);
113
+ const file = await safePath(job.directory, join('calls', `${String(number).padStart(6, '0')}.json`), false);
114
+ const receipt = await readJson(file, receiptSchema, null);
112
115
  records.push(z.json().parse(JSON.parse(JSON.stringify({ call: number, state: receipt?.state ?? 'reserved', usage: receipt?.usage ?? null }))));
113
116
  }
114
117
  return { records, offset, total, next: end < total ? end : null };
package/lib/jobs.js CHANGED
@@ -94,7 +94,7 @@ export class Jobs {
94
94
  });
95
95
  return { jobs: page, jobTotal: all.length, jobOffset: offset, activeJobs: [...this.jobs.values()].filter(j => ACTIVE.has(j.state)).length };
96
96
  }
97
- async details(jobId, includeReport = true) { const job = this.get(jobId), { report: _report, ...value } = this.public(job); return { ...value, ...includeReport ? { report: await readReport(job) } : {} }; }
97
+ async details(jobId, includeReport = true) { const job = this.get(jobId); const { report: _report, ...value } = this.public(job); return { ...value, ...includeReport ? { report: await readReport(job) } : {} }; }
98
98
  async usage(jobId, offset, limit, signal) { return usagePage(this.get(jobId), offset, limit, signal); }
99
99
  get(jobId) {
100
100
  const job = this.jobs.get(id(jobId, 'jobId'));
@@ -111,7 +111,8 @@ export class Jobs {
111
111
  const kb = await this.store.kb(text(args.kbId, 'kbId', 120));
112
112
  if (Boolean(args.sourcePath) === Boolean(args.receiptId))
113
113
  throw new Error('sourcePath / receiptId 必须提供其中一个');
114
- let source, filename;
114
+ let source;
115
+ let filename;
115
116
  if (args.receiptId) {
116
117
  const uploaded = await this.store.uploaded(args.receiptId);
117
118
  source = uploaded.data;
@@ -154,12 +155,14 @@ export class Jobs {
154
155
  await session.close();
155
156
  await rm(temp, { force: true });
156
157
  }
157
- const current = await this.store.current(kb.kbId), old = current ? (await this.store.revision(kb.kbId, current.revision)).manifest.documents.find(d => d.docId === docId) : null;
158
+ const current = await this.store.current(kb.kbId);
159
+ const old = current ? (await this.store.revision(kb.kbId, current.revision)).manifest.documents.find(d => d.docId === docId) : null;
158
160
  const previous = old?.selectedPages;
159
161
  if (previous && (previous.partial || selectedPages.partial) && (previous.start !== selectedPages.start || previous.end !== selectedPages.end) && args.confirmRangeChange !== true)
160
162
  throw new Error('PDF 导入范围已变化,本次发布会替换该文档当前内容;请明确确认范围变化,历史版本保留');
161
163
  }
162
- const schema = await this.store.schema(kb.kbId), schemaHash = hash(schema);
164
+ const schema = await this.store.schema(kb.kbId);
165
+ const schemaHash = hash(schema);
163
166
  const routes = { vision: config.visionProvider ? { provider: config.visionProvider, model: config.visionModel } : null, summary: config.summaryProvider ? { provider: config.summaryProvider, model: config.summaryModel } : null };
164
167
  if (!routes.summary) {
165
168
  const defaultRoute = this.models.ctx.get('agentDefaultModel')?.currentSelection() || execution.route;
@@ -173,7 +176,8 @@ export class Jobs {
173
176
  const existing = [...this.jobs.values()].find(j => j.kbId === kb.kbId && j.buildKey === optionsKey && (ACTIVE.has(j.state) || j.state === 'ready' && current?.revision === j.revision));
174
177
  if (existing)
175
178
  return { ...this.public(existing), reused: true };
176
- const jobId = `job-${randomUUID()}`, directory = join(this.store.root, 'jobs', jobId);
179
+ const jobId = `job-${randomUUID()}`;
180
+ const directory = join(this.store.root, 'jobs', jobId);
177
181
  await mkdir(directory, { recursive: true });
178
182
  await atomicWrite(join(directory, 'input' + extname(filename).toLowerCase()), source);
179
183
  const job = this.attach(jobSchema.parse({ jobId, kbId: kb.kbId, docId, title: text(args.title || filename.replace(/\.(?:docx?|pdf)$/i, ''), '文档标题', 300), sourceVersion: args.sourceVersion ? text(args.sourceVersion, '文档版本', 120) : '', filename, sourceFormat, ...selectedPages ? { pageStart: selectedPages.start, pageEnd: selectedPages.end, pdf: { pageCount: selectedPages.total, selectedPages } } : {}, sourceOriginalHash, buildKey: optionsKey, config, routes, schema, schemaHash, overlays, sessionId: execution.sessionId || null, state: 'queued', createdAt: now(), progress: {}, usage: { calls: 0, tokens: 0, reportedCalls: 0, records: [] }, failures: [] }), directory);
@@ -222,7 +226,8 @@ export class Jobs {
222
226
  return { root, manifest, revision: required(job.revision), kbId: job.kbId, jobId };
223
227
  }
224
228
  async run(job, signal) {
225
- const stage = join(job.directory, 'stage'), sourceDir = join(stage, 'sources', job.docId);
229
+ const stage = join(job.directory, 'stage');
230
+ const sourceDir = join(stage, 'sources', job.docId);
226
231
  try {
227
232
  await this.phase(job, 'preflight');
228
233
  checkAbort(signal);
@@ -358,7 +363,9 @@ export class Jobs {
358
363
  async reviewUnlocked(kbId, revision, docId, occurrenceId, value, expectedHash, options = {}) {
359
364
  if (Boolean(options.visionProvider) !== Boolean(options.visionModel))
360
365
  throw new Error('visionProvider / visionModel 必须成对配置');
361
- const snapshot = await this.store.revision(kbId, revision), doc = snapshot.manifest.documents.find(d => d.docId === docId), figure = doc?.figures.find(f => f.occurrenceId === occurrenceId);
366
+ const snapshot = await this.store.revision(kbId, revision);
367
+ const doc = snapshot.manifest.documents.find(d => d.docId === docId);
368
+ const figure = doc?.figures.find(f => f.occurrenceId === occurrenceId);
362
369
  if (!figure)
363
370
  throw new Error('图片不存在');
364
371
  const current = await this.store.current(kbId);
@@ -367,7 +374,8 @@ export class Jobs {
367
374
  const original = await this.store.rawSource(kbId, required(doc));
368
375
  const previous = [...this.jobs.values()].find(j => j.revision === revision && j.docId === docId);
369
376
  const vision = options.visionProvider && options.visionModel ? { provider: options.visionProvider, model: options.visionModel } : previous?.routes.vision || required(doc).figures.find(f => f.model)?.model || (this.config.visionProvider ? { provider: this.config.visionProvider, model: this.config.visionModel } : null);
370
- const overlays = await this.store.overlays(kbId), previousOverlay = overlays[figure.overlayKey] || null;
377
+ const overlays = await this.store.overlays(kbId);
378
+ const previousOverlay = overlays[figure.overlayKey] || null;
371
379
  if (options.restore) {
372
380
  if (!previousOverlay?.ignore)
373
381
  throw new Error('仅已忽略的图片可以恢复导入');
@@ -384,7 +392,8 @@ export class Jobs {
384
392
  }
385
393
  async trash(kbId, expectedName) {
386
394
  return this.mutate(async () => {
387
- const kb = await this.store.kb(kbId), related = [...this.jobs.values()].filter(j => j.kbId === kb.kbId);
395
+ const kb = await this.store.kb(kbId);
396
+ const related = [...this.jobs.values()].filter(j => j.kbId === kb.kbId);
388
397
  if (this.work.has(kb.kbId) || related.some(j => ACTIVE.has(j.state) || this.controllers.has(j.jobId)))
389
398
  throw new Error('知识库仍有构建任务未退出,请先取消或等待完成后删除');
390
399
  const removed = await this.store.trash(kb.kbId, expectedName);
package/lib/models.js CHANGED
@@ -31,10 +31,15 @@ export class Models {
31
31
  const number = ++job.usage.calls;
32
32
  job.pendingCalls.push(number);
33
33
  const file = join(job.directory, 'calls', `${String(number).padStart(6, '0')}.json`);
34
- const controller = new AbortController(), timeout = setTimeout(() => controller.abort(new Error('模型调用超时')), job.config.modelTimeoutMs);
34
+ const controller = new AbortController();
35
+ const timeout = setTimeout(() => controller.abort(new Error('模型调用超时')), job.config.modelTimeoutMs);
35
36
  const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
36
37
  const request = { kind, route, prompt, system, images, startedAt: now(), maxTokens: job.config.modelMaxOutputTokens };
37
- let response = '', complete = '', usage, finish, accounted = false;
38
+ let response = '';
39
+ let complete = '';
40
+ let usage;
41
+ let finish;
42
+ let accounted = false;
38
43
  const account = () => {
39
44
  if (accounted)
40
45
  return;
package/lib/retrieval.js CHANGED
@@ -37,10 +37,12 @@ function windows(chunk, content, terms, config, options = {}) {
37
37
  }
38
38
  const ordered = [...indices].sort((a, b) => a - b);
39
39
  const read = [];
40
- let body = '', continuation = null;
40
+ let body = '';
41
+ let continuation = null;
41
42
  const max = Math.max(128, Math.min(config.evidenceMaxChars - 2000, options.windowBudget || config.evidenceMaxChars - 2000));
42
43
  for (const i of ordered) {
43
- const s = required(chunk.segments[i], '语义块'), heading = s.startLine === undefined || s.endLine === undefined ? `[${s.type} · segment ${i}]\n` : `[L${s.startLine}–L${s.endLine} · ${s.type}]\n`;
44
+ const s = required(chunk.segments[i], '语义块');
45
+ const heading = s.startLine === undefined || s.endLine === undefined ? `[${s.type} · segment ${i}]\n` : `[L${s.startLine}–L${s.endLine} · ${s.type}]\n`;
44
46
  let value = s.text;
45
47
  const start = options.segmentIndex === i ? options.offset || 0 : 0;
46
48
  if (start > chars(value))
@@ -51,11 +53,31 @@ function windows(chunk, content, terms, config, options = {}) {
51
53
  continuation = { segmentIndex: i, offset: 0 };
52
54
  break;
53
55
  }
54
- const characters = Array.from(s.text), hit = terms.map(t => value.toLowerCase().indexOf(t)).filter(x => x >= 0).sort((a, b) => a - b)[0];
55
- const offset = options.segmentIndex !== undefined ? start : (hit === undefined ? 0 : Math.max(0, chars(value.slice(0, hit)) - 250));
56
+ const characters = Array.from(s.text);
57
+ const hit = terms.map(t => value.toLowerCase().indexOf(t)).filter(x => x >= 0).sort((a, b) => a - b)[0];
58
+ let offset;
59
+ if (options.segmentIndex !== undefined) {
60
+ offset = start;
61
+ }
62
+ else {
63
+ if (hit === undefined) {
64
+ offset = 0;
65
+ }
66
+ else {
67
+ offset = Math.max(0, chars(value.slice(0, hit)) - 250);
68
+ }
69
+ }
56
70
  const available = max - chars(heading) - 50;
57
71
  value = clip(characters.slice(offset).join(''), available);
58
- continuation = offset + chars(value) < characters.length ? { segmentIndex: i, offset: offset + chars(value) } : i + 1 < chunk.segments.length ? { segmentIndex: i + 1, offset: 0 } : null;
72
+ continuation = (() => {
73
+ if (offset + chars(value) < characters.length) {
74
+ return { segmentIndex: i, offset: offset + chars(value) };
75
+ }
76
+ if (i + 1 < chunk.segments.length) {
77
+ return { segmentIndex: i + 1, offset: 0 };
78
+ }
79
+ return null;
80
+ })();
59
81
  read.push({ segmentIndex: i, startLine: s.startLine, endLine: s.endLine, charOffset: offset, partial: true });
60
82
  body += heading + value + '\n[此语义块仅返回部分,请按 continuation 续读]\n';
61
83
  break;
@@ -76,7 +98,8 @@ export class Retrieval {
76
98
  }
77
99
  async catalog(args = {}) {
78
100
  if (!args.kbId) {
79
- const kbs = await this.store.list(), entries = [];
101
+ const kbs = await this.store.list();
102
+ const entries = [];
80
103
  for (const kb of kbs) {
81
104
  const current = await this.store.current(kb.kbId);
82
105
  entries.push({ ...kb, revision: current?.revision || null, coverageIncomplete: current?.coverageIncomplete || false });
@@ -103,13 +126,16 @@ export class Retrieval {
103
126
  }
104
127
  async search(args) {
105
128
  const { kb, snapshot } = await this.store.open(args.kbId, args.revision);
106
- const terms = queryTerms(args.query, args.keywords), scored = [];
129
+ const terms = queryTerms(args.query, args.keywords);
130
+ const scored = [];
107
131
  for (const doc of snapshot.manifest.documents) {
108
132
  if (args.docId && doc.docId !== args.docId)
109
133
  continue;
110
134
  for (const chunk of doc.chunks) {
111
135
  const { content, lower: body } = await this.store.chunkView(snapshot, chunk);
112
- const hits = terms.filter(t => body.includes(t)), title = chunk.headingPath.join('/').toLowerCase(), keys = (chunk.keywords || []).map(k => k.toLowerCase());
136
+ const hits = terms.filter(t => body.includes(t));
137
+ const title = chunk.headingPath.join('/').toLowerCase();
138
+ const keys = (chunk.keywords || []).map(k => k.toLowerCase());
113
139
  if (!hits.length)
114
140
  continue;
115
141
  const score = hits.reduce((n, t) => n + (keys.includes(t) ? 12 : 0) + (title.includes(t) ? 6 : 0) + Math.min(5, body.split(t).length - 1), 0);
@@ -151,7 +177,8 @@ export class Retrieval {
151
177
  return bounded(value, this.config.evidenceMaxChars);
152
178
  }
153
179
  async read(args) {
154
- const kb = await this.store.kb(args.kbId), snapshot = await this.store.readChunk(kb.kbId, args.revision, args.chunkId);
180
+ const kb = await this.store.kb(args.kbId);
181
+ const snapshot = await this.store.readChunk(kb.kbId, args.revision, args.chunkId);
155
182
  await this.store.kb(kb.kbId);
156
183
  return this.readSnapshot(kb.kbId, snapshot, args);
157
184
  }
package/lib/routes.js CHANGED
@@ -6,10 +6,15 @@ import { figureDataSchema, overlaySchema } from "./data.js";
6
6
  import { inspectPdf } from "./document/pdf-session.js";
7
7
  import { hash, safePath, message, required, fileBytes, checkAbort } from "./util.js";
8
8
  const HOST_VERSION = z.object({ version: z.string() }).parse(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))).version;
9
- const id = z.string().regex(/^[a-z0-9][a-z0-9-]{0,95}$/u), name = z.string().trim().min(1).max(120), digest = z.string().regex(/^[a-f0-9]{64}$/u);
10
- const libraryArgs = z.strictObject({ kbId: id }), managementArgs = libraryArgs.extend({ confirmName: name });
9
+ const id = z.string().regex(/^[a-z0-9][a-z0-9-]{0,95}$/u);
10
+ const name = z.string().trim().min(1).max(120);
11
+ const digest = z.string().regex(/^[a-f0-9]{64}$/u);
12
+ const libraryArgs = z.strictObject({ kbId: id });
13
+ const managementArgs = libraryArgs.extend({ confirmName: name });
11
14
  const reviewSchema = z.strictObject({ kbId: id, revision: id, docId: id, occurrenceId: id, expectedHash: digest, action: z.enum(['reidentify', 'restore']).optional(), figure: z.union([figureDataSchema.strict(), z.strictObject({ ignore: z.literal(true) }), z.strictObject({ skip: z.literal(true) })]).optional(), visionProvider: z.string().optional(), visionModel: z.string().optional() }).refine(value => Boolean(value.action) !== Boolean(value.figure), '指定一个复核操作或内容');
12
- const draftQuery = z.strictObject({ jobId: id }), sourceQuery = revisionQuerySchema.extend({ docId: id }), draftAssetQuery = draftQuery.extend({ docId: id, assetId: id });
15
+ const draftQuery = z.strictObject({ jobId: id });
16
+ const sourceQuery = revisionQuerySchema.extend({ docId: id });
17
+ const draftAssetQuery = draftQuery.extend({ docId: id, assetId: id });
13
18
  const draftReadQuery = draftQuery.extend(readQuerySchema.omit({ kbId: true, revision: true }).shape);
14
19
  const headers = { 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', 'referrer-policy': 'no-referrer' };
15
20
  function query(request) {
@@ -93,32 +98,52 @@ export function routes(runtime, ctx) {
93
98
  return store.upload(await bytes(request, config.maxInputBytes), args.filename);
94
99
  });
95
100
  add('inspect-document', 'POST', async (request) => {
96
- const { receiptId } = await body(request, z.strictObject({ receiptId: id })), uploaded = await store.uploaded(receiptId);
101
+ const { receiptId } = await body(request, z.strictObject({ receiptId: id }));
102
+ const uploaded = await store.uploaded(receiptId);
97
103
  if (!/\.pdf$/iu.test(uploaded.filename))
98
104
  throw new Error('文档预检只支持 PDF');
99
105
  return inspectPdf(await safePath(store.root, join('uploads', receiptId + '.bin')), config, request.signal);
100
106
  });
101
- add('source', 'GET', async (request) => { const args = sourceQuery.parse(query(request)), kb = await store.kb(args.kbId); return original({ ...await store.revision(kb.kbId, args.revision), kbId: kb.kbId }, args.docId); });
107
+ add('source', 'GET', async (request) => { const args = sourceQuery.parse(query(request)); const kb = await store.kb(args.kbId); return original({ ...await store.revision(kb.kbId, args.revision), kbId: kb.kbId }, args.docId); });
102
108
  add('draft-source', 'GET', async (request) => { const args = draftQuery.extend({ docId: id }).parse(query(request)); return original(await jobs.preview(args.jobId), args.docId); });
103
109
  add('build', 'POST', async (request) => jobs.submit(await body(request, browserBuildSchema)));
104
110
  add('job', 'POST', async (request) => { const args = await body(request, jobActionSchema); return jobs.action(args.jobId, args.action, args); });
105
111
  add('draft', 'GET', async (request) => { const snapshot = await jobs.preview(draftQuery.parse(query(request)).jobId); return { ...tree(snapshot, snapshot.kbId), previewJobId: snapshot.jobId, coverageIncomplete: true }; });
106
112
  add('draft-read', 'GET', async (request) => {
107
- const { jobId, ...args } = draftReadQuery.parse(query(request)), snapshot = await jobs.preview(jobId), chunk = required(snapshot.manifest.documents.flatMap(d => d.chunks).find(c => c.chunkId === args.chunkId), '草稿片段');
113
+ const { jobId, ...args } = draftReadQuery.parse(query(request));
114
+ const snapshot = await jobs.preview(jobId);
115
+ const chunk = required(snapshot.manifest.documents.flatMap(d => d.chunks).find(c => c.chunkId === args.chunkId), '草稿片段');
108
116
  const value = retrieval.readSnapshot(snapshot.kbId, { ...snapshot, chunk, content: await store.chunkContent(snapshot, chunk) }, { ...args, kbId: snapshot.kbId });
109
117
  return { ...value, previewJobId: jobId, warnings: ['待发布预览:未向 Agent 检索开放', ...value.warnings] };
110
118
  });
111
- add('draft-asset', 'GET', async (request) => { const args = draftAssetQuery.parse(query(request)), snapshot = await jobs.preview(args.jobId), doc = required(snapshot.manifest.documents.find(d => d.docId === args.docId), '草稿文档'), asset = required(doc.assets.find(a => a.assetId === args.assetId), '草稿图片'); return image(asset, await safePath(snapshot.root, join('sources', doc.docId, asset.path))); });
119
+ add('draft-asset', 'GET', async (request) => { const args = draftAssetQuery.parse(query(request)); const snapshot = await jobs.preview(args.jobId); const doc = required(snapshot.manifest.documents.find(d => d.docId === args.docId), '草稿文档'); const asset = required(doc.assets.find(a => a.assetId === args.assetId), '草稿图片'); return image(asset, await safePath(snapshot.root, join('sources', doc.docId, asset.path))); });
112
120
  add('catalog', 'GET', async (request) => retrieval.catalog(catalogQuerySchema.parse(query(request))));
113
- add('tree', 'GET', async (request) => { const args = revisionQuerySchema.parse(query(request)), { kb, snapshot } = await store.open(args.kbId, args.revision), overlays = await store.overlays(kb.kbId), value = tree(snapshot, kb.kbId); return { ...value, location: await store.snapshotLocation(kb.kbId, snapshot), documents: value.documents.map(doc => ({ ...doc, figures: doc.figures.map(figure => ({ ...figure, expectedHash: hash(overlays[figure.overlayKey] || null) })) })) }; });
121
+ add('tree', 'GET', async (request) => { const args = revisionQuerySchema.parse(query(request)); const { kb, snapshot } = await store.open(args.kbId, args.revision); const overlays = await store.overlays(kb.kbId); const value = tree(snapshot, kb.kbId); return { ...value, location: await store.snapshotLocation(kb.kbId, snapshot), documents: value.documents.map(doc => ({ ...doc, figures: doc.figures.map(figure => ({ ...figure, expectedHash: hash(overlays[figure.overlayKey] || null) })) })) }; });
114
122
  add('read', 'GET', async (request) => retrieval.read(readQuerySchema.parse(query(request))));
115
123
  add('search', 'POST', async (request) => retrieval.search(await body(request, searchSchema)));
116
- add('asset', 'GET', async (request) => { const args = assetQuerySchema.parse(query(request)), kb = await store.kb(args.kbId), asset = await store.asset(kb.kbId, args.revision, args.docId, args.assetId); return image(asset.asset, asset.file, asset.data); });
117
- add('schema', 'GET', async (request) => { const kb = await store.kb(libraryArgs.parse(query(request)).kbId), content = await store.schema(kb.kbId); return { content, hash: hash(content) }; });
118
- add('schema', 'POST', async (request) => { const args = await body(request, libraryArgs.extend({ content: z.string().max(20000), expectedHash: digest })), kb = await store.kb(args.kbId); return store.setSchema(kb.kbId, args.content, args.expectedHash); });
124
+ add('asset', 'GET', async (request) => { const args = assetQuerySchema.parse(query(request)); const kb = await store.kb(args.kbId); const asset = await store.asset(kb.kbId, args.revision, args.docId, args.assetId); return image(asset.asset, asset.file, asset.data); });
125
+ add('schema', 'GET', async (request) => { const kb = await store.kb(libraryArgs.parse(query(request)).kbId); const content = await store.schema(kb.kbId); return { content, hash: hash(content) }; });
126
+ add('schema', 'POST', async (request) => { const args = await body(request, libraryArgs.extend({ content: z.string().max(20000), expectedHash: digest })); const kb = await store.kb(args.kbId); return store.setSchema(kb.kbId, args.content, args.expectedHash); });
119
127
  add('review', 'POST', async (request) => {
120
- const args = await body(request, reviewSchema), kb = await store.kb(args.kbId);
121
- const figure = args.action ? null : args.figure && 'ignore' in args.figure ? { ignore: true } : args.figure && 'skip' in args.figure ? { skip: true, types: ['decoration'], description: '人工确认装饰图', visibleLabels: [], relations: [], uncertainties: [], needsReview: false } : args.figure;
128
+ const args = await body(request, reviewSchema);
129
+ const kb = await store.kb(args.kbId);
130
+ let figure;
131
+ if (args.action) {
132
+ figure = null;
133
+ }
134
+ else {
135
+ if (args.figure && 'ignore' in args.figure) {
136
+ figure = { ignore: true };
137
+ }
138
+ else {
139
+ if (args.figure && 'skip' in args.figure) {
140
+ figure = { skip: true, types: ['decoration'], description: '人工确认装饰图', visibleLabels: [], relations: [], uncertainties: [], needsReview: false };
141
+ }
142
+ else {
143
+ figure = args.figure;
144
+ }
145
+ }
146
+ }
122
147
  if (figure && JSON.stringify(figure).length > config.figureMaxChars * 3)
123
148
  throw new Error('图片复核内容超过预算');
124
149
  return jobs.review(kb.kbId, args.revision, args.docId, args.occurrenceId, figure === null ? null : overlaySchema.parse(figure), args.expectedHash, { restore: args.action === 'restore', visionProvider: args.visionProvider, visionModel: args.visionModel });
@@ -142,11 +167,29 @@ export function routes(runtime, ctx) {
142
167
  try {
143
168
  await runtime.ready;
144
169
  checkAbort(request.signal);
145
- const handler = required(methods.get(z.enum(['GET', 'POST', 'HEAD']).parse(request.method)), '请求方法'), value = await runtime.run(signal => handler(new Request(request, { signal })), request.signal);
170
+ const handler = required(methods.get(z.enum(['GET', 'POST', 'HEAD']).parse(request.method)), '请求方法');
171
+ const value = await runtime.run(signal => handler(new Request(request, { signal })), request.signal);
146
172
  return value instanceof Response ? value : json(value);
147
173
  }
148
174
  catch (error) {
149
- const detail = message(error), status = error instanceof z.ZodError || error instanceof SyntaxError ? 400 : /变化|冲突|已存在|未退出|正在关闭/u.test(detail) ? 409 : /无效|必须|不支持|不能|只能|不在回收站|超过|过大|缺少|缺失|尚无|不存在|提供|请选择|待复核|重复/u.test(detail) ? 400 : 500;
175
+ const detail = message(error);
176
+ let status;
177
+ if (error instanceof z.ZodError || error instanceof SyntaxError) {
178
+ status = 400;
179
+ }
180
+ else {
181
+ if (/变化|冲突|已存在|未退出|正在关闭/u.test(detail)) {
182
+ status = 409;
183
+ }
184
+ else {
185
+ if (/无效|必须|不支持|不能|只能|不在回收站|超过|过大|缺少|缺失|尚无|不存在|提供|请选择|待复核|重复/u.test(detail)) {
186
+ status = 400;
187
+ }
188
+ else {
189
+ status = 500;
190
+ }
191
+ }
192
+ }
150
193
  if (status === 500)
151
194
  ctx.logger.warn(`knowledge-base: ${detail}`);
152
195
  return json({ error: detail }, status);
package/lib/runtime.js CHANGED
@@ -7,10 +7,16 @@ import { Jobs } from "./jobs.js";
7
7
  import { Retrieval } from "./retrieval.js";
8
8
  /** One root owner; admitted tasks are durable and the close promise drains them once. */
9
9
  export function createRuntime(ctx, raw = {}) {
10
- const config = resolveConfig(raw), store = new Store(config), models = new Models(ctx, config), jobs = new Jobs(store, models, config), retrieval = new Retrieval(store, config);
10
+ const config = resolveConfig(raw);
11
+ const store = new Store(config);
12
+ const models = new Models(ctx, config);
13
+ const jobs = new Jobs(store, models, config);
14
+ const retrieval = new Retrieval(store, config);
11
15
  const ready = mkdir(store.root, { recursive: true }).then(() => jobs.init());
12
- let closing, stopped = false;
13
- const controller = new AbortController(), pending = new Set();
16
+ let closing;
17
+ let stopped = false;
18
+ const controller = new AbortController();
19
+ const pending = new Set();
14
20
  const run = (operation, signal) => {
15
21
  if (stopped)
16
22
  return Promise.reject(new Error('任务服务正在关闭'));
package/lib/skills.js CHANGED
@@ -3,10 +3,13 @@ import { fileURLToPath } from 'node:url';
3
3
  import { checkAbort, required } from "./util.js";
4
4
  /** Packaged, invocation-neutral skills; no copies in the operator's user directory. */
5
5
  export function skillProvider() {
6
- const provider = 'libing-knowledge-base', names = ['libing-knowledge-base', 'libing-kb-build', 'libing-kb-search'];
6
+ const provider = 'libing-knowledge-base';
7
+ const names = ['libing-knowledge-base', 'libing-kb-build', 'libing-kb-search'];
7
8
  const candidates = names.map(name => {
8
- const directory = fileURLToPath(new URL(`../skills/${name}/`, import.meta.url)), path = fileURLToPath(new URL(`../skills/${name}/SKILL.md`, import.meta.url));
9
- const raw = readFileSync(path, 'utf8'), header = required(/^---\n([\s\S]*?)\n---\n/u.exec(raw), `skill frontmatter: ${name}`);
9
+ const directory = fileURLToPath(new URL(`../skills/${name}/`, import.meta.url));
10
+ const path = fileURLToPath(new URL(`../skills/${name}/SKILL.md`, import.meta.url));
11
+ const raw = readFileSync(path, 'utf8');
12
+ const header = required(/^---\n([\s\S]*?)\n---\n/u.exec(raw), `skill frontmatter: ${name}`);
10
13
  const description = required(/^description:\s*(.+)$/mu.exec(required(header[1]))?.[1], `skill description: ${name}`);
11
14
  return { name, description, source: 'bundled', provider, rank: 600, path, resourceBase: { kind: 'directory', path: directory }, invocation: { modelInvocable: true, userInvocable: true }, locator: path };
12
15
  });