dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (117) hide show
  1. package/README.md +31 -7
  2. package/cordis.patch.yml +11 -5
  3. package/lib/client.js +5557 -3056
  4. package/lib/doc-page.js +7740 -0
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +36 -9
  7. package/lib/types/convert/host.js.map +1 -1
  8. package/lib/types/diagnosis/cards.d.ts +8 -7
  9. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  10. package/lib/types/diagnosis/cards.js +119 -49
  11. package/lib/types/diagnosis/cards.js.map +1 -1
  12. package/lib/types/diagnosis/run.d.ts +51 -0
  13. package/lib/types/diagnosis/run.d.ts.map +1 -0
  14. package/lib/types/diagnosis/run.js +239 -0
  15. package/lib/types/diagnosis/run.js.map +1 -0
  16. package/lib/types/diagnosis/schema.d.ts +25 -2
  17. package/lib/types/diagnosis/schema.d.ts.map +1 -1
  18. package/lib/types/diagnosis/schema.js +64 -25
  19. package/lib/types/diagnosis/schema.js.map +1 -1
  20. package/lib/types/diagnosis/seed-source.d.ts +17 -2
  21. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  22. package/lib/types/diagnosis/seed-source.js +18 -1
  23. package/lib/types/diagnosis/seed-source.js.map +1 -1
  24. package/lib/types/diagnosis/seed.d.ts +32 -42
  25. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  26. package/lib/types/diagnosis/seed.js +187 -51
  27. package/lib/types/diagnosis/seed.js.map +1 -1
  28. package/lib/types/diagnosis/store.d.ts +114 -26
  29. package/lib/types/diagnosis/store.d.ts.map +1 -1
  30. package/lib/types/diagnosis/store.js +336 -167
  31. package/lib/types/diagnosis/store.js.map +1 -1
  32. package/lib/types/diagnosis/types.d.ts +168 -16
  33. package/lib/types/diagnosis/types.d.ts.map +1 -1
  34. package/lib/types/diagnosis/types.js +1 -1
  35. package/lib/types/diagnosis/types.js.map +1 -1
  36. package/lib/types/document-page.d.ts +39 -0
  37. package/lib/types/document-page.d.ts.map +1 -0
  38. package/lib/types/document-page.js +43 -0
  39. package/lib/types/document-page.js.map +1 -0
  40. package/lib/types/fs-atomic.d.ts +12 -0
  41. package/lib/types/fs-atomic.d.ts.map +1 -0
  42. package/lib/types/fs-atomic.js +42 -0
  43. package/lib/types/fs-atomic.js.map +1 -0
  44. package/lib/types/index.d.ts.map +1 -1
  45. package/lib/types/index.js +214 -24
  46. package/lib/types/index.js.map +1 -1
  47. package/lib/types/knowledge-bases.d.ts +10 -2
  48. package/lib/types/knowledge-bases.d.ts.map +1 -1
  49. package/lib/types/knowledge-bases.js +20 -18
  50. package/lib/types/knowledge-bases.js.map +1 -1
  51. package/lib/types/proposals.d.ts +166 -0
  52. package/lib/types/proposals.d.ts.map +1 -0
  53. package/lib/types/proposals.js +238 -0
  54. package/lib/types/proposals.js.map +1 -0
  55. package/lib/types/runtime.d.ts +199 -7
  56. package/lib/types/runtime.d.ts.map +1 -1
  57. package/lib/types/runtime.js +728 -145
  58. package/lib/types/runtime.js.map +1 -1
  59. package/lib/types/settings-schema.d.ts +2 -2
  60. package/lib/types/types.d.ts +61 -10
  61. package/lib/types/types.d.ts.map +1 -1
  62. package/lib/types/types.js +56 -8
  63. package/lib/types/types.js.map +1 -1
  64. package/lib/types/wiki-types.d.ts +8 -1
  65. package/lib/types/wiki-types.d.ts.map +1 -1
  66. package/lib/types/wiki-types.js +16 -25
  67. package/lib/types/wiki-types.js.map +1 -1
  68. package/package.json +4 -3
  69. package/preset/tita-data-agent/agent.cordis.yml +11 -5
  70. package/src/client/DirectoryPicker.tsx +12 -12
  71. package/src/client/InfoHint.tsx +21 -8
  72. package/src/client/IntentTab.tsx +20 -26
  73. package/src/client/KnowledgeBaseTab.tsx +190 -80
  74. package/src/client/KnowledgePanel.tsx +42 -9
  75. package/src/client/KnowledgeStepRail.tsx +3 -0
  76. package/src/client/NodeSettings.tsx +20 -22
  77. package/src/client/PreviewSections.tsx +87 -33
  78. package/src/client/ProposalReview.tsx +313 -0
  79. package/src/client/TypeDefinitionDrawer.tsx +72 -38
  80. package/src/client/WikiTypeEditor.tsx +193 -77
  81. package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
  82. package/src/client/arch/NodeLogicPanel.tsx +6 -3
  83. package/src/client/arch/model.ts +164 -26
  84. package/src/client/architecture.css +29 -9
  85. package/src/client/architecture.tsx +54 -29
  86. package/src/client/attribution/AttributionGraph.tsx +481 -34
  87. package/src/client/attribution/AttributionTab.tsx +507 -106
  88. package/src/client/attribution/CaseView.tsx +9 -14
  89. package/src/client/attribution/GraphStage.tsx +25 -18
  90. package/src/client/attribution/RunView.tsx +27 -9
  91. package/src/client/attribution/StageChrome.tsx +296 -140
  92. package/src/client/attribution/graph-options.ts +611 -47
  93. package/src/client/attribution/model.ts +559 -30
  94. package/src/client/convert/ConvertPanel.tsx +6 -1
  95. package/src/client/doc-page.css +134 -0
  96. package/src/client/doc-page.tsx +263 -0
  97. package/src/client/panel-state.ts +105 -0
  98. package/src/client/request.ts +36 -0
  99. package/src/client/styles.css +317 -43
  100. package/src/client/useFitScale.ts +15 -2
  101. package/src/client/wiki-type-groups.ts +86 -0
  102. package/src/convert/host.ts +41 -7
  103. package/src/diagnosis/cards.ts +120 -50
  104. package/src/diagnosis/run.ts +245 -0
  105. package/src/diagnosis/schema.ts +67 -26
  106. package/src/diagnosis/seed-source.ts +28 -3
  107. package/src/diagnosis/seed.ts +187 -85
  108. package/src/diagnosis/store.ts +358 -174
  109. package/src/diagnosis/types.ts +181 -16
  110. package/src/document-page.ts +51 -0
  111. package/src/fs-atomic.ts +42 -0
  112. package/src/index.ts +209 -22
  113. package/src/knowledge-bases.ts +20 -18
  114. package/src/proposals.ts +337 -0
  115. package/src/runtime.ts +736 -135
  116. package/src/types.ts +113 -19
  117. package/src/wiki-types.ts +17 -27
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
2
  import { createReadStream, existsSync } from 'node:fs'
