@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +8 -2
  3. package/lib/client-types/client/JobDetails.d.ts +16 -0
  4. package/lib/client-types/client/JobsPanel.d.ts +6 -1
  5. package/lib/client-types/client/QualityReport.d.ts +3 -2
  6. package/lib/client-types/client/api.d.ts +83 -52
  7. package/lib/client-types/client/locales.d.ts +2 -0
  8. package/lib/client-types/client/useKnowledge.d.ts +30 -15
  9. package/lib/client-types/contract.d.ts +83 -52
  10. package/lib/client-types/data.d.ts +11 -0
  11. package/lib/client-types/settings.d.ts +4 -0
  12. package/lib/client-types/shared.d.ts +17 -0
  13. package/lib/client.js +40 -40
  14. package/lib/contract.d.ts +33 -2
  15. package/lib/contract.js +2 -1
  16. package/lib/data.d.ts +11 -0
  17. package/lib/data.js +2 -0
  18. package/lib/figures/image-client.d.ts +5 -0
  19. package/lib/figures/image-client.js +58 -0
  20. package/lib/figures/image-gate.d.ts +12 -0
  21. package/lib/figures/image-gate.js +39 -0
  22. package/lib/figures/image-protocol.d.ts +19 -0
  23. package/lib/figures/image-protocol.js +9 -0
  24. package/lib/figures/image-worker.d.ts +1 -0
  25. package/lib/figures/image-worker.js +60 -0
  26. package/lib/figures/process.d.ts +1 -1
  27. package/lib/figures/process.js +6 -35
  28. package/lib/indexes.js +9 -2
  29. package/lib/job-storage.d.ts +264 -0
  30. package/lib/job-storage.js +115 -0
  31. package/lib/jobs.d.ts +782 -1489
  32. package/lib/jobs.js +47 -23
  33. package/lib/models.d.ts +1 -1
  34. package/lib/models.js +8 -7
  35. package/lib/retrieval.js +8 -4
  36. package/lib/revision-cache.d.ts +24 -0
  37. package/lib/revision-cache.js +100 -0
  38. package/lib/routes.js +10 -7
  39. package/lib/runtime.d.ts +4 -0
  40. package/lib/runtime.js +6 -1
  41. package/lib/settings.d.ts +4 -0
  42. package/lib/settings.js +5 -2
  43. package/lib/shared.d.ts +17 -0
  44. package/lib/shared.js +3 -0
  45. package/lib/store.d.ts +101 -5
  46. package/lib/store.js +54 -20
  47. 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());
@@ -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;
@@ -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 { Jimp } from 'jimp';
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 add = async (bitmap, coordinates) => {
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
- const overview = image.clone();
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(document.nodes.find(n => n.nodeId === current.parentId));
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(document.nodes.find(x => x.nodeId === childId))).map(child => ({ title: child.title, path: child.indexPath, nodeId: child.nodeId, docId: document.docId, summary: '章节导航', reviewStatus: 'navigation' })), ...document.chunks.filter(c => c.nodeId === 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 } : {} }))];
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) {