@lemoncat7/dsh-knowledge 0.6.0-alpha.6
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/LICENSE +21 -0
- package/README.md +184 -0
- package/cordis.patch.yml +12 -0
- package/docs/architecture.md +107 -0
- package/docs/document-knowledge-design.zh-CN.md +329 -0
- package/docs/requirements.md +86 -0
- package/lib/api.d.ts +4 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +497 -0
- package/lib/api.js.map +1 -0
- package/lib/client.js +420 -0
- package/lib/client.js.map +7 -0
- package/lib/config.d.ts +38 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +84 -0
- package/lib/config.js.map +1 -0
- package/lib/connection.d.ts +15 -0
- package/lib/connection.d.ts.map +1 -0
- package/lib/connection.js +95 -0
- package/lib/connection.js.map +1 -0
- package/lib/control.d.ts +26 -0
- package/lib/control.d.ts.map +1 -0
- package/lib/control.js +149 -0
- package/lib/control.js.map +1 -0
- package/lib/domain.d.ts +207 -0
- package/lib/domain.d.ts.map +1 -0
- package/lib/domain.js +122 -0
- package/lib/domain.js.map +1 -0
- package/lib/extraction.d.ts +27 -0
- package/lib/extraction.d.ts.map +1 -0
- package/lib/extraction.js +367 -0
- package/lib/extraction.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +198 -0
- package/lib/index.js.map +1 -0
- package/lib/local-provider.d.ts +60 -0
- package/lib/local-provider.d.ts.map +1 -0
- package/lib/local-provider.js +1096 -0
- package/lib/local-provider.js.map +1 -0
- package/lib/provider-router.d.ts +19 -0
- package/lib/provider-router.d.ts.map +1 -0
- package/lib/provider-router.js +83 -0
- package/lib/provider-router.js.map +1 -0
- package/lib/provider.d.ts +38 -0
- package/lib/provider.d.ts.map +1 -0
- package/lib/provider.js +2 -0
- package/lib/provider.js.map +1 -0
- package/lib/recall.d.ts +8 -0
- package/lib/recall.d.ts.map +1 -0
- package/lib/recall.js +76 -0
- package/lib/recall.js.map +1 -0
- package/lib/remote-provider.d.ts +51 -0
- package/lib/remote-provider.d.ts.map +1 -0
- package/lib/remote-provider.js +216 -0
- package/lib/remote-provider.js.map +1 -0
- package/lib/retrieval.d.ts +35 -0
- package/lib/retrieval.d.ts.map +1 -0
- package/lib/retrieval.js +206 -0
- package/lib/retrieval.js.map +1 -0
- package/lib/runtime.d.ts +147 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +26 -0
- package/lib/runtime.js.map +1 -0
- package/lib/tools.d.ts +6 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +99 -0
- package/lib/tools.js.map +1 -0
- package/lib/web.d.ts +3 -0
- package/lib/web.d.ts.map +1 -0
- package/lib/web.js +71 -0
- package/lib/web.js.map +1 -0
- package/package.json +79 -0
- package/web/app.js +1686 -0
- package/web/index.html +19 -0
- package/web/styles.css +786 -0
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { contentHash, DEFAULT_KNOWLEDGE_BASE_ID, newId, normalizeDraft, normalizeKnowledgeBaseDraft, normalizeKnowledgeMountDraft, nowIso, } from './domain.js';
|
|
6
|
+
const ENTRY_COLUMNS = `
|
|
7
|
+
id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
|
|
8
|
+
status, version, source_json, created_at, updated_at
|
|
9
|
+
`;
|
|
10
|
+
const JOINED_ENTRY_COLUMNS = `
|
|
11
|
+
e.id AS id, e.knowledge_base_id AS knowledge_base_id, e.title AS title, e.body AS body, e.type AS type,
|
|
12
|
+
e.tags_json AS tags_json, e.scope_kind AS scope_kind, e.scope_id AS scope_id,
|
|
13
|
+
e.confidence AS confidence, e.status AS status, e.version AS version,
|
|
14
|
+
e.source_json AS source_json, e.created_at AS created_at, e.updated_at AS updated_at
|
|
15
|
+
`;
|
|
16
|
+
export class LocalKnowledgeProvider {
|
|
17
|
+
mode = 'local';
|
|
18
|
+
db;
|
|
19
|
+
closed = false;
|
|
20
|
+
constructor(path) {
|
|
21
|
+
if (path !== ':memory:')
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
this.db = new DatabaseSync(path);
|
|
24
|
+
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
25
|
+
this.migrate();
|
|
26
|
+
}
|
|
27
|
+
migrate() {
|
|
28
|
+
let version = Number(this.db.prepare('PRAGMA user_version').get().user_version ?? 0);
|
|
29
|
+
if (version > 4)
|
|
30
|
+
throw new Error(`knowledge database schema ${version} is newer than this plugin supports`);
|
|
31
|
+
if (version === 0)
|
|
32
|
+
this.db.exec(`
|
|
33
|
+
BEGIN IMMEDIATE;
|
|
34
|
+
CREATE TABLE knowledge_entries (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
title TEXT NOT NULL,
|
|
37
|
+
body TEXT NOT NULL,
|
|
38
|
+
type TEXT NOT NULL CHECK(type IN ('preference','fact','decision','procedure','lesson')),
|
|
39
|
+
tags_json TEXT NOT NULL,
|
|
40
|
+
scope_kind TEXT NOT NULL CHECK(scope_kind IN ('global','project')),
|
|
41
|
+
scope_id TEXT,
|
|
42
|
+
confidence REAL NOT NULL CHECK(confidence >= 0 AND confidence <= 1),
|
|
43
|
+
status TEXT NOT NULL CHECK(status IN ('active','archived')),
|
|
44
|
+
version INTEGER NOT NULL,
|
|
45
|
+
content_hash TEXT NOT NULL,
|
|
46
|
+
source_json TEXT,
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
updated_at TEXT NOT NULL,
|
|
49
|
+
CHECK((scope_kind = 'global' AND scope_id IS NULL) OR (scope_kind = 'project' AND length(scope_id) > 0))
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX knowledge_entries_scope_status ON knowledge_entries(status, scope_kind, scope_id, updated_at DESC);
|
|
52
|
+
CREATE INDEX knowledge_entries_type ON knowledge_entries(type, status);
|
|
53
|
+
CREATE UNIQUE INDEX knowledge_entries_active_hash ON knowledge_entries(content_hash) WHERE status = 'active';
|
|
54
|
+
|
|
55
|
+
CREATE TABLE knowledge_versions (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
knowledge_id TEXT NOT NULL REFERENCES knowledge_entries(id) ON DELETE CASCADE,
|
|
58
|
+
version INTEGER NOT NULL,
|
|
59
|
+
snapshot_json TEXT NOT NULL,
|
|
60
|
+
change_kind TEXT NOT NULL CHECK(change_kind IN ('create','update','archive','restore')),
|
|
61
|
+
created_at TEXT NOT NULL,
|
|
62
|
+
UNIQUE(knowledge_id, version)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE knowledge_candidates (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
action TEXT NOT NULL CHECK(action IN ('create','update','conflict')),
|
|
68
|
+
target_id TEXT,
|
|
69
|
+
draft_json TEXT NOT NULL,
|
|
70
|
+
reason TEXT NOT NULL,
|
|
71
|
+
status TEXT NOT NULL CHECK(status IN ('pending','approved','rejected')),
|
|
72
|
+
source_key TEXT,
|
|
73
|
+
proposal_hash TEXT NOT NULL,
|
|
74
|
+
created_at TEXT NOT NULL,
|
|
75
|
+
reviewed_at TEXT,
|
|
76
|
+
review_note TEXT
|
|
77
|
+
);
|
|
78
|
+
CREATE UNIQUE INDEX knowledge_candidates_dedupe ON knowledge_candidates(source_key, proposal_hash) WHERE source_key IS NOT NULL;
|
|
79
|
+
CREATE INDEX knowledge_candidates_status ON knowledge_candidates(status, created_at DESC);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE extraction_jobs (
|
|
82
|
+
source_key TEXT PRIMARY KEY,
|
|
83
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
84
|
+
attempts INTEGER NOT NULL,
|
|
85
|
+
candidate_count INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
last_error TEXT,
|
|
87
|
+
updated_at TEXT NOT NULL
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE api_tokens (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
name TEXT NOT NULL,
|
|
93
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
94
|
+
permissions_json TEXT NOT NULL,
|
|
95
|
+
created_at TEXT NOT NULL,
|
|
96
|
+
last_used_at TEXT,
|
|
97
|
+
revoked_at TEXT
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE VIRTUAL TABLE knowledge_fts USING fts5(
|
|
101
|
+
knowledge_id UNINDEXED,
|
|
102
|
+
title,
|
|
103
|
+
body,
|
|
104
|
+
tags,
|
|
105
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
106
|
+
);
|
|
107
|
+
PRAGMA user_version = 1;
|
|
108
|
+
COMMIT;
|
|
109
|
+
`);
|
|
110
|
+
if (version === 0)
|
|
111
|
+
version = 1;
|
|
112
|
+
if (version === 1)
|
|
113
|
+
this.db.exec(`
|
|
114
|
+
BEGIN IMMEDIATE;
|
|
115
|
+
CREATE TABLE knowledge_bases (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
name TEXT NOT NULL,
|
|
118
|
+
description TEXT NOT NULL,
|
|
119
|
+
default_tags_json TEXT NOT NULL,
|
|
120
|
+
extraction_instructions TEXT NOT NULL,
|
|
121
|
+
status TEXT NOT NULL CHECK(status IN ('active','archived')),
|
|
122
|
+
created_at TEXT NOT NULL,
|
|
123
|
+
updated_at TEXT NOT NULL
|
|
124
|
+
);
|
|
125
|
+
INSERT INTO knowledge_bases(
|
|
126
|
+
id,name,description,default_tags_json,extraction_instructions,status,created_at,updated_at
|
|
127
|
+
) VALUES(
|
|
128
|
+
'default','默认知识库','','[]','仅收录可跨会话复用、且与当前挂载范围相关的知识。','active',datetime('now'),datetime('now')
|
|
129
|
+
);
|
|
130
|
+
ALTER TABLE knowledge_entries ADD COLUMN knowledge_base_id TEXT NOT NULL DEFAULT 'default';
|
|
131
|
+
CREATE INDEX knowledge_entries_base_status ON knowledge_entries(knowledge_base_id, status, updated_at DESC);
|
|
132
|
+
CREATE TABLE knowledge_mounts (
|
|
133
|
+
id TEXT PRIMARY KEY,
|
|
134
|
+
target_kind TEXT NOT NULL CHECK(target_kind IN ('project','session')),
|
|
135
|
+
target_id TEXT NOT NULL,
|
|
136
|
+
knowledge_base_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
|
137
|
+
enabled INTEGER NOT NULL CHECK(enabled IN (0,1)),
|
|
138
|
+
recall_enabled INTEGER NOT NULL CHECK(recall_enabled IN (0,1)),
|
|
139
|
+
write_mode TEXT NOT NULL CHECK(write_mode IN ('none','audit','direct')),
|
|
140
|
+
include_tags_json TEXT NOT NULL,
|
|
141
|
+
exclude_tags_json TEXT NOT NULL,
|
|
142
|
+
extraction_instructions TEXT NOT NULL,
|
|
143
|
+
created_at TEXT NOT NULL,
|
|
144
|
+
updated_at TEXT NOT NULL,
|
|
145
|
+
UNIQUE(target_kind, target_id, knowledge_base_id)
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX knowledge_mounts_target ON knowledge_mounts(target_kind, target_id, enabled);
|
|
148
|
+
PRAGMA user_version = 2;
|
|
149
|
+
COMMIT;
|
|
150
|
+
`);
|
|
151
|
+
if (version <= 1)
|
|
152
|
+
version = 2;
|
|
153
|
+
if (version === 2)
|
|
154
|
+
this.db.exec(`
|
|
155
|
+
BEGIN IMMEDIATE;
|
|
156
|
+
ALTER TABLE knowledge_bases ADD COLUMN writeback_provider TEXT;
|
|
157
|
+
ALTER TABLE knowledge_bases ADD COLUMN writeback_model TEXT;
|
|
158
|
+
PRAGMA user_version = 3;
|
|
159
|
+
COMMIT;
|
|
160
|
+
`);
|
|
161
|
+
if (version <= 2)
|
|
162
|
+
version = 3;
|
|
163
|
+
if (version === 3)
|
|
164
|
+
this.db.exec(`
|
|
165
|
+
BEGIN IMMEDIATE;
|
|
166
|
+
CREATE TABLE knowledge_documents (
|
|
167
|
+
id TEXT PRIMARY KEY,
|
|
168
|
+
knowledge_base_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
|
169
|
+
rel_path TEXT NOT NULL,
|
|
170
|
+
title TEXT NOT NULL,
|
|
171
|
+
content TEXT NOT NULL,
|
|
172
|
+
entry_count INTEGER NOT NULL CHECK(entry_count >= 0),
|
|
173
|
+
content_hash TEXT NOT NULL,
|
|
174
|
+
created_at TEXT NOT NULL,
|
|
175
|
+
updated_at TEXT NOT NULL,
|
|
176
|
+
UNIQUE(knowledge_base_id, rel_path)
|
|
177
|
+
);
|
|
178
|
+
CREATE INDEX knowledge_documents_base_updated ON knowledge_documents(knowledge_base_id, updated_at DESC);
|
|
179
|
+
PRAGMA user_version = 4;
|
|
180
|
+
COMMIT;
|
|
181
|
+
`);
|
|
182
|
+
// Alpha v2 used a migration note as the default base's routing description.
|
|
183
|
+
// Clear only that exact placeholder so existing user-authored descriptions stay untouched.
|
|
184
|
+
this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
|
|
185
|
+
.run(DEFAULT_KNOWLEDGE_BASE_ID, '由 0.2 版本迁移的知识。');
|
|
186
|
+
this.syncAllDocuments();
|
|
187
|
+
}
|
|
188
|
+
assertOpen() {
|
|
189
|
+
if (this.closed)
|
|
190
|
+
throw new Error('knowledge provider is closed');
|
|
191
|
+
}
|
|
192
|
+
async listKnowledgeBases() {
|
|
193
|
+
this.assertOpen();
|
|
194
|
+
return this.db.prepare('SELECT * FROM knowledge_bases ORDER BY status, updated_at DESC, id').all()
|
|
195
|
+
.map(rowToKnowledgeBase);
|
|
196
|
+
}
|
|
197
|
+
async getKnowledgeBase(id) {
|
|
198
|
+
this.assertOpen();
|
|
199
|
+
const row = this.db.prepare('SELECT * FROM knowledge_bases WHERE id = ?').get(id);
|
|
200
|
+
return row === undefined ? undefined : rowToKnowledgeBase(row);
|
|
201
|
+
}
|
|
202
|
+
async createKnowledgeBase(input) {
|
|
203
|
+
this.assertOpen();
|
|
204
|
+
const draft = normalizeKnowledgeBaseDraft(input);
|
|
205
|
+
const timestamp = nowIso();
|
|
206
|
+
const base = { ...draft, id: newId(), status: 'active', createdAt: timestamp, updatedAt: timestamp };
|
|
207
|
+
this.db.prepare(`
|
|
208
|
+
INSERT INTO knowledge_bases(
|
|
209
|
+
id,name,description,default_tags_json,extraction_instructions,writeback_provider,writeback_model,status,created_at,updated_at
|
|
210
|
+
) VALUES(?,?,?,?,?,?,?,'active',?,?)
|
|
211
|
+
`).run(base.id, base.name, base.description, JSON.stringify(base.defaultTags), base.extractionInstructions, base.writebackProvider ?? null, base.writebackModel ?? null, timestamp, timestamp);
|
|
212
|
+
this.syncKnowledgeDocuments(base.id);
|
|
213
|
+
return base;
|
|
214
|
+
}
|
|
215
|
+
async updateKnowledgeBase(id, input) {
|
|
216
|
+
this.assertOpen();
|
|
217
|
+
const current = await this.getKnowledgeBase(id);
|
|
218
|
+
if (current === undefined)
|
|
219
|
+
throw notFound('knowledge base', id);
|
|
220
|
+
const draft = normalizeKnowledgeBaseDraft(input);
|
|
221
|
+
const updated = { ...current, ...draft, updatedAt: nowIso() };
|
|
222
|
+
this.db.prepare(`
|
|
223
|
+
UPDATE knowledge_bases SET
|
|
224
|
+
name=?,description=?,default_tags_json=?,extraction_instructions=?,writeback_provider=?,writeback_model=?,updated_at=?
|
|
225
|
+
WHERE id=?
|
|
226
|
+
`).run(updated.name, updated.description, JSON.stringify(updated.defaultTags), updated.extractionInstructions, updated.writebackProvider ?? null, updated.writebackModel ?? null, updated.updatedAt, id);
|
|
227
|
+
this.syncKnowledgeDocuments(id);
|
|
228
|
+
return updated;
|
|
229
|
+
}
|
|
230
|
+
async patchKnowledgeBase(id, patch) {
|
|
231
|
+
this.assertOpen();
|
|
232
|
+
const current = await this.getKnowledgeBase(id);
|
|
233
|
+
if (current === undefined)
|
|
234
|
+
throw notFound('knowledge base', id);
|
|
235
|
+
const clearRoute = patch.writebackProvider === null || patch.writebackModel === null;
|
|
236
|
+
const provider = typeof patch.writebackProvider === 'string' ? patch.writebackProvider : current.writebackProvider;
|
|
237
|
+
const model = typeof patch.writebackModel === 'string' ? patch.writebackModel : current.writebackModel;
|
|
238
|
+
return this.updateKnowledgeBase(id, {
|
|
239
|
+
name: patch.name ?? current.name,
|
|
240
|
+
description: patch.description ?? current.description,
|
|
241
|
+
defaultTags: patch.defaultTags ?? current.defaultTags,
|
|
242
|
+
extractionInstructions: patch.extractionInstructions ?? current.extractionInstructions,
|
|
243
|
+
...clearRoute || provider === undefined || model === undefined ? {} : { writebackProvider: provider, writebackModel: model },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async archiveKnowledgeBase(id) {
|
|
247
|
+
this.assertOpen();
|
|
248
|
+
if (id === DEFAULT_KNOWLEDGE_BASE_ID)
|
|
249
|
+
throw conflict('the default knowledge base cannot be archived');
|
|
250
|
+
const current = await this.getKnowledgeBase(id);
|
|
251
|
+
if (current === undefined)
|
|
252
|
+
throw notFound('knowledge base', id);
|
|
253
|
+
if (current.status === 'archived')
|
|
254
|
+
return current;
|
|
255
|
+
const updated = { ...current, status: 'archived', updatedAt: nowIso() };
|
|
256
|
+
this.db.prepare("UPDATE knowledge_bases SET status='archived',updated_at=? WHERE id=?").run(updated.updatedAt, id);
|
|
257
|
+
this.db.prepare('UPDATE knowledge_mounts SET enabled=0,updated_at=? WHERE knowledge_base_id=?').run(updated.updatedAt, id);
|
|
258
|
+
return updated;
|
|
259
|
+
}
|
|
260
|
+
async restoreKnowledgeBase(id) {
|
|
261
|
+
this.assertOpen();
|
|
262
|
+
const current = await this.getKnowledgeBase(id);
|
|
263
|
+
if (current === undefined)
|
|
264
|
+
throw notFound('knowledge base', id);
|
|
265
|
+
if (current.status === 'active')
|
|
266
|
+
return current;
|
|
267
|
+
const updated = { ...current, status: 'active', updatedAt: nowIso() };
|
|
268
|
+
this.db.prepare("UPDATE knowledge_bases SET status='active',updated_at=? WHERE id=?").run(updated.updatedAt, id);
|
|
269
|
+
return updated;
|
|
270
|
+
}
|
|
271
|
+
async deleteKnowledgeBase(id) {
|
|
272
|
+
this.assertOpen();
|
|
273
|
+
if (id === DEFAULT_KNOWLEDGE_BASE_ID)
|
|
274
|
+
throw conflict('the default knowledge base cannot be deleted');
|
|
275
|
+
this.transaction(() => {
|
|
276
|
+
const row = this.db.prepare('SELECT status FROM knowledge_bases WHERE id=?').get(id);
|
|
277
|
+
if (row === undefined)
|
|
278
|
+
throw notFound('knowledge base', id);
|
|
279
|
+
if (row.status !== 'archived')
|
|
280
|
+
throw conflict('knowledge base must be archived before deletion');
|
|
281
|
+
this.db.prepare(`
|
|
282
|
+
DELETE FROM knowledge_candidates
|
|
283
|
+
WHERE json_extract(draft_json, '$.knowledgeBaseId')=?
|
|
284
|
+
OR target_id IN (SELECT id FROM knowledge_entries WHERE knowledge_base_id=?)
|
|
285
|
+
`).run(id, id);
|
|
286
|
+
this.db.prepare(`
|
|
287
|
+
DELETE FROM knowledge_fts
|
|
288
|
+
WHERE knowledge_id IN (SELECT id FROM knowledge_entries WHERE knowledge_base_id=?)
|
|
289
|
+
`).run(id);
|
|
290
|
+
this.db.prepare('DELETE FROM knowledge_entries WHERE knowledge_base_id=?').run(id);
|
|
291
|
+
this.db.prepare('DELETE FROM knowledge_mounts WHERE knowledge_base_id=?').run(id);
|
|
292
|
+
this.db.prepare('DELETE FROM knowledge_documents WHERE knowledge_base_id=?').run(id);
|
|
293
|
+
this.db.prepare('DELETE FROM knowledge_bases WHERE id=?').run(id);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async listDocuments(knowledgeBaseId, query) {
|
|
297
|
+
this.assertOpen();
|
|
298
|
+
const where = [];
|
|
299
|
+
const args = [];
|
|
300
|
+
if (knowledgeBaseId !== undefined) {
|
|
301
|
+
where.push('knowledge_base_id=?');
|
|
302
|
+
args.push(knowledgeBaseId);
|
|
303
|
+
}
|
|
304
|
+
const text = query?.trim();
|
|
305
|
+
if (text) {
|
|
306
|
+
where.push('(title LIKE ? ESCAPE \'\\\' OR rel_path LIKE ? ESCAPE \'\\\' OR content LIKE ? ESCAPE \'\\\')');
|
|
307
|
+
const like = `%${text.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_')}%`;
|
|
308
|
+
args.push(like, like, like);
|
|
309
|
+
}
|
|
310
|
+
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`;
|
|
311
|
+
return this.db.prepare(sql).all(...args).map(rowToDocument);
|
|
312
|
+
}
|
|
313
|
+
async getDocument(id) {
|
|
314
|
+
this.assertOpen();
|
|
315
|
+
const row = this.db.prepare('SELECT * FROM knowledge_documents WHERE id=?').get(id);
|
|
316
|
+
return row === undefined ? undefined : rowToDocument(row);
|
|
317
|
+
}
|
|
318
|
+
async listMounts(targetKind, targetId) {
|
|
319
|
+
this.assertOpen();
|
|
320
|
+
const where = [];
|
|
321
|
+
const args = [];
|
|
322
|
+
if (targetKind !== undefined) {
|
|
323
|
+
where.push('target_kind=?');
|
|
324
|
+
args.push(targetKind);
|
|
325
|
+
}
|
|
326
|
+
if (targetId !== undefined) {
|
|
327
|
+
where.push('target_id=?');
|
|
328
|
+
args.push(targetId);
|
|
329
|
+
}
|
|
330
|
+
return this.db.prepare(`SELECT * FROM knowledge_mounts${where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`} ORDER BY updated_at DESC`)
|
|
331
|
+
.all(...args).map(rowToMount);
|
|
332
|
+
}
|
|
333
|
+
async upsertMount(input) {
|
|
334
|
+
this.assertOpen();
|
|
335
|
+
return this.upsertMountRow(input);
|
|
336
|
+
}
|
|
337
|
+
async applyMountBatch(batch) {
|
|
338
|
+
this.assertOpen();
|
|
339
|
+
if (batch.upserts.length + batch.deleteIds.length > 500)
|
|
340
|
+
throw new Error('mount batch must contain at most 500 operations');
|
|
341
|
+
const deleteIds = [...new Set(batch.deleteIds.map(id => id.trim()).filter(Boolean))];
|
|
342
|
+
return this.transaction(() => {
|
|
343
|
+
const mounts = batch.upserts.map(input => this.upsertMountRow(input));
|
|
344
|
+
for (const id of deleteIds) {
|
|
345
|
+
const result = this.db.prepare('DELETE FROM knowledge_mounts WHERE id=?').run(id);
|
|
346
|
+
if (result.changes === 0)
|
|
347
|
+
throw notFound('knowledge mount', id);
|
|
348
|
+
}
|
|
349
|
+
return { mounts, deletedIds: deleteIds };
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
upsertMountRow(input) {
|
|
353
|
+
const draft = normalizeKnowledgeMountDraft(input);
|
|
354
|
+
const base = this.db.prepare('SELECT status FROM knowledge_bases WHERE id=?').get(draft.knowledgeBaseId);
|
|
355
|
+
if (base === undefined)
|
|
356
|
+
throw notFound('knowledge base', draft.knowledgeBaseId);
|
|
357
|
+
if (base.status !== 'active')
|
|
358
|
+
throw conflict(`knowledge base "${draft.knowledgeBaseId}" is archived`);
|
|
359
|
+
const previous = this.db.prepare(`
|
|
360
|
+
SELECT * FROM knowledge_mounts WHERE target_kind=? AND target_id=? AND knowledge_base_id=?
|
|
361
|
+
`).get(draft.targetKind, draft.targetId, draft.knowledgeBaseId);
|
|
362
|
+
const timestamp = nowIso();
|
|
363
|
+
const mount = {
|
|
364
|
+
...draft,
|
|
365
|
+
id: previous === undefined ? newId() : String(previous.id),
|
|
366
|
+
createdAt: previous === undefined ? timestamp : String(previous.created_at),
|
|
367
|
+
updatedAt: timestamp,
|
|
368
|
+
};
|
|
369
|
+
this.db.prepare(`
|
|
370
|
+
INSERT INTO knowledge_mounts(
|
|
371
|
+
id,target_kind,target_id,knowledge_base_id,enabled,recall_enabled,write_mode,
|
|
372
|
+
include_tags_json,exclude_tags_json,extraction_instructions,created_at,updated_at
|
|
373
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
374
|
+
ON CONFLICT(target_kind,target_id,knowledge_base_id) DO UPDATE SET
|
|
375
|
+
enabled=excluded.enabled,recall_enabled=excluded.recall_enabled,write_mode=excluded.write_mode,
|
|
376
|
+
include_tags_json=excluded.include_tags_json,exclude_tags_json=excluded.exclude_tags_json,
|
|
377
|
+
extraction_instructions=excluded.extraction_instructions,updated_at=excluded.updated_at
|
|
378
|
+
`).run(mount.id, mount.targetKind, mount.targetId, mount.knowledgeBaseId, mount.enabled ? 1 : 0, mount.recallEnabled ? 1 : 0, mount.writeMode, JSON.stringify(mount.includeTags), JSON.stringify(mount.excludeTags), mount.extractionInstructions, mount.createdAt, mount.updatedAt);
|
|
379
|
+
return mount;
|
|
380
|
+
}
|
|
381
|
+
async deleteMount(id) {
|
|
382
|
+
this.assertOpen();
|
|
383
|
+
const result = this.db.prepare('DELETE FROM knowledge_mounts WHERE id=?').run(id);
|
|
384
|
+
if (result.changes === 0)
|
|
385
|
+
throw notFound('knowledge mount', id);
|
|
386
|
+
}
|
|
387
|
+
async resolveMounts(sessionId, projectId) {
|
|
388
|
+
this.assertOpen();
|
|
389
|
+
const project = projectId === undefined ? [] : await this.listMounts('project', projectId);
|
|
390
|
+
const session = await this.listMounts('session', sessionId);
|
|
391
|
+
const resolved = new Map();
|
|
392
|
+
for (const mount of project)
|
|
393
|
+
resolved.set(mount.knowledgeBaseId, { mount, inheritedFrom: 'project' });
|
|
394
|
+
for (const mount of session)
|
|
395
|
+
resolved.set(mount.knowledgeBaseId, { mount });
|
|
396
|
+
const output = [];
|
|
397
|
+
for (const { mount, inheritedFrom } of resolved.values()) {
|
|
398
|
+
if (!mount.enabled)
|
|
399
|
+
continue;
|
|
400
|
+
const base = await this.getKnowledgeBase(mount.knowledgeBaseId);
|
|
401
|
+
if (base === undefined || base.status !== 'active')
|
|
402
|
+
continue;
|
|
403
|
+
output.push({ ...mount, base, ...inheritedFrom === undefined ? {} : { inheritedFrom } });
|
|
404
|
+
}
|
|
405
|
+
return output.sort((left, right) => left.base.name.localeCompare(right.base.name, 'zh-CN'));
|
|
406
|
+
}
|
|
407
|
+
async stats() {
|
|
408
|
+
this.assertOpen();
|
|
409
|
+
const entryRows = this.db.prepare('SELECT status, type, COUNT(*) AS count FROM knowledge_entries GROUP BY status, type').all();
|
|
410
|
+
const candidateRows = this.db.prepare('SELECT status, COUNT(*) AS count FROM knowledge_candidates GROUP BY status').all();
|
|
411
|
+
const jobRows = this.db.prepare('SELECT status, COUNT(*) AS count FROM extraction_jobs GROUP BY status').all();
|
|
412
|
+
const baseRows = this.db.prepare('SELECT status, COUNT(*) AS count FROM knowledge_bases GROUP BY status').all();
|
|
413
|
+
const byType = {
|
|
414
|
+
preference: 0,
|
|
415
|
+
fact: 0,
|
|
416
|
+
decision: 0,
|
|
417
|
+
procedure: 0,
|
|
418
|
+
lesson: 0,
|
|
419
|
+
};
|
|
420
|
+
let active = 0;
|
|
421
|
+
let archived = 0;
|
|
422
|
+
for (const row of entryRows) {
|
|
423
|
+
const count = Number(row.count);
|
|
424
|
+
byType[String(row.type)] += count;
|
|
425
|
+
if (row.status === 'active')
|
|
426
|
+
active += count;
|
|
427
|
+
if (row.status === 'archived')
|
|
428
|
+
archived += count;
|
|
429
|
+
}
|
|
430
|
+
const candidates = { pending: 0, approved: 0, rejected: 0 };
|
|
431
|
+
for (const row of candidateRows)
|
|
432
|
+
candidates[String(row.status)] = Number(row.count);
|
|
433
|
+
const extractionJobs = { running: 0, completed: 0, failed: 0 };
|
|
434
|
+
for (const row of jobRows)
|
|
435
|
+
extractionJobs[String(row.status)] = Number(row.count);
|
|
436
|
+
const knowledgeBases = { active: 0, archived: 0 };
|
|
437
|
+
for (const row of baseRows)
|
|
438
|
+
knowledgeBases[String(row.status)] = Number(row.count);
|
|
439
|
+
return {
|
|
440
|
+
knowledgeBases: { total: knowledgeBases.active + knowledgeBases.archived, ...knowledgeBases },
|
|
441
|
+
entries: { total: active + archived, active, archived, byType },
|
|
442
|
+
candidates: { total: candidates.pending + candidates.approved + candidates.rejected, ...candidates },
|
|
443
|
+
extractionJobs: { total: extractionJobs.running + extractionJobs.completed + extractionJobs.failed, ...extractionJobs },
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
transaction(operation) {
|
|
447
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
448
|
+
try {
|
|
449
|
+
const result = operation();
|
|
450
|
+
this.db.exec('COMMIT');
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
this.db.exec('ROLLBACK');
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async search(request) {
|
|
459
|
+
this.assertOpen();
|
|
460
|
+
const limit = Math.max(0, Math.min(request.limit, 100));
|
|
461
|
+
if (limit === 0)
|
|
462
|
+
return [];
|
|
463
|
+
const scopeSql = request.projectId === undefined
|
|
464
|
+
? `e.scope_kind = 'global'`
|
|
465
|
+
: `(e.scope_kind = 'global' OR (e.scope_kind = 'project' AND e.scope_id = ?))`;
|
|
466
|
+
const scopeArgs = request.projectId === undefined ? [] : [request.projectId];
|
|
467
|
+
const typeSql = request.types === undefined || request.types.length === 0
|
|
468
|
+
? ''
|
|
469
|
+
: ` AND e.type IN (${request.types.map(() => '?').join(',')})`;
|
|
470
|
+
const typeArgs = request.types ?? [];
|
|
471
|
+
const baseIds = [...new Set(request.knowledgeBaseIds ?? [])].filter(Boolean);
|
|
472
|
+
const baseSql = baseIds.length === 0 ? '' : ` AND e.knowledge_base_id IN (${baseIds.map(() => '?').join(',')})`;
|
|
473
|
+
const includeTags = [...new Set(request.includeTags ?? [])].filter(Boolean);
|
|
474
|
+
const includeSql = includeTags.length === 0 ? '' : ` AND EXISTS (
|
|
475
|
+
SELECT 1 FROM json_each(e.tags_json) tags WHERE tags.value IN (${includeTags.map(() => '?').join(',')})
|
|
476
|
+
)`;
|
|
477
|
+
const excludeTags = [...new Set(request.excludeTags ?? [])].filter(Boolean);
|
|
478
|
+
const excludeSql = excludeTags.length === 0 ? '' : ` AND NOT EXISTS (
|
|
479
|
+
SELECT 1 FROM json_each(e.tags_json) tags WHERE tags.value IN (${excludeTags.map(() => '?').join(',')})
|
|
480
|
+
)`;
|
|
481
|
+
const filterSql = `${typeSql}${baseSql}${includeSql}${excludeSql}`;
|
|
482
|
+
const filterArgs = [...typeArgs, ...baseIds, ...includeTags, ...excludeTags];
|
|
483
|
+
const text = request.text.trim();
|
|
484
|
+
let rows;
|
|
485
|
+
if (text.length === 0) {
|
|
486
|
+
rows = this.db.prepare(`
|
|
487
|
+
SELECT ${ENTRY_COLUMNS}, 0.0 AS rank
|
|
488
|
+
FROM knowledge_entries e
|
|
489
|
+
WHERE e.status = 'active' AND ${scopeSql}${filterSql}
|
|
490
|
+
ORDER BY CASE WHEN e.scope_kind = 'project' THEN 0 ELSE 1 END, e.updated_at DESC
|
|
491
|
+
LIMIT ?
|
|
492
|
+
`).all(...scopeArgs, ...filterArgs, limit);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const ftsQuery = toFtsQuery(text);
|
|
496
|
+
try {
|
|
497
|
+
rows = this.db.prepare(`
|
|
498
|
+
SELECT ${JOINED_ENTRY_COLUMNS}, bm25(knowledge_fts, 0.0, 4.0, 1.0, 0.5) AS rank
|
|
499
|
+
FROM knowledge_fts
|
|
500
|
+
JOIN knowledge_entries e ON e.id = knowledge_fts.knowledge_id
|
|
501
|
+
WHERE knowledge_fts MATCH ? AND e.status = 'active' AND ${scopeSql}${filterSql}
|
|
502
|
+
ORDER BY CASE WHEN e.scope_kind = 'project' THEN 0 ELSE 1 END, rank, e.updated_at DESC
|
|
503
|
+
LIMIT ?
|
|
504
|
+
`).all(ftsQuery, ...scopeArgs, ...filterArgs, limit);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
rows = [];
|
|
508
|
+
}
|
|
509
|
+
if (rows.length < limit) {
|
|
510
|
+
const supplements = this.searchByTerms(text, scopeSql, scopeArgs, filterSql, filterArgs, limit);
|
|
511
|
+
const seen = new Set(rows.map(row => String(row.id)));
|
|
512
|
+
rows.push(...supplements.filter(row => !seen.has(String(row.id))).slice(0, limit - rows.length));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return rows.map(row => ({ entry: rowToEntry(row), score: rankToScore(Number(row.rank ?? 0)) }));
|
|
516
|
+
}
|
|
517
|
+
searchByTerms(text, scopeSql, scopeArgs, filterSql, filterArgs, limit) {
|
|
518
|
+
const terms = fallbackTerms(text);
|
|
519
|
+
if (terms.length === 0)
|
|
520
|
+
return [];
|
|
521
|
+
const clauses = terms.map(() => `(e.title LIKE ? ESCAPE '\\' OR e.body LIKE ? ESCAPE '\\' OR e.tags_json LIKE ? ESCAPE '\\')`);
|
|
522
|
+
const args = terms.flatMap((term) => {
|
|
523
|
+
const like = `%${term.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_')}%`;
|
|
524
|
+
return [like, like, like];
|
|
525
|
+
});
|
|
526
|
+
return this.db.prepare(`
|
|
527
|
+
SELECT ${ENTRY_COLUMNS}, 3.0 AS rank
|
|
528
|
+
FROM knowledge_entries e
|
|
529
|
+
WHERE e.status = 'active' AND ${scopeSql}${filterSql} AND (${clauses.join(' OR ')})
|
|
530
|
+
ORDER BY CASE WHEN e.scope_kind = 'project' THEN 0 ELSE 1 END, e.updated_at DESC
|
|
531
|
+
LIMIT ?
|
|
532
|
+
`).all(...scopeArgs, ...filterArgs, ...args, limit);
|
|
533
|
+
}
|
|
534
|
+
async list(request) {
|
|
535
|
+
this.assertOpen();
|
|
536
|
+
const limit = Math.max(1, Math.min(request.limit, 100));
|
|
537
|
+
const where = [];
|
|
538
|
+
const args = [];
|
|
539
|
+
if (request.status !== undefined) {
|
|
540
|
+
where.push('status = ?');
|
|
541
|
+
args.push(request.status);
|
|
542
|
+
}
|
|
543
|
+
if (request.type !== undefined) {
|
|
544
|
+
where.push('type = ?');
|
|
545
|
+
args.push(request.type);
|
|
546
|
+
}
|
|
547
|
+
if (request.knowledgeBaseId !== undefined) {
|
|
548
|
+
where.push('knowledge_base_id = ?');
|
|
549
|
+
args.push(request.knowledgeBaseId);
|
|
550
|
+
}
|
|
551
|
+
if (request.projectId !== undefined) {
|
|
552
|
+
where.push(`(scope_kind = 'global' OR (scope_kind = 'project' AND scope_id = ?))`);
|
|
553
|
+
args.push(request.projectId);
|
|
554
|
+
}
|
|
555
|
+
if (request.cursor !== undefined) {
|
|
556
|
+
const cursor = decodeCursor(request.cursor);
|
|
557
|
+
where.push('(updated_at < ? OR (updated_at = ? AND id < ?))');
|
|
558
|
+
args.push(cursor.updatedAt, cursor.updatedAt, cursor.id);
|
|
559
|
+
}
|
|
560
|
+
const sql = `SELECT ${ENTRY_COLUMNS} FROM knowledge_entries${where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`} ORDER BY updated_at DESC, id DESC LIMIT ?`;
|
|
561
|
+
const rows = this.db.prepare(sql).all(...args, limit + 1);
|
|
562
|
+
const page = rows.slice(0, limit).map(rowToEntry);
|
|
563
|
+
const last = page.at(-1);
|
|
564
|
+
return {
|
|
565
|
+
items: page,
|
|
566
|
+
...rows.length <= limit || last === undefined ? {} : { nextCursor: encodeCursor(last.updatedAt, last.id) },
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async get(id) {
|
|
570
|
+
this.assertOpen();
|
|
571
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(id);
|
|
572
|
+
return row === undefined ? undefined : rowToEntry(row);
|
|
573
|
+
}
|
|
574
|
+
async versions(id) {
|
|
575
|
+
this.assertOpen();
|
|
576
|
+
const rows = this.db.prepare('SELECT * FROM knowledge_versions WHERE knowledge_id = ? ORDER BY version DESC').all(id);
|
|
577
|
+
return rows.map(rowToVersion);
|
|
578
|
+
}
|
|
579
|
+
async create(draft) {
|
|
580
|
+
this.assertOpen();
|
|
581
|
+
const entry = this.transaction(() => this.insertEntry(draft));
|
|
582
|
+
this.syncKnowledgeDocuments(entry.knowledgeBaseId);
|
|
583
|
+
return entry;
|
|
584
|
+
}
|
|
585
|
+
insertEntry(input) {
|
|
586
|
+
const draft = normalizeDraft(input);
|
|
587
|
+
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
|
|
588
|
+
throw notFound('active knowledge base', draft.knowledgeBaseId);
|
|
589
|
+
}
|
|
590
|
+
const id = newId();
|
|
591
|
+
const timestamp = nowIso();
|
|
592
|
+
const entry = { ...draft, id, status: 'active', version: 1, createdAt: timestamp, updatedAt: timestamp };
|
|
593
|
+
this.db.prepare(`
|
|
594
|
+
INSERT INTO knowledge_entries (
|
|
595
|
+
id,knowledge_base_id,title,body,type,tags_json,scope_kind,scope_id,confidence,status,version,content_hash,source_json,created_at,updated_at
|
|
596
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
597
|
+
`).run(id, draft.knowledgeBaseId, draft.title, draft.body, draft.type, JSON.stringify(draft.tags), draft.scope.kind, draft.scope.kind === 'project' ? draft.scope.id : null, draft.confidence, 'active', 1, contentHash(draft), draft.source === undefined ? null : JSON.stringify(draft.source), timestamp, timestamp);
|
|
598
|
+
this.writeVersion(entry, 'create');
|
|
599
|
+
this.upsertFts(entry);
|
|
600
|
+
return entry;
|
|
601
|
+
}
|
|
602
|
+
async update(id, draft) {
|
|
603
|
+
this.assertOpen();
|
|
604
|
+
const current = await this.get(id);
|
|
605
|
+
const entry = this.transaction(() => this.updateEntry(id, draft, 'update'));
|
|
606
|
+
if (current !== undefined && current.knowledgeBaseId !== entry.knowledgeBaseId)
|
|
607
|
+
this.syncKnowledgeDocuments(current.knowledgeBaseId);
|
|
608
|
+
this.syncKnowledgeDocuments(entry.knowledgeBaseId);
|
|
609
|
+
return entry;
|
|
610
|
+
}
|
|
611
|
+
updateEntry(id, input, changeKind) {
|
|
612
|
+
const currentRow = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(id);
|
|
613
|
+
if (currentRow === undefined)
|
|
614
|
+
throw notFound('knowledge entry', id);
|
|
615
|
+
const current = rowToEntry(currentRow);
|
|
616
|
+
const draft = normalizeDraft(input);
|
|
617
|
+
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
|
|
618
|
+
throw notFound('active knowledge base', draft.knowledgeBaseId);
|
|
619
|
+
}
|
|
620
|
+
const timestamp = nowIso();
|
|
621
|
+
const entry = {
|
|
622
|
+
...draft,
|
|
623
|
+
id,
|
|
624
|
+
status: 'active',
|
|
625
|
+
version: current.version + 1,
|
|
626
|
+
createdAt: current.createdAt,
|
|
627
|
+
updatedAt: timestamp,
|
|
628
|
+
};
|
|
629
|
+
this.db.prepare(`
|
|
630
|
+
UPDATE knowledge_entries SET
|
|
631
|
+
knowledge_base_id=?,title=?,body=?,type=?,tags_json=?,scope_kind=?,scope_id=?,confidence=?,status='active',
|
|
632
|
+
version=?,content_hash=?,source_json=?,updated_at=?
|
|
633
|
+
WHERE id=?
|
|
634
|
+
`).run(draft.knowledgeBaseId, draft.title, draft.body, draft.type, JSON.stringify(draft.tags), draft.scope.kind, draft.scope.kind === 'project' ? draft.scope.id : null, draft.confidence, entry.version, contentHash(draft), draft.source === undefined ? null : JSON.stringify(draft.source), timestamp, id);
|
|
635
|
+
this.writeVersion(entry, changeKind);
|
|
636
|
+
this.upsertFts(entry);
|
|
637
|
+
return entry;
|
|
638
|
+
}
|
|
639
|
+
async archive(id) {
|
|
640
|
+
this.assertOpen();
|
|
641
|
+
const entry = this.transaction(() => {
|
|
642
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(id);
|
|
643
|
+
if (row === undefined)
|
|
644
|
+
throw notFound('knowledge entry', id);
|
|
645
|
+
const current = rowToEntry(row);
|
|
646
|
+
if (current.status === 'archived')
|
|
647
|
+
return current;
|
|
648
|
+
const updated = { ...current, status: 'archived', version: current.version + 1, updatedAt: nowIso() };
|
|
649
|
+
this.db.prepare(`UPDATE knowledge_entries SET status='archived', version=?, updated_at=? WHERE id=?`).run(updated.version, updated.updatedAt, id);
|
|
650
|
+
this.writeVersion(updated, 'archive');
|
|
651
|
+
this.db.prepare('DELETE FROM knowledge_fts WHERE knowledge_id = ?').run(id);
|
|
652
|
+
return updated;
|
|
653
|
+
});
|
|
654
|
+
this.syncKnowledgeDocuments(entry.knowledgeBaseId);
|
|
655
|
+
return entry;
|
|
656
|
+
}
|
|
657
|
+
async delete(id) {
|
|
658
|
+
this.assertOpen();
|
|
659
|
+
const current = await this.get(id);
|
|
660
|
+
this.transaction(() => {
|
|
661
|
+
this.db.prepare('DELETE FROM knowledge_fts WHERE knowledge_id = ?').run(id);
|
|
662
|
+
const result = this.db.prepare('DELETE FROM knowledge_entries WHERE id = ?').run(id);
|
|
663
|
+
if (result.changes === 0)
|
|
664
|
+
throw notFound('knowledge entry', id);
|
|
665
|
+
});
|
|
666
|
+
if (current !== undefined)
|
|
667
|
+
this.syncKnowledgeDocuments(current.knowledgeBaseId);
|
|
668
|
+
}
|
|
669
|
+
async propose(input, sourceKey) {
|
|
670
|
+
this.assertOpen();
|
|
671
|
+
const proposal = {
|
|
672
|
+
action: input.action,
|
|
673
|
+
...input.targetId === undefined ? {} : { targetId: input.targetId },
|
|
674
|
+
draft: normalizeDraft(input.draft),
|
|
675
|
+
reason: input.reason.trim().slice(0, 2000),
|
|
676
|
+
};
|
|
677
|
+
if (proposal.action !== 'create' && proposal.targetId === undefined) {
|
|
678
|
+
throw new Error(`${proposal.action} candidate requires targetId`);
|
|
679
|
+
}
|
|
680
|
+
const hash = contentHash(proposal.draft) + `:${proposal.action}:${proposal.targetId ?? ''}`;
|
|
681
|
+
if (sourceKey !== undefined) {
|
|
682
|
+
const existing = this.db.prepare('SELECT * FROM knowledge_candidates WHERE source_key = ? AND proposal_hash = ?').get(sourceKey, hash);
|
|
683
|
+
if (existing !== undefined)
|
|
684
|
+
return rowToCandidate(existing);
|
|
685
|
+
}
|
|
686
|
+
const candidate = {
|
|
687
|
+
...proposal,
|
|
688
|
+
id: newId(),
|
|
689
|
+
status: 'pending',
|
|
690
|
+
...sourceKey === undefined ? {} : { sourceKey },
|
|
691
|
+
createdAt: nowIso(),
|
|
692
|
+
};
|
|
693
|
+
this.db.prepare(`
|
|
694
|
+
INSERT INTO knowledge_candidates(id,action,target_id,draft_json,reason,status,source_key,proposal_hash,created_at)
|
|
695
|
+
VALUES(?,?,?,?,?,'pending',?,?,?)
|
|
696
|
+
`).run(candidate.id, candidate.action, candidate.targetId ?? null, JSON.stringify(candidate.draft), candidate.reason, sourceKey ?? null, hash, candidate.createdAt);
|
|
697
|
+
return candidate;
|
|
698
|
+
}
|
|
699
|
+
async listCandidates(status, limit) {
|
|
700
|
+
this.assertOpen();
|
|
701
|
+
return this.db.prepare('SELECT * FROM knowledge_candidates WHERE status = ? ORDER BY created_at DESC LIMIT ?')
|
|
702
|
+
.all(status, Math.max(1, Math.min(limit, 100))).map(rowToCandidate);
|
|
703
|
+
}
|
|
704
|
+
async review(id, decision) {
|
|
705
|
+
this.assertOpen();
|
|
706
|
+
const reviewed = this.transaction(() => {
|
|
707
|
+
const row = this.db.prepare('SELECT * FROM knowledge_candidates WHERE id = ?').get(id);
|
|
708
|
+
if (row === undefined)
|
|
709
|
+
throw notFound('knowledge candidate', id);
|
|
710
|
+
const candidate = rowToCandidate(row);
|
|
711
|
+
if (candidate.status !== 'pending')
|
|
712
|
+
throw conflict(`candidate ${id} was already ${candidate.status}`);
|
|
713
|
+
let draft = decision.draft === undefined ? candidate.draft : normalizeDraft(decision.draft);
|
|
714
|
+
if (decision.decision === 'approve') {
|
|
715
|
+
if (candidate.action === 'create') {
|
|
716
|
+
this.insertEntry(draft);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
if (candidate.targetId === undefined)
|
|
720
|
+
throw new Error('candidate target is missing');
|
|
721
|
+
const target = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(candidate.targetId);
|
|
722
|
+
if (target === undefined)
|
|
723
|
+
throw notFound('candidate target', candidate.targetId);
|
|
724
|
+
if (decision.draft === undefined)
|
|
725
|
+
draft = candidate.draft;
|
|
726
|
+
this.updateEntry(candidate.targetId, draft, 'update');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const status = decision.decision === 'approve' ? 'approved' : 'rejected';
|
|
730
|
+
const reviewedAt = nowIso();
|
|
731
|
+
const note = decision.note?.trim().slice(0, 2000);
|
|
732
|
+
this.db.prepare('UPDATE knowledge_candidates SET status=?, reviewed_at=?, review_note=? WHERE id=?')
|
|
733
|
+
.run(status, reviewedAt, note ?? null, id);
|
|
734
|
+
return { ...candidate, status, reviewedAt, ...note === undefined ? {} : { reviewNote: note } };
|
|
735
|
+
});
|
|
736
|
+
if (decision.decision === 'approve')
|
|
737
|
+
this.syncKnowledgeDocuments(reviewed.draft.knowledgeBaseId);
|
|
738
|
+
return reviewed;
|
|
739
|
+
}
|
|
740
|
+
async claimExtraction(sourceKey) {
|
|
741
|
+
this.assertOpen();
|
|
742
|
+
const result = this.db.prepare(`
|
|
743
|
+
INSERT INTO extraction_jobs(source_key,status,attempts,candidate_count,updated_at)
|
|
744
|
+
VALUES(?,'running',1,0,?)
|
|
745
|
+
ON CONFLICT(source_key) DO UPDATE SET
|
|
746
|
+
status='running', attempts=extraction_jobs.attempts+1, candidate_count=0,
|
|
747
|
+
last_error=NULL, updated_at=excluded.updated_at
|
|
748
|
+
WHERE extraction_jobs.status='failed' AND extraction_jobs.attempts < 3
|
|
749
|
+
`).run(sourceKey, nowIso());
|
|
750
|
+
return result.changes === 1;
|
|
751
|
+
}
|
|
752
|
+
async completeExtraction(sourceKey, candidateCount) {
|
|
753
|
+
this.assertOpen();
|
|
754
|
+
this.db.prepare(`UPDATE extraction_jobs SET status='completed', candidate_count=?, last_error=NULL, updated_at=? WHERE source_key=?`)
|
|
755
|
+
.run(candidateCount, nowIso(), sourceKey);
|
|
756
|
+
}
|
|
757
|
+
async failExtraction(sourceKey, error) {
|
|
758
|
+
this.assertOpen();
|
|
759
|
+
this.db.prepare(`UPDATE extraction_jobs SET status='failed', last_error=?, updated_at=? WHERE source_key=?`)
|
|
760
|
+
.run(error.slice(0, 4000), nowIso(), sourceKey);
|
|
761
|
+
}
|
|
762
|
+
async extractionJob(sourceKey) {
|
|
763
|
+
this.assertOpen();
|
|
764
|
+
const row = this.db.prepare('SELECT * FROM extraction_jobs WHERE source_key = ?').get(sourceKey);
|
|
765
|
+
return row === undefined ? undefined : rowToExtractionJob(row);
|
|
766
|
+
}
|
|
767
|
+
ensureBootstrapToken(token) {
|
|
768
|
+
this.assertOpen();
|
|
769
|
+
const value = token.trim();
|
|
770
|
+
if (value.length < 24)
|
|
771
|
+
throw new Error('knowledge API token must contain at least 24 characters');
|
|
772
|
+
const hash = tokenHash(value);
|
|
773
|
+
const existing = this.db.prepare('SELECT id FROM api_tokens WHERE token_hash = ?').get(hash);
|
|
774
|
+
if (existing !== undefined)
|
|
775
|
+
return;
|
|
776
|
+
this.db.prepare(`INSERT INTO api_tokens(id,name,token_hash,permissions_json,created_at) VALUES(?,?,?,?,?)`)
|
|
777
|
+
.run(newId(), 'bootstrap-admin', hash, JSON.stringify(['read', 'propose', 'write', 'admin']), nowIso());
|
|
778
|
+
}
|
|
779
|
+
authenticate(token) {
|
|
780
|
+
this.assertOpen();
|
|
781
|
+
const row = this.db.prepare('SELECT * FROM api_tokens WHERE token_hash = ? AND revoked_at IS NULL')
|
|
782
|
+
.get(tokenHash(token));
|
|
783
|
+
if (row === undefined)
|
|
784
|
+
return undefined;
|
|
785
|
+
const usedAt = nowIso();
|
|
786
|
+
this.db.prepare('UPDATE api_tokens SET last_used_at = ? WHERE id = ?').run(usedAt, String(row.id));
|
|
787
|
+
return rowToToken({ ...row, last_used_at: usedAt });
|
|
788
|
+
}
|
|
789
|
+
createApiToken(name, permissions) {
|
|
790
|
+
this.assertOpen();
|
|
791
|
+
const cleanName = name.trim();
|
|
792
|
+
if (cleanName.length === 0 || cleanName.length > 100)
|
|
793
|
+
throw new Error('token name must contain 1-100 characters');
|
|
794
|
+
const allowed = new Set(['read', 'propose', 'write', 'admin']);
|
|
795
|
+
const normalized = [...new Set(permissions)];
|
|
796
|
+
if (normalized.length === 0 || normalized.some(permission => !allowed.has(permission))) {
|
|
797
|
+
throw new Error('token permissions must contain read, propose, write, or admin');
|
|
798
|
+
}
|
|
799
|
+
const token = `dshk_${randomBytes(32).toString('base64url')}`;
|
|
800
|
+
const record = { id: newId(), name: cleanName, permissions: normalized, createdAt: nowIso() };
|
|
801
|
+
this.db.prepare(`INSERT INTO api_tokens(id,name,token_hash,permissions_json,created_at) VALUES(?,?,?,?,?)`)
|
|
802
|
+
.run(record.id, record.name, tokenHash(token), JSON.stringify(record.permissions), record.createdAt);
|
|
803
|
+
return { record, token };
|
|
804
|
+
}
|
|
805
|
+
listApiTokens() {
|
|
806
|
+
this.assertOpen();
|
|
807
|
+
return this.db.prepare('SELECT * FROM api_tokens ORDER BY created_at DESC').all().map(rowToToken);
|
|
808
|
+
}
|
|
809
|
+
revokeApiToken(id) {
|
|
810
|
+
this.assertOpen();
|
|
811
|
+
const result = this.db.prepare('UPDATE api_tokens SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL').run(nowIso(), id);
|
|
812
|
+
if (result.changes === 0)
|
|
813
|
+
throw notFound('API token', id);
|
|
814
|
+
}
|
|
815
|
+
async close() {
|
|
816
|
+
if (this.closed)
|
|
817
|
+
return;
|
|
818
|
+
this.closed = true;
|
|
819
|
+
this.db.close();
|
|
820
|
+
}
|
|
821
|
+
writeVersion(entry, changeKind) {
|
|
822
|
+
const snapshot = {
|
|
823
|
+
knowledgeBaseId: entry.knowledgeBaseId,
|
|
824
|
+
title: entry.title,
|
|
825
|
+
body: entry.body,
|
|
826
|
+
type: entry.type,
|
|
827
|
+
tags: entry.tags,
|
|
828
|
+
scope: entry.scope,
|
|
829
|
+
confidence: entry.confidence,
|
|
830
|
+
...entry.source === undefined ? {} : { source: entry.source },
|
|
831
|
+
status: entry.status,
|
|
832
|
+
};
|
|
833
|
+
this.db.prepare(`INSERT INTO knowledge_versions(id,knowledge_id,version,snapshot_json,change_kind,created_at) VALUES(?,?,?,?,?,?)`)
|
|
834
|
+
.run(newId(), entry.id, entry.version, JSON.stringify(snapshot), changeKind, nowIso());
|
|
835
|
+
}
|
|
836
|
+
upsertFts(entry) {
|
|
837
|
+
this.db.prepare('DELETE FROM knowledge_fts WHERE knowledge_id = ?').run(entry.id);
|
|
838
|
+
if (entry.status === 'active') {
|
|
839
|
+
this.db.prepare('INSERT INTO knowledge_fts(knowledge_id,title,body,tags) VALUES(?,?,?,?)')
|
|
840
|
+
.run(entry.id, entry.title, entry.body, entry.tags.join(' '));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
syncAllDocuments() {
|
|
844
|
+
const bases = this.db.prepare('SELECT id FROM knowledge_bases').all();
|
|
845
|
+
for (const base of bases)
|
|
846
|
+
this.syncKnowledgeDocuments(String(base.id));
|
|
847
|
+
}
|
|
848
|
+
syncKnowledgeDocuments(knowledgeBaseId) {
|
|
849
|
+
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
850
|
+
if (baseRow === undefined)
|
|
851
|
+
return;
|
|
852
|
+
const base = rowToKnowledgeBase(baseRow);
|
|
853
|
+
const entries = this.db.prepare(`
|
|
854
|
+
SELECT ${ENTRY_COLUMNS} FROM knowledge_entries
|
|
855
|
+
WHERE knowledge_base_id=? AND status='active'
|
|
856
|
+
ORDER BY type, updated_at DESC, id
|
|
857
|
+
`).all(knowledgeBaseId).map(rowToEntry);
|
|
858
|
+
const desired = new Map();
|
|
859
|
+
desired.set('README.md', {
|
|
860
|
+
title: base.name,
|
|
861
|
+
content: renderKnowledgeBaseReadme(base, entries.length),
|
|
862
|
+
entryCount: entries.length,
|
|
863
|
+
});
|
|
864
|
+
for (const [type, meta] of Object.entries(DOCUMENT_TYPES)) {
|
|
865
|
+
const typed = entries.filter(entry => entry.type === type);
|
|
866
|
+
if (typed.length === 0)
|
|
867
|
+
continue;
|
|
868
|
+
desired.set(meta.relPath, {
|
|
869
|
+
title: meta.title,
|
|
870
|
+
content: renderEntryDocument(base, meta.title, typed),
|
|
871
|
+
entryCount: typed.length,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
const timestamp = nowIso();
|
|
875
|
+
const existing = this.db.prepare('SELECT id,rel_path,created_at FROM knowledge_documents WHERE knowledge_base_id=?')
|
|
876
|
+
.all(knowledgeBaseId);
|
|
877
|
+
const byPath = new Map(existing.map(row => [String(row.rel_path), row]));
|
|
878
|
+
for (const [relPath, document] of desired) {
|
|
879
|
+
const previous = byPath.get(relPath);
|
|
880
|
+
const hash = createHash('sha256').update(document.content).digest('hex');
|
|
881
|
+
this.db.prepare(`
|
|
882
|
+
INSERT INTO knowledge_documents(
|
|
883
|
+
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,created_at,updated_at
|
|
884
|
+
) VALUES(?,?,?,?,?,?,?,?,?)
|
|
885
|
+
ON CONFLICT(knowledge_base_id,rel_path) DO UPDATE SET
|
|
886
|
+
title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
|
|
887
|
+
content_hash=excluded.content_hash,updated_at=excluded.updated_at
|
|
888
|
+
WHERE knowledge_documents.content_hash<>excluded.content_hash
|
|
889
|
+
OR knowledge_documents.title<>excluded.title
|
|
890
|
+
OR knowledge_documents.entry_count<>excluded.entry_count
|
|
891
|
+
`).run(previous === undefined ? newId() : String(previous.id), knowledgeBaseId, relPath, document.title, document.content, document.entryCount, hash, previous === undefined ? timestamp : String(previous.created_at), timestamp);
|
|
892
|
+
}
|
|
893
|
+
for (const row of existing) {
|
|
894
|
+
if (!desired.has(String(row.rel_path)))
|
|
895
|
+
this.db.prepare('DELETE FROM knowledge_documents WHERE id=?').run(String(row.id));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const DOCUMENT_TYPES = {
|
|
900
|
+
preference: { relPath: 'preferences.md', title: '偏好' },
|
|
901
|
+
fact: { relPath: 'facts.md', title: '事实' },
|
|
902
|
+
decision: { relPath: 'decisions.md', title: '决策' },
|
|
903
|
+
procedure: { relPath: 'procedures.md', title: '流程' },
|
|
904
|
+
lesson: { relPath: 'lessons.md', title: '经验' },
|
|
905
|
+
};
|
|
906
|
+
function renderKnowledgeBaseReadme(base, entryCount) {
|
|
907
|
+
const lines = [`# ${markdownHeading(base.name)}`, ''];
|
|
908
|
+
if (base.description)
|
|
909
|
+
lines.push(base.description, '');
|
|
910
|
+
lines.push(`> 当前包含 ${entryCount} 条已生效知识,由 DSH Knowledge 自动整理为 Markdown 文档。`, '');
|
|
911
|
+
if (base.defaultTags.length > 0)
|
|
912
|
+
lines.push(`默认标签:${base.defaultTags.map(tag => `#${tag}`).join(' ')}`, '');
|
|
913
|
+
lines.push(`回写模型:${base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : '跟随当前会话模型'}`, '');
|
|
914
|
+
if (base.extractionInstructions)
|
|
915
|
+
lines.push('## 提取要求', '', base.extractionInstructions, '');
|
|
916
|
+
return `${lines.join('\n').trim()}\n`;
|
|
917
|
+
}
|
|
918
|
+
function renderEntryDocument(base, title, entries) {
|
|
919
|
+
const lines = [`# ${title}`, '', `> ${base.name} · ${entries.length} 条知识`, ''];
|
|
920
|
+
for (const entry of entries) {
|
|
921
|
+
lines.push(`## ${markdownHeading(entry.title)}`, '', entry.body.trim(), '');
|
|
922
|
+
const scope = entry.scope.kind === 'global' ? '全局' : `项目:${entry.scope.id}`;
|
|
923
|
+
const metadata = [scope, ...entry.tags.map(tag => `#${tag}`), `置信度 ${Math.round(entry.confidence * 100)}%`];
|
|
924
|
+
lines.push(`<small>${metadata.join(' · ')}</small>`, '');
|
|
925
|
+
}
|
|
926
|
+
return `${lines.join('\n').trim()}\n`;
|
|
927
|
+
}
|
|
928
|
+
function markdownHeading(value) {
|
|
929
|
+
return value.replace(/[\r\n]+/g, ' ').replace(/^#+\s*/, '').trim();
|
|
930
|
+
}
|
|
931
|
+
function rowToEntry(row) {
|
|
932
|
+
const source = row.source_json == null ? undefined : JSON.parse(String(row.source_json));
|
|
933
|
+
return {
|
|
934
|
+
id: String(row.id),
|
|
935
|
+
knowledgeBaseId: row.knowledge_base_id == null ? DEFAULT_KNOWLEDGE_BASE_ID : String(row.knowledge_base_id),
|
|
936
|
+
title: String(row.title),
|
|
937
|
+
body: String(row.body),
|
|
938
|
+
type: String(row.type),
|
|
939
|
+
tags: JSON.parse(String(row.tags_json)),
|
|
940
|
+
scope: String(row.scope_kind) === 'global'
|
|
941
|
+
? { kind: 'global' }
|
|
942
|
+
: { kind: 'project', id: String(row.scope_id) },
|
|
943
|
+
confidence: Number(row.confidence),
|
|
944
|
+
status: String(row.status),
|
|
945
|
+
version: Number(row.version),
|
|
946
|
+
...source === undefined ? {} : { source },
|
|
947
|
+
createdAt: String(row.created_at),
|
|
948
|
+
updatedAt: String(row.updated_at),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function rowToDocument(row) {
|
|
952
|
+
return {
|
|
953
|
+
id: String(row.id),
|
|
954
|
+
knowledgeBaseId: String(row.knowledge_base_id),
|
|
955
|
+
relPath: String(row.rel_path),
|
|
956
|
+
title: String(row.title),
|
|
957
|
+
content: String(row.content),
|
|
958
|
+
entryCount: Number(row.entry_count),
|
|
959
|
+
contentHash: String(row.content_hash),
|
|
960
|
+
createdAt: String(row.created_at),
|
|
961
|
+
updatedAt: String(row.updated_at),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function rowToVersion(row) {
|
|
965
|
+
const snapshot = JSON.parse(String(row.snapshot_json));
|
|
966
|
+
if (snapshot.knowledgeBaseId === undefined)
|
|
967
|
+
snapshot.knowledgeBaseId = DEFAULT_KNOWLEDGE_BASE_ID;
|
|
968
|
+
return {
|
|
969
|
+
id: String(row.id),
|
|
970
|
+
knowledgeId: String(row.knowledge_id),
|
|
971
|
+
version: Number(row.version),
|
|
972
|
+
snapshot,
|
|
973
|
+
changeKind: String(row.change_kind),
|
|
974
|
+
createdAt: String(row.created_at),
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function rowToCandidate(row) {
|
|
978
|
+
const targetId = row.target_id == null ? undefined : String(row.target_id);
|
|
979
|
+
const sourceKey = row.source_key == null ? undefined : String(row.source_key);
|
|
980
|
+
const reviewedAt = row.reviewed_at == null ? undefined : String(row.reviewed_at);
|
|
981
|
+
const reviewNote = row.review_note == null ? undefined : String(row.review_note);
|
|
982
|
+
const draft = JSON.parse(String(row.draft_json));
|
|
983
|
+
if (draft.knowledgeBaseId === undefined)
|
|
984
|
+
draft.knowledgeBaseId = DEFAULT_KNOWLEDGE_BASE_ID;
|
|
985
|
+
return {
|
|
986
|
+
id: String(row.id),
|
|
987
|
+
action: String(row.action),
|
|
988
|
+
...targetId === undefined ? {} : { targetId },
|
|
989
|
+
draft,
|
|
990
|
+
reason: String(row.reason),
|
|
991
|
+
status: String(row.status),
|
|
992
|
+
...sourceKey === undefined ? {} : { sourceKey },
|
|
993
|
+
createdAt: String(row.created_at),
|
|
994
|
+
...reviewedAt === undefined ? {} : { reviewedAt },
|
|
995
|
+
...reviewNote === undefined ? {} : { reviewNote },
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function rowToKnowledgeBase(row) {
|
|
999
|
+
const writebackProvider = row.writeback_provider == null ? undefined : String(row.writeback_provider);
|
|
1000
|
+
const writebackModel = row.writeback_model == null ? undefined : String(row.writeback_model);
|
|
1001
|
+
return {
|
|
1002
|
+
id: String(row.id),
|
|
1003
|
+
name: String(row.name),
|
|
1004
|
+
description: String(row.description),
|
|
1005
|
+
defaultTags: JSON.parse(String(row.default_tags_json)),
|
|
1006
|
+
extractionInstructions: String(row.extraction_instructions),
|
|
1007
|
+
...writebackProvider === undefined || writebackModel === undefined ? {} : { writebackProvider, writebackModel },
|
|
1008
|
+
status: String(row.status),
|
|
1009
|
+
createdAt: String(row.created_at),
|
|
1010
|
+
updatedAt: String(row.updated_at),
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function rowToMount(row) {
|
|
1014
|
+
return {
|
|
1015
|
+
id: String(row.id),
|
|
1016
|
+
targetKind: String(row.target_kind),
|
|
1017
|
+
targetId: String(row.target_id),
|
|
1018
|
+
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1019
|
+
enabled: Number(row.enabled) === 1,
|
|
1020
|
+
recallEnabled: Number(row.recall_enabled) === 1,
|
|
1021
|
+
writeMode: String(row.write_mode),
|
|
1022
|
+
includeTags: JSON.parse(String(row.include_tags_json)),
|
|
1023
|
+
excludeTags: JSON.parse(String(row.exclude_tags_json)),
|
|
1024
|
+
extractionInstructions: String(row.extraction_instructions),
|
|
1025
|
+
createdAt: String(row.created_at),
|
|
1026
|
+
updatedAt: String(row.updated_at),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function rowToExtractionJob(row) {
|
|
1030
|
+
const lastError = row.last_error == null ? undefined : String(row.last_error);
|
|
1031
|
+
return {
|
|
1032
|
+
sourceKey: String(row.source_key),
|
|
1033
|
+
status: String(row.status),
|
|
1034
|
+
attempts: Number(row.attempts),
|
|
1035
|
+
candidateCount: Number(row.candidate_count),
|
|
1036
|
+
...lastError === undefined ? {} : { lastError },
|
|
1037
|
+
updatedAt: String(row.updated_at),
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function rowToToken(row) {
|
|
1041
|
+
const lastUsedAt = row.last_used_at == null ? undefined : String(row.last_used_at);
|
|
1042
|
+
const revokedAt = row.revoked_at == null ? undefined : String(row.revoked_at);
|
|
1043
|
+
return {
|
|
1044
|
+
id: String(row.id),
|
|
1045
|
+
name: String(row.name),
|
|
1046
|
+
permissions: JSON.parse(String(row.permissions_json)),
|
|
1047
|
+
createdAt: String(row.created_at),
|
|
1048
|
+
...lastUsedAt === undefined ? {} : { lastUsedAt },
|
|
1049
|
+
...revokedAt === undefined ? {} : { revokedAt },
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function tokenHash(token) {
|
|
1053
|
+
return createHash('sha256').update(token).digest('hex');
|
|
1054
|
+
}
|
|
1055
|
+
function toFtsQuery(text) {
|
|
1056
|
+
const terms = text.split(/\s+/u).map(term => term.trim()).filter(Boolean).slice(0, 20);
|
|
1057
|
+
return terms.map(term => `"${term.replaceAll('"', '""')}"`).join(' OR ');
|
|
1058
|
+
}
|
|
1059
|
+
function fallbackTerms(text) {
|
|
1060
|
+
const terms = new Set();
|
|
1061
|
+
for (const word of text.toLowerCase().match(/[a-z0-9][a-z0-9_.-]{1,}/g) ?? [])
|
|
1062
|
+
terms.add(word);
|
|
1063
|
+
for (const sequence of text.match(/\p{Script=Han}+/gu) ?? []) {
|
|
1064
|
+
const chars = [...sequence];
|
|
1065
|
+
if (chars.length === 1)
|
|
1066
|
+
terms.add(chars[0]);
|
|
1067
|
+
for (let index = 0; index < chars.length - 1; index += 1) {
|
|
1068
|
+
terms.add(`${chars[index]}${chars[index + 1]}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return [...terms].slice(0, 20);
|
|
1072
|
+
}
|
|
1073
|
+
function rankToScore(rank) {
|
|
1074
|
+
return 1 / (1 + Math.max(0, rank));
|
|
1075
|
+
}
|
|
1076
|
+
function encodeCursor(updatedAt, id) {
|
|
1077
|
+
return Buffer.from(JSON.stringify({ updatedAt, id })).toString('base64url');
|
|
1078
|
+
}
|
|
1079
|
+
function decodeCursor(cursor) {
|
|
1080
|
+
try {
|
|
1081
|
+
const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
1082
|
+
if (typeof value.updatedAt !== 'string' || typeof value.id !== 'string')
|
|
1083
|
+
throw new Error();
|
|
1084
|
+
return { updatedAt: value.updatedAt, id: value.id };
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
throw Object.assign(new Error('invalid pagination cursor'), { code: 'BAD_REQUEST' });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function notFound(kind, id) {
|
|
1091
|
+
return Object.assign(new Error(`${kind} "${id}" was not found`), { code: 'NOT_FOUND' });
|
|
1092
|
+
}
|
|
1093
|
+
function conflict(message) {
|
|
1094
|
+
return Object.assign(new Error(message), { code: 'CONFLICT' });
|
|
1095
|
+
}
|
|
1096
|
+
//# sourceMappingURL=local-provider.js.map
|