@lemoncat7/dsh-knowledge 2.9.6 → 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.
Files changed (63) hide show
  1. package/README.md +7 -1
  2. package/docs/document-groups.md +22 -0
  3. package/docs/releases/2.10.0.md +17 -0
  4. package/lib/api.d.ts.map +1 -1
  5. package/lib/api.js +22 -1
  6. package/lib/api.js.map +1 -1
  7. package/lib/client.js +33 -10
  8. package/lib/client.js.map +3 -3
  9. package/lib/document-group-tools.d.ts +6 -0
  10. package/lib/document-group-tools.d.ts.map +1 -0
  11. package/lib/document-group-tools.js +96 -0
  12. package/lib/document-group-tools.js.map +1 -0
  13. package/lib/document-groups.d.ts +8 -0
  14. package/lib/document-groups.d.ts.map +1 -0
  15. package/lib/document-groups.js +18 -0
  16. package/lib/document-groups.js.map +1 -0
  17. package/lib/documents/markdown.d.ts +1 -0
  18. package/lib/documents/markdown.d.ts.map +1 -1
  19. package/lib/documents/markdown.js +2 -0
  20. package/lib/documents/markdown.js.map +1 -1
  21. package/lib/domain.d.ts +6 -0
  22. package/lib/domain.d.ts.map +1 -1
  23. package/lib/domain.js +2 -0
  24. package/lib/domain.js.map +1 -1
  25. package/lib/extraction.d.ts.map +1 -1
  26. package/lib/extraction.js +16 -4
  27. package/lib/extraction.js.map +1 -1
  28. package/lib/knowledge-activity-panel.d.ts.map +1 -1
  29. package/lib/knowledge-activity-panel.js +23 -1
  30. package/lib/knowledge-activity-panel.js.map +1 -1
  31. package/lib/local-provider.d.ts +6 -0
  32. package/lib/local-provider.d.ts.map +1 -1
  33. package/lib/local-provider.js +94 -19
  34. package/lib/local-provider.js.map +1 -1
  35. package/lib/note-excerpt.d.ts +1 -0
  36. package/lib/note-excerpt.d.ts.map +1 -1
  37. package/lib/note-excerpt.js.map +1 -1
  38. package/lib/provider.d.ts +3 -0
  39. package/lib/provider.d.ts.map +1 -1
  40. package/lib/remote-provider.d.ts +2 -0
  41. package/lib/remote-provider.d.ts.map +1 -1
  42. package/lib/remote-provider.js +6 -0
  43. package/lib/remote-provider.js.map +1 -1
  44. package/lib/retrieval.d.ts +1 -0
  45. package/lib/retrieval.d.ts.map +1 -1
  46. package/lib/retrieval.js +2 -1
  47. package/lib/retrieval.js.map +1 -1
  48. package/lib/storage/migrations.d.ts.map +1 -1
  49. package/lib/storage/migrations.js +18 -1
  50. package/lib/storage/migrations.js.map +1 -1
  51. package/lib/tools.d.ts.map +1 -1
  52. package/lib/tools.js +3 -0
  53. package/lib/tools.js.map +1 -1
  54. package/lib/tracking.d.ts.map +1 -1
  55. package/lib/tracking.js +5 -0
  56. package/lib/tracking.js.map +1 -1
  57. package/lib/web.js +1 -1
  58. package/lib/web.js.map +1 -1
  59. package/package.json +1 -1
  60. package/web/app.js +88 -12
  61. package/web/document-groups.js +99 -0
  62. package/web/note-excerpt.js +13 -3
  63. package/web/styles.css +24 -0
@@ -13,13 +13,14 @@ import { enqueueDocumentProjection } from './documents/projection-queue.js';
13
13
  import { applyKnowledgeTextEdits, mergeKnowledgeBodies } from './knowledge-merge.js';
14
14
  import { normalizeFinalizationChange } from './document-lifecycle.js';
15
15
  import { NoteStore } from './notes/store.js';
