@lemoncat7/dsh-knowledge 1.2.0 → 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 +22 -2
- package/docs/architecture.md +30 -4
- package/docs/requirements.md +10 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +194 -1
- package/lib/api.js.map +1 -1
- package/lib/atomic-file.d.ts +1 -1
- package/lib/atomic-file.d.ts.map +1 -1
- package/lib/atomic-file.js +0 -1
- package/lib/atomic-file.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +37 -34
- package/lib/client.js.map +2 -2
- 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/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 +8 -2
- 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 +6 -1
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts +24 -1
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +302 -2
- 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 +139 -13
- package/lib/remote-provider.js.map +1 -1
- package/lib/runtime.d.ts +1 -0
- package/lib/runtime.d.ts.map +1 -1
- package/lib/runtime.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 +29 -179
- 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-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 +2 -1
- package/lib/web.js.map +1 -1
- package/package.json +12 -1
- package/web/app.js +1617 -89
- package/web/index.html +1 -0
- package/web/markdown-preview.js +8 -8
- package/web/note-editor.js +220 -0
- package/web/styles.css +512 -160
package/lib/local-provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
@@ -8,6 +9,7 @@ import { knowledgeDocumentPath } from './documents/path.js';
|
|
|
8
9
|
import { KnowledgeDocumentStore } from './documents/store.js';
|
|
9
10
|
import { enqueueDocumentProjection } from './documents/projection-queue.js';
|
|
10
11
|
import { mergeKnowledgeBodies } from './knowledge-merge.js';
|
|
12
|
+
import { NoteStore } from './notes/store.js';
|
|
11
13
|
const ENTRY_COLUMNS = `
|
|
12
14
|
id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
|
|
13
15
|
status, document_state, finalized_at, finalization_note, version, source_json, created_at, updated_at
|
|
@@ -22,6 +24,7 @@ const JOINED_ENTRY_COLUMNS = `
|
|
|
22
24
|
const EXTRACTION_LEASE_MS = 15 * 60 * 1000;
|
|
23
25
|
export class LocalKnowledgeProvider {
|
|
24
26
|
mode = 'local';
|
|
27
|
+
notes;
|
|
25
28
|
db;
|
|
26
29
|
documentStore;
|
|
27
30
|
documentsReady;
|
|
@@ -29,6 +32,10 @@ export class LocalKnowledgeProvider {
|
|
|
29
32
|
constructor(path) {
|
|
30
33
|
if (path !== ':memory:')
|
|
31
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);
|
|
32
39
|
this.documentStore = new KnowledgeDocumentStore(join(dirname(path), 'documents'));
|
|
33
40
|
this.db = new DatabaseSync(path);
|
|
34
41
|
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
@@ -37,7 +44,7 @@ export class LocalKnowledgeProvider {
|
|
|
37
44
|
}
|
|
38
45
|
migrate() {
|
|
39
46
|
let version = Number(this.db.prepare('PRAGMA user_version').get().user_version ?? 0);
|
|
40
|
-
if (version >
|
|
47
|
+
if (version > 10)
|
|
41
48
|
throw new Error(`knowledge database schema ${version} is newer than this plugin supports`);
|
|
42
49
|
if (version === 0)
|
|
43
50
|
this.db.exec(`
|
|
@@ -246,11 +253,52 @@ export class LocalKnowledgeProvider {
|
|
|
246
253
|
PRAGMA user_version = 9;
|
|
247
254
|
COMMIT;
|
|
248
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
|
+
}
|
|
249
280
|
// Alpha v2 used a migration note as the default base's routing description.
|
|
250
281
|
// Clear only that exact placeholder so existing user-authored descriptions stay untouched.
|
|
251
282
|
this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
|
|
252
283
|
.run(DEFAULT_KNOWLEDGE_BASE_ID, '由 0.2 版本迁移的知识。');
|
|
253
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
|
+
}
|
|
254
302
|
assertOpen() {
|
|
255
303
|
if (this.closed)
|
|
256
304
|
throw new Error('knowledge provider is closed');
|
|
@@ -429,6 +477,68 @@ export class LocalKnowledgeProvider {
|
|
|
429
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`;
|
|
430
478
|
return this.db.prepare(sql).all(...args).map(rowToDocument);
|
|
431
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
|
+
}
|
|
432
542
|
async getDocument(id) {
|
|
433
543
|
this.assertOpen();
|
|
434
544
|
await this.documentsReady;
|
|
@@ -871,6 +981,152 @@ export class LocalKnowledgeProvider {
|
|
|
871
981
|
if (current !== undefined)
|
|
872
982
|
await this.syncKnowledgeDocumentsQueued(current.knowledgeBaseId);
|
|
873
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);
|
|
1129
|
+
}
|
|
874
1130
|
async propose(input, sourceKey) {
|
|
875
1131
|
this.assertOpen();
|
|
876
1132
|
const proposal = normalizeProposal(input);
|
|
@@ -1200,6 +1456,7 @@ export class LocalKnowledgeProvider {
|
|
|
1200
1456
|
await this.documentsReady;
|
|
1201
1457
|
this.closed = true;
|
|
1202
1458
|
this.db.close();
|
|
1459
|
+
await this.notes.close();
|
|
1203
1460
|
}
|
|
1204
1461
|
writeVersion(entry, changeKind) {
|
|
1205
1462
|
const snapshot = {
|
|
@@ -1353,6 +1610,21 @@ function rowToDocument(row) {
|
|
|
1353
1610
|
updatedAt: String(row.updated_at),
|
|
1354
1611
|
};
|
|
1355
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
|
+
}
|
|
1356
1628
|
function rowToVersion(row) {
|
|
1357
1629
|
const snapshot = JSON.parse(String(row.snapshot_json));
|
|
1358
1630
|
if (snapshot.knowledgeBaseId === undefined)
|
|
@@ -1613,12 +1885,40 @@ function decodeCursor(cursor) {
|
|
|
1613
1885
|
throw Object.assign(new Error('invalid pagination cursor'), { code: 'BAD_REQUEST' });
|
|
1614
1886
|
}
|
|
1615
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
|
+
}
|
|
1616
1913
|
function notFound(kind, id) {
|
|
1617
1914
|
return Object.assign(new Error(`${kind} "${id}" was not found`), { code: 'NOT_FOUND' });
|
|
1618
1915
|
}
|
|
1619
1916
|
function conflict(message) {
|
|
1620
1917
|
return Object.assign(new Error(message), { code: 'CONFLICT' });
|
|
1621
1918
|
}
|
|
1919
|
+
function inputError(message) {
|
|
1920
|
+
return Object.assign(new Error(message), { code: 'BAD_REQUEST', status: 400 });
|
|
1921
|
+
}
|
|
1622
1922
|
function finalizedConflict(entry) {
|
|
1623
1923
|
const label = entry.documentState === 'resolved' ? 'resolved' : 'collection complete';
|
|
1624
1924
|
return conflict(`knowledge document "${entry.title}" (${entry.id}) is finalized as ${label}; reopen it before making changes`);
|