3
- import { stat } from 'node:fs/promises'
4
- import { extname, isAbsolute, join, resolve, sep } from 'node:path'
3
+ import { readFile, stat } from 'node:fs/promises'
4
+ import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
5
6
  import type { Context } from '@deepseek-ai/cordis'
6
7
  import z from '@deepseek-ai/schemastery'
7
8
  import { defineTool, type PreToolDecision, type ToolRunContext } from '@deepseek-ai/dsh-tools'
@@ -13,6 +14,7 @@ import {
13
14
  } from './types.js'
14
15
  import { defaultKnowledgeRoot, KnowledgeRuntime, success, failure, type KnowledgeRuntimeOptions, type WikiSchemaProvider } from './runtime.js'
15
16
  import { unwrapGroup } from './settings-volatile.js'
17
+ import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './document-page.js'
16
18
  import type { ModelCall } from './extract-model.js'
17
19
  import {
18
20
  agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot, legacyDefaultKnowledgeRoot,
@@ -24,6 +26,7 @@ import {
24
26
  PickerError, createDirectory, listDirectory, pickDirectory, pickerKind, pickerOf,
25
27
  } from './directory-picker.js'
26
28
  import { DiagnosisStore } from './diagnosis/store.js'
29
+ import type { DiagnosisRunRecord } from './diagnosis/types.js'
27
30
  import { KNOWLEDGE_SETTINGS_SCHEMA, SETTINGS_NAMESPACE } from './settings-schema.js'
28
31
  import { DEFAULT_KB_ID, KnowledgeBases } from './knowledge-bases.js'
29
32
  import {
@@ -180,6 +183,39 @@ function clampInt(raw: string | null, fallback: number, min: number, max: number
180
183
  return Math.min(max, Math.max(min, value))
181
184
  }
182
185
 
186
+ function escapeHtml(value: string): string {
187
+ return value.replace(/[&<>'"]/g, character => ({
188
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;',
189
+ })[character] ?? character)
190
+ }
191
+
192
+ /**
193
+ * 独立整页的 HTML 外壳。
194
+ *
195
+ * 与 Notebook 的整页同一个形状,而且**只做三件事**:标题、挂载点、把自己那个 bundle
196
+ * 拉起来。正文不在外壳里 —— 页面自己去 `/knowledge/documents/:docId/content` 一页一页地取,
197
+ * 所以这份 HTML 的大小与文档大小无关。
198
+ */
199
+ function standaloneDocumentHtml(title: string): string {
200
+ return `<!doctype html>
201
+ <html lang="zh-CN">
202
+ <head>
203
+ <meta charset="utf-8">
204
+ <meta name="viewport" content="width=device-width, initial-scale=1">
205
+ <title>${escapeHtml(title)} · 原文</title>
206
+ </head>
207
+ <body>
208
+ <div id="tita-document-root"></div>
209
+ <script src="/knowledge/doc-page.js"></script>
210
+ </body>
211
+ </html>`
212
+ }
213
+
214
+ /** 独立整页那个 bundle 的位置:`lib/types/index.js` → `lib/doc-page.js`。 */
215
+ function documentPageBundle(): string {
216
+ return resolve(dirname(fileURLToPath(import.meta.url)), '../doc-page.js')
217
+ }
218
+
183
219
  /** 删除守卫的 HTTP 状态:这三类冲突必须是 409,前端据此提示用户。 */
184
220
  const CONFLICT_CODES = new Set([
185
221
  'FILE_TYPE_IN_USE', 'BASE_IS_DEFAULT', 'BASE_IN_USE',
@@ -633,9 +669,40 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
633
669
  })),