16
+ import { normalizeDocumentGroup, matchDocumentGroup } from './document-groups.js';
16
17
  const ENTRY_COLUMNS = `
17
18
  id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
18
- 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
19
20
  `;
20
21
  const JOINED_ENTRY_COLUMNS = `
21
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,
22
- 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,
23
24
  e.confidence AS confidence, e.status AS status, e.document_state AS document_state,
24
25
  e.finalized_at AS finalized_at, e.finalization_note AS finalization_note, e.version AS version,
25
26
  e.source_json AS source_json, e.created_at AS created_at, e.updated_at AS updated_at
@@ -297,7 +298,7 @@ export class LocalKnowledgeProvider {
297
298
  const rows = this.db.prepare(`
298
299
  WITH document_index AS (
299
300
  SELECT
300
- 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,
301
302
  document_state, finalized_at, finalization_note, created_at, updated_at,
302
303
  CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END AS sort_rank
303
304
  FROM knowledge_documents${sourceWhere}
@@ -325,6 +326,50 @@ export class LocalKnowledgeProvider {
325
326
  const row = this.db.prepare('SELECT * FROM knowledge_documents WHERE id=?').get(id);
326
327
  return row === undefined ? undefined : rowToDocument(row);
327
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
+ }
328
373
  async listMounts(targetKind, targetId) {
329
374
  this.assertOpen();
330
375
  const where = [];
@@ -654,7 +699,7 @@ export class LocalKnowledgeProvider {
654
699
  result = this.updateEntry(current.id, { ...current, body: `${current.body.trimEnd()}\n\n${body}`.trim() }, 'update');
655
700
  }
656
701
  else {
657
- result = this.insertEntry({ knowledgeBaseId: input.knowledgeBaseId, title: input.title?.trim() || note.name.replace(/\.md$/i, ''), body, type: 'fact', tags: [], scope: { kind: 'global' }, confidence: 0.8 });
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 });
658
703
  }
659
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());
660
705
  this.db.prepare('INSERT INTO note_excerpt_receipts(request_id,input_hash,knowledge_id) VALUES(?,?,?)').run(input.requestId, hash, result.id);
@@ -665,6 +710,7 @@ export class LocalKnowledgeProvider {
665
710
  }
666
711
  insertEntry(input) {
667
712
  const draft = normalizeDraft(input);
713
+ draft.group = this.resolveDocumentGroup(draft.knowledgeBaseId, normalizeDocumentGroup(input.group, true));
668
714
  if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
669
715
  throw notFound('active knowledge base', draft.knowledgeBaseId);
670
716
  }
@@ -677,9 +723,9 @@ export class LocalKnowledgeProvider {
677
723
  this.db.prepare(`
678
724
  INSERT INTO knowledge_entries (
679
725
  id,knowledge_base_id,title,body,type,tags_json,scope_kind,scope_id,confidence,status,
680
- document_state,finalized_at,finalization_note,version,content_hash,source_json,created_at,updated_at
681
- ) VALUES (?,?,?,?,?,?,?,?,?,?,'open',NULL,NULL,?,?,?,?,?)
682
- `).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);
683
729
  this.writeVersion(entry, 'create');
684
730
  this.upsertFts(entry);
685
731
  return entry;
@@ -813,6 +859,7 @@ export class LocalKnowledgeProvider {
813
859
  if (current.documentState !== 'open')
814
860
  throw finalizedConflict(current);
815
861
  const draft = normalizeDraft(input);
862
+ draft.group = input.group === undefined ? current.group ?? '' : this.resolveDocumentGroup(draft.knowledgeBaseId, normalizeDocumentGroup(input.group));
816
863
  if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
817
864
  throw notFound('active knowledge base', draft.knowledgeBaseId);
818
865
  }
