@dsh-libing/knowledge-base 0.1.0 → 0.1.3
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 +16 -0
- package/README.md +8 -2
- 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/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/figures/image-client.d.ts +5 -0
- package/lib/figures/image-client.js +58 -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 +60 -0
- package/lib/figures/process.d.ts +1 -1
- package/lib/figures/process.js +6 -35
- package/lib/indexes.js +9 -2
- package/lib/job-storage.d.ts +264 -0
- package/lib/job-storage.js +115 -0
- package/lib/jobs.d.ts +782 -1489
- package/lib/jobs.js +47 -23
- package/lib/models.d.ts +1 -1
- package/lib/models.js +8 -7
- package/lib/retrieval.js +8 -4
- package/lib/revision-cache.d.ts +24 -0
- package/lib/revision-cache.js +100 -0
- package/lib/routes.js +10 -7
- package/lib/runtime.d.ts +4 -0
- package/lib/runtime.js +6 -1
- 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/store.d.ts +101 -5
- package/lib/store.js +54 -20
- package/package.json +1 -1
package/lib/contract.d.ts
CHANGED
|
@@ -119,8 +119,12 @@ export declare const publicJobSchema: z.ZodObject<{
|
|
|
119
119
|
modelTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
120
120
|
modelAttempts: z.ZodDefault<z.ZodNumber>;
|
|
121
121
|
visionConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
122
|
+
maxModelCallsLimit: z.ZodDefault<z.ZodNumber>;
|
|
123
|
+
maxModelTokensLimit: z.ZodDefault<z.ZodNumber>;
|
|
124
|
+
uploadRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
122
125
|
maxModelCalls: z.ZodDefault<z.ZodNumber>;
|
|
123
126
|
maxModelTokens: z.ZodDefault<z.ZodNumber>;
|
|
127
|
+
imageTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
124
128
|
visionMaxEdge: z.ZodDefault<z.ZodNumber>;
|
|
125
129
|
visionTileSize: z.ZodDefault<z.ZodNumber>;
|
|
126
130
|
visionMaxTiles: z.ZodDefault<z.ZodNumber>;
|
|
@@ -189,6 +193,13 @@ export declare const publicJobSchema: z.ZodObject<{
|
|
|
189
193
|
chunkId: z.ZodString;
|
|
190
194
|
}, z.core.$strip>>;
|
|
191
195
|
}, z.core.$strip>;
|
|
196
|
+
pendingCalls: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
197
|
+
legacyUsageCount: z.ZodDefault<z.ZodNumber>;
|
|
198
|
+
legacyCallCount: z.ZodDefault<z.ZodNumber>;
|
|
199
|
+
reportSummary: z.ZodOptional<z.ZodObject<{
|
|
200
|
+
issues: z.ZodNumber;
|
|
201
|
+
blocking: z.ZodNumber;
|
|
202
|
+
}, z.core.$strip>>;
|
|
192
203
|
failures: z.ZodArray<z.ZodObject<{
|
|
193
204
|
at: z.ZodString;
|
|
194
205
|
stage: z.ZodEnum<{
|
|
@@ -437,8 +448,12 @@ export declare const stateSchema: z.ZodObject<{
|
|
|
437
448
|
modelTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
438
449
|
modelAttempts: z.ZodDefault<z.ZodNumber>;
|
|
439
450
|
visionConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
451
|
+
maxModelCallsLimit: z.ZodDefault<z.ZodNumber>;
|
|
452
|
+
maxModelTokensLimit: z.ZodDefault<z.ZodNumber>;
|
|
453
|
+
uploadRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
440
454
|
maxModelCalls: z.ZodDefault<z.ZodNumber>;
|
|
441
455
|
maxModelTokens: z.ZodDefault<z.ZodNumber>;
|
|
456
|
+
imageTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
442
457
|
visionMaxEdge: z.ZodDefault<z.ZodNumber>;
|
|
443
458
|
visionTileSize: z.ZodDefault<z.ZodNumber>;
|
|
444
459
|
visionMaxTiles: z.ZodDefault<z.ZodNumber>;
|
|
@@ -507,6 +522,13 @@ export declare const stateSchema: z.ZodObject<{
|
|
|
507
522
|
chunkId: z.ZodString;
|
|
508
523
|
}, z.core.$strip>>;
|
|
509
524
|
}, z.core.$strip>;
|
|
525
|
+
pendingCalls: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
526
|
+
legacyUsageCount: z.ZodDefault<z.ZodNumber>;
|
|
527
|
+
legacyCallCount: z.ZodDefault<z.ZodNumber>;
|
|
528
|
+
reportSummary: z.ZodOptional<z.ZodObject<{
|
|
529
|
+
issues: z.ZodNumber;
|
|
530
|
+
blocking: z.ZodNumber;
|
|
531
|
+
}, z.core.$strip>>;
|
|
510
532
|
failures: z.ZodArray<z.ZodObject<{
|
|
511
533
|
at: z.ZodString;
|
|
512
534
|
stage: z.ZodEnum<{
|
|
@@ -557,6 +579,9 @@ export declare const stateSchema: z.ZodObject<{
|
|
|
557
579
|
outputPath: z.ZodNullable<z.ZodString>;
|
|
558
580
|
reused: z.ZodOptional<z.ZodBoolean>;
|
|
559
581
|
}, z.core.$strip>>;
|
|
582
|
+
jobTotal: z.ZodDefault<z.ZodNumber>;
|
|
583
|
+
jobOffset: z.ZodDefault<z.ZodNumber>;
|
|
584
|
+
activeJobs: z.ZodDefault<z.ZodNumber>;
|
|
560
585
|
dataRoot: z.ZodString;
|
|
561
586
|
config: z.ZodObject<{
|
|
562
587
|
chunkTargetChars: z.ZodNumber;
|
|
@@ -669,8 +694,8 @@ export declare const treeDocumentSchema: z.ZodObject<{
|
|
|
669
694
|
reason: z.ZodOptional<z.ZodString>;
|
|
670
695
|
}, z.core.$strip>>>;
|
|
671
696
|
chunks: z.ZodArray<z.ZodObject<{
|
|
672
|
-
summary: z.ZodDefault<z.ZodString>;
|
|
673
697
|
path: z.ZodString;
|
|
698
|
+
summary: z.ZodDefault<z.ZodString>;
|
|
674
699
|
title: z.ZodString;
|
|
675
700
|
headingPath: z.ZodArray<z.ZodString>;
|
|
676
701
|
oversizedReason: z.ZodOptional<z.ZodString>;
|
|
@@ -972,8 +997,8 @@ export declare const treeSchema: z.ZodObject<{
|
|
|
972
997
|
reason: z.ZodOptional<z.ZodString>;
|
|
973
998
|
}, z.core.$strip>>>;
|
|
974
999
|
chunks: z.ZodArray<z.ZodObject<{
|
|
975
|
-
summary: z.ZodDefault<z.ZodString>;
|
|
976
1000
|
path: z.ZodString;
|
|
1001
|
+
summary: z.ZodDefault<z.ZodString>;
|
|
977
1002
|
title: z.ZodString;
|
|
978
1003
|
headingPath: z.ZodArray<z.ZodString>;
|
|
979
1004
|
oversizedReason: z.ZodOptional<z.ZodString>;
|
|
@@ -1195,3 +1220,9 @@ export declare const uploadResultSchema: z.ZodObject<{
|
|
|
1195
1220
|
filename: z.ZodString;
|
|
1196
1221
|
bytes: z.ZodNumber;
|
|
1197
1222
|
}, z.core.$strip>;
|
|
1223
|
+
export declare const jobUsagePageSchema: z.ZodObject<{
|
|
1224
|
+
records: z.ZodArray<z.ZodJSONSchema>;
|
|
1225
|
+
offset: z.ZodNumber;
|
|
1226
|
+
total: z.ZodNumber;
|
|
1227
|
+
next: z.ZodNullable<z.ZodNumber>;
|
|
1228
|
+
}, z.core.$strip>;
|
package/lib/contract.js
CHANGED
|
@@ -4,7 +4,7 @@ import { jobSchema, registrySchema, librarySchema, publishedDocumentSchema, repo
|
|
|
4
4
|
export const publicJobSchema = jobSchema.omit({ schema: true, overlays: true }).extend({ outputPath: z.string().nullable(), reused: z.boolean().optional() });
|
|
5
5
|
export const locationSchema = z.object({ kbPath: z.string(), revision: z.string().nullable(), wikiPath: z.string().nullable(), indexPath: z.string().nullable() });
|
|
6
6
|
export const libraryViewSchema = librarySchema.extend({ location: locationSchema, summary: z.object({ revision: z.string().nullable(), documentCount: z.number(), chunkCount: z.number(), coverageIncomplete: z.boolean() }) });
|
|
7
|
-
export const stateSchema = z.object({ hostVersion: z.string(), kbs: z.array(libraryViewSchema), deletedKbs: z.array(librarySchema), activities: registrySchema.shape.activities, jobs: z.array(publicJobSchema), dataRoot: z.string(), config: z.object({ chunkTargetChars: z.number(), visionProvider: z.string(), visionModel: z.string(), maxInputBytes: z.number() }) });
|
|
7
|
+
export const stateSchema = z.object({ hostVersion: z.string(), kbs: z.array(libraryViewSchema), deletedKbs: z.array(librarySchema), activities: registrySchema.shape.activities, jobs: z.array(publicJobSchema), jobTotal: z.number().int().nonnegative().default(0), jobOffset: z.number().int().nonnegative().default(0), activeJobs: z.number().int().nonnegative().default(0), dataRoot: z.string(), config: z.object({ chunkTargetChars: z.number(), visionProvider: z.string(), visionModel: z.string(), maxInputBytes: z.number() }) });
|
|
8
8
|
export const treeDocumentSchema = publishedDocumentSchema.extend({ chunks: z.array(publishedDocumentSchema.shape.chunks.element.omit({ segments: true, anchors: true })), figures: z.array(publishedDocumentSchema.shape.figures.element.extend({ expectedHash: z.string().optional() })) });
|
|
9
9
|
export const treeSchema = z.object({ kbId: z.string(), revision: z.string(), location: locationSchema.optional(), previewJobId: z.string().optional(), coverageIncomplete: z.boolean(), report: reportSchema, documents: z.array(treeDocumentSchema) });
|
|
10
10
|
export const readSchema = z.object({ kbId: z.string(), chunkId: z.string(), revision: z.string(), text: z.string(), complete: z.boolean(), truncated: z.boolean().optional(), previewJobId: z.string().optional(),
|
|
@@ -16,3 +16,4 @@ export const modelsSchema = z.object({ providers: z.array(z.object({ id: z.strin
|
|
|
16
16
|
export const schemaResultSchema = z.object({ content: z.string(), hash: z.string() });
|
|
17
17
|
export const inspectSchema = z.object({ sourceFormat: z.literal('pdf'), pageCount: z.number(), outlineCount: z.number(), samples: z.array(z.object({ page: z.number(), chars: z.number(), hasGraphics: z.boolean(), possibleOcr: z.boolean(), hasStructure: z.boolean() })), possibleOcr: z.boolean(), note: z.string() });
|
|
18
18
|
export const uploadResultSchema = z.object({ receiptId: z.string(), filename: z.string(), bytes: z.number() });
|
|
19
|
+
export const jobUsagePageSchema = z.object({ records: z.array(z.json()), offset: z.number().int().nonnegative(), total: z.number().int().nonnegative(), next: z.number().int().nonnegative().nullable() });
|
package/lib/data.d.ts
CHANGED
|
@@ -1528,8 +1528,12 @@ export declare const jobSchema: z.ZodObject<{
|
|
|
1528
1528
|
modelTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
1529
1529
|
modelAttempts: z.ZodDefault<z.ZodNumber>;
|
|
1530
1530
|
visionConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
1531
|
+
maxModelCallsLimit: z.ZodDefault<z.ZodNumber>;
|
|
1532
|
+
maxModelTokensLimit: z.ZodDefault<z.ZodNumber>;
|
|
1533
|
+
uploadRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
1531
1534
|
maxModelCalls: z.ZodDefault<z.ZodNumber>;
|
|
1532
1535
|
maxModelTokens: z.ZodDefault<z.ZodNumber>;
|
|
1536
|
+
imageTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
1533
1537
|
visionMaxEdge: z.ZodDefault<z.ZodNumber>;
|
|
1534
1538
|
visionTileSize: z.ZodDefault<z.ZodNumber>;
|
|
1535
1539
|
visionMaxTiles: z.ZodDefault<z.ZodNumber>;
|
|
@@ -1599,6 +1603,13 @@ export declare const jobSchema: z.ZodObject<{
|
|
|
1599
1603
|
chunkId: z.ZodString;
|
|
1600
1604
|
}, z.core.$strip>>;
|
|
1601
1605
|
}, z.core.$strip>;
|
|
1606
|
+
pendingCalls: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
1607
|
+
legacyUsageCount: z.ZodDefault<z.ZodNumber>;
|
|
1608
|
+
legacyCallCount: z.ZodDefault<z.ZodNumber>;
|
|
1609
|
+
reportSummary: z.ZodOptional<z.ZodObject<{
|
|
1610
|
+
issues: z.ZodNumber;
|
|
1611
|
+
blocking: z.ZodNumber;
|
|
1612
|
+
}, z.core.$strip>>;
|
|
1602
1613
|
usage: z.ZodObject<{
|
|
1603
1614
|
calls: z.ZodNumber;
|
|
1604
1615
|
tokens: z.ZodNumber;
|
package/lib/data.js
CHANGED
|
@@ -69,6 +69,8 @@ export const jobSchema = z.object({ jobId: idSchema, kbId: idSchema, docId: idSc
|
|
|
69
69
|
progress: z.object({ totalPages: z.number().optional(), parsedPages: z.number().optional(), ocrPages: z.number().optional(), tables: z.number().optional(), blocks: z.number().optional(), currentPage: z.number().optional(),
|
|
70
70
|
totalFigures: z.number().optional(), completedFigures: z.number().optional(), inflightFigures: z.array(z.object({ occurrenceId: z.string(), caption: z.string() })).optional(),
|
|
71
71
|
totalChunks: z.number().optional(), indexedChunks: z.number().optional(), currentChunk: z.object({ title: z.string(), headingPath: z.array(z.string()), chunkId: z.string() }).optional() }),
|
|
72
|
+
pendingCalls: z.array(z.number().int().positive()).max(16).default([]), legacyUsageCount: z.number().int().nonnegative().default(0), legacyCallCount: z.number().int().nonnegative().default(0),
|
|
73
|
+
reportSummary: z.object({ issues: z.number().int().nonnegative(), blocking: z.number().int().nonnegative() }).optional(),
|
|
72
74
|
usage: usageSchema, failures: z.array(z.object({ at: z.string(), stage: jobStateSchema, error: z.string() })), events: z.array(z.object({ at: z.string(), state: jobStateSchema })).optional(),
|
|
73
75
|
figureRefresh: z.object({ occurrenceId: z.string(), token: z.string() }).nullable().optional(), visionCapability: z.string().optional(), parserVersion: z.string().optional(),
|
|
74
76
|
pageStart: z.number().optional(), pageEnd: z.number().optional(), pdf: z.object({ pageCount: z.number(), selectedPages: rangeSchema, outlineCount: z.number().optional() }).optional(),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Settings } from '../config.ts';
|
|
2
|
+
export declare function runImageWorker(file: string, config: Settings, save: (data: Uint8Array, coordinates: Record<string, string | number>) => Promise<void>, signal?: AbortSignal): Promise<{
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { checkAbort, deferred } from "../util.js";
|
|
5
|
+
import { imageGate } from "./image-gate.js";
|
|
6
|
+
import { imageMessageSchema, imageOptionsSchema } from "./image-protocol.js";
|
|
7
|
+
export async function runImageWorker(file, config, save, signal) {
|
|
8
|
+
const options = imageOptionsSchema.parse({ visionMaxEdge: config.visionMaxEdge, visionTileSize: config.visionTileSize, visionMaxTiles: config.visionMaxTiles });
|
|
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;
|
|
13
|
+
try {
|
|
14
|
+
checkAbort(signal);
|
|
15
|
+
const js = new URL('./image-worker.js', import.meta.url), source = !existsSync(fileURLToPath(js));
|
|
16
|
+
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
|
+
const current = worker;
|
|
18
|
+
timer = setTimeout(() => result.reject(new Error('图片预处理超时')), Math.min(config.imageTimeoutMs, 120000));
|
|
19
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
20
|
+
if (signal?.aborted)
|
|
21
|
+
abort();
|
|
22
|
+
current.on('error', error => result.reject(error));
|
|
23
|
+
current.on('exit', code => { if (!complete)
|
|
24
|
+
result.reject(new Error(`图片预处理进程退出(${code})`)); });
|
|
25
|
+
current.on('message', (raw) => {
|
|
26
|
+
try {
|
|
27
|
+
const value = imageMessageSchema.parse(raw);
|
|
28
|
+
if (value.kind === 'error')
|
|
29
|
+
result.reject(new Error(value.message));
|
|
30
|
+
else if (value.kind === 'done') {
|
|
31
|
+
complete = true;
|
|
32
|
+
result.resolve({ width: value.width, height: value.height });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (saving || value.id !== next++ || value.data.length > 8 * 1024 * 1024)
|
|
36
|
+
throw new Error('图片预处理输出无效');
|
|
37
|
+
saving = Promise.resolve().then(() => { checkAbort(signal); return save(value.data, value.coordinates); }).then(() => { checkAbort(signal); saving = undefined; current.postMessage({ ack: value.id }); });
|
|
38
|
+
void saving.catch(error => result.reject(error));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
result.reject(error);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return await result.promise;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
signal?.removeEventListener('abort', abort);
|
|
50
|
+
try {
|
|
51
|
+
await worker?.terminate();
|
|
52
|
+
await saving?.catch(() => { });
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** One process-wide bitmap worker, including all knowledge bases and runtime instances. */
|
|
2
|
+
export declare class ImageGate {
|
|
3
|
+
active: boolean;
|
|
4
|
+
private queue;
|
|
5
|
+
get waiting(): number;
|
|
6
|
+
acquire(signal?: AbortSignal): Promise<() => void>;
|
|
7
|
+
private release;
|
|
8
|
+
}
|
|
9
|
+
declare global {
|
|
10
|
+
var __dshLibingKnowledgeImageGateV1: ImageGate | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare const imageGate: ImageGate;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { checkAbort } from "../util.js";
|
|
2
|
+
/** One process-wide bitmap worker, including all knowledge bases and runtime instances. */
|
|
3
|
+
export class ImageGate {
|
|
4
|
+
active = false;
|
|
5
|
+
queue = [];
|
|
6
|
+
get waiting() { return this.queue.length; }
|
|
7
|
+
async acquire(signal) {
|
|
8
|
+
checkAbort(signal);
|
|
9
|
+
if (!this.active) {
|
|
10
|
+
this.active = true;
|
|
11
|
+
return this.release();
|
|
12
|
+
}
|
|
13
|
+
if (this.queue.length >= 32)
|
|
14
|
+
throw new Error('图片预处理等待队列已满,请稍后重试');
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const entry = { ...signal ? { signal } : {}, resolve, reject, abort: () => { signal?.removeEventListener('abort', entry.abort); this.queue = this.queue.filter(e => e !== entry); reject(signal?.reason ?? new Error('图片预处理已取消')); } };
|
|
17
|
+
this.queue.push(entry);
|
|
18
|
+
signal?.addEventListener('abort', entry.abort, { once: true });
|
|
19
|
+
if (signal?.aborted)
|
|
20
|
+
entry.abort();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
release() {
|
|
24
|
+
let released = false;
|
|
25
|
+
return () => {
|
|
26
|
+
if (released)
|
|
27
|
+
return;
|
|
28
|
+
released = true;
|
|
29
|
+
const next = this.queue.shift();
|
|
30
|
+
if (next) {
|
|
31
|
+
next.signal?.removeEventListener('abort', next.abort);
|
|
32
|
+
next.resolve(this.release());
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
this.active = false;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export const imageGate = globalThis.__dshLibingKnowledgeImageGateV1 ??= new ImageGate();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const imageOptionsSchema: z.ZodObject<{
|
|
3
|
+
visionMaxEdge: z.ZodNumber;
|
|
4
|
+
visionTileSize: z.ZodNumber;
|
|
5
|
+
visionMaxTiles: z.ZodNumber;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
export declare const imageMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
8
|
+
kind: z.ZodLiteral<"image">;
|
|
9
|
+
id: z.ZodNumber;
|
|
10
|
+
data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
11
|
+
coordinates: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
12
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
13
|
+
kind: z.ZodLiteral<"done">;
|
|
14
|
+
width: z.ZodNumber;
|
|
15
|
+
height: z.ZodNumber;
|
|
16
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
17
|
+
kind: z.ZodLiteral<"error">;
|
|
18
|
+
message: z.ZodString;
|
|
19
|
+
}, z.core.$strip>], "kind">;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const imageOptionsSchema = z.strictObject({
|
|
3
|
+
visionMaxEdge: z.number().int().min(1).max(4096), visionTileSize: z.number().int().min(1).max(4096), visionMaxTiles: z.number().int().min(1).max(64),
|
|
4
|
+
});
|
|
5
|
+
export const imageMessageSchema = z.discriminatedUnion('kind', [
|
|
6
|
+
z.object({ kind: z.literal('image'), id: z.number().int().positive(), data: z.instanceof(Uint8Array), coordinates: z.record(z.string(), z.union([z.string(), z.number()])) }),
|
|
7
|
+
z.object({ kind: z.literal('done'), width: z.number().int().positive(), height: z.number().int().positive() }),
|
|
8
|
+
z.object({ kind: z.literal('error'), message: z.string() }),
|
|
9
|
+
]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { Jimp, ResizeStrategy } from 'jimp';
|
|
4
|
+
import { imageSize } from 'image-size';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { imageOptionsSchema } from "./image-protocol.js";
|
|
7
|
+
import { message, required } from "../util.js";
|
|
8
|
+
const port = required(parentPort);
|
|
9
|
+
async function run() {
|
|
10
|
+
const { file, options } = z.object({ file: z.string(), options: imageOptionsSchema }).parse(workerData);
|
|
11
|
+
if ((await stat(file)).size > 32 * 1024 * 1024)
|
|
12
|
+
throw new Error('单张图片超过 32 MiB,请提供较小的位图');
|
|
13
|
+
const data = await readFile(file), dimensions = imageSize(data);
|
|
14
|
+
if (data.length > 32 * 1024 * 1024)
|
|
15
|
+
throw new Error('单张图片超过 32 MiB,请提供较小的位图');
|
|
16
|
+
if (!dimensions.width || !dimensions.height || dimensions.width * dimensions.height > 50000000)
|
|
17
|
+
throw new Error('图片像素过大,请提供分辨率较低的版本');
|
|
18
|
+
const image = await Jimp.read(data), { width, height } = image.bitmap;
|
|
19
|
+
if (width * height > 50000000)
|
|
20
|
+
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
|
+
if (tiled && columns * rows > options.visionMaxTiles)
|
|
24
|
+
throw new Error(`大图需要 ${columns * rows} 个裁片,超过 visionMaxTiles;请拆分图片或提高上限`);
|
|
25
|
+
let id = 0;
|
|
26
|
+
const send = async (bitmap, coordinates) => {
|
|
27
|
+
const bytes = await bitmap.getBuffer('image/png');
|
|
28
|
+
if (bytes.length > 8 * 1024 * 1024)
|
|
29
|
+
throw new Error('图像转换后超过单张 8 MiB,请降低 visionTileSize');
|
|
30
|
+
const packet = new Uint8Array(bytes), current = ++id;
|
|
31
|
+
const acknowledged = new Promise((resolve, reject) => port.once('message', raw => {
|
|
32
|
+
const ack = z.object({ ack: z.literal(current) }).safeParse(raw);
|
|
33
|
+
if (ack.success)
|
|
34
|
+
resolve();
|
|
35
|
+
else
|
|
36
|
+
reject(new Error('图片保存确认无效'));
|
|
37
|
+
}));
|
|
38
|
+
port.postMessage({ kind: 'image', id: current, data: packet, coordinates }, [packet.buffer]);
|
|
39
|
+
await acknowledged;
|
|
40
|
+
};
|
|
41
|
+
// A separate bitmap descriptor shares the read-only source buffer; resize allocates only the overview.
|
|
42
|
+
const overview = new Jimp({ ...image.bitmap });
|
|
43
|
+
if (tiled) {
|
|
44
|
+
const ratio = options.visionMaxEdge / Math.max(width, height);
|
|
45
|
+
overview.resize({ w: Math.max(1, Math.round(width * ratio)), h: Math.max(1, Math.round(height * ratio)), mode: ResizeStrategy.BILINEAR });
|
|
46
|
+
}
|
|
47
|
+
await send(overview, { kind: 'overview', originalWidth: width, originalHeight: height });
|
|
48
|
+
if (tiled)
|
|
49
|
+
for (let row = 0; row < rows; row++)
|
|
50
|
+
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);
|
|
52
|
+
// Copy only the requested region; never clone the full source for a tile.
|
|
53
|
+
const bitmap = new Jimp({ width: w, height: h });
|
|
54
|
+
for (let line = 0; line < h; line++)
|
|
55
|
+
image.bitmap.data.copy(bitmap.bitmap.data, line * w * 4, ((y + line) * width + x) * 4, ((y + line) * width + x + w) * 4);
|
|
56
|
+
await send(bitmap, { kind: 'tile', x, y, width: w, height: h, originalWidth: width, originalHeight: height });
|
|
57
|
+
}
|
|
58
|
+
port.postMessage({ kind: 'done', width, height });
|
|
59
|
+
}
|
|
60
|
+
void run().catch(error => port.postMessage({ kind: 'error', message: message(error) })).finally(() => port.close());
|
package/lib/figures/process.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ import type { Block, Document, Job, Overlay } from '../data.ts';
|
|
|
5
5
|
import type { Models, PreparedImage } from '../models.ts';
|
|
6
6
|
export declare function figureContext(blocks: Block[], index: number, headingPath: string[]): string;
|
|
7
7
|
export declare function prepareImage(ctx: Context, file: string, config: Settings, signal?: AbortSignal): Promise<{
|
|
8
|
-
images: PreparedImage[];
|
|
9
8
|
width: number;
|
|
10
9
|
height: number;
|
|
10
|
+
images: PreparedImage[];
|
|
11
11
|
}>;
|
|
12
12
|
export declare function figureEntries(document: Document): {
|
|
13
13
|
block: Block;
|
package/lib/figures/process.js
CHANGED
|
@@ -2,9 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { figureSchema, figureDataSchema, FIGURE_TYPES } from "../data.js";
|
|
3
3
|
import { required, message } from "../util.js";
|
|
4
4
|
const classificationSchema = z.object({ types: z.array(z.enum(FIGURE_TYPES)).min(1), reason: z.string() });
|
|
5
|
-
import {
|
|
6
|
-
import { imageSize } from 'image-size';
|
|
7
|
-
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { runImageWorker } from "./image-client.js";
|
|
8
6
|
import { join } from 'node:path';
|
|
9
7
|
import { atomicWrite, checkAbort, clip, hash, readJson } from "../util.js";
|
|
10
8
|
import { BudgetError } from "../models.js";
|
|
@@ -13,41 +11,14 @@ export function figureContext(blocks, index, headingPath) {
|
|
|
13
11
|
return JSON.stringify({ headingPath, caption: required(blocks[index]).caption, nearby: blocks.slice(Math.max(0, index - 2), index + 3).filter(b => b.type !== 'figure').map(b => clip(b.text || '', 1000)) });
|
|
14
12
|
}
|
|
15
13
|
export async function prepareImage(ctx, file, config, signal) {
|
|
16
|
-
checkAbort(signal);
|
|
17
|
-
const data = await readFile(file);
|
|
18
|
-
if (data.length > 32 * 1024 * 1024)
|
|
19
|
-
throw new Error('单张图片超过 32 MiB,请提供较小的位图');
|
|
20
|
-
const dimensions = imageSize(data);
|
|
21
|
-
if (!dimensions.width || !dimensions.height || dimensions.width * dimensions.height > 50000000)
|
|
22
|
-
throw new Error('图片像素过大,请提供分辨率较低的版本');
|
|
23
|
-
const image = await Jimp.read(data);
|
|
24
|
-
const { width, height } = image.bitmap;
|
|
25
|
-
if (width * height > 50000000)
|
|
26
|
-
throw new Error('图片像素过大,请提供分辨率较低的版本');
|
|
27
14
|
const images = [];
|
|
28
|
-
const
|
|
15
|
+
const dimensions = await runImageWorker(file, config, async (data, coordinates) => {
|
|
16
|
+
checkAbort(signal);
|
|
17
|
+
const attachment = await ctx.attachments.saveImage({ data: Buffer.from(data.buffer, data.byteOffset, data.byteLength), mediaType: 'image/png', name: 'kb-figure.png' });
|
|
29
18
|
checkAbort(signal);
|
|
30
|
-
const bytes = await bitmap.getBuffer('image/png');
|
|
31
|
-
if (bytes.length > 8 * 1024 * 1024)
|
|
32
|
-
throw new Error('图像转换后超过单张 8 MiB,请降低 visionTileSize');
|
|
33
|
-
const attachment = await ctx.attachments.saveImage({ data: bytes, mediaType: 'image/png', name: 'kb-figure.png' });
|
|
34
19
|
images.push({ attachment, coordinates });
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
if (Math.max(width, height) > config.visionMaxEdge)
|
|
38
|
-
overview.scale(config.visionMaxEdge / Math.max(width, height));
|
|
39
|
-
await add(overview, { kind: 'overview', originalWidth: width, originalHeight: height });
|
|
40
|
-
if (Math.max(width, height) > config.visionMaxEdge) {
|
|
41
|
-
const tile = config.visionTileSize, step = Math.max(1, Math.floor(tile * .85)), columns = Math.max(1, Math.ceil((width - tile) / step) + 1), rows = Math.max(1, Math.ceil((height - tile) / step) + 1);
|
|
42
|
-
if (columns * rows > config.visionMaxTiles)
|
|
43
|
-
throw new Error(`大图需要 ${columns * rows} 个裁片,超过 visionMaxTiles;请拆分图片或提高上限`);
|
|
44
|
-
for (let row = 0; row < rows; row++)
|
|
45
|
-
for (let col = 0; col < columns; col++) {
|
|
46
|
-
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);
|
|
47
|
-
await add(image.clone().crop({ x, y, w, h }), { kind: 'tile', x, y, width: w, height: h, originalWidth: width, originalHeight: height });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return { images, width, height };
|
|
20
|
+
}, signal);
|
|
21
|
+
return { images, ...dimensions };
|
|
51
22
|
}
|
|
52
23
|
export function figureEntries(document) {
|
|
53
24
|
let heading = [];
|
package/lib/indexes.js
CHANGED
|
@@ -71,11 +71,18 @@ export async function writeIndexes(dir, documents, config, revision) {
|
|
|
71
71
|
};
|
|
72
72
|
for (const document of documents) {
|
|
73
73
|
const base = posix.join('wiki/documents', document.docId);
|
|
74
|
+
const byId = new Map(document.nodes.map(node => [node.nodeId, node]));
|
|
75
|
+
const byNode = new Map();
|
|
76
|
+
for (const chunk of document.chunks) {
|
|
77
|
+
const group = byNode.get(chunk.nodeId) || [];
|
|
78
|
+
group.push(chunk);
|
|
79
|
+
byNode.set(chunk.nodeId, group);
|
|
80
|
+
}
|
|
74
81
|
for (const node of document.nodes) {
|
|
75
82
|
const parents = [];
|
|
76
83
|
let current = node;
|
|
77
84
|
while (current.parentId) {
|
|
78
|
-
current = required(
|
|
85
|
+
current = required(byId.get(current.parentId));
|
|
79
86
|
if (current.nodeId !== 'root')
|
|
80
87
|
parents.unshift(current.nodeId);
|
|
81
88
|
}
|
|
@@ -83,7 +90,7 @@ export async function writeIndexes(dir, documents, config, revision) {
|
|
|
83
90
|
node.indexPath = path;
|
|
84
91
|
}
|
|
85
92
|
for (const node of document.nodes) {
|
|
86
|
-
const entries = [...node.children.map(childId => required(
|
|
93
|
+
const entries = [...node.children.map(childId => required(byId.get(childId))).map(child => ({ title: child.title, path: child.indexPath, nodeId: child.nodeId, docId: document.docId, summary: '章节导航', reviewStatus: 'navigation' })), ...(byNode.get(node.nodeId) || []).map(c => ({ title: c.title, path: c.path, chunkId: c.chunkId, docId: c.docId, summary: c.summary, fullChars: c.fullChars, keywords: c.keywords, reviewStatus: c.reviewStatus, ...c.pageRange ? { pageRange: c.pageRange } : {} }))];
|
|
87
94
|
await writeCatalog(`${document.docId}-${node.nodeId}`, required(node.indexPath), node.title, document.sourceFormat === 'pdf' ? [{ line: `> ${pdfScope(document)}\n\n` }, ...entries] : entries);
|
|
88
95
|
}
|
|
89
96
|
for (const chunk of document.chunks) {
|