634
670
  })))
635
671
  ctx.tools.register(tool('list_source_types', 'List the SourceRaw business classifications (the per-knowledge-base source_type registry) together with the built-in read-only classifier prompt. Call this before add_source whenever the classification is not obvious; the registry is editable per knowledge base, so custom ids such as test_unit may exist.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => Promise.resolve(target.sourceTypeCatalog()))))
636
- ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields, expected source types and template card. Call this before write_knowledge_card: entity_type must be one of these ids, and the required sections/fields are what makes the card usable.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => target.wikiTypeCatalog())))
672
+ ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields, template card, and the number of card proposals awaiting human review. Call this before propose_wiki_cards / write_knowledge_card: entity_type must be one of these ids, and the required sections/fields are what makes the card usable.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => target.wikiTypeCatalog())))
637
673
  ctx.tools.register(tool('add_source', 'Add or update a SourceRaw document (原始可追溯文档). source_type is a per-knowledge-base business classification: call list_source_types first when unsure, and never guess silently. DSH requests one user approval before execution.', { source_type: { type: 'string', required: true, description: '来源业务分类 id(按业务场景,不是文件格式)。先调 list_source_types 拿注册表;拿不准就返回候选问用户,不要默认 document。' }, title: { type: 'string' }, content: { type: 'string', required: true }, external_id: { type: 'string' }, metadata: { type: 'json' }, source_id: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.addSource({ sourceType: args.source_type, title: args.title, content: args.content, externalId: args.external_id, metadata: args.metadata, sourceId: args.source_id, lineage: lineageOf(args, exec) }))))
