@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/store.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { RevisionCache } from './revision-cache.ts';
1
2
  import type { Settings } from './config.ts';
2
3
  import { z } from 'zod';
3
4
  import { registrySchema } from './data.ts';
4
5
  import type { Library, Manifest, Snapshot, Chunk, PublishedDocument, BuildDocument, Overlay } from './data.ts';
5
6
  export declare const DEFAULT_SCHEMA = "# \u77E5\u8BC6\u5E93\u6CBB\u7406\u7EA6\u5B9A\n\n\u4FDD\u7559\u539F\u6587\u4E2D\u7684\u578B\u53F7\u3001\u7BA1\u811A\u3001\u5355\u4F4D\u3001\u6570\u503C\u3001\u6781\u6027\u548C\u9002\u7528\u6761\u4EF6\u3002\u56FE\u7247\u63CF\u8FF0\u4E0E\u539F\u6587\u5206\u522B\u6807\u6CE8\uFF1B\u4E0D\u786E\u5B9A\u7684\u4FE1\u606F\u4FDD\u7559\u5F85\u590D\u6838\u3002\u6458\u8981\u7528\u4E8E\u5BFC\u822A\uFF0C\u56DE\u7B54\u4F9D\u636E\u539F\u6587\u548C\u53EF\u6838\u9A8C\u56FE\u50CF\u3002\u5F15\u7528\u5305\u542B\u6587\u6863\u7248\u672C\u3001\u7AE0\u8282\u3001\u7247\u6BB5\u548C\u56FE\u8868\u7F16\u53F7\u3002\n";
6
7
  export declare class Store {
8
+ readonly revisionCache: RevisionCache;
7
9
  readonly config: Settings;
8
10
  readonly root: string;
9
11
  private registryTail;
@@ -103,16 +105,88 @@ export declare class Store {
103
105
  coverageIncomplete: boolean;
104
106
  };
105
107
  }>;
