@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.
- package/CHANGELOG.md +20 -0
- package/README.md +12 -2
- package/lib/agent-access.d.ts +23 -0
- package/lib/agent-access.js +28 -0
- package/lib/chunks.js +27 -5
- package/lib/client-types/client/JobDetails.d.ts +16 -0
- package/lib/client-types/client/JobsPanel.d.ts +6 -1
- package/lib/client-types/client/QualityReport.d.ts +3 -2
- package/lib/client-types/client/api.d.ts +83 -52
- package/lib/client-types/client/locales.d.ts +2 -0
- package/lib/client-types/client/useKnowledge.d.ts +30 -15
- package/lib/client-types/contract.d.ts +83 -52
- package/lib/client-types/data.d.ts +11 -0
- package/lib/client-types/settings.d.ts +4 -0
- package/lib/client-types/shared.d.ts +17 -0
- package/lib/client.js +40 -40
- package/lib/config.js +9 -1
- package/lib/contract.d.ts +33 -2
- package/lib/contract.js +2 -1
- package/lib/data.d.ts +11 -0
- package/lib/data.js +2 -0
- 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.d.ts +5 -0
- package/lib/figures/image-client.js +63 -0
- package/lib/figures/image-gate.d.ts +12 -0
- package/lib/figures/image-gate.js +39 -0
- package/lib/figures/image-protocol.d.ts +19 -0
- package/lib/figures/image-protocol.js +9 -0
- package/lib/figures/image-worker.d.ts +1 -0
- package/lib/figures/image-worker.js +69 -0
- package/lib/figures/process.d.ts +1 -1
- package/lib/figures/process.js +19 -38
- package/lib/index.js +6 -2
- package/lib/indexes.js +17 -6
- package/lib/job-storage.d.ts +264 -0
- package/lib/job-storage.js +118 -0
- package/lib/jobs.d.ts +782 -1489
- package/lib/jobs.js +64 -31
- package/lib/models.d.ts +1 -1
- package/lib/models.js +15 -9
- package/lib/retrieval.js +44 -13
- package/lib/revision-cache.d.ts +24 -0
- package/lib/revision-cache.js +100 -0
- package/lib/routes.js +66 -20
- package/lib/runtime.d.ts +4 -0
- package/lib/runtime.js +15 -4
- package/lib/settings.d.ts +4 -0
- package/lib/settings.js +5 -2
- package/lib/shared.d.ts +17 -0
- package/lib/shared.js +3 -0
- package/lib/skills.js +6 -3
- package/lib/store.d.ts +101 -5
- package/lib/store.js +78 -29
- package/lib/tools.js +12 -4
- package/lib/util.js +2 -1
- 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)
|
|
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)
|
|
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)
|
|
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)
|
|
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()
|
|
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
|
-
|
|
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.
|
|
138
|
-
return { location:
|
|
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()}
|
|
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(
|
|
171
|
-
async
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
178
|
-
const manifest = await
|
|
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
|
-
|
|
193
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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)
|
|
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')
|
|
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
|
-
|
|
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')
|
|
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 或唯一库名')
|
|
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) =>
|
|
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)
|
|
59
|
-
const
|
|
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)
|
|
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('路径越界');
|