@@ -829,9 +876,9 @@ export class LocalKnowledgeProvider {
829
876
  this.db.prepare(`
830
877
  UPDATE knowledge_entries SET
831
878
  knowledge_base_id=?,title=?,body=?,type=?,tags_json=?,scope_kind=?,scope_id=?,confidence=?,status='active',
832
- version=?,content_hash=?,source_json=?,updated_at=?
879
+ version=?,content_hash=?,source_json=?,updated_at=?,document_group=?
833
880
  WHERE id=?
834
- `).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);
835
882
  this.writeVersion(entry, changeKind);
836
883
  this.upsertFts(entry);
837
884
  return entry;
@@ -1091,9 +1138,11 @@ export class LocalKnowledgeProvider {
1091
1138
  }
1092
1139
  const entry = resolution.proposal.action === 'create'
1093
1140
  ? this.insertEntry(resolution.proposal.draft)
1094
- : resolution.proposal.change?.kind === 'finalize'
1095
- ? this.finalizeEntry(resolution.proposal.targetId, resolution.proposal.change.state, resolution.proposal.change.note)
1096
- : this.updateEntry(resolution.proposal.targetId, resolution.proposal.draft, 'update');
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');
1097
1146
  const reviewedAt = nowIso();
1098
1147
  this.db.prepare('UPDATE knowledge_candidates SET status=?, reviewed_at=?, review_note=? WHERE id=?')