106
- location(kbId: string, revision?: string): Promise<{
108
+ private inspectKnown;
109
+ overview(): Promise<{
110
+ kbs: ({
111
+ location: {
112
+ kbPath: string;
113
+ revision: null;
114
+ wikiPath: null;
115
+ indexPath: null;
116
+ };
117
+ summary: {
118
+ revision: null;
119
+ documentCount: number;
120
+ chunkCount: number;
121
+ coverageIncomplete: boolean;
122
+ };
123
+ kbId: string;
124
+ name: string;
125
+ createdAt: string;
126
+ deletedAt?: string | undefined;
127
+ purge?: {
128
+ startedAt: string;
129
+ jobIds: string[];
130
+ error?: string | undefined;
131
+ } | undefined;
132
+ } | {
133
+ location: {
134
+ kbPath: string;
135
+ revision: string;
136
+ wikiPath: string;
137
+ indexPath: string;
138
+ };
139
+ summary: {
140
+ revision: string;
141
+ documentCount: number;
142
+ chunkCount: number;
143
+ coverageIncomplete: boolean;
144
+ };
145
+ kbId: string;
146
+ name: string;
147
+ createdAt: string;
148
+ deletedAt?: string | undefined;
149
+ purge?: {
150
+ startedAt: string;
151
+ jobIds: string[];
152
+ error?: string | undefined;
153
+ } | undefined;
154
+ })[];
155
+ deletedKbs: {
156
+ kbId: string;
157
+ name: string;
158
+ createdAt: string;
159
+ deletedAt?: string | undefined;
160
+ purge?: {
161
+ startedAt: string;
162
+ jobIds: string[];
163
+ error?: string | undefined;
164
+ } | undefined;
165
+ }[];
166
+ activities: {
167
+ eventId: string;
168
+ at: string;
169
+ action: "purge" | "create" | "trash" | "restore";
170
+ kbId: string;
171
+ name: string;
172
+ }[];
173
+ }>;
174
+ snapshotLocation(kbId: string, snapshot: Snapshot): Promise<{
107
175
  kbPath: string;
108
- revision: null;
109
- wikiPath: null;
110
- indexPath: null;
111
- } | {
176
+ revision: string;
177
+ wikiPath: string;
178
+ indexPath: string;
179
+ }>;
180
+ location(kbId: string, revision?: string): Promise<{
112
181
  kbPath: string;
113
182
  revision: string;
114
183
  wikiPath: string;
115
184
  indexPath: string;
185
+ } | {
186
+ kbPath: string;
187
+ revision: null;
188
+ wikiPath: null;
189
+ indexPath: null;
116
190
  }>;
117
191
  schema(kbId: string): Promise<string>;
118
192
  setSchema(kbId: string, content: string, expectedHash: string): Promise<{
@@ -138,7 +212,22 @@ export declare class Store {
138
212
  buildKey: string;
139
213
  coverageIncomplete: boolean;
140
214
  } | null>;
215
+ open(selector: string, requested?: string): Promise<{
216
+ kb: {
217
+ kbId: string;
218
+ name: string;
219
+ createdAt: string;
220
+ deletedAt?: string | undefined;
221
+ purge?: {
222
+ startedAt: string;
223
+ jobIds: string[];
224
+ error?: string | undefined;
225
+ } | undefined;
226
+ };
227
+ snapshot: Snapshot;
228
+ }>;
141
229
  revision(kbId: string, requested?: string): Promise<Snapshot>;
230
+ private revisionKnown;
142
231
  readChunk(kbId: string, revision: string | undefined, chunkId: string): Promise<{
143
232
  chunk: {
144
233
  chunkId: string;
@@ -187,6 +276,10 @@ export declare class Store {
187
276
  manifest: Manifest;
188
277
  }>;
189
278
  chunkContent(snapshot: Snapshot, chunk: Chunk): Promise<string>;
279
+ chunkView(snapshot: Snapshot, chunk: Chunk, fresh?: boolean): Promise<{
280
+ content: string;
281
+ lower: string;
282
+ }>;
190
283
  asset(kbId: string, revision: string | undefined, docId: string, assetId: string): Promise<{
191
284
  asset: {
192
285
  assetId: string;
@@ -197,6 +290,7 @@ export declare class Store {
197
290
  source?: import("./data.ts").Source | undefined;
198
291
  };
199
292
  file: string;
293
+ data: Buffer<ArrayBufferLike>;
200
294
  doc: {
201
295
  docId: string;
202
296
  title: string;
@@ -404,6 +498,8 @@ export declare class Store {
404
498
  }): Promise<string>;
405
499
  overlays(kbId: string): Promise<Record<string, Overlay>>;
406
500
  setOverlay(kbId: string, key: string, value: Overlay | null, expectedHash: string): Promise<Overlay | undefined>;
501
+ /** Cleanup runs with the sole owner before accepting uploads or starting jobs. Inputs are independent copies. */
502
+ cleanUploads(at?: number): Promise<void>;
407
503
  jobDirectories(): Promise<string[]>;
408
504
  acquireOwner(): Promise<() => Promise<void>>;
409
505
  }
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;
@@ -35,6 +37,7 @@ export class Store {
35
37
  throw new Error('知识库不存在,请刷新');
36
38
  if (kb.name !== expectedName)
37
39
  throw new Error('知识库名称已变化,请刷新后确认删除');
40
+ this.revisionCache.clear();
38
41
  kb.deletedAt = now();
39
42
  this.record(registry, 'trash', kb);
40
43
  await atomicWrite(join(this.root, 'registry.json'), registry);
@@ -50,6 +53,7 @@ export class Store {
50
53
  throw new Error('知识库已开始彻底删除,不能恢复;请继续彻底删除');
51
54
  if (registry.kbs.some(k => !k.deletedAt && k.name === kb.name))
52
55
  throw new Error('同名知识库已存在,不能恢复;请先删除同名库');
56
+ this.revisionCache.clear();
53
57
  delete kb.deletedAt;
54
58
  this.record(registry, 'restore', kb);
55
59
  await atomicWrite(join(this.root, 'registry.json'), registry);
@@ -72,6 +76,7 @@ export class Store {
72
76
  }
73
77
  async removePurgeTree(path) { await rm(path, { recursive: true, force: true, maxRetries: 2, retryDelay: 100 }); }
74
78
  async purge(kbId, expectedName, confirmed, jobIds = []) {
79
+ this.revisionCache.clear();
75
80
  if (confirmed !== true)
76
81
  throw new Error('必须明确确认彻底删除不可恢复');
77
82
  return this.serialized(async () => {
@@ -129,13 +134,20 @@ export class Store {
129
134
  });
130
135
  }
131
136
  kbRoot(kbId) { return join(this.root, 'kbs', id(kbId, 'kbId')); }
132
- async inspect(kbId, revision) {
133
- const kb = await this.kb(kbId), kbPath = await safePath(this.root, join('kbs', kb.kbId));
134
- const current = await this.current(kb.kbId);
137
+ async inspect(kbId, revision) { return this.inspectKnown(await this.kb(kbId), revision); }
138
+ async inspectKnown(kb, revision) {
139
+ const kbPath = await safePath(this.root, join('kbs', kb.kbId)), current = await this.current(kb.kbId);
135
140
  if (!revision && !current)
136
141
  return { location: { kbPath, revision: null, wikiPath: null, indexPath: null }, summary: { revision: null, documentCount: 0, chunkCount: 0, coverageIncomplete: false } };
137
- const snapshot = await this.revision(kb.kbId, revision || required(current).revision);
138
- return { location: { kbPath, revision: snapshot.revision, wikiPath: await safePath(snapshot.root, 'wiki'), indexPath: await safePath(snapshot.root, 'index.md') }, 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) } };
142
+ const snapshot = await this.revisionKnown(kb, revision || required(current).revision);
143
+ 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) } };
144
+ }
145
+ async overview() {
146
+ const registry = await readJson(join(this.root, 'registry.json'), registrySchema);
147
+ 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 || [] };
148
+ }
149
+ async snapshotLocation(kbId, snapshot) {
150
+ 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
151
  }
140
152
  async location(kbId, revision) { return (await this.inspect(kbId, revision)).location; }
141
153
  async schema(kbId) { return readFile(await safePath(this.kbRoot(kbId), 'schema.md'), 'utf8'); }
@@ -167,15 +179,16 @@ export class Store {
167
179
  throw new Error('上传内容已变化');
168
180
  return { ...receipt, data };
169
181
  }
170
- async current(kbId) { return readJson(join(this.kbRoot(kbId), 'current.json'), currentSchema, null); }
171
- async revision(kbId, requested) {
172
- await this.kb(kbId);
173
- const current = await this.current(kbId), revision = requested || current?.revision;
182
+ async current(kbId) { return readJson(await safePath(this.root, join('kbs', id(kbId), 'current.json'), false), currentSchema, null); }
183
+ async open(selector, requested) { const kb = await this.kb(selector); return { kb, snapshot: await this.revisionKnown(kb, requested) }; }
184
+ async revision(kbId, requested) { return (await this.open(kbId, requested)).snapshot; }
185
+ async revisionKnown(kb, requested) {
186
+ const current = requested ? null : await this.current(kb.kbId), revision = requested || current?.revision;
174
187
  if (!revision)
175
188
  throw new Error('知识库尚无已发布版本');
176
189
  id(revision, 'revision');
177
- const root = await safePath(this.kbRoot(kbId), join('revisions', revision));
178
- const manifest = await readJson(join(root, 'manifest.json'), manifestSchema);
190
+ const root = await safePath(this.root, join('kbs', kb.kbId, 'revisions', revision));
191
+ const manifest = await this.revisionCache.manifest(await safePath(root, 'manifest.json'));
179
192
  if (manifest.revision !== revision)
180
193
  throw new Error('版本清单不一致');
181
194
  return { root, manifest, revision };
@@ -188,11 +201,9 @@ export class Store {
188
201
  const content = await this.chunkContent(snapshot, chunk);
189
202
  return { ...snapshot, chunk, content };
190
203
  }
191
- async chunkContent(snapshot, chunk) {
192
- const content = await readFile(await safePath(snapshot.root, chunk.path), 'utf8');
193
- if (hash(content) !== chunk.contentHash)
194
- throw new Error('片段内容与清单不一致,请重建');
195
- return content;
204
+ async chunkContent(snapshot, chunk) { return (await this.chunkView(snapshot, chunk, true)).content; }
205
+ async chunkView(snapshot, chunk, fresh = false) {
206
+ return this.revisionCache.chunk(await safePath(snapshot.root, chunk.path), chunk.contentHash, fresh);
196
207
  }
197
208
  async asset(kbId, revision, docId, assetId) {
198
209
  const snapshot = await this.revision(kbId, revision), doc = snapshot.manifest.documents.find(d => d.docId === docId);
@@ -200,9 +211,10 @@ export class Store {
200
211
  if (!asset)
201
212
  throw new Error('图片不在所选文档版本中');
202
213
  const file = await safePath(snapshot.root, join('sources', id(docId), asset.path));
203
- if (`a-${hash(await fileBytes(file, this.config.maxUncompressedBytes)).slice(0, 20)}` !== asset.assetId)
214
+ const data = await fileBytes(file, this.config.maxUncompressedBytes);
215
+ if (`a-${hash(data).slice(0, 20)}` !== asset.assetId)
204
216
  throw new Error('图片内容与清单不一致');
205
- return { ...snapshot, asset, file, doc: required(doc) };
217
+ return { ...snapshot, asset, file, data, doc: required(doc) };
206
218
  }
207
219
  async rawSnapshot(kbId, docId, sourceHash, data, info) {
208
220
  const root = join(this.kbRoot(kbId), 'raw', id(docId), id(sourceHash));
@@ -233,7 +245,8 @@ export class Store {
233
245
  return [];
234
246
  const { root, manifest } = await this.revision(kbId, current.revision);
235
247
  const docs = [];
236
- for (const document of manifest.documents.filter(d => d.docId !== replaceDocId)) {
248
+ for (const saved of manifest.documents.filter(d => d.docId !== replaceDocId)) {
249
+ const document = structuredClone(saved);
237
250
  const chunks = [];
238
251
  for (const chunk of document.chunks) {
239
252
  const content = await this.chunkContent({ root, manifest, revision: manifest.revision }, chunk);
@@ -267,6 +280,7 @@ export class Store {
267
280
  if (existing.buildKey !== manifest.buildKey)
268
281
  throw new Error('版本发布冲突');
269
282
  }
283
+ this.revisionCache.clear();
270
284
  // The pointer owns visibility. Replaying the same job after a crash is idempotent.
271
285
  await atomicWrite(join(kbRoot, 'current.json'), { revision, publishedAt: now(), buildKey: manifest.buildKey, coverageIncomplete: manifest.coverageIncomplete });
272
286
  await this.serialized(async () => {
@@ -290,7 +304,27 @@ export class Store {
290
304
  return overlays[key];
291
305
  });
292
306
  }
293
- async jobDirectories() { return (await readdir(join(this.root, 'jobs'), { withFileTypes: true })).filter(d => d.isDirectory()).map(d => join(this.root, 'jobs', d.name)); }
307
+ /** Cleanup runs with the sole owner before accepting uploads or starting jobs. Inputs are independent copies. */
308
+ async cleanUploads(at = Date.now()) {
309
+ const root = await safePath(this.root, 'uploads'), cutoff = at - this.config.uploadRetentionDays * 86400000;
310
+ for (const entry of await readdir(root, { withFileTypes: true })) {
311
+ if (!entry.isFile() || !/^u-[a-f0-9-]{36}\.json$/u.test(entry.name))
312
+ continue;
313
+ const file = await safePath(root, entry.name), receipt = await readJson(file, uploadSchema);
314
+ if (entry.name !== receipt.receiptId + '.json')
315
+ throw new Error('上传收据身份不一致');
316
+ const time = Date.parse(receipt.createdAt);
317
+ if (!Number.isFinite(time))
318
+ throw new Error('上传收据日期无效');
319
+ if (time >= cutoff)
320
+ continue;
321
+ const binary = await safePath(root, receipt.receiptId + '.bin', false);
322
+ // Remove the binary first. If interrupted, the validated receipt makes cleanup retryable.
323
+ await rm(binary, { force: true });
324
+ await rm(file);
325
+ }
326
+ }
327
+ 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
328
  async acquireOwner() {
295
329
  const dir = join(this.root, '.owner.lock'), nonce = randomUUID();
296
330
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-libing/knowledge-base",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "知识治理:Word/PDF 导入、图表复核、可恢复任务与固定版本证据检索",
5
5
  "type": "module",
6
6
  "icon": "icon.svg",