638
- ctx.tools.register(tool('write_knowledge_card', 'Write a Wiki knowledge card with mandatory source_refs. New cards default to unreviewed. DSH requests one user approval before execution.', { entity_type: { type: 'string', required: true, description: 'Wiki 卡片类型 id:先按 list_wiki_types 的类型定义与「例如」判断,命中就直接用它;必填小节与必填字段见该类型的 schema' }, entity_id: { type: 'string', required: true }, title: { type: 'string', required: true }, content: { type: 'string', required: true }, source_refs: { type: 'array', required: true }, status: { type: 'string', enum: ['unreviewed', 'draft', 'reviewed', 'active', 'deprecated', 'problem'] }, summary: { type: 'string' }, metadata: { type: 'json' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.writeCard({ entityType: args.entity_type, entityId: args.entity_id, title: args.title, content: args.content, sourceRefs: args.source_refs, status: args.status, summary: args.summary, metadata: args.metadata, lineage: lineageOf(args, exec) }))))
674
+ /**
675
+ * `write_knowledge_card` —— **它只接受一条已确认、且尚未落卡的提议**(0021 §6.1)。
676
+ *
677
+ * 工具名与参数形状不变(冻结契约),但语义收紧了:没有 `proposal_id` 就没有写入。
678
+ * 这不是流程洁癖 —— 类型层是**收敛**的那一层,它错一条,所有现象的排序一起错;
679
+ * 而 Agent 手里只要还留着一条「未经确认也能写」的路,上面那条规则就只是文档上的一句话。
680
+ *
681
+ * 正常动线是「Agent 提提议 → 人在面板确认 → 面板直接落卡」。这个工具服务于另一条
682
+ * 更慢的路:人在面板上**只确认**(不落卡),落卡交给 Agent 这一轮来做。
683
+ */
684
+ ctx.tools.register(tool('write_knowledge_card', 'Write a Wiki knowledge card for a proposal that a HUMAN has already confirmed (status=confirmed, not yet written). Cards can no longer be written without a confirmed proposal_id: call propose_wiki_cards first, then ask the user to review it in the knowledge panel. DSH requests one user approval before execution.', { proposal_id: { type: 'string', required: true, description: '已确认、尚未落卡的提议 id。它来自 propose_wiki_cards 的返回,确认动作只能由人在面板上做' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.applyProposal(args.proposal_id, lineageOf(args, exec)))))
685
+
686
+ /**
687
+ * `propose_wiki_cards` —— 草案工具(0024 §4)。
688
+ *
689
+ * 入参是一条 `source_id`(提议挂在触发它的那条 SourceRaw 上)加一批草案。
690
+ * **它不落库**:草案停在待办队列里,等人确认。
691
+ */
692
+ ctx.tools.register(tool('propose_wiki_cards', 'Propose Wiki card drafts attached to a SourceRaw. This does NOT write any card: drafts stay in the review queue until a HUMAN confirms them in the knowledge panel. Call list_wiki_types first to pick entity_type and see the required sections/fields.', { source_id: { type: 'string', required: true, description: '触发这批提议的那条 SourceRaw 的 source_id(先 add_source)' }, proposals: { type: 'array', required: true, description: '草案数组,每项 { action?, entity_type, entity_id, title, summary?, content, source_refs?, metadata? };content 是卡片正文(小节 + ```json diagnosis 结构化块)' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async args => withRuntime(args.kb, target => target.proposeCards({ sourceId: args.source_id, proposals: (Array.isArray(args.proposals) ? args.proposals : []).map(item => ({ action: item?.action, entityType: item?.entity_type, entityId: item?.entity_id, title: item?.title, summary: item?.summary, content: item?.content, sourceRefs: item?.source_refs, metadata: item?.metadata })) }))))
693
+
694
+ /**
695
+ * `list_card_proposals` —— Agent 自己看得到待办队列。
696
+ *
697
+ * 少了它,Agent 会**反复提同一条**:它看不到「我已经提过了」「这条被人拒了」。
698
+ * 判重其实在存储层(内容指纹),但工具层给出清单,模型才有机会换一种写法。
699
+ */
700
+ ctx.tools.register(tool('list_card_proposals', 'List Wiki card proposals awaiting human review (plus the rejected ones, so you do not re-propose something a human already turned down). Proposals never land in 02_wiki until a human confirms them.', { status: { type: 'string', enum: ['pending', 'confirmed', 'rejected', 'all'], description: '默认 pending' }, kb: { type: 'string' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, async target => {
701
+ const wanted = args.status ?? 'pending'
702
+ const all = await target.cardProposals().list()
703
+ const items = all.filter(item => wanted === 'all' || item.status === wanted)
704
+ return success({ total: items.length, proposals: items.map(item => ({ id: item.id, status: item.status, trigger: item.trigger, action: item.action, entity_type: item.entityType, entity_id: item.entityId, title: item.title, source_ref: item.sourceRef, issues: item.issues, ...(item.reason ? { reason: item.reason } : {}) })) })
705
+ })))
639
706
  ctx.tools.register(tool('set_knowledge_status', 'Change a Wiki card status. problem requires problem_note and recovery must pass through unreviewed. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, status: { type: 'string', enum: ['unreviewed', 'draft', 'reviewed', 'active', 'deprecated', 'problem'], required: true }, problem_note: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.setStatus(args.entity_id, args.status as KnowledgeStatus, args.problem_note, lineageOf(args, exec)))))
640
707
  ctx.tools.register(tool('rebuild_knowledge', 'Rebuild schema/wiki/compiled knowledge indexes. DSH requests one user approval before execution.', { only: { type: 'string', enum: ['all', 'schema', 'wiki', 'embeddings'] }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.rebuild(args.only ?? 'all', lineageOf(args, exec)))))
641
708
  ctx.tools.register(tool('record_feedback', 'Record bounded feedback for a knowledge result. DSH requests one user approval before execution.', { feedback_id: { type: 'string' }, unit_id: { type: 'string' }, rating: { type: 'integer', required: true }, comment: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.recordFeedback({ feedbackId: args.feedback_id, unitId: args.unit_id, rating: args.rating, comment: args.comment, sessionKey: args.session_key, lineage: lineageOf(args, exec) }))))
@@ -663,16 +730,26 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
663
730
  * 每个知识库有自己的 `05_diagnosis/`,和它的 wiki / 编译产物同源;缓存键用
664
731
  * rootDir 而不是 baseId,因为独立 runtime(未挂到知识库列表上的)也要能读写。
665
732
  */
666
- const diagnosisStores = new Map<string, DiagnosisStore>()
667
- const storeOf = (target: KnowledgeRuntime): DiagnosisStore => {
668
- const cached = diagnosisStores.get(target.rootDir)
669
- if (cached) return cached
670
- // 卡片要经运行时的 writeCard 落盘(它同时校验 schema 与「出处必须真实存在」),
671
- // 所以 store 拿的是运行时本身,不只是 rootDir。
672
- const created = new DiagnosisStore(target)
673
- diagnosisStores.set(target.rootDir, created)
674
- return created
675
- }
733
+ const storeOf = (target: KnowledgeRuntime): DiagnosisStore => target.diagnosisStore()
734
+
735
+ /**
736
+ * `/diagnosis/graph` 与 `/diagnosis/sample` 的同一个响应体。
737
+ *
738
+ * 两条路由必须长得一样:界面拿到的要么是「这个库当前的知识」,要么是「刚载入示例之后
739
+ * 的知识」,它不该为第二种再写一条解析分支。
740
+ *
741
+ * 字段各有理由,不是把 snapshot 摊平:
742
+ * - `issues` 三层校验的结果要能直接显示,而不是只写进日志;
743
+ * - `source` / `migrated` 让界面说清「读的是卡片还是旧 jsonl」「迁移有没有生效」;
744
+ * - `canLoadSample` 是空态的开关 —— 这个库还什么都没有时才为真。
745
+ */
746
+ const diagnosisPayload = (snapshot: Awaited<ReturnType<DiagnosisStore['snapshot']>>): Record<string, unknown> => ({
747
+ graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
748
+ seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source,
749
+ canLoadSample: snapshot.canLoadSample, problems: snapshot.problems,
750
+ // 待确认的提议条数:归因这一栏只挂角标,review 在「知识库 → Wiki」的待办区。
751
+ proposals: snapshot.proposals,
752
+ })
676
753
 
677
754
  const routes = [
678
755
  { kind: 'exact' as const, path: '/knowledge/stats', handler: async (req: IncomingMessage, res: ServerResponse) => {
@@ -818,20 +895,80 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
818
895
  { kind: 'exact' as const, path: '/diagnosis/graph', handler: async (req: IncomingMessage, res: ServerResponse) => {
819
896
  const resolved = resolveBase(query(req).searchParams.get('kb'))
820
897
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
821
- const snapshot = await storeOf(resolved.runtime).snapshot()
822
- // issues 一并返回:三层校验的结果要能直接显示在界面上,而不是只写进日志。
823
- // source / migrated / problems 一并返回:界面必须能说清「现在读的是卡片还是旧 jsonl」,
824
- // 以及迁移有没有生效 —— 这类状态靠猜就会变成一句含糊的「05_diagnosis」。
825
- return send(res, 200, success({
826
- graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
827
- seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source, problems: snapshot.problems,
828
- }))
898
+ return send(res, 200, success(diagnosisPayload(await storeOf(resolved.runtime).snapshot())))
899
+ } },
900
+ /**
901
+ * 载入**内置示例**(0021 的示例内容,原来是自动写入的)。
902
+ *
903
+ * 它是一次显式的**写**:用户在这个库上还没录任何排查知识时,界面上那枚
904
+ * 「载入内置示例」按钮打的就是这里。单独一条路由而不是让读取顺手写 ——
905
+ * 读接口写用户的 `01_source_truth/` 正是上一版的毛病。
906
+ */
907
+ { kind: 'exact' as const, path: '/diagnosis/sample', handler: async (req: IncomingMessage, res: ServerResponse) => {
908
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
909
+ const resolved = resolveBase(query(req).searchParams.get('kb'))
910
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
911
+ const store = storeOf(resolved.runtime)
912
+ const { problems } = await store.loadSample()
913
+ return send(res, 200, success(diagnosisPayload(await store.snapshot()), problems))
829
914
  } },
830
915
  { kind: 'exact' as const, path: '/diagnosis/cases', handler: async (req: IncomingMessage, res: ServerResponse) => {
831
916
  const resolved = resolveBase(query(req).searchParams.get('kb'))
832
917
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
833
918
  return send(res, 200, success({ cases: (await storeOf(resolved.runtime).snapshot()).cases }))
834
919
  } },
920
+ /**
921
+ * 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并顺手**提议**一张案例卡片。
922
+ *
923
+ * 两件事一起做但性质不同:日志是实例层的原始记录(Agent 的产物,可以直接落),
924
+ * 案例卡片落在收敛的那一侧(只能提议,等人确认)。所以响应里分别给 `sourceId`
925
+ * 与 `proposalId` —— 界面要能说清「记录已经留下了,但那张卡片还在等人 review」。
926
+ */
927
+ { kind: 'exact' as const, path: '/diagnosis/run', handler: async (req: IncomingMessage, res: ServerResponse) => {
928
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
929
+ const body = await readBody(req, 2_000_000)
930
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
931
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
932
+ const record = body.record as DiagnosisRunRecord | undefined
933
+ if (!record || typeof record.runId !== 'string' || typeof record.symptomId !== 'string') {
934
+ return send(res, 400, envelopeError('RUN_RECORD_INVALID', 'record 必须是 { runId, symptomId, startedAt, durationMinutes, path, evidence, rootCause?, misleading? }'))
935
+ }
936
+ const outcome = await storeOf(resolved.runtime).recordRun(record)
937
+ if (outcome.problems.length > 0 && outcome.sourceId === undefined) {
938
+ return send(res, 200, failure({ sourceId: '', proposalId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
939
+ }
940
+ return send(res, 200, success({ sourceId: outcome.sourceId ?? '', proposalId: outcome.proposalId ?? '' }, outcome.problems))
941
+ } },
942
+ /**
943
+ * 卡片提议的待办队列(0021 §6.1)。
944
+ *
945
+ * 读接口给「知识库 → Wiki」的待办区用;写接口是**人的 review 动作**,
946
+ * 刻意不做成 Agent 工具 —— 确认权收在人手里,Agent 只有 `propose_wiki_cards`。
947
+ */
948
+ { kind: 'exact' as const, path: '/knowledge/card-proposals', handler: async (req: IncomingMessage, res: ServerResponse) => {
949
+ const url = query(req)
950
+ const resolved = resolveBase(url.searchParams.get('kb'))
951
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
952
+ const wanted = url.searchParams.get('status') ?? 'pending'
953
+ const all = await resolved.runtime.cardProposals().list()
954
+ const items = all.filter(item => wanted === 'all' || item.status === wanted)
955
+ return send(res, 200, success({ total: items.length, proposals: items }))
956
+ } },
957
+ { kind: 'exact' as const, path: '/knowledge/card-proposals/review', handler: async (req: IncomingMessage, res: ServerResponse) => {
958
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
959
+ const body = await readBody(req, 2_000_000)
960
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
961
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
962
+ const action = body.action === 'reject' ? 'reject' : body.action === 'confirm' ? 'confirm' : 'confirm-and-write'
963
+ return sendEnvelope(res, await resolved.runtime.reviewProposal({
964
+ proposalId: String(body.proposal_id ?? ''),
965
+ action,
966
+ ...(body.reviewer ? { reviewer: String(body.reviewer) } : {}),
967
+ ...(body.reason ? { reason: String(body.reason) } : {}),
968
+ ...(body.patch ? { patch: body.patch as { title?: string; summary?: string; content?: string } } : {}),
969
+ lineage: lineageOf(body),
970
+ }))
971
+ } },
835
972
  { kind: 'prefix' as const, path: '/knowledge', handler: async (req: IncomingMessage, res: ServerResponse) => {
836
973
  const method = req.method ?? 'GET'
837
974
  try {
@@ -852,6 +989,25 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
852
989
  return sendEnvelope(res, await withRuntime(body.kb, target => target.rebuild(body.only ?? 'all', lineageOf(body))))
853
990
  }
854
991
 
992
+ /**
993
+ * 独立整页的 bundle(`lib/doc-page.js`,构建产物)。
994
+ *
995
+ * 它挂在本插件的 `/knowledge` 前缀下而不是另开一条顶层路由:整页是这个插件
996
+ * 的一部分,路径上不该假装是一个独立服务。`no-cache` 是刻意的 —— 这份文件在
997
+ * 开发时会被反复重建,缓存住它等于让人对着旧页面调试。
998
+ */
999
+ if (rest.length === 1 && rest[0] === 'doc-page.js') {
1000
+ if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
1001
+ try {
1002
+ const bundle = await readFile(documentPageBundle())
1003
+ res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' })
1004
+ res.end(bundle)
1005
+ } catch {
1006
+ return send(res, 404, envelopeError('NOT_FOUND', 'doc-page.js 不存在;先构建这个插件(pnpm build)'))
1007
+ }
1008
+ return
1009
+ }
1010
+
855
1011
  // ---------------------------------------------- /knowledge/bases
856
1012
  if (rest[0] === 'bases') {
857
1013
  if (rest.length === 1) {
@@ -1025,6 +1181,12 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1025
1181
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1026
1182
  if (rest.length === 2) {
1027
1183
  if (method === 'GET') {
1184
+ /**
1185
+ * 正文在这里**按 `SOURCE_PREVIEW_CHARS` 封顶**(`readDocument` 的默认值)。
1186
+ * 抽屉只渲染前 4000 字符,而过去这个接口把整份正文发过去 —— 传输、JSON
1187
+ * 解析、进 React state 全按全文的量级付费,那就是「打开弹层要等半天」。
1188
+ * 要读全文有独立整页:`.../view`,一页一页地取。
1189
+ */
1028
1190
  const document = await resolved.runtime.readDocument(docId)
1029
1191
  if (!document) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
1030
1192
  return send(res, 200, success({ document }))
@@ -1042,6 +1204,31 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1042
1204
  if (versions.length === 0) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
1043
1205
  return send(res, 200, success({ versions }))
1044
1206
  }
1207
+ /**
1208
+ * 独立整页的**正文分页**接口:一次一段,切点对齐空行(见 `documentContentPage`)。
1209
+ *
1210
+ * 它是 `.../view` 那个页面的数据源,也是「原文全文」唯一的分发口 ——
1211
+ * 面板那条明细接口刻意不发全文。
1212
+ */
1213
+ if (rest.length === 3 && rest[2] === 'content') {
1214
+ if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
1215
+ const body = await resolved.runtime.readDocumentBody(docId)
1216
+ if (body === undefined) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
1217
+ const offset = clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
1218
+ const limit = clampInt(url.searchParams.get('limit'), DOC_PAGE_CHARS, 1, DOC_PAGE_CHARS_MAX)
1219
+ return send(res, 200, success(documentContentPage(body, offset, limit)))
1220
+ }
1221
+ /**
1222
+ * 独立整页的外壳。前端从抽屉里「新页面打开」跳过来,也可以直接粘贴这个地址。
1223
+ */
1224
+ if (rest.length === 3 && rest[2] === 'view') {
1225
+ if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
1226
+ const document = await resolved.runtime.readDocument(docId)
1227
+ if (!document) return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true))
1228
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' })
1229
+ res.end(standaloneDocumentHtml(document.title))
1230
+ return
1231
+ }
1045
1232
  if (rest.length === 3 && rest[2] === 'revert') {
1046
1233
  if (method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
1047
1234
  const body = await readBody(req)
@@ -29,12 +29,12 @@ import { homedir } from 'node:os'
29
29
  import { dirname, join, resolve } from 'node:path'
30
30
  import {
31
31
  BUILTIN_SOURCE_TYPES, defaultSourceTypes, FALLBACK_SOURCE_TYPE, isBuiltinSourceType,
32
- MAX_SOURCE_TYPES, SOURCE_TYPE_ID_PATTERN, validateSourceTypeDraft, validateSourceTypeText, WIKI_TYPE_ID_PATTERN,
32
+ LEGACY_SOURCE_TYPE_IDS, MAX_SOURCE_TYPES, SOURCE_TYPE_ID_PATTERN, validateSourceTypeDraft, validateSourceTypeText, WIKI_TYPE_ID_PATTERN,
33
33
  type Envelope, type KnowledgeBase, type KnowledgeConfig,
34
34
  type SourceTypeDef, type SourceTypeDraft, type WikiSchemaMode, type WikiTypeDef,
35
35
  } from './types.js'
36
36
  import {
37
- defaultWikiTypes, isBuiltinWikiType, parseFieldLines, parseSectionLines, templateCardFor,
37
+ defaultWikiTypes, isBuiltinWikiType, LEGACY_WIKI_TYPE_IDS, parseFieldLines, parseSectionLines, templateCardFor,
38
38
  validateWikiTypeDraft, validateWikiTypeText, type WikiTypeDraft,
39
39
  } from './wiki-types.js'
40
40
  import { defaultKnowledgeRoot, failure, KnowledgeRuntime, success } from './runtime.js'
@@ -592,7 +592,6 @@ export class KnowledgeBases {
592
592
  layer: patch.layer ?? existing.layer,
593
593
  sections: patch.sections ?? existing.sections,
594
594
  fields: patch.fields ?? existing.fields,
595
- sourceTypes: patch.sourceTypes ?? existing.sourceTypes,
596
595
  examples: patch.examples ?? existing.examples,
597
596
  }
598
597
  const problem = validateWikiTypeText(merged)
@@ -606,7 +605,6 @@ export class KnowledgeBases {
606
605
  layer: merged.layer === 'instance' ? 'instance' : 'type',
607
606
  sections,
608
607
  fields,
609
- sourceTypes: this.stringList(merged.sourceTypes),
610
608
  examples: this.stringList(merged.examples),
611
609
  template: templateCardFor(sections, fields),
612
610
  enabled: patch.enabled === undefined ? existing.enabled : patch.enabled !== false,
@@ -664,7 +662,6 @@ export class KnowledgeBases {
664
662
  layer: draft.layer === 'instance' ? 'instance' : 'type',
665
663
  sections,
666
664
  fields,
667
- sourceTypes: this.stringList(draft.sourceTypes),
668
665
  examples: this.stringList(draft.examples),
669
666
  template: templateCardFor(sections, fields),
670
667
  enabled: true,
@@ -683,15 +680,21 @@ export class KnowledgeBases {
683
680
  *
684
681
  * 与 `normalizeSourceTypes` 同一套语义:老配置(没有 `wikiTypes` 字段)升级时补齐内置,
685
682
  * 缺失的内置类型被恢复(内置只允许停用,不允许删除)。
683
+ *
684
+ * 0040 的两次改名(`check` → `check_point`、`failure-mode` → `failure_reason`)在这里
685
+ * 也认别名:存量配置里那一项写着旧 id,不认的话它会被当成一个**自定义类型**留下来,
686
+ * 而内置的那一项会被补回来 —— 于是注册表里同时有 `diagnosis/check` 与
687
+ * `diagnosis/check-point` 两个类型,卡片该归哪个就再也说不清了。
686
688
  */
687
689
  private normalizeWikiTypes(raw: unknown): WikiTypeDef[] {
688
690
  const persisted = Array.isArray(raw) ? raw : []
689
691
  const byId = new Map<string, Record<string, unknown>>()
690
692
  for (const item of persisted) {
691
693
  const value = record(item)
692
- const id = typeof value.id === 'string' ? value.id.trim() : ''
694
+ const rawId = typeof value.id === 'string' ? value.id.trim() : ''
695
+ const id = LEGACY_WIKI_TYPE_IDS[rawId] ?? rawId
693
696
  if (!id || !WIKI_TYPE_ID_PATTERN.test(id) || byId.has(id)) continue
694
- byId.set(id, value)
697
+ byId.set(id, { ...value, id })
695
698
  }
696
699
  const defaults = defaultWikiTypes()
697
700
  const builtins = defaults.map(def => {
@@ -703,7 +706,7 @@ export class KnowledgeBases {
703
706
  const customs: WikiTypeDef[] = []
704
707
  for (const [id, value] of byId) {
705
708
  if (isBuiltinWikiType(id)) continue
706
- const base: WikiTypeDef = { id, label: id, description: '', layer: 'type', sections: [], fields: [], sourceTypes: [], examples: [], template: '', enabled: true, builtin: false, cardCount: 0 }
709
+ const base: WikiTypeDef = { id, label: id, description: '', layer: 'type', sections: [], fields: [], examples: [], template: '', enabled: true, builtin: false, cardCount: 0 }
707
710
  const draft = this.wikiTypeFromRecord(base, value)
708
711
  // 只有能写出骨架的自定义类型才值得留下;否则它既没有模板也校验不了。
709
712
  if (validateWikiTypeText(draft).ok) customs.push(draft)
@@ -722,7 +725,6 @@ export class KnowledgeBases {
722
725
  layer: value.layer === 'instance' ? 'instance' : base.layer,
723
726
  sections: sections.length > 0 ? sections : base.sections.map(section => ({ ...section })),
724
727
  fields: Array.isArray(value.fields) ? fields : base.fields.map(field => ({ ...field })),
725
- sourceTypes: Array.isArray(value.sourceTypes) ? this.stringList(value.sourceTypes) : [...base.sourceTypes],
726
728
  examples: this.stringList(value.examples).length > 0 ? this.stringList(value.examples) : [...base.examples],
727
729
  template: textOr(value.template, base.template),
728
730
  enabled: value.enabled !== false,
@@ -735,7 +737,7 @@ export class KnowledgeBases {
735
737
  private wikiTypesDifferFromDefaults(types: WikiTypeDef[]): boolean {
736
738
  const signature = (list: WikiTypeDef[]): string => JSON.stringify(list.map(def => ({
737
739
  id: def.id, label: def.label, description: def.description, layer: def.layer, enabled: def.enabled,
738
- sections: def.sections, fields: def.fields, sourceTypes: def.sourceTypes, examples: def.examples,
740
+ sections: def.sections, fields: def.fields, examples: def.examples,
739
741
  })))
740
742
  const defaults = defaultWikiTypes()
741
743
  if (types.length !== defaults.length) return true
@@ -880,18 +882,22 @@ export class KnowledgeBases {
880
882
  /**
881
883
  * 把磁盘上的 `sourceTypes` 规整成一个**必定包含全部内置分类**的合法注册表。
882
884
  *
883
- * 两个关键行为:
885
+ * 三个关键行为:
884
886
  * 1. 老注册表(没有 `sourceTypes` 字段)升级时自动补上全部内置分类;
885
- * 2. 任何缺失的内置分类都会被补回来——内置分类只允许停用,不允许删除。
887
+ * 2. 任何缺失的内置分类都会被补回来——内置分类只允许停用,不允许删除;
888
+ * 3. **改名过的内置分类按别名认**(`LEGACY_SOURCE_TYPE_IDS`):存量配置里那一项写着
889
+ * 旧 id,不认的话它会被当成一个自定义分类留下来,而新的内置项同时被补回来 ——
890
+ * 于是「注册表里到底有什么」永远说不清。
886
891
  */
887
892
  private normalizeSourceTypes(raw: unknown): SourceTypeDef[] {
888
893
  const persisted = Array.isArray(raw) ? raw : []
889
894
  const byId = new Map<string, Record<string, unknown>>()
890
895
  for (const item of persisted) {
891
896
  const value = record(item)
892
- const id = typeof value.id === 'string' ? value.id.trim() : ''
897
+ const rawId = typeof value.id === 'string' ? value.id.trim() : ''
898
+ const id = LEGACY_SOURCE_TYPE_IDS[rawId] ?? rawId
893
899
  if (!id || !SOURCE_TYPE_ID_PATTERN.test(id) || byId.has(id)) continue
894
- byId.set(id, value)
900
+ byId.set(id, { ...value, id })
895
901
  }
896
902
  const defaults = defaultSourceTypes()
897
903
  const builtins = defaults.map(def => {
@@ -1065,7 +1071,6 @@ export class KnowledgeBases {
1065
1071
  layer: value.layer === 'instance' ? 'instance' : previous.layer,
1066
1072
  sections: Array.isArray(value.sections) ? value.sections : previous.sections,
1067
1073
  fields: Array.isArray(value.fields) ? value.fields : previous.fields,
1068
- sourceTypes: Array.isArray(value.sourceTypes) ? value.sourceTypes : previous.sourceTypes,
1069
1074
  examples: Array.isArray(value.examples) ? value.examples : previous.examples,
1070
1075
  }
1071
1076
  const problem = validateWikiTypeText(draft)
@@ -1079,7 +1084,6 @@ export class KnowledgeBases {
1079
1084
  layer: draft.layer === 'instance' ? 'instance' : 'type',
1080
1085
  sections,
1081
1086
  fields,
1082
- sourceTypes: this.stringList(draft.sourceTypes),
1083
1087
  examples: this.stringList(draft.examples),
1084
1088
  template: templateCardFor(sections, fields),
1085
1089
  enabled: value.enabled === undefined ? previous.enabled : value.enabled !== false,
@@ -1098,7 +1102,6 @@ export class KnowledgeBases {
1098
1102
  layer: value.layer === 'instance' ? 'instance' : previous?.layer ?? 'type',
1099
1103
  sections: Array.isArray(value.sections) ? value.sections : previous?.sections ?? [],
1100
1104
  fields: Array.isArray(value.fields) ? value.fields : previous?.fields ?? [],
1101
- sourceTypes: Array.isArray(value.sourceTypes) ? value.sourceTypes : previous?.sourceTypes ?? [],
1102
1105
  examples: Array.isArray(value.examples) ? value.examples : previous?.examples ?? [],
1103
1106
  }
1104
1107
  const problem = previous ? validateWikiTypeText(draft) : validateWikiTypeDraft(draft, new Set(existing.map(def => def.id)))
@@ -1112,7 +1115,6 @@ export class KnowledgeBases {
1112
1115
  layer: draft.layer === 'instance' ? 'instance' : 'type',
1113
1116
  sections,
1114
1117
  fields,
1115
- sourceTypes: this.stringList(draft.sourceTypes),
1116
1118
  examples: this.stringList(draft.examples),
1117
1119
  template: templateCardFor(sections, fields),
1118
1120
  enabled: value.enabled === undefined ? previous?.enabled ?? true : value.enabled !== false,