@lemoncat7/dsh-knowledge 1.1.5 → 2.0.0
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/README.md +24 -4
- package/docs/architecture.md +32 -6
- package/docs/requirements.md +10 -0
- package/lib/api.d.ts +2 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +235 -13
- package/lib/api.js.map +1 -1
- package/lib/atomic-file.d.ts +16 -0
- package/lib/atomic-file.d.ts.map +1 -0
- package/lib/atomic-file.js +92 -0
- package/lib/atomic-file.js.map +1 -0
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +38 -35
- package/lib/client.js.map +2 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -5
- package/lib/config.js.map +1 -1
- package/lib/connection.d.ts.map +1 -1
- package/lib/connection.js +5 -15
- package/lib/connection.js.map +1 -1
- package/lib/content-safety.d.ts +12 -0
- package/lib/content-safety.d.ts.map +1 -0
- package/lib/content-safety.js +38 -0
- package/lib/content-safety.js.map +1 -0
- package/lib/documents/projection-queue.d.ts +10 -0
- package/lib/documents/projection-queue.d.ts.map +1 -0
- package/lib/documents/projection-queue.js +21 -0
- package/lib/documents/projection-queue.js.map +1 -0
- package/lib/documents/store.d.ts +4 -2
- package/lib/documents/store.d.ts.map +1 -1
- package/lib/documents/store.js +54 -73
- package/lib/documents/store.js.map +1 -1
- package/lib/domain.d.ts +13 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +18 -4
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +91 -32
- package/lib/index.js.map +1 -1
- package/lib/knowledge-merge.d.ts +8 -0
- package/lib/knowledge-merge.d.ts.map +1 -0
- package/lib/knowledge-merge.js +19 -0
- package/lib/knowledge-merge.js.map +1 -0
- package/lib/local-provider.d.ts +26 -1
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +402 -63
- package/lib/local-provider.js.map +1 -1
- package/lib/management-proxy.d.ts.map +1 -1
- package/lib/management-proxy.js +22 -13
- package/lib/management-proxy.js.map +1 -1
- package/lib/note-reference-handle.d.ts +15 -0
- package/lib/note-reference-handle.d.ts.map +1 -0
- package/lib/note-reference-handle.js +54 -0
- package/lib/note-reference-handle.js.map +1 -0
- package/lib/note-reference-tools.d.ts +6 -0
- package/lib/note-reference-tools.d.ts.map +1 -0
- package/lib/note-reference-tools.js +119 -0
- package/lib/note-reference-tools.js.map +1 -0
- package/lib/note-tools.d.ts +6 -0
- package/lib/note-tools.d.ts.map +1 -0
- package/lib/note-tools.js +283 -0
- package/lib/note-tools.js.map +1 -0
- package/lib/notes/domain.d.ts +44 -0
- package/lib/notes/domain.d.ts.map +1 -0
- package/lib/notes/domain.js +38 -0
- package/lib/notes/domain.js.map +1 -0
- package/lib/notes/store.d.ts +39 -0
- package/lib/notes/store.d.ts.map +1 -0
- package/lib/notes/store.js +407 -0
- package/lib/notes/store.js.map +1 -0
- package/lib/provider.d.ts +17 -0
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +19 -0
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +172 -14
- package/lib/remote-provider.js.map +1 -1
- package/lib/remote-url.d.ts +2 -0
- package/lib/remote-url.d.ts.map +1 -0
- package/lib/remote-url.js +14 -0
- package/lib/remote-url.js.map +1 -0
- package/lib/runtime.d.ts +1 -0
- package/lib/runtime.d.ts.map +1 -1
- package/lib/runtime.js.map +1 -1
- package/lib/service-settings.d.ts.map +1 -1
- package/lib/service-settings.js +3 -10
- package/lib/service-settings.js.map +1 -1
- package/lib/theme-bridge.js +12 -12
- package/lib/theme-bridge.js.map +1 -1
- package/lib/tool-authorization.d.ts +14 -0
- package/lib/tool-authorization.d.ts.map +1 -0
- package/lib/tool-authorization.js +124 -0
- package/lib/tool-authorization.js.map +1 -0
- package/lib/tool-input.d.ts +6 -0
- package/lib/tool-input.d.ts.map +1 -0
- package/lib/tool-input.js +27 -0
- package/lib/tool-input.js.map +1 -0
- package/lib/tools.d.ts +2 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +70 -177
- package/lib/tools.js.map +1 -1
- package/lib/tracking.d.ts +29 -0
- package/lib/tracking.d.ts.map +1 -0
- package/lib/tracking.js +141 -0
- package/lib/tracking.js.map +1 -0
- package/lib/web-change-review.d.ts +34 -0
- package/lib/web-change-review.d.ts.map +1 -0
- package/lib/web-change-review.js +147 -0
- package/lib/web-change-review.js.map +1 -0
- package/lib/web-markdown-preview.d.ts.map +1 -1
- package/lib/web-markdown-preview.js +10 -0
- package/lib/web-markdown-preview.js.map +1 -1
- package/lib/web-note-editor.d.ts +44 -0
- package/lib/web-note-editor.d.ts.map +1 -0
- package/lib/web-note-editor.js +118 -0
- package/lib/web-note-editor.js.map +1 -0
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +3 -1
- package/lib/web.js.map +1 -1
- package/package.json +12 -1
- package/web/app.js +1888 -154
- package/web/change-review.js +5 -0
- package/web/index.html +2 -0
- package/web/markdown-preview.js +8 -8
- package/web/note-editor.js +220 -0
- package/web/styles.css +599 -170
package/lib/local-provider.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
4
5
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
6
|
import { contentHash, DEFAULT_KNOWLEDGE_BASE_ID, newId, normalizeDraft, normalizeKnowledgeBaseDraft, normalizeKnowledgeMountDraft, normalizeKnowledgeSettings, nowIso, } from './domain.js';
|
|
6
7
|
import { renderKnowledgeMarkdown } from './documents/markdown.js';
|
|
7
8
|
import { knowledgeDocumentPath } from './documents/path.js';
|
|
8
9
|
import { KnowledgeDocumentStore } from './documents/store.js';
|
|
10
|
+
import { enqueueDocumentProjection } from './documents/projection-queue.js';
|
|
11
|
+
import { mergeKnowledgeBodies } from './knowledge-merge.js';
|
|
12
|
+
import { NoteStore } from './notes/store.js';
|
|
9
13
|
const ENTRY_COLUMNS = `
|
|
10
14
|
id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
|
|
11
15
|
status, document_state, finalized_at, finalization_note, version, source_json, created_at, updated_at
|
|
@@ -17,8 +21,10 @@ const JOINED_ENTRY_COLUMNS = `
|
|
|
17
21
|
e.finalized_at AS finalized_at, e.finalization_note AS finalization_note, e.version AS version,
|
|
18
22
|
e.source_json AS source_json, e.created_at AS created_at, e.updated_at AS updated_at
|
|
19
23
|
`;
|
|
24
|
+
const EXTRACTION_LEASE_MS = 15 * 60 * 1000;
|
|
20
25
|
export class LocalKnowledgeProvider {
|
|
21
26
|
mode = 'local';
|
|
27
|
+
notes;
|
|
22
28
|
db;
|
|
23
29
|
documentStore;
|
|
24
30
|
documentsReady;
|
|
@@ -26,15 +32,19 @@ export class LocalKnowledgeProvider {
|
|
|
26
32
|
constructor(path) {
|
|
27
33
|
if (path !== ':memory:')
|
|
28
34
|
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
const inMemory = path === ':memory:';
|
|
36
|
+
this.notes = new NoteStore(inMemory
|
|
37
|
+
? join(tmpdir(), `dsh-knowledge-notes-${randomUUID()}`)
|
|
38
|
+
: join(dirname(path), 'notes'), inMemory);
|
|
29
39
|
this.documentStore = new KnowledgeDocumentStore(join(dirname(path), 'documents'));
|
|
30
40
|
this.db = new DatabaseSync(path);
|
|
31
41
|
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
32
42
|
this.migrate();
|
|
33
|
-
this.documentsReady = this.syncAllDocuments();
|
|
43
|
+
this.documentsReady = this.enqueueDocumentSync(() => this.syncAllDocuments());
|
|
34
44
|
}
|
|
35
45
|
migrate() {
|
|
36
46
|
let version = Number(this.db.prepare('PRAGMA user_version').get().user_version ?? 0);
|
|
37
|
-
if (version >
|
|
47
|
+
if (version > 10)
|
|
38
48
|
throw new Error(`knowledge database schema ${version} is newer than this plugin supports`);
|
|
39
49
|
if (version === 0)
|
|
40
50
|
this.db.exec(`
|
|
@@ -243,11 +253,52 @@ export class LocalKnowledgeProvider {
|
|
|
243
253
|
PRAGMA user_version = 9;
|
|
244
254
|
COMMIT;
|
|
245
255
|
`);
|
|
256
|
+
if (version <= 8)
|
|
257
|
+
version = 9;
|
|
258
|
+
if (version === 9) {
|
|
259
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
260
|
+
try {
|
|
261
|
+
this.db.exec(`
|
|
262
|
+
CREATE TABLE knowledge_note_references (
|
|
263
|
+
knowledge_id TEXT NOT NULL REFERENCES knowledge_entries(id) ON DELETE CASCADE,
|
|
264
|
+
note_id TEXT NOT NULL,
|
|
265
|
+
source TEXT NOT NULL CHECK(source IN ('user','agent','legacy')),
|
|
266
|
+
source_session_id TEXT,
|
|
267
|
+
created_at TEXT NOT NULL,
|
|
268
|
+
PRIMARY KEY(knowledge_id, note_id)
|
|
269
|
+
);
|
|
270
|
+
CREATE INDEX knowledge_note_references_note ON knowledge_note_references(note_id, created_at DESC);
|
|
271
|
+
`);
|
|
272
|
+
this.backfillLegacyNoteReferences();
|
|
273
|
+
this.db.exec('PRAGMA user_version = 10; COMMIT');
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
this.db.exec('ROLLBACK');
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
246
280
|
// Alpha v2 used a migration note as the default base's routing description.
|
|
247
281
|
// Clear only that exact placeholder so existing user-authored descriptions stay untouched.
|
|
248
282
|
this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
|
|
249
283
|
.run(DEFAULT_KNOWLEDGE_BASE_ID, '由 0.2 版本迁移的知识。');
|
|
250
284
|
}
|
|
285
|
+
backfillLegacyNoteReferences() {
|
|
286
|
+
const insert = this.db.prepare(`
|
|
287
|
+
INSERT OR IGNORE INTO knowledge_note_references(knowledge_id,note_id,source,source_session_id,created_at)
|
|
288
|
+
VALUES(?,?,'legacy',NULL,?)
|
|
289
|
+
`);
|
|
290
|
+
const timestamp = nowIso();
|
|
291
|
+
const rows = this.db.prepare("SELECT id,body FROM knowledge_entries WHERE body LIKE '%note://note_%'").all();
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
const noteIds = String(row.body).match(/note:\/\/(note_[a-f0-9]{32})/giu) ?? [];
|
|
294
|
+
for (const reference of new Set(noteIds)) {
|
|
295
|
+
const noteId = reference.slice('note://'.length).toLocaleLowerCase();
|
|
296
|
+
const note = this.notes.get(noteId);
|
|
297
|
+
if (note !== undefined && note.kind !== 'folder')
|
|
298
|
+
insert.run(String(row.id), note.id, timestamp);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
251
302
|
assertOpen() {
|
|
252
303
|
if (this.closed)
|
|
253
304
|
throw new Error('knowledge provider is closed');
|
|
@@ -301,7 +352,7 @@ export class LocalKnowledgeProvider {
|
|
|
301
352
|
id,name,description,default_tags_json,extraction_instructions,writeback_policy,writeback_provider,writeback_model,status,created_at,updated_at
|
|
302
353
|
) VALUES(?,?,?,?,?,?,?,?,'active',?,?)
|
|
303
354
|
`).run(base.id, base.name, base.description, JSON.stringify(base.defaultTags), base.extractionInstructions, base.writebackPolicy, base.writebackProvider ?? null, base.writebackModel ?? null, timestamp, timestamp);
|
|
304
|
-
await this.
|
|
355
|
+
await this.syncKnowledgeDocumentsQueued(base.id);
|
|
305
356
|
return base;
|
|
306
357
|
}
|
|
307
358
|
async updateKnowledgeBase(id, input) {
|
|
@@ -311,13 +362,19 @@ export class LocalKnowledgeProvider {
|
|
|
311
362
|
if (current === undefined)
|
|
312
363
|
throw notFound('knowledge base', id);
|
|
313
364
|
const draft = normalizeKnowledgeBaseDraft(input);
|
|
314
|
-
const updated = {
|
|
365
|
+
const updated = {
|
|
366
|
+
id: current.id,
|
|
367
|
+
status: current.status,
|
|
368
|
+
createdAt: current.createdAt,
|
|
369
|
+
...draft,
|
|
370
|
+
updatedAt: nowIso(),
|
|
371
|
+
};
|
|
315
372
|
this.db.prepare(`
|
|
316
373
|
UPDATE knowledge_bases SET
|
|
317
374
|
name=?,description=?,default_tags_json=?,extraction_instructions=?,writeback_policy=?,writeback_provider=?,writeback_model=?,updated_at=?
|
|
318
375
|
WHERE id=?
|
|
319
376
|
`).run(updated.name, updated.description, JSON.stringify(updated.defaultTags), updated.extractionInstructions, updated.writebackPolicy, updated.writebackProvider ?? null, updated.writebackModel ?? null, updated.updatedAt, id);
|
|
320
|
-
await this.
|
|
377
|
+
await this.syncKnowledgeDocumentsQueued(id);
|
|
321
378
|
return updated;
|
|
322
379
|
}
|
|
323
380
|
async patchKnowledgeBase(id, patch) {
|
|
@@ -347,8 +404,10 @@ export class LocalKnowledgeProvider {
|
|
|
347
404
|
if (current.status === 'archived')
|
|
348
405
|
return current;
|
|
349
406
|
const updated = { ...current, status: 'archived', updatedAt: nowIso() };
|
|
350
|
-
this.
|
|
351
|
-
|
|
407
|
+
this.transaction(() => {
|
|
408
|
+
this.db.prepare("UPDATE knowledge_bases SET status='archived',updated_at=? WHERE id=?").run(updated.updatedAt, id);
|
|
409
|
+
this.db.prepare('UPDATE knowledge_mounts SET enabled=0,updated_at=? WHERE knowledge_base_id=?').run(updated.updatedAt, id);
|
|
410
|
+
});
|
|
352
411
|
return updated;
|
|
353
412
|
}
|
|
354
413
|
async restoreKnowledgeBase(id) {
|
|
@@ -368,28 +427,37 @@ export class LocalKnowledgeProvider {
|
|
|
368
427
|
const base = await this.getKnowledgeBase(id);
|
|
369
428
|
if (id === DEFAULT_KNOWLEDGE_BASE_ID)
|
|
370
429
|
throw conflict('the default knowledge base cannot be deleted');
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
430
|
+
if (base === undefined)
|
|
431
|
+
throw notFound('knowledge base', id);
|
|
432
|
+
if (base.status !== 'archived')
|
|
433
|
+
throw conflict('knowledge base must be archived before deletion');
|
|
434
|
+
await this.enqueueDocumentSync(() => this.documentStore.deleteBase(this.documentStore.baseDirectory(base)));
|
|
435
|
+
try {
|
|
436
|
+
this.transaction(() => {
|
|
437
|
+
const row = this.db.prepare('SELECT status FROM knowledge_bases WHERE id=?').get(id);
|
|
438
|
+
if (row === undefined)
|
|
439
|
+
throw notFound('knowledge base', id);
|
|
440
|
+
if (row.status !== 'archived')
|
|
441
|
+
throw conflict('knowledge base must be archived before deletion');
|
|
442
|
+
this.db.prepare(`
|
|
443
|
+
DELETE FROM knowledge_candidates
|
|
444
|
+
WHERE json_extract(draft_json, '$.knowledgeBaseId')=?
|
|
445
|
+
OR target_id IN (SELECT id FROM knowledge_entries WHERE knowledge_base_id=?)
|
|
446
|
+
`).run(id, id);
|
|
447
|
+
this.db.prepare(`
|
|
448
|
+
DELETE FROM knowledge_fts
|
|
449
|
+
WHERE knowledge_id IN (SELECT id FROM knowledge_entries WHERE knowledge_base_id=?)
|
|
450
|
+
`).run(id);
|
|
451
|
+
this.db.prepare('DELETE FROM knowledge_entries WHERE knowledge_base_id=?').run(id);
|
|
452
|
+
this.db.prepare('DELETE FROM knowledge_mounts WHERE knowledge_base_id=?').run(id);
|
|
453
|
+
this.db.prepare('DELETE FROM knowledge_documents WHERE knowledge_base_id=?').run(id);
|
|
454
|
+
this.db.prepare('DELETE FROM knowledge_bases WHERE id=?').run(id);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
await this.syncKnowledgeDocumentsQueued(id).catch(() => { });
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
393
461
|
}
|
|
394
462
|
async listDocuments(knowledgeBaseId, query) {
|
|
395
463
|
this.assertOpen();
|
|
@@ -409,6 +477,68 @@ export class LocalKnowledgeProvider {
|
|
|
409
477
|
const sql = `SELECT * FROM knowledge_documents${where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`} ORDER BY knowledge_base_id, CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END, rel_path`;
|
|
410
478
|
return this.db.prepare(sql).all(...args).map(rowToDocument);
|
|
411
479
|
}
|
|
480
|
+
async listDocumentIndex(request) {
|
|
481
|
+
this.assertOpen();
|
|
482
|
+
await this.documentsReady;
|
|
483
|
+
const limit = Math.max(1, Math.min(request.limit, 100));
|
|
484
|
+
const where = [];
|
|
485
|
+
const args = [];
|
|
486
|
+
const knowledgeBaseIds = request.knowledgeBaseIds === undefined
|
|
487
|
+
? undefined
|
|
488
|
+
: [...new Set(request.knowledgeBaseIds.map(id => id.trim()).filter(Boolean))];
|
|
489
|
+
if (knowledgeBaseIds !== undefined && knowledgeBaseIds.length === 0)
|
|
490
|
+
return { items: [], total: 0 };
|
|
491
|
+
if (knowledgeBaseIds !== undefined) {
|
|
492
|
+
where.push(`knowledge_base_id IN (${knowledgeBaseIds.map(() => '?').join(',')})`);
|
|
493
|
+
args.push(...knowledgeBaseIds);
|
|
494
|
+
}
|
|
495
|
+
if (request.activeKnowledgeBasesOnly) {
|
|
496
|
+
where.push("knowledge_base_id IN (SELECT id FROM knowledge_bases WHERE status='active')");
|
|
497
|
+
}
|
|
498
|
+
const query = request.query?.trim();
|
|
499
|
+
if (query) {
|
|
500
|
+
const like = `%${escapeSqlLike(query)}%`;
|
|
501
|
+
where.push(`(title LIKE ? ESCAPE '\\' OR rel_path LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')`);
|
|
502
|
+
args.push(like, like, like);
|
|
503
|
+
}
|
|
504
|
+
const sourceWhere = where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`;
|
|
505
|
+
const cursorWhere = [];
|
|
506
|
+
const cursorArgs = [];
|
|
507
|
+
if (request.cursor !== undefined) {
|
|
508
|
+
const cursor = decodeDocumentCursor(request.cursor);
|
|
509
|
+
cursorWhere.push(`(
|
|
510
|
+
knowledge_base_id > ? OR
|
|
511
|
+
(knowledge_base_id = ? AND sort_rank > ?) OR
|
|
512
|
+
(knowledge_base_id = ? AND sort_rank = ? AND rel_path > ?) OR
|
|
513
|
+
(knowledge_base_id = ? AND sort_rank = ? AND rel_path = ? AND id > ?)
|
|
514
|
+
)`);
|
|
515
|
+
cursorArgs.push(cursor.knowledgeBaseId, cursor.knowledgeBaseId, cursor.sortRank, cursor.knowledgeBaseId, cursor.sortRank, cursor.relPath, cursor.knowledgeBaseId, cursor.sortRank, cursor.relPath, cursor.id);
|
|
516
|
+
}
|
|
517
|
+
const rows = this.db.prepare(`
|
|
518
|
+
WITH document_index AS (
|
|
519
|
+
SELECT
|
|
520
|
+
id, knowledge_base_id, rel_path, title, entry_count, content_hash,
|
|
521
|
+
document_state, finalized_at, finalization_note, created_at, updated_at,
|
|
522
|
+
CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END AS sort_rank
|
|
523
|
+
FROM knowledge_documents${sourceWhere}
|
|
524
|
+
)
|
|
525
|
+
SELECT * FROM document_index
|
|
526
|
+
${cursorWhere.length === 0 ? '' : `WHERE ${cursorWhere.join(' AND ')}`}
|
|
527
|
+
ORDER BY knowledge_base_id, sort_rank, rel_path, id
|
|
528
|
+
LIMIT ?
|
|
529
|
+
`).all(...args, ...cursorArgs, limit + 1);
|
|
530
|
+
const pageRows = rows.slice(0, limit);
|
|
531
|
+
const items = pageRows.map(rowToDocumentSummary);
|
|
532
|
+
const last = pageRows.at(-1);
|
|
533
|
+
const total = Number(this.db.prepare(`SELECT COUNT(*) AS total FROM knowledge_documents${sourceWhere}`).get(...args).total ?? 0);
|
|
534
|
+
return {
|
|
535
|
+
items,
|
|
536
|
+
total,
|
|
537
|
+
...rows.length <= limit || last === undefined ? {} : {
|
|
538
|
+
nextCursor: encodeDocumentCursor(String(last.knowledge_base_id), Number(last.sort_rank), String(last.rel_path), String(last.id)),
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
412
542
|
async getDocument(id) {
|
|
413
543
|
this.assertOpen();
|
|
414
544
|
await this.documentsReady;
|
|
@@ -615,7 +745,7 @@ export class LocalKnowledgeProvider {
|
|
|
615
745
|
return rows.map(row => {
|
|
616
746
|
const entry = rowToEntry(row);
|
|
617
747
|
return { entry, score: relevanceScore(entry, text) };
|
|
618
|
-
});
|
|
748
|
+
}).sort((left, right) => right.score - left.score || right.entry.updatedAt.localeCompare(left.entry.updatedAt));
|
|
619
749
|
}
|
|
620
750
|
searchByTerms(text, scopeSql, scopeArgs, filterSql, filterArgs, limit) {
|
|
621
751
|
const terms = fallbackTerms(text);
|
|
@@ -683,7 +813,7 @@ export class LocalKnowledgeProvider {
|
|
|
683
813
|
this.assertOpen();
|
|
684
814
|
await this.documentsReady;
|
|
685
815
|
const entry = this.transaction(() => this.insertEntry(draft));
|
|
686
|
-
await this.
|
|
816
|
+
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
687
817
|
return entry;
|
|
688
818
|
}
|
|
689
819
|
insertEntry(input) {
|
|
@@ -713,8 +843,8 @@ export class LocalKnowledgeProvider {
|
|
|
713
843
|
const current = await this.get(id);
|
|
714
844
|
const entry = this.transaction(() => this.updateEntry(id, draft, 'update'));
|
|
715
845
|
if (current !== undefined && current.knowledgeBaseId !== entry.knowledgeBaseId)
|
|
716
|
-
await this.
|
|
717
|
-
await this.
|
|
846
|
+
await this.syncKnowledgeDocumentsQueued(current.knowledgeBaseId);
|
|
847
|
+
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
718
848
|
return entry;
|
|
719
849
|
}
|
|
720
850
|
async finalize(id, state, note) {
|
|
@@ -750,7 +880,7 @@ export class LocalKnowledgeProvider {
|
|
|
750
880
|
this.upsertFts(updated);
|
|
751
881
|
return updated;
|
|
752
882
|
});
|
|
753
|
-
await this.
|
|
883
|
+
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
754
884
|
return entry;
|
|
755
885
|
}
|
|
756
886
|
async reopen(id) {
|
|
@@ -782,7 +912,7 @@ export class LocalKnowledgeProvider {
|
|
|
782
912
|
this.upsertFts(updated);
|
|
783
913
|
return updated;
|
|
784
914
|
});
|
|
785
|
-
await this.
|
|
915
|
+
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
786
916
|
return entry;
|
|
787
917
|
}
|
|
788
918
|
updateEntry(id, input, changeKind) {
|
|
@@ -790,6 +920,9 @@ export class LocalKnowledgeProvider {
|
|
|
790
920
|
if (currentRow === undefined)
|
|
791
921
|
throw notFound('knowledge entry', id);
|
|
792
922
|
const current = rowToEntry(currentRow);
|
|
923
|
+
if (current.status !== 'active' && changeKind !== 'restore') {
|
|
924
|
+
throw conflict(`knowledge document "${current.title}" is archived and cannot be edited`);
|
|
925
|
+
}
|
|
793
926
|
if (current.documentState !== 'open')
|
|
794
927
|
throw finalizedConflict(current);
|
|
795
928
|
const draft = normalizeDraft(input);
|
|
@@ -832,7 +965,7 @@ export class LocalKnowledgeProvider {
|
|
|
832
965
|
this.db.prepare('DELETE FROM knowledge_fts WHERE knowledge_id = ?').run(id);
|
|
833
966
|
return updated;
|
|
834
967
|
});
|
|
835
|
-
await this.
|
|
968
|
+
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
836
969
|
return entry;
|
|
837
970
|
}
|
|
838
971
|
async delete(id) {
|
|
@@ -846,7 +979,153 @@ export class LocalKnowledgeProvider {
|
|
|
846
979
|
throw notFound('knowledge entry', id);
|
|
847
980
|
});
|
|
848
981
|
if (current !== undefined)
|
|
849
|
-
await this.
|
|
982
|
+
await this.syncKnowledgeDocumentsQueued(current.knowledgeBaseId);
|
|
983
|
+
}
|
|
984
|
+
async listNotes(request = {}) {
|
|
985
|
+
this.assertOpen();
|
|
986
|
+
return this.notes.list(request);
|
|
987
|
+
}
|
|
988
|
+
async getNote(id) {
|
|
989
|
+
this.assertOpen();
|
|
990
|
+
return this.notes.get(id);
|
|
991
|
+
}
|
|
992
|
+
async readNote(id) {
|
|
993
|
+
this.assertOpen();
|
|
994
|
+
return this.notes.read(id);
|
|
995
|
+
}
|
|
996
|
+
async createNoteFolder(name, parentId = null) {
|
|
997
|
+
this.assertOpen();
|
|
998
|
+
return this.notes.createFolder(name, parentId);
|
|
999
|
+
}
|
|
1000
|
+
async createNoteDocument(name, parentId = null, content = '') {
|
|
1001
|
+
this.assertOpen();
|
|
1002
|
+
return this.notes.createDocument(name, parentId, content);
|
|
1003
|
+
}
|
|
1004
|
+
async updateNoteContent(id, content) {
|
|
1005
|
+
this.assertOpen();
|
|
1006
|
+
return this.notes.updateContent(id, content);
|
|
1007
|
+
}
|
|
1008
|
+
async renameNote(id, name) {
|
|
1009
|
+
this.assertOpen();
|
|
1010
|
+
return this.notes.rename(id, name);
|
|
1011
|
+
}
|
|
1012
|
+
async moveNote(id, parentId) {
|
|
1013
|
+
this.assertOpen();
|
|
1014
|
+
return this.notes.move(id, parentId);
|
|
1015
|
+
}
|
|
1016
|
+
async deleteNote(id) {
|
|
1017
|
+
this.assertOpen();
|
|
1018
|
+
const noteIds = this.notes.subtree(id).filter(node => node.kind !== 'folder').map(node => node.id);
|
|
1019
|
+
const references = this.noteReferencesForNotes(noteIds);
|
|
1020
|
+
if (references.length > 0)
|
|
1021
|
+
throw conflict(`note content is referenced by ${references.length} knowledge document(s)`);
|
|
1022
|
+
await this.notes.delete(id);
|
|
1023
|
+
this.deleteNoteReferences(noteIds);
|
|
1024
|
+
}
|
|
1025
|
+
async searchNotes(query, limit) {
|
|
1026
|
+
this.assertOpen();
|
|
1027
|
+
const value = query.trim();
|
|
1028
|
+
if (value.length === 0)
|
|
1029
|
+
return [];
|
|
1030
|
+
const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 100));
|
|
1031
|
+
return this.notes.list({ query: value, limit: Math.min(500, boundedLimit * 3) })
|
|
1032
|
+
.filter(node => node.kind !== 'folder')
|
|
1033
|
+
.slice(0, boundedLimit);
|
|
1034
|
+
}
|
|
1035
|
+
async listKnowledgeNoteReferences(knowledgeId) {
|
|
1036
|
+
this.assertOpen();
|
|
1037
|
+
const entry = await this.get(knowledgeId);
|
|
1038
|
+
if (entry === undefined)
|
|
1039
|
+
throw notFound('knowledge entry', knowledgeId);
|
|
1040
|
+
const rows = this.db.prepare(`
|
|
1041
|
+
SELECT knowledge_id,note_id,source,source_session_id,created_at
|
|
1042
|
+
FROM knowledge_note_references
|
|
1043
|
+
WHERE knowledge_id=?
|
|
1044
|
+
ORDER BY created_at,note_id
|
|
1045
|
+
`).all(knowledgeId);
|
|
1046
|
+
return rows.flatMap(row => {
|
|
1047
|
+
const note = this.notes.get(String(row.note_id));
|
|
1048
|
+
if (note === undefined || note.kind === 'folder')
|
|
1049
|
+
return [];
|
|
1050
|
+
return [{
|
|
1051
|
+
knowledgeId: entry.id,
|
|
1052
|
+
knowledgeBaseId: entry.knowledgeBaseId,
|
|
1053
|
+
documentTitle: entry.title,
|
|
1054
|
+
note,
|
|
1055
|
+
source: String(row.source),
|
|
1056
|
+
...row.source_session_id == null ? {} : { sourceSessionId: String(row.source_session_id) },
|
|
1057
|
+
createdAt: String(row.created_at),
|
|
1058
|
+
}];
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
async addKnowledgeNoteReference(knowledgeId, noteId, source, sourceSessionId) {
|
|
1062
|
+
this.assertOpen();
|
|
1063
|
+
const entry = await this.get(knowledgeId);
|
|
1064
|
+
if (entry === undefined)
|
|
1065
|
+
throw notFound('knowledge entry', knowledgeId);
|
|
1066
|
+
if (entry.status !== 'active')
|
|
1067
|
+
throw conflict('archived knowledge documents cannot change note references');
|
|
1068
|
+
if (entry.documentState !== 'open')
|
|
1069
|
+
throw finalizedConflict(entry);
|
|
1070
|
+
const note = this.notes.get(noteId);
|
|
1071
|
+
if (note === undefined)
|
|
1072
|
+
throw notFound('note node', noteId);
|
|
1073
|
+
if (note.kind === 'folder')
|
|
1074
|
+
throw inputError('knowledge documents can reference note documents or files, not folders');
|
|
1075
|
+
if (source !== 'user' && source !== 'agent' && source !== 'legacy')
|
|
1076
|
+
throw inputError('invalid knowledge note reference source');
|
|
1077
|
+
const sessionId = sourceSessionId?.trim() || undefined;
|
|
1078
|
+
if (sessionId !== undefined && sessionId.length > 200)
|
|
1079
|
+
throw inputError('reference source session id is too long');
|
|
1080
|
+
const createdAt = nowIso();
|
|
1081
|
+
this.db.prepare(`
|
|
1082
|
+
INSERT OR IGNORE INTO knowledge_note_references(knowledge_id,note_id,source,source_session_id,created_at)
|
|
1083
|
+
VALUES(?,?,?,?,?)
|
|
1084
|
+
`).run(entry.id, note.id, source, sessionId ?? null, createdAt);
|
|
1085
|
+
const reference = (await this.listKnowledgeNoteReferences(entry.id)).find(item => item.note.id === note.id);
|
|
1086
|
+
if (reference === undefined)
|
|
1087
|
+
throw new Error('knowledge note reference was not persisted');
|
|
1088
|
+
return reference;
|
|
1089
|
+
}
|
|
1090
|
+
async deleteKnowledgeNoteReference(knowledgeId, noteId) {
|
|
1091
|
+
this.assertOpen();
|
|
1092
|
+
const entry = await this.get(knowledgeId);
|
|
1093
|
+
if (entry === undefined)
|
|
1094
|
+
throw notFound('knowledge entry', knowledgeId);
|
|
1095
|
+
if (entry.status !== 'active')
|
|
1096
|
+
throw conflict('archived knowledge documents cannot change note references');
|
|
1097
|
+
if (entry.documentState !== 'open')
|
|
1098
|
+
throw finalizedConflict(entry);
|
|
1099
|
+
const result = this.db.prepare('DELETE FROM knowledge_note_references WHERE knowledge_id=? AND note_id=?').run(knowledgeId, noteId);
|
|
1100
|
+
if (result.changes === 0)
|
|
1101
|
+
throw notFound('knowledge note reference', `${knowledgeId}/${noteId}`);
|
|
1102
|
+
}
|
|
1103
|
+
noteReferencesForNotes(noteIds) {
|
|
1104
|
+
this.assertOpen();
|
|
1105
|
+
const ids = [...new Set(noteIds)];
|
|
1106
|
+
if (ids.length === 0)
|
|
1107
|
+
return [];
|
|
1108
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
1109
|
+
return this.db.prepare(`
|
|
1110
|
+
SELECT r.note_id,e.knowledge_base_id,e.id AS document_id,e.title AS document_title
|
|
1111
|
+
FROM knowledge_note_references r
|
|
1112
|
+
JOIN knowledge_entries e ON e.id=r.knowledge_id
|
|
1113
|
+
WHERE r.note_id IN (${placeholders})
|
|
1114
|
+
ORDER BY e.updated_at DESC,e.id
|
|
1115
|
+
`).all(...ids).map(row => ({
|
|
1116
|
+
noteId: String(row.note_id),
|
|
1117
|
+
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1118
|
+
documentId: String(row.document_id),
|
|
1119
|
+
documentTitle: String(row.document_title),
|
|
1120
|
+
}));
|
|
1121
|
+
}
|
|
1122
|
+
deleteNoteReferences(noteIds) {
|
|
1123
|
+
this.assertOpen();
|
|
1124
|
+
const ids = [...new Set(noteIds)];
|
|
1125
|
+
if (ids.length === 0)
|
|
1126
|
+
return;
|
|
1127
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
1128
|
+
this.db.prepare(`DELETE FROM knowledge_note_references WHERE note_id IN (${placeholders})`).run(...ids);
|
|
850
1129
|
}
|
|
851
1130
|
async propose(input, sourceKey) {
|
|
852
1131
|
this.assertOpen();
|
|
@@ -894,7 +1173,7 @@ export class LocalKnowledgeProvider {
|
|
|
894
1173
|
};
|
|
895
1174
|
});
|
|
896
1175
|
if (touchedBaseId !== undefined)
|
|
897
|
-
await this.
|
|
1176
|
+
await this.syncKnowledgeDocumentsQueued(touchedBaseId);
|
|
898
1177
|
return result;
|
|
899
1178
|
}
|
|
900
1179
|
insertCandidate(proposal, sourceKey) {
|
|
@@ -1009,10 +1288,7 @@ export class LocalKnowledgeProvider {
|
|
|
1009
1288
|
}
|
|
1010
1289
|
if (candidate.targetId === undefined)
|
|
1011
1290
|
throw new Error('candidate target is missing');
|
|
1012
|
-
const
|
|
1013
|
-
if (targetRow === undefined)
|
|
1014
|
-
throw notFound('candidate target', candidate.targetId);
|
|
1015
|
-
const target = rowToEntry(targetRow);
|
|
1291
|
+
const target = this.activeEntry(candidate.targetId);
|
|
1016
1292
|
if (draft.knowledgeBaseId !== target.knowledgeBaseId) {
|
|
1017
1293
|
throw conflict('candidate approval cannot move a document between knowledge bases');
|
|
1018
1294
|
}
|
|
@@ -1058,30 +1334,49 @@ export class LocalKnowledgeProvider {
|
|
|
1058
1334
|
return { ...candidate, status, reviewedAt, ...note === undefined ? {} : { reviewNote: note } };
|
|
1059
1335
|
});
|
|
1060
1336
|
if (decision.decision === 'approve')
|
|
1061
|
-
await this.
|
|
1337
|
+
await this.syncKnowledgeDocumentsQueued(reviewed.draft.knowledgeBaseId);
|
|
1062
1338
|
return reviewed;
|
|
1063
1339
|
}
|
|
1064
1340
|
async claimExtraction(sourceKey) {
|
|
1065
1341
|
this.assertOpen();
|
|
1342
|
+
const claimedAt = nowIso();
|
|
1343
|
+
const staleBefore = new Date(Date.now() - EXTRACTION_LEASE_MS).toISOString();
|
|
1066
1344
|
const result = this.db.prepare(`
|
|
1067
1345
|
INSERT INTO extraction_jobs(source_key,status,attempts,candidate_count,updated_at)
|
|
1068
1346
|
VALUES(?,'running',1,0,?)
|
|
1069
1347
|
ON CONFLICT(source_key) DO UPDATE SET
|
|
1070
1348
|
status='running', attempts=extraction_jobs.attempts+1, candidate_count=0,
|
|
1071
1349
|
last_error=NULL, updated_at=excluded.updated_at
|
|
1072
|
-
WHERE extraction_jobs.
|
|
1073
|
-
|
|
1350
|
+
WHERE extraction_jobs.attempts < 3 AND (
|
|
1351
|
+
extraction_jobs.status='failed'
|
|
1352
|
+
OR (extraction_jobs.status='running' AND extraction_jobs.updated_at < ?)
|
|
1353
|
+
)
|
|
1354
|
+
`).run(sourceKey, claimedAt, staleBefore);
|
|
1074
1355
|
return result.changes === 1;
|
|
1075
1356
|
}
|
|
1076
1357
|
async completeExtraction(sourceKey, candidateCount) {
|
|
1077
1358
|
this.assertOpen();
|
|
1078
|
-
this.db.prepare(`
|
|
1079
|
-
|
|
1359
|
+
const result = this.db.prepare(`
|
|
1360
|
+
UPDATE extraction_jobs SET status='completed', candidate_count=?, last_error=NULL, updated_at=?
|
|
1361
|
+
WHERE source_key=? AND status='running'
|
|
1362
|
+
`).run(candidateCount, nowIso(), sourceKey);
|
|
1363
|
+
if (result.changes === 0) {
|
|
1364
|
+
const current = await this.extractionJob(sourceKey);
|
|
1365
|
+
if (current?.status !== 'completed')
|
|
1366
|
+
throw conflict(`extraction job "${sourceKey}" is not running`);
|
|
1367
|
+
}
|
|
1080
1368
|
}
|
|
1081
1369
|
async failExtraction(sourceKey, error) {
|
|
1082
1370
|
this.assertOpen();
|
|
1083
|
-
this.db.prepare(`
|
|
1084
|
-
|
|
1371
|
+
const result = this.db.prepare(`
|
|
1372
|
+
UPDATE extraction_jobs SET status='failed', last_error=?, updated_at=?
|
|
1373
|
+
WHERE source_key=? AND status='running'
|
|
1374
|
+
`).run(error.slice(0, 4000), nowIso(), sourceKey);
|
|
1375
|
+
if (result.changes === 0) {
|
|
1376
|
+
const current = await this.extractionJob(sourceKey);
|
|
1377
|
+
if (current?.status !== 'failed')
|
|
1378
|
+
throw conflict(`extraction job "${sourceKey}" is not running`);
|
|
1379
|
+
}
|
|
1085
1380
|
}
|
|
1086
1381
|
async resetExtraction(sourceKey) {
|
|
1087
1382
|
this.assertOpen();
|
|
@@ -1111,7 +1406,12 @@ export class LocalKnowledgeProvider {
|
|
|
1111
1406
|
.get(tokenHash(token));
|
|
1112
1407
|
if (row === undefined)
|
|
1113
1408
|
return undefined;
|
|
1114
|
-
const
|
|
1409
|
+
const previous = row.last_used_at == null ? undefined : String(row.last_used_at);
|
|
1410
|
+
const now = Date.now();
|
|
1411
|
+
const shouldRefresh = previous === undefined || !Number.isFinite(Date.parse(previous)) || now - Date.parse(previous) >= 60_000;
|
|
1412
|
+
if (!shouldRefresh)
|
|
1413
|
+
return rowToToken(row);
|
|
1414
|
+
const usedAt = new Date(now).toISOString();
|
|
1115
1415
|
this.db.prepare('UPDATE api_tokens SET last_used_at = ? WHERE id = ?').run(usedAt, String(row.id));
|
|
1116
1416
|
return rowToToken({ ...row, last_used_at: usedAt });
|
|
1117
1417
|
}
|
|
@@ -1156,6 +1456,7 @@ export class LocalKnowledgeProvider {
|
|
|
1156
1456
|
await this.documentsReady;
|
|
1157
1457
|
this.closed = true;
|
|
1158
1458
|
this.db.close();
|
|
1459
|
+
await this.notes.close();
|
|
1159
1460
|
}
|
|
1160
1461
|
writeVersion(entry, changeKind) {
|
|
1161
1462
|
const snapshot = {
|
|
@@ -1188,6 +1489,12 @@ export class LocalKnowledgeProvider {
|
|
|
1188
1489
|
for (const base of bases)
|
|
1189
1490
|
await this.syncKnowledgeDocuments(String(base.id));
|
|
1190
1491
|
}
|
|
1492
|
+
enqueueDocumentSync(operation) {
|
|
1493
|
+
return enqueueDocumentProjection(this.documentStore.root, operation);
|
|
1494
|
+
}
|
|
1495
|
+
syncKnowledgeDocumentsQueued(knowledgeBaseId) {
|
|
1496
|
+
return this.enqueueDocumentSync(() => this.syncKnowledgeDocuments(knowledgeBaseId));
|
|
1497
|
+
}
|
|
1191
1498
|
async syncKnowledgeDocuments(knowledgeBaseId) {
|
|
1192
1499
|
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
1193
1500
|
if (baseRow === undefined)
|
|
@@ -1303,6 +1610,21 @@ function rowToDocument(row) {
|
|
|
1303
1610
|
updatedAt: String(row.updated_at),
|
|
1304
1611
|
};
|
|
1305
1612
|
}
|
|
1613
|
+
function rowToDocumentSummary(row) {
|
|
1614
|
+
return {
|
|
1615
|
+
id: String(row.id),
|
|
1616
|
+
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1617
|
+
relPath: String(row.rel_path),
|
|
1618
|
+
title: String(row.title),
|
|
1619
|
+
entryCount: Number(row.entry_count),
|
|
1620
|
+
contentHash: String(row.content_hash),
|
|
1621
|
+
documentState: row.document_state == null ? 'open' : String(row.document_state),
|
|
1622
|
+
...row.finalized_at == null ? {} : { finalizedAt: String(row.finalized_at) },
|
|
1623
|
+
...row.finalization_note == null ? {} : { finalizationNote: String(row.finalization_note) },
|
|
1624
|
+
createdAt: String(row.created_at),
|
|
1625
|
+
updatedAt: String(row.updated_at),
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1306
1628
|
function rowToVersion(row) {
|
|
1307
1629
|
const snapshot = JSON.parse(String(row.snapshot_json));
|
|
1308
1630
|
if (snapshot.knowledgeBaseId === undefined)
|
|
@@ -1355,7 +1677,7 @@ function mergeKnowledgeDraft(current, incoming, preferIncomingTitle) {
|
|
|
1355
1677
|
return normalizeDraft({
|
|
1356
1678
|
knowledgeBaseId: current.knowledgeBaseId,
|
|
1357
1679
|
title: preferIncomingTitle ? incoming.title : current.title,
|
|
1358
|
-
body:
|
|
1680
|
+
body: mergeKnowledgeBodies(current.body, incoming.body),
|
|
1359
1681
|
type: current.type,
|
|
1360
1682
|
tags: [...current.tags, ...incoming.tags],
|
|
1361
1683
|
scope: current.scope,
|
|
@@ -1365,18 +1687,7 @@ function mergeKnowledgeDraft(current, incoming, preferIncomingTitle) {
|
|
|
1365
1687
|
: { source: incoming.source },
|
|
1366
1688
|
});
|
|
1367
1689
|
}
|
|
1368
|
-
function mergeBodies(current, incoming) {
|
|
1369
|
-
const currentKey = normalizedBody(current);
|
|
1370
|
-
const incomingKey = normalizedBody(incoming);
|
|
1371
|
-
if (currentKey === incomingKey || currentKey.includes(incomingKey))
|
|
1372
|
-
return current.trim();
|
|
1373
|
-
if (incomingKey.includes(currentKey))
|
|
1374
|
-
return incoming.trim();
|
|
1375
|
-
return `${current.trim()}\n\n${incoming.trim()}`;
|
|
1376
|
-
}
|
|
1377
1690
|
function potentiallyConflicts(current, incoming) {
|
|
1378
|
-
if (current.type !== incoming.type)
|
|
1379
|
-
return true;
|
|
1380
1691
|
const currentBody = normalizedBody(current.body);
|
|
1381
1692
|
const incomingBody = normalizedBody(incoming.body);
|
|
1382
1693
|
if (currentBody === incomingBody || currentBody.includes(incomingBody) || incomingBody.includes(currentBody))
|
|
@@ -1574,12 +1885,40 @@ function decodeCursor(cursor) {
|
|
|
1574
1885
|
throw Object.assign(new Error('invalid pagination cursor'), { code: 'BAD_REQUEST' });
|
|
1575
1886
|
}
|
|
1576
1887
|
}
|
|
1888
|
+
function encodeDocumentCursor(knowledgeBaseId, sortRank, relPath, id) {
|
|
1889
|
+
return Buffer.from(JSON.stringify({ knowledgeBaseId, sortRank, relPath, id })).toString('base64url');
|
|
1890
|
+
}
|
|
1891
|
+
function decodeDocumentCursor(cursor) {
|
|
1892
|
+
try {
|
|
1893
|
+
const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
1894
|
+
if (typeof value.knowledgeBaseId !== 'string'
|
|
1895
|
+
|| (value.sortRank !== 0 && value.sortRank !== 1)
|
|
1896
|
+
|| typeof value.relPath !== 'string'
|
|
1897
|
+
|| typeof value.id !== 'string')
|
|
1898
|
+
throw new Error();
|
|
1899
|
+
return {
|
|
1900
|
+
knowledgeBaseId: value.knowledgeBaseId,
|
|
1901
|
+
sortRank: value.sortRank,
|
|
1902
|
+
relPath: value.relPath,
|
|
1903
|
+
id: value.id,
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
catch {
|
|
1907
|
+
throw Object.assign(new Error('invalid document pagination cursor'), { code: 'BAD_REQUEST', status: 400 });
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function escapeSqlLike(value) {
|
|
1911
|
+
return value.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_');
|
|
1912
|
+
}
|
|
1577
1913
|
function notFound(kind, id) {
|
|
1578
1914
|
return Object.assign(new Error(`${kind} "${id}" was not found`), { code: 'NOT_FOUND' });
|
|
1579
1915
|
}
|
|
1580
1916
|
function conflict(message) {
|
|
1581
1917
|
return Object.assign(new Error(message), { code: 'CONFLICT' });
|
|
1582
1918
|
}
|
|
1919
|
+
function inputError(message) {
|
|
1920
|
+
return Object.assign(new Error(message), { code: 'BAD_REQUEST', status: 400 });
|
|
1921
|
+
}
|
|
1583
1922
|
function finalizedConflict(entry) {
|
|
1584
1923
|
const label = entry.documentState === 'resolved' ? 'resolved' : 'collection complete';
|
|
1585
1924
|
return conflict(`knowledge document "${entry.title}" (${entry.id}) is finalized as ${label}; reopen it before making changes`);
|