dsh-data-domain-knowledge 0.1.0-alpha.1

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/cordis.patch.yml +410 -0
  4. package/eval/README.md +76 -0
  5. package/eval/evaluate-retrieval.mjs +121 -0
  6. package/eval/queries.example.jsonl +2 -0
  7. package/lib/client.js +111415 -0
  8. package/lib/types/config-schema.d.ts +35 -0
  9. package/lib/types/config-schema.d.ts.map +1 -0
  10. package/lib/types/config-schema.js +34 -0
  11. package/lib/types/config-schema.js.map +1 -0
  12. package/lib/types/convert/core-loader.d.ts +32 -0
  13. package/lib/types/convert/core-loader.d.ts.map +1 -0
  14. package/lib/types/convert/core-loader.js +60 -0
  15. package/lib/types/convert/core-loader.js.map +1 -0
  16. package/lib/types/convert/host.d.ts +88 -0
  17. package/lib/types/convert/host.d.ts.map +1 -0
  18. package/lib/types/convert/host.js +479 -0
  19. package/lib/types/convert/host.js.map +1 -0
  20. package/lib/types/convert/ingest.d.ts +125 -0
  21. package/lib/types/convert/ingest.d.ts.map +1 -0
  22. package/lib/types/convert/ingest.js +228 -0
  23. package/lib/types/convert/ingest.js.map +1 -0
  24. package/lib/types/convert/settings-schema.d.ts +18 -0
  25. package/lib/types/convert/settings-schema.d.ts.map +1 -0
  26. package/lib/types/convert/settings-schema.js +26 -0
  27. package/lib/types/convert/settings-schema.js.map +1 -0
  28. package/lib/types/convert/types.d.ts +46 -0
  29. package/lib/types/convert/types.d.ts.map +1 -0
  30. package/lib/types/convert/types.js +41 -0
  31. package/lib/types/convert/types.js.map +1 -0
  32. package/lib/types/diagnosis/cards.d.ts +71 -0
  33. package/lib/types/diagnosis/cards.d.ts.map +1 -0
  34. package/lib/types/diagnosis/cards.js +482 -0
  35. package/lib/types/diagnosis/cards.js.map +1 -0
  36. package/lib/types/diagnosis/problem-classes.d.ts +51 -0
  37. package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
  38. package/lib/types/diagnosis/problem-classes.js +66 -0
  39. package/lib/types/diagnosis/problem-classes.js.map +1 -0
  40. package/lib/types/diagnosis/schema.d.ts +65 -0
  41. package/lib/types/diagnosis/schema.d.ts.map +1 -0
  42. package/lib/types/diagnosis/schema.js +208 -0
  43. package/lib/types/diagnosis/schema.js.map +1 -0
  44. package/lib/types/diagnosis/seed-source.d.ts +20 -0
  45. package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
  46. package/lib/types/diagnosis/seed-source.js +96 -0
  47. package/lib/types/diagnosis/seed-source.js.map +1 -0
  48. package/lib/types/diagnosis/seed.d.ts +50 -0
  49. package/lib/types/diagnosis/seed.d.ts.map +1 -0
  50. package/lib/types/diagnosis/seed.js +242 -0
  51. package/lib/types/diagnosis/seed.js.map +1 -0
  52. package/lib/types/diagnosis/store.d.ts +81 -0
  53. package/lib/types/diagnosis/store.d.ts.map +1 -0
  54. package/lib/types/diagnosis/store.js +409 -0
  55. package/lib/types/diagnosis/store.js.map +1 -0
  56. package/lib/types/diagnosis/types.d.ts +124 -0
  57. package/lib/types/diagnosis/types.d.ts.map +1 -0
  58. package/lib/types/diagnosis/types.js +13 -0
  59. package/lib/types/diagnosis/types.js.map +1 -0
  60. package/lib/types/directory-picker.d.ts +94 -0
  61. package/lib/types/directory-picker.d.ts.map +1 -0
  62. package/lib/types/directory-picker.js +110 -0
  63. package/lib/types/directory-picker.js.map +1 -0
  64. package/lib/types/extract-model.d.ts +76 -0
  65. package/lib/types/extract-model.d.ts.map +1 -0
  66. package/lib/types/extract-model.js +193 -0
  67. package/lib/types/extract-model.js.map +1 -0
  68. package/lib/types/extract.d.ts +121 -0
  69. package/lib/types/extract.d.ts.map +1 -0
  70. package/lib/types/extract.js +549 -0
  71. package/lib/types/extract.js.map +1 -0
  72. package/lib/types/fts.d.ts +96 -0
  73. package/lib/types/fts.d.ts.map +1 -0
  74. package/lib/types/fts.js +184 -0
  75. package/lib/types/fts.js.map +1 -0
  76. package/lib/types/index.d.ts +39 -0
  77. package/lib/types/index.d.ts.map +1 -0
  78. package/lib/types/index.js +1112 -0
  79. package/lib/types/index.js.map +1 -0
  80. package/lib/types/intent/engine.d.ts +108 -0
  81. package/lib/types/intent/engine.d.ts.map +1 -0
  82. package/lib/types/intent/engine.js +501 -0
  83. package/lib/types/intent/engine.js.map +1 -0
  84. package/lib/types/intent/host.d.ts +88 -0
  85. package/lib/types/intent/host.d.ts.map +1 -0
  86. package/lib/types/intent/host.js +389 -0
  87. package/lib/types/intent/host.js.map +1 -0
  88. package/lib/types/intent/preset-tools.d.ts +37 -0
  89. package/lib/types/intent/preset-tools.d.ts.map +1 -0
  90. package/lib/types/intent/preset-tools.js +45 -0
  91. package/lib/types/intent/preset-tools.js.map +1 -0
  92. package/lib/types/intent/settings-schema.d.ts +59 -0
  93. package/lib/types/intent/settings-schema.d.ts.map +1 -0
  94. package/lib/types/intent/settings-schema.js +57 -0
  95. package/lib/types/intent/settings-schema.js.map +1 -0
  96. package/lib/types/intent/store.d.ts +75 -0
  97. package/lib/types/intent/store.d.ts.map +1 -0
  98. package/lib/types/intent/store.js +139 -0
  99. package/lib/types/intent/store.js.map +1 -0
  100. package/lib/types/intent/tools.d.ts +63 -0
  101. package/lib/types/intent/tools.d.ts.map +1 -0
  102. package/lib/types/intent/tools.js +108 -0
  103. package/lib/types/intent/tools.js.map +1 -0
  104. package/lib/types/intent/types.d.ts +189 -0
  105. package/lib/types/intent/types.d.ts.map +1 -0
  106. package/lib/types/intent/types.js +62 -0
  107. package/lib/types/intent/types.js.map +1 -0
  108. package/lib/types/knowledge-bases.d.ts +266 -0
  109. package/lib/types/knowledge-bases.d.ts.map +1 -0
  110. package/lib/types/knowledge-bases.js +1107 -0
  111. package/lib/types/knowledge-bases.js.map +1 -0
  112. package/lib/types/layout.d.ts +262 -0
  113. package/lib/types/layout.d.ts.map +1 -0
  114. package/lib/types/layout.js +312 -0
  115. package/lib/types/layout.js.map +1 -0
  116. package/lib/types/migrate.d.ts +110 -0
  117. package/lib/types/migrate.d.ts.map +1 -0
  118. package/lib/types/migrate.js +280 -0
  119. package/lib/types/migrate.js.map +1 -0
  120. package/lib/types/recall-rank.d.ts +158 -0
  121. package/lib/types/recall-rank.d.ts.map +1 -0
  122. package/lib/types/recall-rank.js +261 -0
  123. package/lib/types/recall-rank.js.map +1 -0
  124. package/lib/types/runtime.d.ts +568 -0
  125. package/lib/types/runtime.d.ts.map +1 -0
  126. package/lib/types/runtime.js +3045 -0
  127. package/lib/types/runtime.js.map +1 -0
  128. package/lib/types/settings-schema.d.ts +117 -0
  129. package/lib/types/settings-schema.d.ts.map +1 -0
  130. package/lib/types/settings-schema.js +92 -0
  131. package/lib/types/settings-schema.js.map +1 -0
  132. package/lib/types/settings-volatile.d.ts +102 -0
  133. package/lib/types/settings-volatile.d.ts.map +1 -0
  134. package/lib/types/settings-volatile.js +114 -0
  135. package/lib/types/settings-volatile.js.map +1 -0
  136. package/lib/types/types.d.ts +799 -0
  137. package/lib/types/types.d.ts.map +1 -0
  138. package/lib/types/types.js +387 -0
  139. package/lib/types/types.js.map +1 -0
  140. package/lib/types/wiki-types.d.ts +68 -0
  141. package/lib/types/wiki-types.d.ts.map +1 -0
  142. package/lib/types/wiki-types.js +322 -0
  143. package/lib/types/wiki-types.js.map +1 -0
  144. package/package.json +101 -0
  145. package/preset/tita-data-agent/agent.cordis.yml +376 -0
  146. package/preset/tita-data-agent/preset.yml +3 -0
  147. package/python/requirements.txt +8 -0
  148. package/scripts/doctor.mjs +70 -0
  149. package/scripts/e2e-pipeline.mjs +355 -0
  150. package/scripts/install-agent-preset.mjs +92 -0
  151. package/scripts/setup-embedding.sh +34 -0
  152. package/scripts/sync-agent-preset.mjs +188 -0
  153. package/src/client/AgentPresetCard.tsx +247 -0
  154. package/src/client/DirectoryPicker.tsx +357 -0
  155. package/src/client/InfoHint.tsx +65 -0
  156. package/src/client/IntentLogicTab.tsx +185 -0
  157. package/src/client/IntentTab.tsx +715 -0
  158. package/src/client/KnowledgeBaseTab.tsx +1454 -0
  159. package/src/client/KnowledgeGraphView.tsx +306 -0
  160. package/src/client/KnowledgePanel.tsx +381 -0
  161. package/src/client/KnowledgeStepRail.tsx +60 -0
  162. package/src/client/NodeSettings.tsx +392 -0
  163. package/src/client/PreviewSections.tsx +116 -0
  164. package/src/client/RecallPanel.tsx +399 -0
  165. package/src/client/TypeDefinitionDrawer.tsx +215 -0
  166. package/src/client/WikiFlowCard.tsx +226 -0
  167. package/src/client/WikiTypeEditor.tsx +332 -0
  168. package/src/client/agent-preset.ts +121 -0
  169. package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
  170. package/src/client/arch/NodeLogicPanel.tsx +133 -0
  171. package/src/client/arch/ProblemDetailPanel.tsx +88 -0
  172. package/src/client/arch/model.ts +691 -0
  173. package/src/client/architecture.css +191 -0
  174. package/src/client/architecture.tsx +530 -0
  175. package/src/client/attribution/AttributionGraph.tsx +104 -0
  176. package/src/client/attribution/AttributionTab.tsx +332 -0
  177. package/src/client/attribution/CaseView.tsx +102 -0
  178. package/src/client/attribution/GraphStage.tsx +87 -0
  179. package/src/client/attribution/RunView.tsx +184 -0
  180. package/src/client/attribution/StageChrome.tsx +403 -0
  181. package/src/client/attribution/ViewSwitch.tsx +107 -0
  182. package/src/client/attribution/graph-options.ts +381 -0
  183. package/src/client/attribution/model.ts +462 -0
  184. package/src/client/config-scope.ts +119 -0
  185. package/src/client/convert/ConvertPanel.tsx +594 -0
  186. package/src/client/convert/styles.css +138 -0
  187. package/src/client/convert/styles.ts +49 -0
  188. package/src/client/format.ts +39 -0
  189. package/src/client/graph/layout.ts +181 -0
  190. package/src/client/graph.css +113 -0
  191. package/src/client/index.ts +98 -0
  192. package/src/client/problem-domains.ts +114 -0
  193. package/src/client/styles.css +940 -0
  194. package/src/client/styles.ts +11 -0
  195. package/src/client/useFitScale.ts +68 -0
  196. package/src/config-schema.ts +45 -0
  197. package/src/convert/core-loader.ts +68 -0
  198. package/src/convert/host.ts +579 -0
  199. package/src/convert/ingest.ts +367 -0
  200. package/src/convert/settings-schema.ts +27 -0
  201. package/src/convert/types.ts +65 -0
  202. package/src/diagnosis/cards.ts +498 -0
  203. package/src/diagnosis/problem-classes.ts +117 -0
  204. package/src/diagnosis/schema.ts +260 -0
  205. package/src/diagnosis/seed-source.ts +107 -0
  206. package/src/diagnosis/seed.ts +291 -0
  207. package/src/diagnosis/store.ts +432 -0
  208. package/src/diagnosis/types.ts +152 -0
  209. package/src/directory-picker.ts +164 -0
  210. package/src/extract-model.ts +227 -0
  211. package/src/extract.ts +632 -0
  212. package/src/fts.ts +203 -0
  213. package/src/index.ts +1091 -0
  214. package/src/intent/README.md +63 -0
  215. package/src/intent/engine.ts +527 -0
  216. package/src/intent/host.ts +462 -0
  217. package/src/intent/preset-tools.ts +48 -0
  218. package/src/intent/settings-schema.ts +63 -0
  219. package/src/intent/store.ts +143 -0
  220. package/src/intent/tools.ts +139 -0
  221. package/src/intent/types.ts +245 -0
  222. package/src/knowledge-bases.ts +1160 -0
  223. package/src/layout.ts +350 -0
  224. package/src/migrate.ts +262 -0
  225. package/src/python/embedding_worker.py +471 -0
  226. package/src/recall-rank.ts +299 -0
  227. package/src/runtime.ts +3069 -0
  228. package/src/settings-schema.ts +94 -0
  229. package/src/settings-volatile.ts +122 -0
  230. package/src/types.ts +1140 -0
  231. package/src/wiki-types.ts +357 -0
