@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +12 -2
  3. package/lib/agent-access.d.ts +23 -0
  4. package/lib/agent-access.js +28 -0
  5. package/lib/chunks.js +27 -5
  6. package/lib/client-types/client/JobDetails.d.ts +16 -0
  7. package/lib/client-types/client/JobsPanel.d.ts +6 -1
  8. package/lib/client-types/client/QualityReport.d.ts +3 -2
  9. package/lib/client-types/client/api.d.ts +83 -52
  10. package/lib/client-types/client/locales.d.ts +2 -0
  11. package/lib/client-types/client/useKnowledge.d.ts +30 -15
  12. package/lib/client-types/contract.d.ts +83 -52
  13. package/lib/client-types/data.d.ts +11 -0
  14. package/lib/client-types/settings.d.ts +4 -0
  15. package/lib/client-types/shared.d.ts +17 -0
  16. package/lib/client.js +40 -40
  17. package/lib/config.js +9 -1
  18. package/lib/contract.d.ts +33 -2
  19. package/lib/contract.js +2 -1
  20. package/lib/data.d.ts +11 -0
  21. package/lib/data.js +2 -0
  22. package/lib/document/convert.js +52 -16
  23. package/lib/document/pdf-layout.js +45 -10
  24. package/lib/document/pdf-session.js +10 -4
  25. package/lib/document/pdf-vision.js +8 -3
  26. package/lib/document/pdf-worker.js +76 -21
  27. package/lib/document/pdf.js +39 -9
  28. package/lib/document/zip.js +14 -6
  29. package/lib/figures/image-client.d.ts +5 -0
  30. package/lib/figures/image-client.js +63 -0
  31. package/lib/figures/image-gate.d.ts +12 -0
  32. package/lib/figures/image-gate.js +39 -0
  33. package/lib/figures/image-protocol.d.ts +19 -0
  34. package/lib/figures/image-protocol.js +9 -0
  35. package/lib/figures/image-worker.d.ts +1 -0
  36. package/lib/figures/image-worker.js +69 -0
  37. package/lib/figures/process.d.ts +1 -1
  38. package/lib/figures/process.js +19 -38
  39. package/lib/index.js +6 -2
  40. package/lib/indexes.js +17 -6
  41. package/lib/job-storage.d.ts +264 -0
  42. package/lib/job-storage.js +118 -0
  43. package/lib/jobs.d.ts +782 -1489
  44. package/lib/jobs.js +64 -31
  45. package/lib/models.d.ts +1 -1
  46. package/lib/models.js +15 -9
  47. package/lib/retrieval.js +44 -13
  48. package/lib/revision-cache.d.ts +24 -0
  49. package/lib/revision-cache.js +100 -0
  50. package/lib/routes.js +66 -20
  51. package/lib/runtime.d.ts +4 -0
  52. package/lib/runtime.js +15 -4
  53. package/lib/settings.d.ts +4 -0
  54. package/lib/settings.js +5 -2
  55. package/lib/shared.d.ts +17 -0
  56. package/lib/shared.js +3 -0
  57. package/lib/skills.js +6 -3
  58. package/lib/store.d.ts +101 -5
  59. package/lib/store.js +78 -29
  60. package/lib/tools.js +12 -4
  61. package/lib/util.js +2 -1
  62. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ import { createRuntime } from "./runtime.js";
3
3
  import { routes } from "./routes.js";
4
4
  import { tools } from "./tools.js";
5
5
  import { skillProvider } from "./skills.js";
6
+ import { registerKnowledgeCatalog } from "./agent-access.js";
6
7
  export { Config } from "./config.js";
7
8
  export { createRuntime } from "./runtime.js";
8
9
  export const name = 'libing-knowledge-base';
@@ -10,7 +11,8 @@ export const inject = ['connection', 'tools', 'skills', 'llm', 'attachments'];
10
11
  const lifetimes = new Map();
11
12
  /** Context owns all registrations and drains durable work before releasing the data root. */
