@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.
- package/CHANGELOG.md +4 -0
- package/README.md +4 -0
- package/lib/agent-access.d.ts +23 -0
- package/lib/agent-access.js +28 -0
- package/lib/chunks.js +27 -5
- package/lib/client.js +9 -9
- package/lib/config.js +9 -1
- package/lib/document/convert.js +52 -16
- package/lib/document/pdf-layout.js +45 -10
- package/lib/document/pdf-session.js +10 -4
- package/lib/document/pdf-vision.js +8 -3
- package/lib/document/pdf-worker.js +76 -21
- package/lib/document/pdf.js +39 -9
- package/lib/document/zip.js +14 -6
- package/lib/figures/image-client.js +9 -4
- package/lib/figures/image-worker.js +15 -6
- package/lib/figures/process.js +13 -3
- package/lib/index.js +6 -2
- package/lib/indexes.js +8 -4
- package/lib/job-storage.js +6 -3
- package/lib/jobs.js +18 -9
- package/lib/models.js +7 -2
- package/lib/retrieval.js +36 -9
- package/lib/routes.js +58 -15
- package/lib/runtime.js +9 -3
- package/lib/skills.js +6 -3
- package/lib/store.js +29 -14
- package/lib/tools.js +12 -4
- package/lib/util.js +2 -1
- package/package.json +1 -1
package/lib/document/zip.js
CHANGED
|
@@ -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)
|
|
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
|
|
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)
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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)
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
22
|
-
const
|
|
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)
|
|
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))
|
|
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++)
|
package/lib/figures/process.js
CHANGED
|
@@ -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)
|
|
49
|
-
const
|
|
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:
|
|
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)
|
|
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)
|
|
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
|
|
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 = {}
|
|
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 = []
|
|
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`)
|
|
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 或缩短标题');
|
package/lib/job-storage.js
CHANGED
|
@@ -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
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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)
|
|
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)
|
|
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()}
|
|
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')
|
|
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)
|
|
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)
|
|
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)
|
|
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()
|
|
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 = ''
|
|
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 = ''
|
|
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], '语义块')
|
|
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)
|
|
55
|
-
const
|
|
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 =
|
|
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()
|
|
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)
|
|
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))
|
|
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)
|
|
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)
|
|
10
|
-
const
|
|
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 })
|
|
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 }))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
117
|
-
add('schema', 'GET', async (request) => { const kb = await store.kb(libraryArgs.parse(query(request)).kbId)
|
|
118
|
-
add('schema', 'POST', async (request) => { const args = await body(request, libraryArgs.extend({ content: z.string().max(20000), expectedHash: digest }))
|
|
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)
|
|
121
|
-
const
|
|
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)), '请求方法')
|
|
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)
|
|
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)
|
|
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
|
|
13
|
-
|
|
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'
|
|
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))
|
|
9
|
-
const
|
|
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
|
});
|