@@ -0,0 +1,392 @@
1
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'
2
+ import { Button, Input, SelectMenu, Switch, cn } from '@tita-data-agent/ui/components'
3
+ import { AlertCircle, Check, LoaderCircle, SlidersHorizontal } from '@tita-data-agent/ui/icons'
4
+ import { DirectoryPickerField } from './DirectoryPicker.tsx'
5
+ import { InfoHint } from './InfoHint.tsx'
6
+
7
+ /**
8
+ * 方案级设置。
9
+ *
10
+ * 0025:设置**不再挂在架构节点上**。每个字段组由它真正生效的那个功能 tab 渲染:
11
+ * 意图路由在「意图 → 配置」,收录 / 存储在「知识库 → SourceRaw」,构建在「知识库 →
12
+ * 知识图谱」,召回在「知识库 → 召回」,重排在「知识库 → 重排」。
13
+ *
14
+ * 0029 把后半句推翻了:**知识库相关的设置组全部收进顶部齿轮的抽屉里**,知识库
15
+ * 各个分栏只留内容。理由是分栏的读法是「这条链路上发生了什么」,而设置是「这个
16
+ * 方案怎么跑」—— 把旋钮散在每一段的正文里,用户要改一个参数就得先想「它属于
17
+ * 哪一段」,而那个归属本来就是实现细节。现在归属只有一个:设置。
18
+ *
19
+ * 同一个 0029 里,设置**不再有保存按钮**:改动即生效。设置是几个标量,没有「一次
20
+ * 提交一组变更」的事务语义,攒着不写只会制造「我改了但它没生效」这一种失败。
21
+ *
22
+ * `KnowledgePanel` still binds the `tita-data-agent-*` namespaces, so these forms read
23
+ * and write the same Host settings as before.
24
+ */
25
+
26
+ export interface SettingsSnapshot {
27
+ status: 'loading' | 'ready' | 'unavailable'
28
+ value?: Record<string, unknown>
29
+ /**
30
+ * Raw user layer as stored. A field's **presence** here (not its value) is what
31
+ * marks it overridden, so editors can offer "恢复默认" honestly.
32
+ */
33
+ user?: unknown
34
+ writable?: boolean
35
+ mode?: 'host' | 'memory'
36
+ revision?: number
37
+ }
38
+
39
+ export interface SettingsScopeLike {
40
+ getSnapshot(): SettingsSnapshot
41
+ subscribe(listener: () => void): () => void
42
+ set(field: string, value: unknown): Promise<void>
43
+ /** Clear the user override so the field reverts to the composition default. */
44
+ unset?(field: string): Promise<void>
45
+ }
46
+
47
+ /** 设置组。`compile` 已撤(schema 类型归 Wiki 类型注册表),不再是一个设置组。 */
48
+ export type SettingsNode = 'intent' | 'storage' | 'build' | 'recall' | 'context'
49
+
50
+ interface FieldSpec {
51
+ key: string
52
+ label: string
53
+ kind: 'text' | 'number' | 'select' | 'toggle' | 'picker'
54
+ fallback: string | number | boolean
55
+ hint?: string
56
+ options?: Array<{ value: string; label: string }>
57
+ /** Comma-separated array field: shown as text, saved as `string[]`. */
58
+ list?: boolean
59
+ }
60
+
61
+ interface NodeSpec {
62
+ scope: 'intent' | 'knowledge'
63
+ title: string
64
+ description: string
65
+ fields: FieldSpec[]
66
+ }
67
+
68
+ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
69
+ intent: {
70
+ scope: 'intent',
71
+ title: '意图路由设置',
72
+ description: '控制 Agent 如何理解问题并组织任务。分类与 act 词表在「意图 → 意图配置」里编辑,这里只管下面两个数值。',
73
+ fields: [
74
+ { key: 'clarifyThreshold', label: '澄清阈值', kind: 'number', fallback: 0.65, hint: '低于它的记录标 lowConfidence,只标注不拦截' },
75
+ { key: 'ttlSeconds', label: 'Intent TTL(秒)', kind: 'number', fallback: 600 },
76
+ // 0023:`routingPromptEnabled` 与 `maxComposeDepth` 都已移除。前者是因为分类规则
77
+ // 随 `intent_record` 的工具描述一起下发、必须始终在场(没有关闭这一说);后者是
78
+ // 既不在 schema 里也没人读的死开关。真正生效却一直缺入口的 `fallbackEnabled` 补上。
79
+ { key: 'fallbackEnabled', label: 'Host 兜底记录', kind: 'toggle', fallback: true, hint: '模型漏调 intent_record 时,轮次结束补一条 source=fallback 的 unknown 记录' },
80
+ ],
81
+ },
82
+ /**
83
+ * 存储位置 —— 0029 的**第一块**,也是唯一一块「决定别的东西在哪」的设置。
84
+ *
85
+ * 它只有一个字段:Tita Agent 根路径。知识树、交付物、临时文件都从它推出来,
86
+ * 所以用户不需要理解四段结构就能用,而四段结构又必须**被看见**(见
87
+ * `LayoutPreview`)—— 否则「我的东西在哪」还是要靠猜。
88
+ */
89
+ storage: {
90
+ scope: 'knowledge',
91
+ title: '存储设置',
92
+ description: '知识树、记忆、意图记录、临时文件与家当都在这里。交付物不在这里 —— 它落在工作区的 output/<日期>/。',
93
+ fields: [
94
+ { key: 'agentRoot', label: 'Tita Agent 根路径', kind: 'picker', fallback: '', hint: '改完根路径后重新打开 Tita Data Agent' },
95
+ ],
96
+ },
97
+ // 0025:`compile` 设置组已撤。它曾经只有一个 `schemaTypes` 逗号分隔文本框,
98
+ // 而类型闸门读的一直是 Wiki 类型注册表……那个字段既不参与校验也不被生成逻辑读取,
99
+ // 是 0013 处理 `sourceAppendTypes` 时同一类假开关。schema 的入口只有一个:
100
+ // 「知识库 → Wiki → Wiki 类型」。
101
+ build: {
102
+ scope: 'knowledge',
103
+ title: '构建设置',
104
+ description: 'wikiNotes 的向量编码方式与构建所用的 Python 环境。模型不可用时会降级到关键词路。',
105
+ fields: [
106
+ { key: 'embeddingProvider', label: 'Embedding Provider', kind: 'select', fallback: 'wemm-python', options: [{ value: 'wemm-python', label: 'WeMM Python(本地)' }, { value: 'ollama', label: 'Ollama(文本)' }, { value: 'none', label: '关闭(FTS fallback)' }] },
107
+ { key: 'embeddingDevice', label: '运行设备', kind: 'select', fallback: 'auto', options: [{ value: 'auto', label: '自动(优先 MPS)' }, { value: 'mps', label: 'Apple MPS' }, { value: 'cpu', label: 'CPU' }] },
108
+ { key: 'embeddingModel', label: 'Embedding Model', kind: 'text', fallback: '', hint: '本地模型目录,或一个 HuggingFace 模型名' },
109
+ { key: 'embeddingBaseUrl', label: 'Embedding 地址', kind: 'text', fallback: 'http://127.0.0.1:11434' },
110
+ { key: 'embeddingDimension', label: '向量维度', kind: 'number', fallback: 1024 },
111
+ { key: 'embeddingBatchSize', label: 'Embedding 批大小', kind: 'number', fallback: 8 },
112
+ { key: 'embeddingTimeoutMs', label: '编码超时(毫秒)', kind: 'number', fallback: 120000 },
113
+ { key: 'python', label: 'Python', kind: 'text', fallback: '', hint: '解释器路径;留空用根路径下那个本地环境' },
114
+ ],
115
+ },
116
+ recall: {
117
+ scope: 'knowledge',
118
+ title: '知识库召回设置',
119
+ description: '两路召回(关键词 BM25 + 向量)各自超采成一个候选池,融合(RRF)后交给重排。知识图谱不再是一路,它只给候选池里的条目加权。',
120
+ fields: [
121
+ { key: 'recallTopK', label: '最终条数(hits)', kind: 'number', fallback: 10, hint: '1–50,默认 10。这是给模型的最终条数,不是召回深度' },
122
+ { key: 'recallCandidatePool', label: '候选池深度', kind: 'number', fallback: 50, hint: '1–200,默认 50。融合与重排都作用在它上面 —— 太小则重排救不回排在窗口外的那条' },
123
+ { key: 'recallMode', label: '默认召回路线', kind: 'select', fallback: 'hybrid', options: [{ value: 'hybrid', label: 'Hybrid(关键词 + 向量 + 图谱 boost)' }, { value: 'vector', label: '仅向量' }, { value: 'fts', label: '仅关键词' }, { value: 'graph', label: '同 Hybrid(图谱已不是独立一路)' }] },
124
+ { key: 'keywordThreshold', label: '关键词阈值', kind: 'number', fallback: 0.3, hint: '0–1,默认 0.3。归一化 BM25 的相对阈值:挡得住弱命中,挡不住「整批都弱」' },
125
+ { key: 'vectorThreshold', label: '向量阈值(余弦)', kind: 'number', fallback: 0.15, hint: '−1–1,默认 0.15。**绝对**阈值,弃答主要靠它' },
126
+ ],
127
+ },
128
+ context: {
129
+ scope: 'knowledge',
130
+ title: '知识库重排设置',
131
+ description: 'BERT cross-encoder 对**候选池**精排,低于阈值的一律丢弃,再取上下文条数。阈值是整条链路里唯一跨查询可比的相关性判据,所以「弃答」主要靠它。',
132
+ fields: [
133
+ { key: 'contextTopN', label: '上下文 TopN', kind: 'number', fallback: 5, hint: '1–recallTopK,默认 5。同一张 wiki 最多 2 条,其余名额留给别的卡片' },
134
+ { key: 'rerankThreshold', label: '重排阈值', kind: 'number', fallback: 0.2, hint: '−10–10,默认 0.2。原始 cross-encoder 分(与 WeKnora 同刻度):低于它的不进上下文' },
135
+ { key: 'rerankProvider', label: 'Rerank Provider', kind: 'select', fallback: 'bert-python', options: [{ value: 'bert-python', label: 'BERT Python(本地)' }, { value: 'none', label: '关闭(按融合分数)' }] },
136
+ { key: 'rerankModel', label: 'Rerank Model', kind: 'text', fallback: '', hint: '本地模型目录,或一个 HuggingFace 模型名' },
137
+ { key: 'rerankTimeoutMs', label: '重排超时(毫秒)', kind: 'number', fallback: 60000 },
138
+ ],
139
+ },
140
+ }
141
+
142
+ /** 知识库那一组设置组的**展示顺序**。抽屉按它渲染。 */
143
+ export const KNOWLEDGE_SETTINGS_ORDER: SettingsNode[] = ['storage', 'build', 'recall', 'context']
144
+
145
+ /**
146
+ * 卡片头上的「参数」入口(0034)。
147
+ *
148
+ * 0029 把设置收进抽屉之后,知识库的每一栏都留了一句「设置已移到顶部齿轮」——
149
+ * 那是一块**路牌**:它说明入口离「你会想起要改它」的那一刻太远。chip 把同一件事
150
+ * 变成一次点击,而且它**不新开一个家** —— 打开的还是那一个抽屉、那一块面板,只是
151
+ * 落到对应的分块上(`highlight` + 滚动定位)。
152
+ *
153
+ * 文案取自 `NODE_SETTINGS[target].title`,与抽屉里的分块标题**同源**:两处各写一份,
154
+ * 迟早会出现 chip 写「构建参数」而抽屉写「构建设置」。
155
+ */
156
+ export function SettingsChip({ target, onOpen }: { target: SettingsNode; onOpen: (target: SettingsNode) => void }): ReactNode {
157
+ const title = NODE_SETTINGS[target].title
158
+ return <Button
159
+ variant="ghost"
160
+ size="sm"
161
+ title={`打开设置 · ${title}`}
162
+ onClick={() => onOpen(target)}
163
+ ><SlidersHorizontal size={12} />{title}</Button>
164
+ }
165
+
166
+ /** 分块锚点:抽屉靠它把「参数」chip 的落点滚到眼前。 */
167
+ export function settingsAnchorId(node: SettingsNode): string {
168
+ return `tita-settings-${node}`
169
+ }
170
+
171
+ const UNAVAILABLE_SNAPSHOT: SettingsSnapshot = { status: 'unavailable', writable: false, mode: 'memory' }
172
+
173
+ /**
174
+ * Settings scope → snapshot。导出是因为召回 / 重排面板也要读同一份值做默认参数:
175
+ * 三个面板各订阅一次、各算一次默认值,就会出现「设置里 topK=20、召回面板还按 10 跑」。
176
+ */
177
+ export function useScope(scope: SettingsScopeLike | undefined): SettingsSnapshot {
178
+ return useSyncExternalStore(
179
+ listener => scope?.subscribe(listener) ?? (() => undefined),
180
+ () => scope?.getSnapshot() ?? UNAVAILABLE_SNAPSHOT,
181
+ () => scope?.getSnapshot() ?? UNAVAILABLE_SNAPSHOT,
182
+ )
183
+ }
184
+
185
+ function readField(spec: FieldSpec, value: Record<string, unknown> | undefined): string | boolean {
186
+ const raw = value?.[spec.key]
187
+ if (spec.kind === 'toggle') return typeof raw === 'boolean' ? raw : spec.fallback as boolean
188
+ if (Array.isArray(raw)) return raw.map(String).join(', ')
189
+ if (raw === undefined || raw === null) return String(spec.fallback)
190
+ return String(raw)
191
+ }
192
+
193
+ function writeField(spec: FieldSpec, draft: string | boolean): unknown {
194
+ if (spec.kind === 'toggle') return Boolean(draft)
195
+ if (spec.kind === 'number') return Number(draft)
196
+ if (spec.list) return String(draft).split(',').map(item => item.trim()).filter(Boolean)
197
+ return String(draft)
198
+ }
199
+
200
+ /** 只有打字型的字段需要防抖;开关与下拉点一下就是一个决定,立刻写。 */
201
+ function needsDebounce(spec: FieldSpec): boolean {
202
+ return spec.kind === 'text' || spec.kind === 'number'
203
+ }
204
+
205
+ function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean; resolved?: string }) {
206
+ return <label className="tita-settingField">
207
+ <span>{spec.label}</span>
208
+ {/* 留空 = 自动。占位符显示**解析后**的真实路径 —— 否则「自动」等于没说。 */}
209
+ <Input
210
+ type={spec.kind === 'number' ? 'number' : 'text'}
211
+ value={value}
212
+ disabled={disabled}
213
+ {...(resolved ? { placeholder: resolved } : {})}
214
+ onChange={event => onChange(event.target.value)}
215
+ />
216
+ {/* 说明跟在它解释的那个字段下面,不是攒成一堆放在面板底部 ——
217
+ 底部那行「字段名:说明;字段名:说明」是把标签和说明各念一遍。 */}
218
+ {spec.hint && <small className="tita-settingHint">{spec.hint}</small>}
219
+ {resolved && <small className="tita-settingHint">留空 = 自动:<code>{resolved}</code></small>}
220
+ </label>
221
+ }
222
+
223
+ /**
224
+ * A dropdown field. The wrapper is a `<div>`, not the `<label>` the text and
225
+ * toggle fields use: the control is a `<button>` now (Radix's trigger), and a
226
+ * `<label>` around a button makes the visible field name an ambiguous second
227
+ * name. The name travels on the trigger's `aria-label` instead, and the visible
228
+ * `<span>` stays the thing a person reads.
229
+ */
230
+ function SelectField({ spec, value, onChange, disabled }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean }) {
231
+ return <div className="tita-settingField">
232
+ <span>{spec.label}</span>
233
+ <SelectMenu
234
+ value={value}
235
+ disabled={disabled}
236
+ onValueChange={onChange}
237
+ options={spec.options ?? []}
238
+ aria-label={spec.label}
239
+ />
240
+ {spec.hint && <small className="tita-settingHint">{spec.hint}</small>}
241
+ </div>
242
+ }
243
+
244
+ /**
245
+ * 开关字段。
246
+ *
247
+ * 它现在和文本 / 下拉 / 目录选择**共用同一套结构**:标签在上、控件在中、说明在下。
248
+ *
249
+ * 原来它是「带边框和底色的整行」,另外三种是无边框的堆叠 —— 四种字段两种长相,而且
250
+ * 标签用 `<strong>`、说明塞在标签里,对齐方式也对不上。更实际的问题是那个边框:
251
+ * 小节本身已经是一张卡片,字段再包一层就是**卡片套卡片**。
252
+ */
253
+ function ToggleField({ spec, value, onChange, disabled }: { spec: FieldSpec; value: boolean; onChange: (value: boolean) => void; disabled: boolean }) {
254
+ return <label className="tita-settingField">
255
+ <span>{spec.label}</span>
256
+ <Switch checked={value} disabled={disabled} onCheckedChange={onChange} />
257
+ {spec.hint && <small className="tita-settingHint">{spec.hint}</small>}
258
+ </label>
259
+ }
260
+
261
+ /** 写入状态。`saved` 只短暂出现,`error` 留到下一次改动。 */
262
+ type SaveState = 'idle' | 'saving' | 'saved' | 'error'
263
+
264
+ export function NodeSettingsPanel({ node, intent, knowledge, extra, pickerKind = null, resolved = {}, highlight = false }: {
265
+ node: SettingsNode
266
+ intent?: SettingsScopeLike
267
+ knowledge?: SettingsScopeLike
268
+ /** 字段之后追加的内容。存储组用它放四段布局预览 —— 预览是那个字段的**结果**。 */
269
+ extra?: ReactNode
270
+ /** 宿主提供哪种目录选择交互;`null` 表示没挂目录选择器。 */
271
+ pickerKind?: string | null
272
+ /** 字段键 → 「留空时实际会用什么」。0029:家当路径空着就是跟着根路径走。 */
273
+ resolved?: Record<string, string>
274
+ /** 由卡片头的「参数」chip 定位过来的那一块:一次性强调 + 接管焦点。 */
275
+ highlight?: boolean
276
+ }): ReactNode {
277
+ const spec = NODE_SETTINGS[node]
278
+ const scope = spec.scope === 'intent' ? intent : knowledge
279
+ const state = useScope(scope)
280
+ const [draft, setDraft] = useState<Record<string, string | boolean>>({})
281
+ const [saveState, setSaveState] = useState<SaveState>('idle')
282
+ const [error, setError] = useState<string | null>(null)
283
+ /** 待落地的防抖写入,按字段键索引。 */
284
+ const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>())
285
+ /** 正在进行的写入数。用它挡住「写完 → revision 变 → 重置草稿」把输入抹掉。 */
286
+ const inflight = useRef(0)
287
+ const savedTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
288
+
289
+ useEffect(() => {
290
+ // 有写入在飞的时候不重置草稿:那一刻 `state.value` 还是旧值,重置会把用户刚
291
+ // 敲进去的字符弹回去。写入落地后 revision 会再变一次,那次才重置。
292
+ if (inflight.current > 0) return
293
+ setDraft(Object.fromEntries(spec.fields.map(field => [field.key, readField(field, state.value)])))
294
+ }, [node, state.revision, state.value])
295
+
296
+ // 卸载时把还没落地的防抖写入丢掉并清计时器:一个已经离开的面板不该再写设置。
297
+ useEffect(() => () => {
298
+ for (const timer of timers.current.values()) clearTimeout(timer)
299
+ timers.current.clear()
300
+ if (savedTimer.current) clearTimeout(savedTimer.current)
301
+ }, [])
302
+
303
+ const disabled = !state.writable || state.status !== 'ready'
304
+
305
+ const persist = useCallback(async (field: FieldSpec, raw: string | boolean): Promise<void> => {
306
+ if (!scope || disabled) return
307
+ inflight.current += 1
308
+ setSaveState('saving'); setError(null)
309
+ try {
310
+ await scope.set(field.key, writeField(field, raw))
311
+ setSaveState('saved')
312
+ if (savedTimer.current) clearTimeout(savedTimer.current)
313
+ // 「已保存」只是一个确认,不是一条状态:两秒后收回,免得它常驻成一个
314
+ // 用户以为要点的按钮。
315
+ savedTimer.current = setTimeout(() => setSaveState(current => current === 'saved' ? 'idle' : current), 2000)
316
+ } catch (cause) {
317
+ setSaveState('error')
318
+ setError(cause instanceof Error ? cause.message : '设置保存失败')
319
+ } finally {
320
+ inflight.current -= 1
321
+ }
322
+ }, [scope, disabled])
323
+
324
+ const update = (field: FieldSpec, value: string | boolean): void => {
325
+ setDraft(current => ({ ...current, [field.key]: value }))
326
+ if (!needsDebounce(field)) {
327
+ void persist(field, value)
328
+ return
329
+ }
330
+ const existing = timers.current.get(field.key)
331
+ if (existing) clearTimeout(existing)
332
+ timers.current.set(field.key, setTimeout(() => {
333
+ timers.current.delete(field.key)
334
+ void persist(field, value)
335
+ }, 600))
336
+ }
337
+
338
+ const meta = state.status === 'loading'
339
+ ? '正在读取配置…'
340
+ : state.status === 'unavailable'
341
+ ? '设置服务不可用'
342
+ : saveState === 'saving'
343
+ ? '保存中…'
344
+ : saveState === 'saved'
345
+ ? '已保存'
346
+ : saveState === 'error'
347
+ ? '未保存'
348
+ : state.mode === 'host' ? '改动即生效' : '当前会话'
349
+
350
+ const anchor = settingsAnchorId(node)
351
+ const titleId = `${anchor}-title`
352
+
353
+ return <section
354
+ id={anchor}
355
+ className={cn('tita-settingsSection', highlight && 'tita-settingsSectionOn')}
356
+ // `tabIndex={-1}` 让「参数」chip 打开的抽屉能把焦点交给这一块:键盘用户落到
357
+ // 参数所在的地方,而不是抽屉顶部再自己找一遍。`aria-labelledby` 顺带把这一块
358
+ // 的名字给它 —— 抽屉里六块并排,读屏用户需要知道自己在哪一块上。
359
+ tabIndex={-1}
360
+ aria-labelledby={titleId}
361
+ >
362
+ <div className="tita-settingsSectionHeader">
363
+ {/* 说明从标题下面挪到图标后面:常驻文字会把控件挤出首屏,而说明本身有价值。
364
+ 见 `InfoHint`。 */}
365
+ <div><h3 id={titleId}>{spec.title}<InfoHint text={spec.description} /></h3></div>
366
+ <div className={`tita-settingMeta${saveState === 'error' ? ' tita-settingMetaBad' : ''}`} role="status" aria-live="polite">
367
+ {saveState === 'saving' && <LoaderCircle size={11} className="tita-spin" />}
368
+ {saveState === 'saved' && <Check size={11} />}
369
+ {meta}
370
+ </div>
371
+ </div>
372
+ {error && <div className="tita-settingsError" role="alert"><AlertCircle size={13} />{error}</div>}
373
+ <div className="tita-settingsGrid">
374
+ {spec.fields.map(field => field.kind === 'toggle'
375
+ ? <ToggleField key={field.key} spec={field} value={Boolean(draft[field.key])} onChange={value => update(field, value)} disabled={disabled} />
376
+ : field.kind === 'select'
377
+ ? <SelectField key={field.key} spec={field} value={String(draft[field.key] ?? '')} onChange={value => update(field, value)} disabled={disabled} />
378
+ : field.kind === 'picker'
379
+ ? <DirectoryPickerField
380
+ key={field.key}
381
+ label={field.label}
382
+ value={String(draft[field.key] ?? '')}
383
+ disabled={disabled}
384
+ pickerKind={pickerKind}
385
+ onError={setError}
386
+ onChange={value => update(field, value)}
387
+ />
388
+ : <TextField key={field.key} spec={field} value={String(draft[field.key] ?? '')} onChange={value => update(field, value)} disabled={disabled} {...(resolved[field.key] ? { resolved: resolved[field.key]! } : {})} />)}
389
+ </div>
390
+ {extra}
391
+ </section>
392
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 预览抽屉的三个公共分块(0035):**元信息 → 内容 → 版本**。
3
+ *
4
+ * SourceRaw 的原文预览与 wiki 卡片的预览必须是**同一种形态**。两处各写一遍结构,
5
+ * 就会出现「一个把元信息放最上面、一个放最下面」「一个能给文本预览、另一个不能」
6
+ * 这类漂移 —— 而它们回答的是同一个问题:这份东西是什么、写了什么、改过几次。
7
+ *
8
+ * 这一层只定义**顺序与分块**(小节标题 + 计数 + 内容),字段仍由调用方给:原文有
9
+ * `content_hash`,卡片有 `entity_id`,本来就不一样,硬凑成同一张表只会两处都别扭。
10
+ * 时间也由调用方格式化(见 `format.ts`)—— 元信息用标准格式,列表行用紧凑格式。
11
+ */
12
+
13
+ import { useState, type ReactNode } from 'react'
14
+ import { FileCode2, FileText, History, Image, ListTree } from '@tita-data-agent/ui/icons'
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tita-data-agent/ui/components'
16
+ import { VersionManager, type VersionManagerProps } from '@tita-data-agent/ui/versions'
17
+ // 平台提供的 Markdown 原子(DSH 的模块表里有它,本插件的 client bundle 也已把它列为
18
+ // external)。原文与卡片正文本身就是 Markdown,用 `<pre>` 显示等于把标题、表格、链接
19
+ // 全都拍平成源码 —— 所以默认阅读态走它,源码视图是**另一个**选项,不是替代品。
20
+ import { MarkdownText, type MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives'
21
+ import { formatBytes } from './format.ts'
22
+ import { styles } from './styles.ts'
23
+
24
+ /**
25
+ * Markdown 原子的界面文案。必须是**模块级常量**:平台明确要求这个对象引用稳定
26
+ * (换一个身份会让流式渲染的缓存整体作废),在渲染里现造一个对象正好踩中那条。
27
+ *
28
+ * 这三个词与 `plugins/dsh-data-notebook` 的同名常量逐字相同。没有抽回 `packages/ui`
29
+ * 是刻意的:`MarkdownLabels` 的类型来自**宿主提供的**平台包,而 data-notebook 的
30
+ * 独立整页入口必须能把平台包整块排除在 bundle 之外 —— 共享层一旦引它,那条「独立
31
+ * 页面不拉平台依赖」的约束就断了。三行文案不值得拿那条约束换。
32
+ */
33
+ const MARKDOWN_LABELS: MarkdownLabels = {
34
+ code: { copyLabel: '复制', copiedLabel: '已复制' },
35
+ footnotes: '脚注',
36
+ }
37
+
38
+ /** 正文只渲染前 2 万字符:更长的原文在抽屉里没有阅读意义,也会拖慢 Markdown 渲染。 */
39
+ const PREVIEW_CHARS = 20000
40
+
41
+ export interface PreviewRow {
42
+ label: string
43
+ value: ReactNode
44
+ /** 等宽显示:hash / id / 路径这类要逐字比对的值。 */
45
+ code?: boolean
46
+ }
47
+
48
+ /** 元信息:一张两列的事实表。标签列宽固定,所以标签要短且彼此对齐。 */
49
+ export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
50
+ // 每一块都包一层 `drawerSection`:标题与它自己的内容是一块,块与块之间的间距由
51
+ // 外层 `drawerStack` 给。不包的话「元信息」标题和它下面那张表是两个同级节点,
52
+ // 外层 gap 会把标题和自己的表也拉开。
53
+ return <section className={styles.drawerSection}>
54
+ <div className={styles.kbSubHead}><h4><ListTree size={13} />元信息</h4></div>
55
+ <dl className={styles.kbKv}>
56
+ {rows.map(row => <div key={row.label}>
57
+ <dt>{row.label}</dt>
58
+ <dd className={row.code ? styles.kbCode : undefined}>{row.value}</dd>
59
+ </div>)}
60
+ </dl>
61
+ </section>
62
+ }
63
+
64
+ export interface PreviewAsset { rel: string; url: string; bytes: number }
65
+
66
+ /**
67
+ * 内容:小节标题 + 视图切换 + 正文。
68
+ *
69
+ * 两个视图是**同一份内容**的两种读法:Markdown 是阅读态(默认),文本是源码态,
70
+ * 用于逐字核对 —— 与版本 diff 是同一种读法。切换不重新取数,也不改内容。
71
+ * 图片素材排在这一块的末尾:它也是内容,只是渲染器没能就地展开的那部分。
72
+ */
73
+ export function PreviewContent({ content, assets = [] }: { content: string; assets?: PreviewAsset[] }): ReactNode {
74
+ const [view, setView] = useState<'md' | 'text'>('md')
75
+ const shown = content.slice(0, PREVIEW_CHARS)
76
+ const clipped = shown.length < content.length
77
+ return <section className={styles.drawerSection}><Tabs value={view} onValueChange={next => setView(next as 'md' | 'text')}>
78
+ <div className={styles.kbSubHead}>
79
+ <h4><FileText size={13} />内容</h4>
80
+ <TabsList className={`${styles.tabsList} ${styles.kbViewTabs}`}>
81
+ <TabsTrigger value="md">Markdown</TabsTrigger>
82
+ <TabsTrigger value="text"><FileCode2 size={11} />文本</TabsTrigger>
83
+ </TabsList>
84
+ <span>{content.length} 字符{clipped ? ` · 只渲染前 ${shown.length}` : ''}</span>
85
+ </div>
86
+ <TabsContent value="md">
87
+ <div className={styles.kbMarkdown}><MarkdownText text={shown} labels={MARKDOWN_LABELS} /></div>
88
+ </TabsContent>
89
+ <TabsContent value="text">
90
+ <pre className={styles.kbPre}>{shown}</pre>
91
+ </TabsContent>
92
+ {assets.length > 0 && <>
93
+ <div className={styles.kbSubHead}><h4><Image size={13} />图片素材</h4><span>{assets.length} 张</span></div>
94
+ <div className={styles.kbAssets}>
95
+ {assets.map(asset => <figure key={asset.rel} className={styles.kbAsset}>
96
+ <img src={asset.url} alt={asset.rel} loading="lazy" />
97
+ <figcaption>{formatBytes(asset.bytes)}</figcaption>
98
+ </figure>)}
99
+ </div>
100
+ </>}
101
+ </Tabs></section>
102
+ }
103
+
104
+ /**
105
+ * 版本:小节标题 + 计数 + 一句「什么时候会留快照」+ 时间线。
106
+ *
107
+ * `hint` 不是可有可无的装饰:两种产物的快照触发条件不同(原文是内容变化,卡片是
108
+ * 正文变化 —— 只改状态不造版本),不写清楚,用户会以为「我改过它,为什么没有版本」。
109
+ */
110
+ export function PreviewVersions({ hint, ...manager }: { hint?: string } & VersionManagerProps): ReactNode {
111
+ return <section className={styles.drawerSection}>
112
+ <div className={styles.kbSubHead}><h4><History size={13} />版本</h4><span>{manager.entries?.length ?? 0} 个</span></div>
113
+ {hint && <p className={styles.kbHint}>{hint}</p>}
114
+ <VersionManager {...manager} />
115
+ </section>
116
+ }