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,530 @@
1
+ /**
2
+ * 架构图的**唯一数据源**:节点是什么、属于哪条链路、核心逻辑是什么、现在实现到哪一步。
3
+ *
4
+ * 这个模块刻意不含任何图形代码(0025 之前它自带一套 DOM 车道 + 回字形阶段图)。
5
+ * 「Agent 概览」现在是一张 AntV G6 绘制的整体架构图(`./arch/`),节点卡片、右侧逻辑
6
+ * 面板、键盘可达的节点列表三处读的是**同一份 `NODE_META` 与 `nodeMetrics`**:
7
+ * 图上写「已实现」而面板写「部分实现」这种事,只能靠两处各写一份才会发生。
8
+ *
9
+ * 设置**不在**这里,也不在任何节点上。节点只承载三件事:它是什么、它的数据统计、
10
+ * 它的实现状态;改配置请去它所属的功能 tab(节点上的「跳转到配置」按钮就是那条路)。
11
+ */
12
+
13
+ import {
14
+ BarChart3, Box, BrainCircuit, ClipboardCheck, Clock3, Database, FileCode2, FileText,
15
+ Gauge, GitBranch, HardDrive, Layers3, MessageCircle, RefreshCw, Search, Share2,
16
+ Table2, UserRound,
17
+ } from '@tita-data-agent/ui/icons'
18
+ import type { LucideIcon } from '@tita-data-agent/ui/icons'
19
+
20
+ /**
21
+ * 视图级标记色。**架构图不再用它**:图上每个节点的身份就是它所属的链路(`lane`),
22
+ * 所以节点不需要再单独存一个 `tone` —— 那个字段曾经可以和 `lane` 打架(资金化属于
23
+ * C 带却写着绿色),而「一条链路一个色」是不能靠两处约定维持的。
24
+ */
25
+ export type Tone = 'blue' | 'violet' | 'green' | 'amber' | 'neutral'
26
+
27
+ /** 面板的一级 tab。`KnowledgePanel` 的 `ViewId` 就是它,避免两处各写一个联合。 */
28
+ export type PanelTab = 'overview' | 'intent' | 'knowledge' | 'attribution' | 'memory' | 'eval'
29
+
30
+ /**
31
+ * 知识库内部的分栏。**与知识链路的五个节点一一对应** —— 图上一个节点对应一个
32
+ * 能看内容、也能改设置的地方;对不上就说明要么缺页、要么多了一个没有归属的页。
33
+ */
34
+ /**
35
+ * 知识库分栏(0033 把「召回」与「重排」合成一栏)。
36
+ *
37
+ * 合并不是因为「两个阶段变成一个」—— 它们在流水线上仍然是两步。合的是**界面**:
38
+ * 这两栏回答的是同一个问题(「跑一次真实召回,结果长什么样」),而且分成两栏时
39
+ * 用户必须先跑「召回」再切到「重排」才能看到同一次运行的全貌,两栏各自还有一个
40
+ * 测试按钮和一套 topK/topN 输入。一栏里从上到下就是一条链:三路各自命中什么 →
41
+ * 融合后的排序 → cross-encoder 丢了多少 → 最终进上下文的 topN。
42
+ */
43
+ export type KnowledgeTab = 'source-raw' | 'wiki' | 'graph' | 'recall'
44
+
45
+ /**
46
+ * 五个分栏的顺序、名字与图标。**定义在这里而不是在 `KnowledgeBaseTab` 里**:
47
+ * 渲染它的地方有两个 —— 面板外壳顶部那一条分栏 tab,和知识库内容本身。
48
+ * 两处各写一份,迟早会出现「tab 上写知识图谱、面板里写 Graph」。
49
+ *
50
+ * `step` 是这一栏在知识链路里**做的动作**。它存在的理由:这五栏不是五个并列的
51
+ * 视图,而是一条流水线(收录 → 编译 → 关联 → 检索 → 精排),所以它们的切换器
52
+ * 得说清「谁接谁」——只写名词(SourceRaw / Wiki / 知识图谱)看不出先后。
53
+ */
54
+ export const KNOWLEDGE_TABS: Array<{ id: KnowledgeTab; label: string; step: string; icon: LucideIcon }> = [
55
+ { id: 'source-raw', label: 'SourceRaw', step: '收录', icon: Database },
56
+ { id: 'wiki', label: 'Wiki', step: '编译', icon: Layers3 },
57
+ { id: 'graph', label: '知识图谱', step: '关联', icon: GitBranch },
58
+ { id: 'recall', label: '召回重排', step: '检索 · 精排', icon: Search },
59
+ ]
60
+
61
+ export type LaneId = 'interaction' | 'knowledge' | 'evolution'
62
+
63
+ export interface LaneMeta {
64
+ id: LaneId
65
+ index: string
66
+ title: string
67
+ note: string
68
+ }
69
+
70
+ /**
71
+ * 三条链路。顺序就是阅读顺序,也是 G6 画布上从上到下的三条带。
72
+ *
73
+ * 只有三条:用户操作、知识编译与召回、自进化。多一条「基础设施」之类的带会让图变成
74
+ * 清单;少的那些(转换内核、Embedding)本来就是某一带里某个节点的手段,不是平级链路。
75
+ *
76
+ * `note` 写在每张带自己的左上角,`index` 是它的字母徽标 —— 链路名不进左侧留白,
77
+ * 因为那条留白要留给跨链路的走线(A↔B↔C 的关系就画在那里)。
78
+ */
79
+ export const NODE_LANES: LaneMeta[] = [
80
+ { id: 'interaction', index: 'A', title: '用户操作链路', note: '提问 → 理解 → 执行 → 产出' },
81
+ { id: 'knowledge', index: 'B', title: '知识编译与召回链路', note: 'SourceRaw → 编译 → 构建 → 召回 → 重排' },
82
+ { id: 'evolution', index: 'C', title: '自进化 Agent', note: '交互 → 记忆 → 质量门 → 每日更新 → 资金化' },
83
+ ]
84
+
85
+ export type NodeId =
86
+ | 'conversation' | 'intent' | 'runtime' | 'answer' | 'data-sheet' | 'data-notebook'
87
+ | 'source-raw' | 'compile' | 'build' | 'recall' | 'context'
88
+ | 'interaction' | 'episodic-memory' | 'procedural-memory' | 'memory-quality' | 'daily-update' | 'funded-value'
89
+
90
+ /**
91
+ * 实现状态。**这是这张图最容易被写坏的一格**:把待实现的画成已实现,图就从契约
92
+ * 退化成宣传。所以状态只有四档,且每一档都要在 `statusNote` 里说清依据。
93
+ *
94
+ * - `host`:不是本插件的代码(Runtime / Table / Notebook 由宿主提供),不冒充我们的进度。
95
+ */
96
+ export type NodeStatus = 'live' | 'partial' | 'planned' | 'host'
97
+
98
+ export const NODE_STATUS_LABEL: Record<NodeStatus, string> = {
99
+ live: '已实现',
100
+ partial: '部分实现',
101
+ planned: '待实现',
102
+ host: '宿主提供',
103
+ }
104
+
105
+ /**
106
+ * 卡片上的状态文字。**只有「宿主提供」需要缩短** —— 它是四个字,而卡片右侧那点宽度
107
+ * 是从标题里借的:省下两个字,`记忆整理与质量检查` 这种九字标题才放得下。
108
+ * 面板、无障碍列表和状态记录读的仍是完整措辞,缩的只是图上那一处。
109
+ */
110
+ export const NODE_STATUS_SHORT: Record<NodeStatus, string> = {
111
+ ...NODE_STATUS_LABEL,
112
+ host: '宿主',
113
+ }
114
+
115
+ /**
116
+ * 状态色:**四档只用三种颜色,而且只在文字上**。
117
+ *
118
+ * 「已实现」是常态,所以它拿绿(`--tita-success`,白底 5.7:1);「部分实现」是唯一需要
119
+ * 人动手的一档,拿琥珀(`--tita-warning`,5.4:1);「待实现」和「宿主提供」都不是我们的
120
+ * 进度,拿两级灰:文字分两级(faint / muted),圆点合并成 faint 一档 —— 共享词表里只有
121
+ * 三级墨色,而这两档圆点本来就只差一格,区分它们的是文字。
122
+ * 四档不各配一个背景色药丸 —— 那样四张卡片并排时,颜色比标题还响。
123
+ *
124
+ * `fill` 仍然保留,但它只用在**需要一整块底**的地方(例如图例上的小色块);
125
+ * 卡片上的状态是「一个小圆点 + 状态文字」,不是一块色底。
126
+ *
127
+ * 这些值进的是 G6 **HTML 节点**的 innerHTML,也就是真实 DOM,所以可以用共享令牌。
128
+ */
129
+ export const NODE_STATUS_TONE: Record<NodeStatus, { fill: string; text: string; dot: string }> = {
130
+ live: { fill: 'var(--tita-tone-green-tint)', text: 'var(--tita-success)', dot: 'var(--tita-success)' },
131
+ partial: { fill: 'var(--tita-tone-amber-tint)', text: 'var(--tita-warning)', dot: 'var(--tita-warning)' },
132
+ planned: { fill: 'var(--tita-surface-muted)', text: 'var(--tita-ink-faint)', dot: 'var(--tita-ink-faint)' },
133
+ host: { fill: 'var(--tita-surface-muted)', text: 'var(--tita-ink-muted)', dot: 'var(--tita-ink-faint)' },
134
+ }
135
+
136
+ /** 一个执行步骤。/ `state` 决定面板里显示对勾、半勾还是空心圈。 */
137
+ export interface NodeStep {
138
+ label: string
139
+ detail: string
140
+ state?: 'done' | 'partial' | 'todo'
141
+ }
142
+
143
+ export interface NodeMeta {
144
+ title: string
145
+ /**
146
+ * 卡片上的短名。只给放不进 180px 卡片的标题用 —— 卡片是示意图,它旁边还站着
147
+ * 十来个同宽的字;面板里写的始终是完整名字,卡片的 `title` 悬停也给出完整名字。
148
+ */
149
+ shortTitle?: string
150
+ subtitle: string
151
+ /** 一句话核心逻辑:这一格负责把什么变成什么。 */
152
+ detail: string
153
+ /** 输入 → 输出。图上不写,面板里写 —— 它回答的是「它凭什么能工作」。 */
154
+ input: string
155
+ output: string
156
+ steps: NodeStep[]
157
+ status: NodeStatus
158
+ /** 状态的依据。面板「状态记录」直接展示,图上的徽标用它做 title。 */
159
+ statusNote: string
160
+ icon: LucideIcon
161
+ lane: LaneId
162
+ /** 点「跳转到配置」时切到哪个 tab;没有对应页面的节点不给按钮,而不是给一个假按钮。 */
163
+ tab?: PanelTab
164
+ /** `tab === 'knowledge'` 时落到哪个分栏。 */
165
+ knowledgeTab?: KnowledgeTab
166
+ }
167
+
168
+ export const NODE_META: Record<NodeId, NodeMeta> = {
169
+ conversation: {
170
+ title: '用户对话', subtitle: '提出问题、选择数据、执行分析',
171
+ detail: '接收自然语言问题、任务描述和当前会话上下文,把它转成一次可执行的分析请求。',
172
+ input: '自然语言问题、当前会话上下文', output: '一次带会话标识的分析请求',
173
+ steps: [
174
+ { label: '接收问题', detail: '用户在面板里提问,会话与轮次 id 一并带上', state: 'done' },
175
+ { label: '携带上下文', detail: '当前选中的知识库、数据范围随请求下发', state: 'done' },
176
+ ],
177
+ status: 'host', statusNote: '对话入口由宿主(DSH Web)提供,本插件不实现它,只在链路里标明位置。',
178
+ icon: MessageCircle, lane: 'interaction',
179
+ },
180
+ intent: {
181
+ title: '意图与上下文', subtitle: '两段式判定:命中预置分类,否则模型自行判断',
182
+ detail: '识别用户意图,合并会话、数据范围、知识召回和可用工具,生成 Runtime 可消费的执行上下文。',
183
+ input: '用户原文 + 会话 + 任务上下文', output: '一条 intent 记录:category / act / 置信度 / 引用的知识',
184
+ steps: [
185
+ { label: '预置分类优先', detail: '命中「意图配置」里的分类就直接归类过去', state: 'done' },
186
+ { label: '未命中交模型判断', detail: '不命中时不硬塞,由模型自行给出 category 与 act', state: 'done' },
187
+ { label: '置信度与兜底', detail: '低于阈值的记录标 lowConfidence;漏调由 Host 补一条 fallback', state: 'done' },
188
+ { label: '引用溯源', detail: '记录这一轮引用了哪些知识(source / wiki)', state: 'done' },
189
+ ],
190
+ status: 'live', statusNote: '意图记录、分类注册表、act 词表、两段式路由提示词都已落盘并随 intent_record 工具下发。',
191
+ icon: BrainCircuit, lane: 'interaction', tab: 'intent',
192
+ },
193
+ runtime: {
194
+ title: 'Tita Agent Runtime', shortTitle: 'Agent Runtime', subtitle: '规划、调用工具、执行分析',
195
+ detail: 'Agent 核心运行时:编排知识召回、数据表格、Notebook 和自进化记忆,组织结果并返回给用户。',
196
+ input: '意图与上下文 + 召回得到的知识', output: '回答、引用、结构化数据',
197
+ steps: [
198
+ { label: '注入 Context Pack', detail: '重排后的 wikiNotes 连同 wiki 与关系图进入模型上下文', state: 'done' },
199
+ { label: '调用工具', detail: '取数、表格、Notebook、知识写入都由工具完成', state: 'done' },
200
+ { label: '组织回答', detail: '解释与引用一起返回,结构化结果转交 Table / Notebook', state: 'done' },
201
+ ],
202
+ status: 'host', statusNote: 'Runtime 是宿主 Agent 本体;本插件以「召回上下文 + 知识工具」的方式接入它。',
203
+ icon: Box, lane: 'interaction',
204
+ },
205
+ answer: {
206
+ title: '回答 / 数据工具', subtitle: '生成结果、可视化与方案',
207
+ detail: '输出解释、引用和数据结果,并把结构化结果继续交给 Table 或 Notebook 处理。',
208
+ input: '模型回答 + 工具返回值', output: '可读回答、可编辑表格、可复现 Notebook',
209
+ steps: [
210
+ { label: '回答与引用', detail: '每一条结论都能指回 wiki 与 SourceRaw', state: 'done' },
211
+ { label: '转交数据工具', detail: '结构化结果可继续在 Table / Notebook 里加工', state: 'done' },
212
+ ],
213
+ status: 'host', statusNote: '回答渲染与 Table / Notebook 由宿主工作区提供。',
214
+ icon: BarChart3, lane: 'interaction',
215
+ },
216
+ 'data-sheet': {
217
+ title: 'Table', subtitle: '生成 / 查看数据表格',
218
+ detail: '把 Agent 产出的结构化数据交给 Table,进行筛选、编辑、版本管理和导出。',
219
+ input: '结构化结果集', output: '带版本与导出的表格',
220
+ steps: [{ label: '打开数据表格', detail: '由宿主的 Table 插件承载', state: 'done' }],
221
+ status: 'host', statusNote: '属于 `dsh-data-table` 插件,这里只画调用关系。',
222
+ icon: Table2, lane: 'interaction',
223
+ },
224
+ 'data-notebook': {
225
+ title: 'Notebook', subtitle: '生成 / 分析 Notebook',
226
+ detail: '把查询、SQL、Markdown 和图表组织成可复用的分析 Notebook。',
227
+ input: '查询与代码片段', output: '可复现的分析 Notebook',
228
+ steps: [{ label: '打开数据笔记本', detail: '由宿主的 Notebook 插件承载', state: 'done' }],
229
+ status: 'host', statusNote: '属于 `dsh-data-notebook` 插件,这里只画调用关系。',
230
+ icon: FileCode2, lane: 'interaction',
231
+ },
232
+
233
+ 'source-raw': {
234
+ title: 'SourceRaw', subtitle: '原始可追溯文档',
235
+ detail: '保存原始来源文档(Markdown、图片、对话沉淀),可追溯、按 external_id 幂等;SourceRaw 本身不参与召回。',
236
+ input: '上传文件 / 转换内核产出 / Agent 写入', output: '带来源分类、内容 hash 与版本历史的原始文档',
237
+ steps: [
238
+ { label: '按格式路由解析', detail: '文件类型表决定走 anydoc 内核还是直读文本', state: 'done' },
239
+ { label: '来源分类闸门', detail: '分类注册表校验 source_type,自定义分类建完即可用', state: 'done' },
240
+ { label: '内容幂等写入', detail: '同 external_id 覆盖时先快照旧版本,正文没变则不造版本', state: 'done' },
241
+ { label: '版本历史与回滚', detail: '快照落在 04_memory/source-versions/,回滚本身也可回滚', state: 'done' },
242
+ ],
243
+ status: 'live', statusNote: '写入、分类、版本、回滚、详情都已在知识库 → SourceRaw 里实现。',
244
+ icon: Database, lane: 'knowledge', tab: 'knowledge', knowledgeTab: 'source-raw',
245
+ },
246
+ compile: {
247
+ title: '编译', subtitle: 'SourceRaw → wiki 卡片 + wikiNotes',
248
+ detail: '把 SourceRaw 按主题聚合为 wiki 卡片,并切分出召回单元 wikiNotes。类型与名称一起定,判重按 slug。',
249
+ input: 'SourceRaw 文档 + Wiki 类型注册表', output: 'wiki 卡片(含版本)+ 可召回的 wikiNotes',
250
+ steps: [
251
+ { label: 'Pass 0 · 候选抽取', detail: '从正文标题、[[链接]]、frontmatter 与显式 candidates 抽实体 / 概念 / 关系,类型 + 名称 + 证据一起定', state: 'done' },
252
+ { label: '归一 · 与库内判重', detail: '按 slug 与别名比对,同指一物就复用(合并 source_refs),related ≠ same', state: 'done' },
253
+ { label: '落卡 · 按 schema 写入', detail: 'write_knowledge_card 校验必填小节 / 字段,命中则增量合并', state: 'done' },
254
+ { label: 'Finalize · 索引与质检', detail: '索引页、死链、交叉链接与三层质检,不调模型', state: 'partial' },
255
+ ],
256
+ status: 'partial', statusNote: 'Pass 0 抽取与归一判重已实现(规则抽取,不调模型:候选写进 02_wiki 后仍需人复核);落卡已实现。Finalize 的索引页 / 死链 / 交叉链接还缺,所以这一格仍是「部分实现」。',
257
+ icon: RefreshCw, lane: 'knowledge', tab: 'knowledge', knowledgeTab: 'wiki',
258
+ },
259
+ build: {
260
+ title: '构建', subtitle: 'wikiNotes → 向量 · wiki → 知识图谱',
261
+ detail: '从 wikiNotes 构建向量索引,从 wiki 卡片构建知识图谱:SourceRaw 作为溯源边;构建是确定性计算,可独立重跑。',
262
+ input: 'wikiNotes + wiki 卡片', output: '向量索引(binary + int8)与 knowledge_graph.json',
263
+ steps: [
264
+ { label: '切分召回单元', detail: '长卡片按小节切成多条 wikiNotes,unitId 稳定', state: 'done' },
265
+ { label: '构建向量索引', detail: '逐条 contentHash 复用,只有变化的条目重新编码', state: 'done' },
266
+ { label: '构建知识图谱', detail: 'wiki 节点 + supported_by 溯源边', state: 'done' },
267
+ { label: '图上的关系边', detail: 'Pass 0 抽出的 wiki↔wiki 关系边(relates_to)与溯源边并存', state: 'done' },
268
+ ],
269
+ status: 'live', statusNote: '向量与图谱构建都已实现且已增量(未变的条目复用向量,不重新编码)。',
270
+ icon: HardDrive, lane: 'knowledge', tab: 'knowledge', knowledgeTab: 'graph',
271
+ },
272
+ recall: {
273
+ title: '召回', subtitle: 'wikiNotes 三路召回(关键词 / 向量 / 知识图谱)',
274
+ detail: '对 wikiNotes 做三路召回:关键词、向量和知识图谱(沿 wiki 链接关系扩展),三路融合后取 topK,再解析各自的父级 wiki。',
275
+ input: '查询语句 + topK + 路线', output: '带命中路线与得分的 wikiNotes',
276
+ steps: [
277
+ { label: '三路并行召回', detail: 'FTS 关键词、向量近邻、图谱 1 跳扩展', state: 'done' },
278
+ { label: '融合与截断', detail: '按融合分数排序取 topK,逐条标明是哪一路命中的', state: 'done' },
279
+ { label: '回溯父级 wiki', detail: '每条 wikiNote 通过 wikiId 解析回权威卡片', state: 'done' },
280
+ ],
281
+ status: 'live', statusNote: '三路召回与召回测试(含命中切片、得分、路线)都在知识库 → 召回重排里可跑。',
282
+ icon: Search, lane: 'knowledge', tab: 'knowledge', knowledgeTab: 'recall',
283
+ },
284
+ context: {
285
+ title: '重排', subtitle: 'BERT cross-encoder · topN 上下文',
286
+ detail: '对候选 wikiNotes 做 BERT cross-encoder 精排,取 topN 连同 wiki 与关系图组成 Agent 可消费的上下文包。',
287
+ input: 'topK 条候选 wikiNotes', output: '注入 Runtime 的 Context Pack',
288
+ steps: [
289
+ { label: 'cross-encoder 精排', detail: 'query 与候选逐对打分,按 rerank 分数重排', state: 'done' },
290
+ { label: '降级不造假', detail: '模型或 worker 不可用时按融合分数排序并显式给出警告', state: 'done' },
291
+ { label: '展开上下文包', detail: '取 topN,连同 wiki 卡片与 1 跳关系图一起注入', state: 'done' },
292
+ ],
293
+ status: 'live', statusNote: '重排已实现(provider 可关);不可用时降级为融合分数并返回警告,而不是伪造分数。它和召回在知识库 → 召回重排同一栏里(同一次运行,不切栏)。',
294
+ icon: Layers3, lane: 'knowledge', tab: 'knowledge', knowledgeTab: 'recall',
295
+ },
296
+
297
+ interaction: {
298
+ title: '用户与 Agent 交互', shortTitle: '用户交互', subtitle: '对话、分析、使用功能的全过程',
299
+ detail: '记录用户做了什么、采用了什么结果,以及对分析结果的反馈,为自进化闭环提供输入。',
300
+ input: '每一轮的意图记录与工具调用', output: '可回溯的交互审计流',
301
+ steps: [
302
+ { label: '意图审计流', detail: '每轮一条 intent 记录,只标注不拦截', state: 'done' },
303
+ { label: '结果采纳与反馈', detail: '用户是否采用了结果、事后评价还没采集', state: 'todo' },
304
+ ],
305
+ status: 'partial', statusNote: '意图记录已经是一条真实的审计流;「结果是否被采用」这类反馈还没有采集入口。',
306
+ icon: UserRound, lane: 'evolution', tab: 'memory',
307
+ },
308
+ 'episodic-memory': {
309
+ title: '情节记忆', subtitle: '用户做了什么 / 对什么事的反应',
310
+ detail: '记录一次次任务、选择、反馈和结果评价,保留时间、会话和来源,用于理解用户偏好与上下文。',
311
+ input: '交互审计流', output: '带时间与会话的情节条目',
312
+ steps: [
313
+ { label: '采集', detail: '从交互审计流提取任务、选择、反馈', state: 'todo' },
314
+ { label: '归纳', detail: '按会话聚合为「做过什么」的条目', state: 'todo' },
315
+ ],
316
+ status: 'planned', statusNote: '还没有情节记忆的采集与归纳管线;记忆 Tab 上标的就是「示意」,不展示编造的数字。',
317
+ icon: FileText, lane: 'evolution', tab: 'memory',
318
+ },
319
+ 'procedural-memory': {
320
+ title: '操作记忆', subtitle: '用户常用操作 → Skill',
321
+ detail: '从重复操作中提取稳定步骤、参数和工具组合,经过审核后沉淀为可复用 Skill。',
322
+ input: '重复出现的操作序列', output: '候选 Skill',
323
+ steps: [
324
+ { label: '提取候选', detail: '从重复操作里归纳步骤、参数与工具组合', state: 'todo' },
325
+ { label: '审核后入库', detail: '进入审核队列,通过后写入 Skill Registry', state: 'todo' },
326
+ ],
327
+ status: 'planned', statusNote: '本次实测中,本次实测中还没有 Skill 提取与注册表写入的实现。',
328
+ icon: Share2, lane: 'evolution', tab: 'memory',
329
+ },
330
+ 'memory-quality': {
331
+ title: '记忆整理与质量检查', shortTitle: '记忆质量检查', subtitle: '去重、归纳、评估 / 过滤低质量内容',
332
+ detail: '对记忆进行去重、归纳、敏感信息检查和质量评估;涉及写入知识库或 Skill 的变更进入审核队列。',
333
+ input: '情节记忆 + 候选 Skill', output: '通过质检的变更',
334
+ steps: [
335
+ { label: '去重与归纳', detail: '同一件事的多条记录合并', state: 'todo' },
336
+ { label: '质量与敏感检查', detail: '低质量与敏感内容过滤,变更进入审核队列', state: 'todo' },
337
+ ],
338
+ status: 'planned', statusNote: '质量门还没有实现,只有卡片 schema 的三层校验是真实在跑的质检。',
339
+ icon: ClipboardCheck, lane: 'evolution', tab: 'memory',
340
+ },
341
+ 'daily-update': {
342
+ title: '每日更新知识库 / Skill', shortTitle: '每日更新', subtitle: '沉淀高价值经验,更新知识与 Skill',
343
+ detail: '每天按计划把确认后的情节记忆写入知识库、把操作记忆写入 Skill Registry,并生成版本记录。',
344
+ input: '通过质检的变更', output: '带版本记录的每日更新批次',
345
+ steps: [
346
+ { label: '批量写入', detail: '按计划把确认后的内容写进知识库与 Skill Registry', state: 'todo' },
347
+ { label: '版本与回滚', detail: '每批留版本记录,可回滚', state: 'todo' },
348
+ ],
349
+ status: 'planned', statusNote: '还没有定时任务;知识库本身的增量编译已经有了,但「每日自动跑一次」还没接。',
350
+ icon: Clock3, lane: 'evolution', tab: 'memory',
351
+ },
352
+ 'funded-value': {
353
+ title: '资金化', subtitle: '知识资产 → 可计量价值',
354
+ detail: '把可复用知识、Skill 和数据能力转化为可计量的业务价值入口,例如服务化、效率收益、指标改善与资产目录。',
355
+ input: '沉淀下来的知识与 Skill', output: '可计量的价值入口',
356
+ steps: [{ label: '价值候选', detail: '哪些知识与 Skill 值得被复用和计量的口径还没定', state: 'todo' }],
357
+ status: 'planned', statusNote: '产品目标,未实现;图上保留它是为了让「这条闭环最后要产出什么」可见。',
358
+ icon: Gauge, lane: 'evolution',
359
+ },
360
+ }
361
+
362
+ /** 节点上的一条计数:`label` + 值。 */
363
+ export interface NodeMetric { label: string; value: string }
364
+
365
+ /** `/knowledge/stats` 的响应形状。节点指标与面板都是它的投影。 */
366
+ export interface KnowledgeStats {
367
+ /** 知识树的段 → 文件数(0032 起**不含记忆**,记忆读 `memoryFiles`)。 */
368
+ layout?: Record<string, number>
369
+ /** 记忆段的真实位置(`<agentRoot>/memory/<库 id>`)与文件数(0032)。 */
370
+ memoryRoot?: string
371
+ memoryFiles?: number
372
+ sourceCount?: number
373
+ /** Wiki cards under `02_wiki` (the API calls this field `cards`). */
374
+ cards?: number
375
+ units?: number
376
+ notes?: number
377
+ graphNodes?: number
378
+ graphEdges?: number
379
+ buildVersion?: number
380
+ builtAt?: string
381
+ /** 已纳入增量指纹的卡片数 / 记忆段下的卡片版本快照数。 */
382
+ trackedCards?: number
383
+ cardVersions?: number
384
+ lastBuild?: {
385
+ only?: string
386
+ incremental?: boolean
387
+ skipped?: boolean
388
+ cardsReused?: number
389
+ cardsRecompiled?: number
390
+ cardsAdded?: number
391
+ cardsRemoved?: number
392
+ notesReused?: number
393
+ notesRecompiled?: number
394
+ /** Pass 0 · 候选抽取 + 归一的诚实计数(0028)。 */
395
+ candidatesExtracted?: number
396
+ candidatesRejected?: number
397
+ cardsCreated?: number
398
+ cardsMerged?: number
399
+ relationsExtracted?: number
400
+ relationsDropped?: number
401
+ sourcesExtracted?: number
402
+ sourcesSkipped?: number
403
+ } | null
404
+ lastCompileRunId?: string
405
+ recallTopK?: number
406
+ recallMode?: string
407
+ contextTopN?: number
408
+ rerankProvider?: string
409
+ rerankModel?: string
410
+ embeddingProvider?: string
411
+ embeddingModel?: string
412
+ embeddingDimension?: number
413
+ embeddingDevice?: string
414
+ vectorCount?: number
415
+ vectorQuantization?: string
416
+ embeddingStatus?: string
417
+ searchBackend?: string
418
+ }
419
+
420
+ /** SourceRaw 计数有两个来源:`stats.sourceCount` 与磁盘布局 `01_source_truth`。 */
421
+ export function sourceCountOf(stats: KnowledgeStats): number {
422
+ return stats.sourceCount ?? Number(stats.layout?.['01_source_truth'] ?? 0)
423
+ }
424
+
425
+ /** wikiNotes 计数在接口里有 `notes` / `units` 两个别名。 */
426
+ export function noteCountOf(stats: KnowledgeStats): number {
427
+ return stats.notes ?? stats.units ?? 0
428
+ }
429
+
430
+ /**
431
+ * 记忆段的落盘文件数:记忆相关的几个节点共用它。
432
+ *
433
+ * 0032 起读 `memoryFiles` 而不是 `layout['04_memory']` —— 记忆已经不在知识树里,
434
+ * `layout` 只装知识树的段。**读旧字段会静默变成 0**(段名不存在),而「0 个文件」
435
+ * 看起来像一个合理的数字,所以这里换字段的同时也换了标签文案。
436
+ */
437
+ export function memoryCountOf(stats: KnowledgeStats): number {
438
+ return Number(stats.memoryFiles ?? 0)
439
+ }
440
+
441
+ /**
442
+ * 节点指标:真实计数只在这里派生一次,图上的节点卡片与右侧逻辑面板读的是同一个数组。
443
+ *
444
+ * 不给没有数据来源的节点编数字。宿主节点(对话 / Runtime / 回答 / Table)返回空数组,
445
+ * 卡片上就只是一句说明 —— 这比一个假的「0」诚实。
446
+ */
447
+ export function nodeMetrics(id: NodeId, stats: KnowledgeStats, version: number): NodeMetric[] {
448
+ switch (id) {
449
+ case 'source-raw':
450
+ return [
451
+ { label: 'SourceRaw', value: String(sourceCountOf(stats)) },
452
+ { label: '待编译', value: sourceCountOf(stats) > 0 && version === 0 ? '是' : '否' },
453
+ ]
454
+ case 'compile':
455
+ return [
456
+ { label: 'wiki 卡片', value: String(stats.cards ?? 0) },
457
+ { label: 'wikiNotes', value: String(noteCountOf(stats)) },
458
+ ]
459
+ case 'build':
460
+ return [
461
+ { label: '向量', value: String(stats.vectorCount ?? 0) },
462
+ { label: '图谱', value: `${stats.graphNodes ?? 0} / ${stats.graphEdges ?? 0}` },
463
+ { label: '版本', value: version > 0 ? `v${version}` : '未构建' },
464
+ ]
465
+ case 'runtime':
466
+ return [
467
+ { label: '索引', value: version > 0 ? `v${version}` : '未构建' },
468
+ { label: '检索', value: stats.searchBackend === 'hybrid' ? 'Hybrid' : 'FTS fallback' },
469
+ ]
470
+ case 'recall':
471
+ return [
472
+ { label: 'topK', value: String(stats.recallTopK ?? 10) },
473
+ { label: '默认路线', value: stats.recallMode ?? 'hybrid' },
474
+ ]
475
+ case 'context':
476
+ return [
477
+ { label: 'topN', value: String(stats.contextTopN ?? 5) },
478
+ { label: 'rerank', value: stats.rerankProvider === 'none' ? '关闭' : stats.rerankProvider ?? 'bert-python' },
479
+ ]
480
+ case 'interaction':
481
+ return [{ label: 'memory/', value: `${memoryCountOf(stats)} 文件` }]
482
+ case 'episodic-memory':
483
+ case 'procedural-memory':
484
+ case 'memory-quality':
485
+ case 'daily-update':
486
+ return [
487
+ { label: 'memory/', value: `${memoryCountOf(stats)} 文件` },
488
+ { label: '卡片版本', value: String(stats.cardVersions ?? 0) },
489
+ ]
490
+ default:
491
+ return []
492
+ }
493
+ }
494
+
495
+ /** 一次算完整张图的节点指标(供图上一次性取值)。 */
496
+ export function metricsByNode(stats: KnowledgeStats, version: number): Partial<Record<NodeId, NodeMetric[]>> {
497
+ const map: Partial<Record<NodeId, NodeMetric[]>> = {}
498
+ for (const id of NODE_ORDER) {
499
+ const rows = nodeMetrics(id, stats, version)
500
+ if (rows.length > 0) map[id] = rows
501
+ }
502
+ return map
503
+ }
504
+
505
+ /** 节点在图上出现的顺序:按链路,再按链内阅读顺序。它就是键盘可达列表的顺序。 */
506
+ export const NODE_ORDER: NodeId[] = [
507
+ 'conversation', 'intent', 'runtime', 'answer', 'data-sheet', 'data-notebook',
508
+ 'source-raw', 'compile', 'build', 'recall', 'context',
509
+ 'interaction', 'episodic-memory', 'procedural-memory', 'memory-quality', 'daily-update', 'funded-value',
510
+ ]
511
+
512
+ /** 只在面板里出现、不占图上格子的节点(0025 起 Table / Notebook 已上图,这里保留给未来)。 */
513
+ export const DERIVED_NODES: NodeId[] = []
514
+
515
+ /** 某个节点归属的链路定义;找不到(例如未来新增节点漏填 lane)时返回 null 而不是猜。 */
516
+ export function laneOf(id: NodeId): LaneMeta | null {
517
+ return NODE_LANES.find(lane => lane.id === NODE_META[id].lane) ?? null
518
+ }
519
+
520
+ /** 「跳转到配置」按钮的文案:跳不到就说清为什么,而不是给一个点不动的按钮。 */
521
+ export function jumpTargetOf(id: NodeId): { tab: PanelTab; knowledgeTab?: KnowledgeTab } | null {
522
+ const meta = NODE_META[id]
523
+ if (!meta.tab) return null
524
+ return { tab: meta.tab, ...(meta.knowledgeTab ? { knowledgeTab: meta.knowledgeTab } : {}) }
525
+ }
526
+
527
+ /** 「未实现」的节点集合:概览与面板据此统一措辞。 */
528
+ export function unimplementedNodes(): NodeId[] {
529
+ return NODE_ORDER.filter(id => NODE_META[id].status === 'planned')
530
+ }
@@ -0,0 +1,104 @@
1
+ import { Graph } from '@antv/g6'
2
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
3
+ import { buildGraphOptions, seedPositions, toGraphData, type GraphLayout, type StageMode } from './graph-options.ts'
4
+ import { cn } from '@tita-data-agent/ui/components'
5
+ import type { AttributionGraphData } from './model.ts'
6
+
7
+ export interface AttributionGraphProps {
8
+ data: AttributionGraphData
9
+ /** 空区节点 id:有假设、无检查。由调用方算好传入,画布不重复推导。 */
10
+ gapIds: ReadonlySet<string>
11
+ /**
12
+ * 布局方向。**它随视图模式变**,不是用户偏好:
13
+ * 承载面板的右侧边栏只有 380px(宿主 `width:380px`),所以布局的长宽比必须
14
+ * 配合视口 —— 全景用 LR(竖长,贴住竖屏视口),聚焦用 TB(紧凑,让 7 个节点
15
+ * 能按接近 1:1 渲染而不是被压成一半)。
16
+ */
17
+ rankdir: 'LR' | 'TB'
18
+ /** `force` 浮动节点(默认);`layered` 分层 DAG。 */
19
+ layout: GraphLayout
20
+ /** 视口参数档位:面板内 / 全屏。决定缩放区间与留白。 */
21
+ mode: StageMode
22
+ selectedId?: string
23
+ onSelect: (id: string) => void
24
+ className?: string
25
+ }
26
+
27
+ /**
28
+ * G6 v5 画布。
29
+ *
30
+ * **它铺满父容器,自己不带高度。** 尺寸由父级用 CSS 决定(`absolute inset-0` 或
31
+ * 普通 `h-full`),画布只负责把 `clientWidth/clientHeight` 读出来 —— 这样「图谱占满
32
+ * 整个内容区」是布局层的事,画布不需要知道「面板有多高」这种知识,全屏切换也就
33
+ * 只是父容器换了个尺寸,画布跟着 ResizeObserver 走。
34
+ *
35
+ * 实例策略:**在布局方向、档位或数据变化时重建 Graph,只有选中态变化时才走
36
+ * `setElementState`**。重建一个 20 多节点的图只要几十毫秒,而增量同步布局、
37
+ * 样式、标签的边界情况(G6 会缓存节点位置与 state)远比一次重建更容易出错 ——
38
+ * 这个取舍随图上到几千节点才需要反过来。
39
+ */
40
+ export function AttributionGraph({ data, gapIds, rankdir, layout, mode, selectedId, onSelect, className }: AttributionGraphProps): ReactNode {
41
+ const host = useRef<HTMLDivElement>(null)
42
+ const graphRef = useRef<Graph | null>(null)
43
+ const selectRef = useRef(onSelect)
44
+ selectRef.current = onSelect
45
+ const dataRef = useRef(data)
46
+ dataRef.current = data
47
+ // 重建之后选中态要重新贴上去:新节点上没有任何 state,只靠 selectedId 的
48
+ // 变化触发不了那个 effect。
49
+ const [renderToken, setRenderToken] = useState(0)
50
+
51
+ useEffect(() => {
52
+ const el = host.current
53
+ if (!el) return
54
+ const measure = (): { width: number; height: number } => ({
55
+ // 兜底尺寸:容器还没被布局出来时(首帧、或宿主面板宽度为 0)先给一个能画的值,
56
+ // 免得 G6 拿到 0 之后整块空白。
57
+ width: el.clientWidth || 348,
58
+ height: el.clientHeight || 360,
59
+ })
60
+ const graph = new Graph({
61
+ ...buildGraphOptions(measure(), layout, rankdir, mode),
62
+ container: el,
63
+ })
64
+ graphRef.current = graph
65
+ graph.on('node:click', event => {
66
+ // G6 的 IEvent 是联合类型,取 id 需要一次窄化;断言限制在这一行内。
67
+ const id = (event as { target?: { id?: string } }).target?.id
68
+ if (id) selectRef.current(id)
69
+ })
70
+ const size = measure()
71
+ // 只有力导向才需要喂初始位置:分层布局的坐标由 dagre 算,喂了也会被覆盖。
72
+ const positions = layout === 'force' ? seedPositions(data, size.width, size.height) : undefined
73
+ graph.setData(toGraphData(data, { gapIds, ...(positions ? { positions } : {}) }))
74
+ void graph.render().then(() => setRenderToken(token => token + 1))
75
+
76
+ // 宿主面板宽度可被用户拖动,全屏切换也会改变尺寸,画布必须跟着走,
77
+ // 否则会留下一块空白或裁掉右侧。
78
+ const observed = new ResizeObserver(() => {
79
+ const next = measure()
80
+ if (next.width <= 0 || next.height <= 0) return
81
+ graph.setSize(next.width, next.height)
82
+ void graph.fitView()
83
+ })
84
+ observed.observe(el)
85
+ return () => {
86
+ observed.disconnect()
87
+ graph.destroy()
88
+ graphRef.current = null
89
+ }
90
+ }, [data, gapIds, rankdir, layout, mode])
91
+
92
+ useEffect(() => {
93
+ const graph = graphRef.current
94
+ if (!graph || !selectedId) return
95
+ // 选中节点可能被筛选掉了:G6 的 `setElementState` 对未知 id 是**抛异常**,
96
+ // 不是静默忽略(`data.getElementType` 里 `throw new Error`)。所以先确认它还在图上。
97
+ if (!dataRef.current.nodes.some(node => node.id === selectedId)) return
98
+ void graph.setElementState(selectedId, ['selected'])
99
+ // 上一个选中节点必须显式取消,否则连着点三个节点会留下三个蓝色描边。
100
+ return () => { void graph.setElementState(selectedId, []) }
101
+ }, [selectedId, renderToken])
102
+
103
+ return <div ref={host} className={cn('h-full w-full overflow-hidden bg-tita-surface', className)} />
104
+ }