1099
1148
  .run('approved', reviewedAt, resolution.outcome === 'merged'
@@ -1145,7 +1194,7 @@ export class LocalKnowledgeProvider {
1145
1194
  return { outcome: 'conflict', proposal };
1146
1195
  if (proposal.action === 'update') {
1147
1196
  const target = this.activeEntry(proposal.targetId);
1148
- if (target.documentState !== 'open')
1197
+ if (target.documentState !== 'open' && proposal.change?.kind !== 'group')
1149
1198
  return { outcome: 'finalized', entry: target };
1150
1199
  if (target.knowledgeBaseId !== proposal.draft.knowledgeBaseId) {
1151
1200
  throw conflict('direct-write update cannot move knowledge between knowledge bases');
@@ -1158,7 +1207,7 @@ export class LocalKnowledgeProvider {
1158
1207
  return { outcome: 'conflict', proposal: { ...proposal, action: 'conflict' } };
1159
1208
  }
1160
1209
  const draft = applied.draft;
1161
- 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))
1162
1211
  return { outcome: 'duplicate', entry: target };
1163
1212
  return { outcome: 'merged', proposal: { ...proposal, action: 'update', draft } };
1164
1213
  }
@@ -1238,7 +1287,17 @@ export class LocalKnowledgeProvider {
1238
1287
  throw conflict(`candidate ${id} was already ${candidate.status}`);
1239
1288
  let draft = decision.draft === undefined ? candidate.draft : normalizeDraft(decision.draft);
1240
1289
  if (decision.decision === 'approve') {
1241
- if (candidate.change?.kind === 'finalize') {
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') {
1242
1301
  if (decision.draft !== undefined)
1243
1302
  throw conflict('结束候选不能替换正文,请拒绝后重新提交');
1244
1303
  const target = this.activeEntry(candidate.targetId);
@@ -1507,6 +1566,7 @@ export class LocalKnowledgeProvider {
1507
1566
  body: entry.body,
1508
1567
  type: entry.type,
1509
1568
  tags: entry.tags,
1569
+ group: entry.group ?? '',
1510
1570
  scope: entry.scope,
1511
1571
  confidence: entry.confidence,
1512
1572
  ...entry.source === undefined ? {} : { source: entry.source },
@@ -1591,14 +1651,14 @@ export class LocalKnowledgeProvider {
1591
1651
  this.db.prepare(`
1592
1652
  INSERT INTO knowledge_documents(
1593
1653
  id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
1594
- document_state,finalized_at,finalization_note,created_at,updated_at
1595
- ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
1654
+ document_state,finalized_at,finalization_note,created_at,updated_at,document_group
1655
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
1596
1656
  ON CONFLICT(id) DO UPDATE SET
1597
1657
  knowledge_base_id=excluded.knowledge_base_id,rel_path=excluded.rel_path,
1598
1658
  title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
1599
1659
  content_hash=excluded.content_hash,document_state=excluded.document_state,
1600
1660
  finalized_at=excluded.finalized_at,finalization_note=excluded.finalization_note,
1601
- updated_at=excluded.updated_at
1661
+ updated_at=excluded.updated_at,document_group=excluded.document_group
1602
1662
  WHERE knowledge_documents.knowledge_base_id<>excluded.knowledge_base_id
1603
1663
  OR knowledge_documents.content_hash<>excluded.content_hash
1604
1664
  OR knowledge_documents.rel_path<>excluded.rel_path
@@ -1607,7 +1667,8 @@ export class LocalKnowledgeProvider {
1607
1667
  OR knowledge_documents.document_state<>excluded.document_state
1608
1668
  OR knowledge_documents.finalized_at IS NOT excluded.finalized_at
1609
1669
  OR knowledge_documents.finalization_note IS NOT excluded.finalization_note
1610
- `).run(entry.id, entry.knowledgeBaseId, relPath, entry.title, renderEntryContent(entry), 1, projectedHash, entry.documentState, entry.finalizedAt ?? null, entry.finalizationNote ?? null, entry.createdAt, entry.updatedAt);
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 ?? '');
1611
1672
  }
1612
1673
  async syncKnowledgeDocuments(knowledgeBaseId) {
1613
1674
  const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
@@ -1660,6 +1721,7 @@ function renderEntryMarkdown(entry) {
1660
1721
  id: entry.id,
1661
1722
  type: entry.type,
1662
1723
  tags: entry.tags,
1724
+ group: entry.group ?? '',
1663
1725
  scope: entry.scope,
1664
1726
  confidence: entry.confidence,
1665
1727
  status: entry.status,
@@ -1682,6 +1744,7 @@ function rowToEntry(row) {
1682
1744
  return {
1683
1745
  id: String(row.id),
1684
1746
  knowledgeBaseId: row.knowledge_base_id == null ? DEFAULT_KNOWLEDGE_BASE_ID : String(row.knowledge_base_id),
1747
+ group: String(row.document_group ?? ''),
1685
1748
  title: String(row.title),
1686
1749
  body: String(row.body),
1687
1750
  type: String(row.type),
@@ -1704,6 +1767,7 @@ function rowToDocument(row) {
1704
1767
  return {
1705
1768
  id: String(row.id),
1706
1769
  knowledgeBaseId: String(row.knowledge_base_id),
1770
+ group: String(row.document_group ?? ''),
1707
1771
  relPath: String(row.rel_path),
1708
1772
  title: String(row.title),
1709
1773
  content: String(row.content),
@@ -1720,6 +1784,7 @@ function rowToDocumentSummary(row) {
1720
1784
  return {
1721
1785
  id: String(row.id),
1722
1786
  knowledgeBaseId: String(row.knowledge_base_id),
1787
+ group: String(row.document_group ?? ''),
1723
1788
  relPath: String(row.rel_path),
1724
1789
  title: String(row.title),
1725
1790
  entryCount: Number(row.entry_count),
@@ -1791,6 +1856,11 @@ function normalizeCandidateChange(input) {
1791
1856
  return undefined;
1792
1857
  if (input.kind === 'append')
1793
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
+ }
1794
1864
  if (input.kind === 'finalize')
1795
1865
  return normalizeFinalizationChange(input);
1796
1866
  if (input.kind !== 'revise')
@@ -1833,6 +1903,11 @@ function mergeKnowledgeDraft(current, incoming, preferIncomingTitle) {
1833
1903
  });
1834
1904
  }
1835
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
+ }
1836
1911
  if (proposal.change?.kind === 'finalize') {
1837
1912
  if (current.documentState !== 'open' || current.version !== proposal.change.baseVersion || contentHash(current) !== proposal.change.baseHash
1838
1913
  || current.knowledgeBaseId !== proposal.draft.knowledgeBaseId || contentHash(current) !== contentHash(proposal.draft)) {