12
13
  export async function apply(ctx, config = {}) {
13
- const resolved = resolveConfig(config), previous = lifetimes.get(resolved.dataRoot);
14
+ const resolved = resolveConfig(config);
15
+ const previous = lifetimes.get(resolved.dataRoot);
14
16
  const scope = { disposed: false };
15
17
  // Own cancellation before awaiting a previous generation; disposal during that wait must not leak a newly created runtime.
16
18
  ctx.effect(() => () => {
@@ -28,13 +30,15 @@ export async function apply(ctx, config = {}) {
28
30
  await previous.closing;
29
31
  if (scope.disposed)
30
32
  return;
31
- const runtime = createRuntime(ctx, resolved), lifetime = { runtime };
33
+ const runtime = createRuntime(ctx, resolved);
34
+ const lifetime = { runtime };
32
35
  scope.owned = lifetime;
33
36
  if (!lifetimes.has(resolved.dataRoot))
34
37
  lifetimes.set(resolved.dataRoot, lifetime);
35
38
  await runtime.ready;
36
39
  if (scope.disposed)
37
40
  return;
41
+ registerKnowledgeCatalog(ctx, runtime);
38
42
  for (const definition of tools(runtime))
39
43
  ctx.tools.register(definition);
40
44
  ctx.effect(() => ctx.skills.registerProvider(() => skillProvider()), 'libing-knowledge-base: bundled skills');
package/lib/indexes.js CHANGED
@@ -12,7 +12,8 @@ export async function summarizeChunks(chunks, job, models, signal) {
12
12
  for (const chunk of chunks) {
13
13
  job.progress.currentChunk = { title: chunk.title, headingPath: chunk.headingPath, chunkId: chunk.chunkId };
14
14
  await job.checkpoint();
15
- let summary, fallback = false;
15
+ let summary;
16
+ let fallback = false;
16
17
  if (job.routes.summary) {
17
18
  try {
18
19
  const key = hash({ content: chunk.contentHash, route: job.routes.summary, max: job.config.summaryMaxChars, schema: job.schemaHash, prompt: 1 });
@@ -43,11 +44,13 @@ export async function summarizeChunks(chunks, job, models, signal) {
43
44
  }
44
45
  /** Every node is a bounded navigation page; overflow is split into linked pages. */
45
46
  export async function writeIndexes(dir, documents, config, revision) {
46
- const catalogs = {}, entities = {};
47
+ const catalogs = {};
48
+ const entities = {};
47
49
  const writeCatalog = async (catalogId, path, title, entries) => {
48
50
  const intro = `# ${mdText(title)}\n\n版本:${revision}\n\n`;
49
51
  const pageEntries = [];
50
- let group = [], length = chars(intro);
52
+ let group = [];
53
+ let length = chars(intro);
51
54
  for (const entry of entries) {
52
55
  const line = entry.line || `- [${mdText(entry.title)}](${posix.relative(posix.dirname(path), required(entry.path))}) — ${mdText(entry.summary)};docId=${entry.docId}${entry.nodeId ? '; nodeId=' + entry.nodeId : ''}${entry.chunkId ? '; chunkId=' + entry.chunkId : ''};${entry.fullChars || 0} 字符;${mdText((entry.keywords || []).slice(0, 12).join('、'))};${entry.reviewStatus || ''}${entry.pdfScope ? ';' + mdText(entry.pdfScope) : ''}${entry.pageRange ? ';PDF第' + entry.pageRange.join('–') + '页' : ''}\n`;
53
56
  if (group.length && length + chars(line) > config.catalogMaxChars - 300) {
@@ -61,7 +64,8 @@ export async function writeIndexes(dir, documents, config, revision) {
61
64
  pageEntries.push(group);
62
65
  catalogs[catalogId] = { catalogId, path, title, pages: [] };
63
66
  for (let i = 0; i < pageEntries.length; i++) {
64
- const pagePath = i === 0 ? path : path.replace(/\.md$/, `-${i + 1}.md`), next = i + 1 < pageEntries.length ? path.replace(/\.md$/, `-${i + 2}.md`) : null;
67
+ const pagePath = i === 0 ? path : path.replace(/\.md$/, `-${i + 1}.md`);
68
+ const next = i + 1 < pageEntries.length ? path.replace(/\.md$/, `-${i + 2}.md`) : null;
65
69
  const content = intro + required(pageEntries[i]).map(x => x.line).join('') + (next ? `\n[下一页索引](${posix.basename(next)})\n` : '');
66
70
  if (chars(content) > config.catalogMaxChars)
67
71
  throw new Error('单个索引条目超过导航预算,请提高 catalogMaxChars 或缩短标题');
@@ -71,11 +75,18 @@ export async function writeIndexes(dir, documents, config, revision) {
71
75
  };
72
76
  for (const document of documents) {
73
77
  const base = posix.join('wiki/documents', document.docId);
78
+ const byId = new Map(document.nodes.map(node => [node.nodeId, node]));
79
+ const byNode = new Map();
80
+ for (const chunk of document.chunks) {
81
+ const group = byNode.get(chunk.nodeId) || [];
82
+ group.push(chunk);
83
+ byNode.set(chunk.nodeId, group);
84
+ }
74
85
  for (const node of document.nodes) {
75
86
  const parents = [];
76
87
  let current = node;
77
88
  while (current.parentId) {
78
- current = required(document.nodes.find(n => n.nodeId === current.parentId));
89
+ current = required(byId.get(current.parentId));
79
90
  if (current.nodeId !== 'root')
80
91
  parents.unshift(current.nodeId);
81
92
  }
@@ -83,7 +94,7 @@ export async function writeIndexes(dir, documents, config, revision) {
83
94
  node.indexPath = path;
84
95
  }
85
96
  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 } : {} }))];
97
+ 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
98
  await writeCatalog(`${document.docId}-${node.nodeId}`, required(node.indexPath), node.title, document.sourceFormat === 'pdf' ? [{ line: `> ${pdfScope(document)}\n\n` }, ...entries] : entries);
88
99
  }
89
100
  for (const chunk of document.chunks) {
@@ -0,0 +1,264 @@
1
+ /** Small checkpoints own counters; existing call receipts resolve only unfinished accounting. */
2
+ import { z } from 'zod';
3
+ import { reportSchema } from './data.ts';
4
+ import type { Job, JobRecord } from './data.ts';
5
+ export declare const checkpointSchema: z.ZodObject<{
6
+ error: z.ZodOptional<z.ZodNullable<z.ZodString>>;
7
+ title: z.ZodString;
8
+ updatedAt: z.ZodOptional<z.ZodString>;
9
+ pdf: z.ZodOptional<z.ZodObject<{
10
+ pageCount: z.ZodNumber;
11
+ selectedPages: z.ZodObject<{
12
+ start: z.ZodNumber;
13
+ end: z.ZodNumber;
14
+ count: z.ZodNumber;
15
+ total: z.ZodNumber;
16
+ partial: z.ZodBoolean;
17
+ }, z.core.$strip>;
18
+ outlineCount: z.ZodOptional<z.ZodNumber>;
19
+ }, z.core.$strip>>;
20
+ sourceFormat: z.ZodDefault<z.ZodEnum<{
21
+ docx: "docx";
22
+ pdf: "pdf";
23
+ }>>;
24
+ parserVersion: z.ZodOptional<z.ZodString>;
25
+ sourceHash: z.ZodOptional<z.ZodString>;
26
+ docId: z.ZodString;
27
+ sourceVersion: z.ZodString;
28
+ filename: z.ZodString;
29
+ revision: z.ZodOptional<z.ZodString>;
30
+ buildKey: z.ZodString;
31
+ createdAt: z.ZodString;
32
+ config: z.ZodObject<{
33
+ dataRoot: z.ZodString;
34
+ chunkTargetChars: z.ZodDefault<z.ZodNumber>;
35
+ evidenceMaxChars: z.ZodDefault<z.ZodNumber>;
36
+ catalogMaxChars: z.ZodDefault<z.ZodNumber>;
37
+ candidateLimit: z.ZodDefault<z.ZodNumber>;
38
+ maxInputBytes: z.ZodDefault<z.ZodNumber>;
39
+ maxUncompressedBytes: z.ZodDefault<z.ZodNumber>;
40
+ maxArchiveEntries: z.ZodDefault<z.ZodNumber>;
41
+ modelTimeoutMs: z.ZodDefault<z.ZodNumber>;
42
+ modelAttempts: z.ZodDefault<z.ZodNumber>;
43
+ visionConcurrency: z.ZodDefault<z.ZodNumber>;
44
+ maxModelCallsLimit: z.ZodDefault<z.ZodNumber>;
45
+ maxModelTokensLimit: z.ZodDefault<z.ZodNumber>;
46
+ uploadRetentionDays: z.ZodDefault<z.ZodNumber>;
47
+ maxModelCalls: z.ZodDefault<z.ZodNumber>;
48
+ maxModelTokens: z.ZodDefault<z.ZodNumber>;
49
+ imageTimeoutMs: z.ZodDefault<z.ZodNumber>;
50
+ visionMaxEdge: z.ZodDefault<z.ZodNumber>;
51
+ visionTileSize: z.ZodDefault<z.ZodNumber>;
52
+ visionMaxTiles: z.ZodDefault<z.ZodNumber>;
53
+ modelMaxOutputTokens: z.ZodDefault<z.ZodNumber>;
54
+ summaryMaxChars: z.ZodDefault<z.ZodNumber>;
55
+ figureMaxChars: z.ZodDefault<z.ZodNumber>;
56
+ pdfMaxPages: z.ZodDefault<z.ZodNumber>;
57
+ pdfMaxPagePixels: z.ZodDefault<z.ZodNumber>;
58
+ pdfPageTimeoutMs: z.ZodDefault<z.ZodNumber>;
59
+ libreOfficePath: z.ZodDefault<z.ZodString>;
60
+ visionProvider: z.ZodDefault<z.ZodString>;
61
+ visionModel: z.ZodDefault<z.ZodString>;
62
+ summaryProvider: z.ZodDefault<z.ZodString>;
63
+ summaryModel: z.ZodDefault<z.ZodString>;
64
+ skipImages: z.ZodDefault<z.ZodBoolean>;
65
+ acceptPartial: z.ZodDefault<z.ZodBoolean>;
66
+ }, z.core.$strict>;
67
+ jobId: z.ZodString;
68
+ kbId: z.ZodString;
69
+ state: z.ZodEnum<{
70
+ failed: "failed";
71
+ "needs-review": "needs-review";
72
+ queued: "queued";
73
+ preflight: "preflight";
74
+ converting: "converting";
75
+ describing: "describing";
76
+ splitting: "splitting";
77
+ indexing: "indexing";
78
+ validating: "validating";
79
+ publishing: "publishing";
80
+ "paused-budget": "paused-budget";
81
+ cancelled: "cancelled";
82
+ ready: "ready";
83
+ }>;
84
+ sourceOriginalHash: z.ZodString;
85
+ routes: z.ZodObject<{
86
+ vision: z.ZodNullable<z.ZodObject<{
87
+ provider: z.ZodString;
88
+ model: z.ZodString;
89
+ }, z.core.$strip>>;
90
+ summary: z.ZodNullable<z.ZodObject<{
91
+ provider: z.ZodString;
92
+ model: z.ZodString;
93
+ }, z.core.$strip>>;
94
+ }, z.core.$strip>;
95
+ schema: z.ZodString;
96
+ schemaHash: z.ZodString;
97
+ overlays: z.ZodRecord<z.ZodString, z.ZodType<import("./data.ts").Overlay, unknown, z.core.$ZodTypeInternals<import("./data.ts").Overlay, unknown>>>;
98
+ sessionId: z.ZodNullable<z.ZodString>;
99
+ progress: z.ZodObject<{
100
+ totalPages: z.ZodOptional<z.ZodNumber>;
101
+ parsedPages: z.ZodOptional<z.ZodNumber>;
102
+ ocrPages: z.ZodOptional<z.ZodNumber>;
103
+ tables: z.ZodOptional<z.ZodNumber>;
104
+ blocks: z.ZodOptional<z.ZodNumber>;
105
+ currentPage: z.ZodOptional<z.ZodNumber>;
106
+ totalFigures: z.ZodOptional<z.ZodNumber>;
107
+ completedFigures: z.ZodOptional<z.ZodNumber>;
108
+ inflightFigures: z.ZodOptional<z.ZodArray<z.ZodObject<{
109
+ occurrenceId: z.ZodString;
110
+ caption: z.ZodString;
111
+ }, z.core.$strip>>>;
112
+ totalChunks: z.ZodOptional<z.ZodNumber>;
113
+ indexedChunks: z.ZodOptional<z.ZodNumber>;
114
+ currentChunk: z.ZodOptional<z.ZodObject<{
115
+ title: z.ZodString;
116
+ headingPath: z.ZodArray<z.ZodString>;
117
+ chunkId: z.ZodString;
118
+ }, z.core.$strip>>;
119
+ }, z.core.$strip>;
120
+ pendingCalls: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
121
+ legacyUsageCount: z.ZodDefault<z.ZodNumber>;
122
+ legacyCallCount: z.ZodDefault<z.ZodNumber>;
123
+ reportSummary: z.ZodOptional<z.ZodObject<{
124
+ issues: z.ZodNumber;
125
+ blocking: z.ZodNumber;
126
+ }, z.core.$strip>>;
127
+ failures: z.ZodArray<z.ZodObject<{
128
+ at: z.ZodString;
129
+ stage: z.ZodEnum<{
130
+ failed: "failed";
131
+ "needs-review": "needs-review";
132
+ queued: "queued";
133
+ preflight: "preflight";
134
+ converting: "converting";
135
+ describing: "describing";
136
+ splitting: "splitting";
137
+ indexing: "indexing";
138
+ validating: "validating";
139
+ publishing: "publishing";
140
+ "paused-budget": "paused-budget";
141
+ cancelled: "cancelled";
142
+ ready: "ready";
143
+ }>;
144
+ error: z.ZodString;
145
+ }, z.core.$strip>>;
146
+ events: z.ZodOptional<z.ZodArray<z.ZodObject<{
147
+ at: z.ZodString;
148
+ state: z.ZodEnum<{
149
+ failed: "failed";
150
+ "needs-review": "needs-review";
151
+ queued: "queued";
152
+ preflight: "preflight";
153
+ converting: "converting";
154
+ describing: "describing";
155
+ splitting: "splitting";
156
+ indexing: "indexing";
157
+ validating: "validating";
158
+ publishing: "publishing";
159
+ "paused-budget": "paused-budget";
160
+ cancelled: "cancelled";
161
+ ready: "ready";
162
+ }>;
163
+ }, z.core.$strip>>>;
164
+ figureRefresh: z.ZodOptional<z.ZodNullable<z.ZodObject<{
165
+ occurrenceId: z.ZodString;
166
+ token: z.ZodString;
167
+ }, z.core.$strip>>>;
168
+ visionCapability: z.ZodOptional<z.ZodString>;
169
+ pageStart: z.ZodOptional<z.ZodNumber>;
170
+ pageEnd: z.ZodOptional<z.ZodNumber>;
171
+ outputPath: z.ZodOptional<z.ZodString>;
172
+ recoveredAt: z.ZodOptional<z.ZodString>;
173
+ finishedAt: z.ZodOptional<z.ZodString>;
174
+ cancelRequested: z.ZodOptional<z.ZodBoolean>;
175
+ formatVersion: z.ZodLiteral<2>;
176
+ usage: z.ZodObject<{
177
+ calls: z.ZodNumber;
178
+ tokens: z.ZodNumber;
179
+ reportedCalls: z.ZodNumber;
180
+ }, z.core.$strip>;
181
+ }, z.core.$strip>;
182
+ export declare function loadJob(directory: string): Promise<JobRecord | null>;
183
+ export declare function summarizeReport(report: z.infer<typeof reportSchema>): {
184
+ issues: number;
185
+ blocking: number;
186
+ };
187
+ export declare function saveReport(directory: string, report: z.infer<typeof reportSchema>): Promise<void>;
188
+ export declare function saveCheckpoint(directory: string, job: JobRecord): Promise<void>;
189
+ export declare function recoverUsage(job: Job): Promise<void>;
190
+ export declare function readReport(job: Job): Promise<{
191
+ revision: string;
192
+ sourceHash: string;
193
+ issues: {
194
+ code: string;
195
+ message: string;
196
+ blocking: boolean;
197
+ page?: number | undefined;
198
+ kind?: string | undefined;
199
+ snippet?: string | undefined;
200
+ reason?: string | undefined;
201
+ }[];
202
+ documents: number;
203
+ blocks: number;
204
+ figures: number;
205
+ ignoredFigures: number;
206
+ unreviewedFigures: number;
207
+ chunks: number;
208
+ withinTarget: number;
209
+ oversized: {
210
+ chunkId: string;
211
+ chars: number;
212
+ reason: string;
213
+ }[];
214
+ usage: {
215
+ calls: number;
216
+ tokens: number;
217
+ reportedCalls: number;
218
+ records: z.core.util.JSONType[];
219
+ };
220
+ targetChars: number;
221
+ pdf?: {
222
+ pageCount: number;
223
+ selectedPages: {
224
+ start: number;
225
+ end: number;
226
+ count: number;
227
+ total: number;
228
+ partial: boolean;
229
+ };
230
+ outlineCount: number;
231
+ matchedBookmarks: number;
232
+ ocrPages: number;
233
+ pages: {
234
+ page: number;
235
+ status: "failed" | "needs-review" | "navigation" | "parsed";
236
+ ocr: boolean;
237
+ tables: number;
238
+ figures: number;
239
+ headings: {
240
+ text: string;
241
+ level: number;
242
+ basis?: string | undefined;
243
+ }[];
244
+ width?: number | undefined;
245
+ height?: number | undefined;
246
+ pageLabel?: string | null | undefined;
247
+ spanCount?: number | undefined;
248
+ artifactCount?: number | undefined;
249
+ excluded?: {
250
+ kind: string;
251
+ bbox: [number, number, number, number] | null;
252
+ text?: string | undefined;
253
+ }[] | undefined;
254
+ previewAssetId?: string | null | undefined;
255
+ error?: string | undefined;
256
+ }[];
257
+ } | undefined;
258
+ } | undefined>;
259
+ export declare function usagePage(job: Job, offset: number, limit: number, signal?: AbortSignal): Promise<{
260
+ records: z.core.util.JSONType[];
261
+ offset: number;
262
+ total: number;
263
+ next: number | null;
264
+ }>;
@@ -0,0 +1,118 @@
1
+ /** Small checkpoints own counters; existing call receipts resolve only unfinished accounting. */
2
+ import { z } from 'zod';
3
+ import { basename, join } from 'node:path';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { jobSchema, reportSchema, usageSchema } from "./data.js";
6
+ import { atomicWrite, checkAbort, code, readJson, safePath, tokensUsed } from "./util.js";
7
+ export const checkpointSchema = jobSchema.omit({ report: true, usage: true }).extend({
8
+ formatVersion: z.literal(2), usage: usageSchema.omit({ records: true }),
9
+ });
10
+ const receiptSchema = z.object({ state: z.enum(['running', 'done', 'failed']), usage: z.object({ inputTokens: z.number().nonnegative(), outputTokens: z.number().nonnegative(), cacheReadTokens: z.number().optional(), cacheWriteTokens: z.number().optional(), totalTokens: z.number().optional(), reasoningTokens: z.number().optional() }).optional() });
11
+ export async function loadJob(directory) {
12
+ const file = await safePath(directory, 'job.json', false);
13
+ let raw;
14
+ try {
15
+ raw = JSON.parse(await readFile(file, 'utf8'));
16
+ }
17
+ catch (error) {
18
+ if (code(error) === 'ENOENT')
19
+ return null;
20
+ throw error;
21
+ }
22
+ const small = checkpointSchema.safeParse(raw);
23
+ if (small.success) {
24
+ if (small.data.jobId !== basename(directory))
25
+ throw new Error('任务目录与身份不一致');
26
+ return jobSchema.parse({ ...small.data, usage: { ...small.data.usage, records: [] } });
27
+ }
28
+ // Unknown checkpoint versions must not fall back to interpreting a partial record.
29
+ if (typeof raw === 'object' && raw !== null && 'formatVersion' in raw)
30
+ throw new Error('不支持的任务检查点格式');
31
+ const record = jobSchema.parse(raw);
32
+ if (record.jobId !== basename(directory))
33
+ throw new Error('任务目录与身份不一致');
34
+ if (['queued', 'preflight', 'converting', 'describing', 'splitting', 'indexing', 'validating', 'publishing'].includes(record.state)) {
35
+ let total = 0;
36
+ let reported = 0;
37
+ for (let number = 1; number <= record.usage.calls; number++) {
38
+ const receipt = await readJson(await safePath(directory, join('calls', `${String(number).padStart(6, '0')}.json`), false), receiptSchema, null);
39
+ if (receipt?.usage) {
40
+ total += usageTokens(receipt.usage);
41
+ reported++;
42
+ }
43
+ }
44
+ record.usage.tokens = Math.max(record.usage.tokens, total);
45
+ record.usage.reportedCalls = Math.max(record.usage.reportedCalls, reported);
46
+ }
47
+ if (record.usage.records.length) {
48
+ await atomicWrite(await safePath(directory, 'legacy-usage.json', false), record.usage.records);
49
+ record.legacyUsageCount = record.usage.records.length;
50
+ record.legacyCallCount = record.usage.calls;
51
+ record.usage.records = [];
52
+ }
53
+ if (record.report) {
54
+ await saveReport(directory, record.report);
55
+ record.reportSummary = summarizeReport(record.report);
56
+ delete record.report;
57
+ }
58
+ await saveCheckpoint(directory, record);
59
+ return record;
60
+ }
61
+ export function summarizeReport(report) {
62
+ return { issues: report.issues.length, blocking: report.issues.filter(i => i.blocking).length };
63
+ }
64
+ export async function saveReport(directory, report) {
65
+ await atomicWrite(await safePath(directory, 'quality.json', false), report);
66
+ }
67
+ export async function saveCheckpoint(directory, job) {
68
+ const { report: _report, usage, ...small } = job;
69
+ const { records: _records, ...counters } = usage;
70
+ // Strip heavy values before cloning; JSON.stringify(job) would retain the original cost.
71
+ const snapshot = checkpointSchema.parse({ ...small, formatVersion: 2, usage: counters });
72
+ await atomicWrite(await safePath(directory, 'job.json', false), snapshot);
73
+ }
74
+ function usageTokens(usage) {
75
+ return tokensUsed({ inputTokens: usage?.inputTokens ?? 0, outputTokens: usage?.outputTokens ?? 0, cacheReadTokens: usage?.cacheReadTokens ?? 0, cacheWriteTokens: usage?.cacheWriteTokens ?? 0 });
76
+ }
77
+ export async function recoverUsage(job) {
78
+ if (new Set(job.pendingCalls).size !== job.pendingCalls.length || job.pendingCalls.some(n => n > job.usage.calls))
79
+ throw new Error('任务待结算调用编号无效');
80
+ for (const number of job.pendingCalls) {
81
+ const file = await safePath(job.directory, join('calls', `${String(number).padStart(6, '0')}.json`), false);
82
+ const receipt = await readJson(file, receiptSchema, null);
83
+ // A reserved call remains charged even when no completion/usage reached disk.
84
+ if (receipt?.usage) {
85
+ job.usage.tokens += usageTokens(receipt.usage);
86
+ job.usage.reportedCalls++;
87
+ }
88
+ job.pendingCalls = job.pendingCalls.filter(n => n !== number);
89
+ }
90
+ await job.checkpoint();
91
+ }
92
+ export async function readReport(job) {
93
+ if (!job.reportSummary)
94
+ return undefined;
95
+ return readJson(await safePath(job.directory, 'quality.json'), reportSchema);
96
+ }
97
+ export async function usagePage(job, offset, limit, signal) {
98
+ if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 50)
99
+ throw new Error('用量分页参数无效');
100
+ const total = job.legacyUsageCount + Math.max(0, job.usage.calls - job.legacyCallCount);
101
+ const records = [];
102
+ const end = Math.min(total, offset + limit);
103
+ let legacy;
104
+ if (offset < job.legacyUsageCount)
105
+ legacy = await readJson(await safePath(job.directory, 'legacy-usage.json'), z.array(z.json()));
106
+ for (let index = offset; index < end; index++) {
107
+ checkAbort(signal);
108
+ if (index < job.legacyUsageCount) {
109
+ records.push(legacy?.[index] ?? null);
110
+ continue;
111
+ }
112
+ const number = job.legacyCallCount + index - job.legacyUsageCount + 1;
113
+ const file = await safePath(job.directory, join('calls', `${String(number).padStart(6, '0')}.json`), false);
114
+ const receipt = await readJson(file, receiptSchema, null);
115
+ records.push(z.json().parse(JSON.parse(JSON.stringify({ call: number, state: receipt?.state ?? 'reserved', usage: receipt?.usage ?? null }))));
116
+ }
117
+ return { records, offset, total, next: end < total ? end : null };
118
+ }