@lemoncat7/dsh-knowledge 2.9.5 → 2.10.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 +8 -2
- package/docs/architecture.md +6 -0
- package/docs/document-groups.md +22 -0
- package/docs/releases/2.10.0.md +17 -0
- package/docs/releases/2.9.6.md +12 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +36 -1
- package/lib/api.js.map +1 -1
- package/lib/client.js +34 -11
- package/lib/client.js.map +3 -3
- package/lib/document-group-tools.d.ts +6 -0
- package/lib/document-group-tools.d.ts.map +1 -0
- package/lib/document-group-tools.js +96 -0
- package/lib/document-group-tools.js.map +1 -0
- package/lib/document-groups.d.ts +8 -0
- package/lib/document-groups.d.ts.map +1 -0
- package/lib/document-groups.js +18 -0
- package/lib/document-groups.js.map +1 -0
- package/lib/documents/markdown.d.ts +1 -0
- package/lib/documents/markdown.d.ts.map +1 -1
- package/lib/documents/markdown.js +2 -0
- package/lib/documents/markdown.js.map +1 -1
- package/lib/domain.d.ts +6 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +2 -0
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +16 -4
- package/lib/extraction.js.map +1 -1
- package/lib/knowledge-activity-panel.d.ts.map +1 -1
- package/lib/knowledge-activity-panel.js +23 -1
- package/lib/knowledge-activity-panel.js.map +1 -1
- package/lib/local-provider.d.ts +9 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +143 -18
- package/lib/local-provider.js.map +1 -1
- package/lib/note-excerpt.d.ts +13 -0
- package/lib/note-excerpt.d.ts.map +1 -0
- package/lib/note-excerpt.js +9 -0
- package/lib/note-excerpt.js.map +1 -0
- package/lib/provider.d.ts +3 -0
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +2 -0
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +6 -0
- package/lib/remote-provider.js.map +1 -1
- package/lib/retrieval.d.ts +1 -0
- package/lib/retrieval.d.ts.map +1 -1
- package/lib/retrieval.js +2 -1
- package/lib/retrieval.js.map +1 -1
- package/lib/storage/migrations.d.ts.map +1 -1
- package/lib/storage/migrations.js +18 -1
- package/lib/storage/migrations.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +3 -0
- package/lib/tools.js.map +1 -1
- package/lib/tracking.d.ts.map +1 -1
- package/lib/tracking.js +5 -0
- package/lib/tracking.js.map +1 -1
- package/lib/web-note-editor-chrome.d.ts +1 -0
- package/lib/web-note-editor-chrome.d.ts.map +1 -1
- package/lib/web-note-editor-chrome.js +1 -0
- package/lib/web-note-editor-chrome.js.map +1 -1
- package/lib/web-note-editor-selection.d.ts +1 -0
- package/lib/web-note-editor-selection.d.ts.map +1 -1
- package/lib/web-note-editor-selection.js +21 -1
- package/lib/web-note-editor-selection.js.map +1 -1
- package/lib/web-note-editor.d.ts +2 -0
- package/lib/web-note-editor.d.ts.map +1 -1
- package/lib/web-note-editor.js +12 -0
- package/lib/web-note-editor.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +2 -1
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +101 -12
- package/web/document-groups.js +99 -0
- package/web/note-editor.js +57 -56
- package/web/note-excerpt.js +128 -0
- package/web/styles.css +30 -0
package/lib/local-provider.js
CHANGED
|
@@ -4,6 +4,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite';
|
|
6
6
|
import { migrateKnowledgeDatabase } from './storage/migrations.js';
|
|
7
|
+
import { noteExcerptMarkdown } from './note-excerpt.js';
|
|
7
8
|
import { contentHash, DEFAULT_KNOWLEDGE_BASE_ID, newId, normalizeDraft, normalizeKnowledgeBaseDraft, normalizeKnowledgeBaseGroup, normalizeKnowledgeMountDraft, normalizeKnowledgeSettings, nowIso, } from './domain.js';
|
|
8
9
|
import { markdownHash, renderKnowledgeMarkdown } from './documents/markdown.js';
|
|
9
10
|
import { knowledgeDocumentPath } from './documents/path.js';
|
|
@@ -12,13 +13,14 @@ import { enqueueDocumentProjection } from './documents/projection-queue.js';
|
|
|
12
13
|
import { applyKnowledgeTextEdits, mergeKnowledgeBodies } from './knowledge-merge.js';
|
|
13
14
|
import { normalizeFinalizationChange } from './document-lifecycle.js';
|
|
14
15
|
import { NoteStore } from './notes/store.js';
|
|
16
|
+
import { normalizeDocumentGroup, matchDocumentGroup } from './document-groups.js';
|
|
15
17
|
const ENTRY_COLUMNS = `
|
|
16
18
|
id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
|
|
17
|
-
status, document_state, finalized_at, finalization_note, version, source_json, created_at, updated_at
|
|
19
|
+
status, document_group, document_state, finalized_at, finalization_note, version, source_json, created_at, updated_at
|
|
18
20
|
`;
|
|
19
21
|
const JOINED_ENTRY_COLUMNS = `
|
|
20
22
|
e.id AS id, e.knowledge_base_id AS knowledge_base_id, e.title AS title, e.body AS body, e.type AS type,
|
|
21
|
-
e.tags_json AS tags_json, e.scope_kind AS scope_kind, e.scope_id AS scope_id,
|
|
23
|
+
e.tags_json AS tags_json, e.document_group AS document_group, e.scope_kind AS scope_kind, e.scope_id AS scope_id,
|
|
22
24
|
e.confidence AS confidence, e.status AS status, e.document_state AS document_state,
|
|
23
25
|
e.finalized_at AS finalized_at, e.finalization_note AS finalization_note, e.version AS version,
|
|
24
26
|
e.source_json AS source_json, e.created_at AS created_at, e.updated_at AS updated_at
|
|
@@ -44,6 +46,7 @@ export class LocalKnowledgeProvider {
|
|
|
44
46
|
this.db = new DatabaseSync(path);
|
|
45
47
|
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
46
48
|
migrateKnowledgeDatabase(this.db, this.notes);
|
|
49
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS note_excerpt_receipts (request_id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, knowledge_id TEXT NOT NULL);');
|
|
47
50
|
this.db.exec('CREATE TABLE IF NOT EXISTS writeback_receipts (source_key TEXT NOT NULL, proposal_hash TEXT NOT NULL, result_json TEXT NOT NULL, PRIMARY KEY(source_key, proposal_hash));');
|
|
48
51
|
this.documentsReady = this.enqueueDocumentSync(() => this.syncAllDocuments());
|
|
49
52
|
}
|
|
@@ -295,7 +298,7 @@ export class LocalKnowledgeProvider {
|
|
|
295
298
|
const rows = this.db.prepare(`
|
|
296
299
|
WITH document_index AS (
|
|
297
300
|
SELECT
|
|
298
|
-
id, knowledge_base_id, rel_path, title, entry_count, content_hash,
|
|
301
|
+
id, knowledge_base_id, rel_path, title, entry_count, content_hash, document_group,
|
|
299
302
|
document_state, finalized_at, finalization_note, created_at, updated_at,
|
|
300
303
|
CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END AS sort_rank
|
|
301
304
|
FROM knowledge_documents${sourceWhere}
|
|
@@ -323,6 +326,50 @@ export class LocalKnowledgeProvider {
|
|
|
323
326
|
const row = this.db.prepare('SELECT * FROM knowledge_documents WHERE id=?').get(id);
|
|
324
327
|
return row === undefined ? undefined : rowToDocument(row);
|
|
325
328
|
}
|
|
329
|
+
async listDocumentGroups(knowledgeBaseId) {
|
|
330
|
+
this.assertOpen();
|
|
331
|
+
if (!await this.getKnowledgeBase(knowledgeBaseId))
|
|
332
|
+
throw notFound('knowledge base', knowledgeBaseId);
|
|
333
|
+
return this.documentGroups(knowledgeBaseId);
|
|
334
|
+
}
|
|
335
|
+
documentGroups(knowledgeBaseId) {
|
|
336
|
+
return this.db.prepare("SELECT document_group,COUNT(*) AS count FROM knowledge_entries WHERE knowledge_base_id=? AND status='active' GROUP BY document_group ORDER BY document_group COLLATE NOCASE").all(knowledgeBaseId)
|
|
337
|
+
.map(row => ({ name: String(row.document_group), count: Number(row.count) }));
|
|
338
|
+
}
|
|
339
|
+
resolveDocumentGroup(baseId, name) {
|
|
340
|
+
return matchDocumentGroup(name, this.documentGroups(baseId).map(group => group.name));
|
|
341
|
+
}
|
|
342
|
+
async assignDocumentGroup(knowledgeBaseId, ids, group) {
|
|
343
|
+
this.assertOpen();
|
|
344
|
+
await this.documentsReady;
|
|
345
|
+
const name = normalizeDocumentGroup(group);
|
|
346
|
+
if (!Array.isArray(ids) || ids.length < 1 || ids.length > 500 || ids.some(id => typeof id !== 'string'))
|
|
347
|
+
throw inputError('请选择 1 到 500 篇文档');
|
|
348
|
+
const entries = this.transaction(() => {
|
|
349
|
+
if (!this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(knowledgeBaseId))
|
|
350
|
+
throw notFound('active knowledge base', knowledgeBaseId);
|
|
351
|
+
const canonical = this.resolveDocumentGroup(knowledgeBaseId, name);
|
|
352
|
+
const selected = [...new Set(ids)].map(id => {
|
|
353
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(id);
|
|
354
|
+
if (!row || row.knowledge_base_id !== knowledgeBaseId || row.status !== 'active')
|
|
355
|
+
throw conflict('文档已移动、归档或删除,请刷新分组后重试');
|
|
356
|
+
return rowToEntry(row);
|
|
357
|
+
});
|
|
358
|
+
return selected.map(current => this.setEntryGroup(current, canonical));
|
|
359
|
+
});
|
|
360
|
+
for (const entry of entries)
|
|
361
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
362
|
+
return entries;
|
|
363
|
+
}
|
|
364
|
+
setEntryGroup(current, group) {
|
|
365
|
+
const canonical = this.resolveDocumentGroup(current.knowledgeBaseId, normalizeDocumentGroup(group));
|
|
366
|
+
if ((current.group ?? '') === canonical)
|
|
367
|
+
return current;
|
|
368
|
+
const updated = { ...current, group: canonical, version: current.version + 1, updatedAt: nowIso() };
|
|
369
|
+
this.db.prepare('UPDATE knowledge_entries SET document_group=?,version=?,updated_at=? WHERE id=?').run(canonical, updated.version, updated.updatedAt, updated.id);
|
|
370
|
+
this.writeVersion(updated, 'update');
|
|
371
|
+
return updated;
|
|
372
|
+
}
|
|
326
373
|
async listMounts(targetKind, targetId) {
|
|
327
374
|
this.assertOpen();
|
|
328
375
|
const where = [];
|
|
@@ -613,8 +660,57 @@ export class LocalKnowledgeProvider {
|
|
|
613
660
|
await this.syncKnowledgeEntryQueued(entry.id);
|
|
614
661
|
return entry;
|
|
615
662
|
}
|
|
663
|
+
/** Content, reference, and retry receipt share one SQLite transaction. */
|
|
664
|
+
async excerptNote(input) {
|
|
665
|
+
this.assertOpen();
|
|
666
|
+
await this.documentsReady;
|
|
667
|
+
if (!/^[a-zA-Z0-9_-]{16,100}$/.test(input.requestId))
|
|
668
|
+
throw inputError('无效的摘录请求标识');
|
|
669
|
+
if (!/^note_[a-f0-9]{32}$/.test(input.noteId))
|
|
670
|
+
throw inputError('无效的来源笔记');
|
|
671
|
+
if (!input.text.trim() || input.text.length > 50_000)
|
|
672
|
+
throw inputError('请选择 1 到 50000 字的笔记内容');
|
|
673
|
+
const body = noteExcerptMarkdown(input.noteId, input.text);
|
|
674
|
+
const hash = createHash('sha256').update(JSON.stringify(input)).digest('hex');
|
|
675
|
+
const entry = this.transaction(() => {
|
|
676
|
+
const receipt = this.db.prepare('SELECT input_hash,knowledge_id FROM note_excerpt_receipts WHERE request_id=?').get(input.requestId);
|
|
677
|
+
if (receipt) {
|
|
678
|
+
if (receipt.input_hash !== hash)
|
|
679
|
+
throw conflict('本次摘录请求已提交,请重新打开摘录窗口');
|
|
680
|
+
const previous = this.entriesByIds([String(receipt.knowledge_id)])[0];
|
|
681
|
+
if (!previous)
|
|
682
|
+
throw conflict('已摘录的知识文档已删除,不会重复创建');
|
|
683
|
+
return previous;
|
|
684
|
+
}
|
|
685
|
+
const note = this.notes.get(input.noteId);
|
|
686
|
+
if (!note || note.kind === 'folder')
|
|
687
|
+
throw inputError('来源笔记已删除或不可用');
|
|
688
|
+
let result;
|
|
689
|
+
if (input.documentId) {
|
|
690
|
+
const current = this.entriesByIds([input.documentId])[0];
|
|
691
|
+
if (!current || current.knowledgeBaseId !== input.knowledgeBaseId)
|
|
692
|
+
throw conflict('目标知识文档不存在或已移动');
|
|
693
|
+
if (current.status !== 'active' || current.documentState !== 'open')
|
|
694
|
+
throw conflict('目标文档已归档或定稿,请选择可编辑的文档');
|
|
695
|
+
if (current.version !== input.expectedVersion)
|
|
696
|
+
throw conflict('目标文档已更新,请重新选择后确认摘录');
|
|
697
|
+
if (!this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(current.knowledgeBaseId))
|
|
698
|
+
throw conflict('目标知识库已归档');
|
|
699
|
+
result = this.updateEntry(current.id, { ...current, body: `${current.body.trimEnd()}\n\n${body}`.trim() }, 'update');
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
result = this.insertEntry({ knowledgeBaseId: input.knowledgeBaseId, ...(input.group === undefined ? {} : { group: input.group }), title: input.title?.trim() || note.name.replace(/\.md$/i, ''), body, type: 'fact', tags: [], scope: { kind: 'global' }, confidence: 0.8 });
|
|
703
|
+
}
|
|
704
|
+
this.db.prepare('INSERT OR IGNORE INTO knowledge_note_references(knowledge_id,note_id,source,source_session_id,created_at) VALUES(?,?,?,NULL,?)').run(result.id, note.id, 'user', nowIso());
|
|
705
|
+
this.db.prepare('INSERT INTO note_excerpt_receipts(request_id,input_hash,knowledge_id) VALUES(?,?,?)').run(input.requestId, hash, result.id);
|
|
706
|
+
return result;
|
|
707
|
+
});
|
|
708
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
709
|
+
return entry;
|
|
710
|
+
}
|
|
616
711
|
insertEntry(input) {
|
|
617
712
|
const draft = normalizeDraft(input);
|
|
713
|
+
draft.group = this.resolveDocumentGroup(draft.knowledgeBaseId, normalizeDocumentGroup(input.group, true));
|
|
618
714
|
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
|
|
619
715
|
throw notFound('active knowledge base', draft.knowledgeBaseId);
|
|
620
716
|
}
|
|
@@ -627,9 +723,9 @@ export class LocalKnowledgeProvider {
|
|
|
627
723
|
this.db.prepare(`
|
|
628
724
|
INSERT INTO knowledge_entries (
|
|
629
725
|
id,knowledge_base_id,title,body,type,tags_json,scope_kind,scope_id,confidence,status,
|
|
630
|
-
document_state,finalized_at,finalization_note,version,content_hash,source_json,created_at,updated_at
|
|
631
|
-
) VALUES (?,?,?,?,?,?,?,?,?,?,'open',NULL,NULL
|
|
632
|
-
`).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);
|
|
726
|
+
document_state,finalized_at,finalization_note,version,content_hash,source_json,created_at,updated_at,document_group
|
|
727
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,'open',NULL,NULL,?,?,?,?,?,?)
|
|
728
|
+
`).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, draft.group);
|
|
633
729
|
this.writeVersion(entry, 'create');
|
|
634
730
|
this.upsertFts(entry);
|
|
635
731
|
return entry;
|
|
@@ -763,6 +859,7 @@ export class LocalKnowledgeProvider {
|
|
|
763
859
|
if (current.documentState !== 'open')
|
|
764
860
|
throw finalizedConflict(current);
|
|
765
861
|
const draft = normalizeDraft(input);
|
|
862
|
+
draft.group = input.group === undefined ? current.group ?? '' : this.resolveDocumentGroup(draft.knowledgeBaseId, normalizeDocumentGroup(input.group));
|
|
766
863
|
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
|
|
767
864
|
throw notFound('active knowledge base', draft.knowledgeBaseId);
|
|
768
865
|
}
|
|
@@ -779,9 +876,9 @@ export class LocalKnowledgeProvider {
|
|
|
779
876
|
this.db.prepare(`
|
|
780
877
|
UPDATE knowledge_entries SET
|
|
781
878
|
knowledge_base_id=?,title=?,body=?,type=?,tags_json=?,scope_kind=?,scope_id=?,confidence=?,status='active',
|
|
782
|
-
version=?,content_hash=?,source_json=?,updated_at=?
|
|
879
|
+
version=?,content_hash=?,source_json=?,updated_at=?,document_group=?
|
|
783
880
|
WHERE id=?
|
|
784
|
-
`).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);
|
|
881
|
+
`).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, draft.group, id);
|
|
785
882
|
this.writeVersion(entry, changeKind);
|
|
786
883
|
this.upsertFts(entry);
|
|
787
884
|
return entry;
|
|
@@ -1041,9 +1138,11 @@ export class LocalKnowledgeProvider {
|
|
|
1041
1138
|
}
|
|
1042
1139
|
const entry = resolution.proposal.action === 'create'
|
|
1043
1140
|
? this.insertEntry(resolution.proposal.draft)
|
|
1044
|
-
: resolution.proposal.change?.kind === '
|
|
1045
|
-
? this.
|
|
1046
|
-
:
|
|
1141
|
+
: resolution.proposal.change?.kind === 'group'
|
|
1142
|
+
? this.setEntryGroup(this.activeEntry(resolution.proposal.targetId), resolution.proposal.change.group)
|
|
1143
|
+
: resolution.proposal.change?.kind === 'finalize'
|
|
1144
|
+
? this.finalizeEntry(resolution.proposal.targetId, resolution.proposal.change.state, resolution.proposal.change.note)
|
|
1145
|
+
: this.updateEntry(resolution.proposal.targetId, resolution.proposal.draft, 'update');
|
|
1047
1146
|
const reviewedAt = nowIso();
|
|
1048
1147
|
this.db.prepare('UPDATE knowledge_candidates SET status=?, reviewed_at=?, review_note=? WHERE id=?')
|
|
1049
1148
|
.run('approved', reviewedAt, resolution.outcome === 'merged'
|
|
@@ -1095,7 +1194,7 @@ export class LocalKnowledgeProvider {
|
|
|
1095
1194
|
return { outcome: 'conflict', proposal };
|
|
1096
1195
|
if (proposal.action === 'update') {
|
|
1097
1196
|
const target = this.activeEntry(proposal.targetId);
|
|
1098
|
-
if (target.documentState !== 'open')
|
|
1197
|
+
if (target.documentState !== 'open' && proposal.change?.kind !== 'group')
|
|
1099
1198
|
return { outcome: 'finalized', entry: target };
|
|
1100
1199
|
if (target.knowledgeBaseId !== proposal.draft.knowledgeBaseId) {
|
|
1101
1200
|
throw conflict('direct-write update cannot move knowledge between knowledge bases');
|
|
@@ -1108,7 +1207,7 @@ export class LocalKnowledgeProvider {
|
|
|
1108
1207
|
return { outcome: 'conflict', proposal: { ...proposal, action: 'conflict' } };
|
|
1109
1208
|
}
|
|
1110
1209
|
const draft = applied.draft;
|
|
1111
|
-
if (proposal.change?.kind !== 'finalize' && contentHash(draft) === contentHash(target))
|
|
1210
|
+
if (proposal.change?.kind === 'group' ? (draft.group ?? '') === (target.group ?? '') : proposal.change?.kind !== 'finalize' && contentHash(draft) === contentHash(target))
|
|
1112
1211
|
return { outcome: 'duplicate', entry: target };
|
|
1113
1212
|
return { outcome: 'merged', proposal: { ...proposal, action: 'update', draft } };
|
|
1114
1213
|
}
|
|
@@ -1188,7 +1287,17 @@ export class LocalKnowledgeProvider {
|
|
|
1188
1287
|
throw conflict(`candidate ${id} was already ${candidate.status}`);
|
|
1189
1288
|
let draft = decision.draft === undefined ? candidate.draft : normalizeDraft(decision.draft);
|
|
1190
1289
|
if (decision.decision === 'approve') {
|
|
1191
|
-
if (candidate.change?.kind === '
|
|
1290
|
+
if (candidate.change?.kind === 'group') {
|
|
1291
|
+
if (decision.draft !== undefined)
|
|
1292
|
+
throw conflict('分组候选不能替换正文,请拒绝后重新提交');
|
|
1293
|
+
const target = this.activeEntry(candidate.targetId);
|
|
1294
|
+
assertExpectedReviewVersion(target, decision.expectedVersion);
|
|
1295
|
+
const applied = applyCandidateToTarget(target, candidate, false);
|
|
1296
|
+
if (!applied.ok)
|
|
1297
|
+
throw conflict(applied.reason);
|
|
1298
|
+
touchedEntryId = this.setEntryGroup(target, candidate.change.group).id;
|
|
1299
|
+
}
|
|
1300
|
+
else if (candidate.change?.kind === 'finalize') {
|
|
1192
1301
|
if (decision.draft !== undefined)
|
|
1193
1302
|
throw conflict('结束候选不能替换正文,请拒绝后重新提交');
|
|
1194
1303
|
const target = this.activeEntry(candidate.targetId);
|
|
@@ -1457,6 +1566,7 @@ export class LocalKnowledgeProvider {
|
|
|
1457
1566
|
body: entry.body,
|
|
1458
1567
|
type: entry.type,
|
|
1459
1568
|
tags: entry.tags,
|
|
1569
|
+
group: entry.group ?? '',
|
|
1460
1570
|
scope: entry.scope,
|
|
1461
1571
|
confidence: entry.confidence,
|
|
1462
1572
|
...entry.source === undefined ? {} : { source: entry.source },
|
|
@@ -1541,14 +1651,14 @@ export class LocalKnowledgeProvider {
|
|
|
1541
1651
|
this.db.prepare(`
|
|
1542
1652
|
INSERT INTO knowledge_documents(
|
|
1543
1653
|
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
|
|
1544
|
-
document_state,finalized_at,finalization_note,created_at,updated_at
|
|
1545
|
-
) VALUES(
|
|
1654
|
+
document_state,finalized_at,finalization_note,created_at,updated_at,document_group
|
|
1655
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1546
1656
|
ON CONFLICT(id) DO UPDATE SET
|
|
1547
1657
|
knowledge_base_id=excluded.knowledge_base_id,rel_path=excluded.rel_path,
|
|
1548
1658
|
title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
|
|
1549
1659
|
content_hash=excluded.content_hash,document_state=excluded.document_state,
|
|
1550
1660
|
finalized_at=excluded.finalized_at,finalization_note=excluded.finalization_note,
|
|
1551
|
-
updated_at=excluded.updated_at
|
|
1661
|
+
updated_at=excluded.updated_at,document_group=excluded.document_group
|
|
1552
1662
|
WHERE knowledge_documents.knowledge_base_id<>excluded.knowledge_base_id
|
|
1553
1663
|
OR knowledge_documents.content_hash<>excluded.content_hash
|
|
1554
1664
|
OR knowledge_documents.rel_path<>excluded.rel_path
|
|
@@ -1557,7 +1667,8 @@ export class LocalKnowledgeProvider {
|
|
|
1557
1667
|
OR knowledge_documents.document_state<>excluded.document_state
|
|
1558
1668
|
OR knowledge_documents.finalized_at IS NOT excluded.finalized_at
|
|
1559
1669
|
OR knowledge_documents.finalization_note IS NOT excluded.finalization_note
|
|
1560
|
-
|
|
1670
|
+
OR knowledge_documents.document_group<>excluded.document_group
|
|
1671
|
+
`).run(entry.id, entry.knowledgeBaseId, relPath, entry.title, renderEntryContent(entry), 1, projectedHash, entry.documentState, entry.finalizedAt ?? null, entry.finalizationNote ?? null, entry.createdAt, entry.updatedAt, entry.group ?? '');
|
|
1561
1672
|
}
|
|
1562
1673
|
async syncKnowledgeDocuments(knowledgeBaseId) {
|
|
1563
1674
|
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
@@ -1610,6 +1721,7 @@ function renderEntryMarkdown(entry) {
|
|
|
1610
1721
|
id: entry.id,
|
|
1611
1722
|
type: entry.type,
|
|
1612
1723
|
tags: entry.tags,
|
|
1724
|
+
group: entry.group ?? '',
|
|
1613
1725
|
scope: entry.scope,
|
|
1614
1726
|
confidence: entry.confidence,
|
|
1615
1727
|
status: entry.status,
|
|
@@ -1632,6 +1744,7 @@ function rowToEntry(row) {
|
|
|
1632
1744
|
return {
|
|
1633
1745
|
id: String(row.id),
|
|
1634
1746
|
knowledgeBaseId: row.knowledge_base_id == null ? DEFAULT_KNOWLEDGE_BASE_ID : String(row.knowledge_base_id),
|
|
1747
|
+
group: String(row.document_group ?? ''),
|
|
1635
1748
|
title: String(row.title),
|
|
1636
1749
|
body: String(row.body),
|
|
1637
1750
|
type: String(row.type),
|
|
@@ -1654,6 +1767,7 @@ function rowToDocument(row) {
|
|
|
1654
1767
|
return {
|
|
1655
1768
|
id: String(row.id),
|
|
1656
1769
|
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1770
|
+
group: String(row.document_group ?? ''),
|
|
1657
1771
|
relPath: String(row.rel_path),
|
|
1658
1772
|
title: String(row.title),
|
|
1659
1773
|
content: String(row.content),
|
|
@@ -1670,6 +1784,7 @@ function rowToDocumentSummary(row) {
|
|
|
1670
1784
|
return {
|
|
1671
1785
|
id: String(row.id),
|
|
1672
1786
|
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1787
|
+
group: String(row.document_group ?? ''),
|
|
1673
1788
|
relPath: String(row.rel_path),
|
|
1674
1789
|
title: String(row.title),
|
|
1675
1790
|
entryCount: Number(row.entry_count),
|
|
@@ -1741,6 +1856,11 @@ function normalizeCandidateChange(input) {
|
|
|
1741
1856
|
return undefined;
|
|
1742
1857
|
if (input.kind === 'append')
|
|
1743
1858
|
return { kind: 'append' };
|
|
1859
|
+
if (input.kind === 'group') {
|
|
1860
|
+
if (!Number.isSafeInteger(input.baseVersion) || input.baseVersion < 1)
|
|
1861
|
+
throw new Error('分组 baseVersion 必须为正整数');
|
|
1862
|
+
return { kind: 'group', baseVersion: input.baseVersion, group: normalizeDocumentGroup(input.group, true) };
|
|
1863
|
+
}
|
|
1744
1864
|
if (input.kind === 'finalize')
|
|
1745
1865
|
return normalizeFinalizationChange(input);
|
|
1746
1866
|
if (input.kind !== 'revise')
|
|
@@ -1783,6 +1903,11 @@ function mergeKnowledgeDraft(current, incoming, preferIncomingTitle) {
|
|
|
1783
1903
|
});
|
|
1784
1904
|
}
|
|
1785
1905
|
function applyCandidateToTarget(current, proposal, preferIncomingTitle) {
|
|
1906
|
+
if (proposal.change?.kind === 'group') {
|
|
1907
|
+
if (current.version !== proposal.change.baseVersion || current.knowledgeBaseId !== proposal.draft.knowledgeBaseId)
|
|
1908
|
+
return { ok: false, reason: '分组候选已过期,请重新读取文档后提交' };
|
|
1909
|
+
return { ok: true, draft: { ...current, group: proposal.change.group } };
|
|
1910
|
+
}
|
|
1786
1911
|
if (proposal.change?.kind === 'finalize') {
|
|
1787
1912
|
if (current.documentState !== 'open' || current.version !== proposal.change.baseVersion || contentHash(current) !== proposal.change.baseHash
|
|
1788
1913
|
|| current.knowledgeBaseId !== proposal.draft.knowledgeBaseId || contentHash(current) !== contentHash(proposal.draft)) {
|