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,184 @@
1
+ import { CircleHelp, CircleX, Lightbulb, PenLine, Play, TriangleAlert } from '@tita-data-agent/ui/icons'
2
+ import { useMemo, type ReactNode } from 'react'
3
+ import { Badge, Button, Card, cn } from '@tita-data-agent/ui/components'
4
+ import {
5
+ candidatesOf, problemBadgeOf, PROBLEM_CLASS_LABEL, SAMPLE_SIZE,
6
+ type AttributionGraphData, type AttributionNode,
7
+ } from './model.ts'
8
+ import { SEED_CASES } from '../../diagnosis/seed.js'
9
+
10
+ /**
11
+ * 本文件里凡是没有共享 Tailwind 工具类的颜色(tone 墨色、强调环、红色底边)都走行内
12
+ * `style`:工具类只在 packages/ui 构建时按「源码里出现过的类」生成,而插件不能改
13
+ * packages/ui。行内值仍然是共享令牌,所以主题照常跟着 `body[data-ds-dark-theme]` 翻。
14
+ *
15
+ * 排查视图:把候选假设按优先分排出来,每个候选挂上它的检查。
16
+ *
17
+ * 一条硬约束:**每个检查必须有一个主操作**(自动 → 执行;人工 → 派单)。
18
+ * 只展示文本不给操作,等于把这张图变回了一篇文档 —— 那正是 0021 §7.2 要挡掉的东西。
19
+ *
20
+ * 排序与优先分全部来自 `candidatesOf()`,界面不自己再算一遍「哪个该先查」。
21
+ */
22
+ export function RunView({ data, symptoms, symptomId, onSymptomChange, onSelectNode }: {
23
+ data: AttributionGraphData
24
+ /** 可选的现象清单。**必须能切**:不同现象共享同一批假设,候选排序也各不相同。 */
25
+ symptoms: AttributionNode[]
26
+ symptomId: string
27
+ onSymptomChange: (id: string) => void
28
+ onSelectNode: (id: string) => void
29
+ }): ReactNode {
30
+ const candidates = useMemo(() => candidatesOf(data, symptomId), [data, symptomId])
31
+ const symptom = data.nodes.find(node => node.id === symptomId)
32
+
33
+ /** 来源归因:这次排序把哪些案例的路径算进去了。锚定风险靠它显式可见。 */
34
+ const sourcesOf = (hypothesisId: string): string[] =>
35
+ SEED_CASES.filter(item => item.path.some(step => step.nodeId === hypothesisId) || item.misleading?.nodeId === hypothesisId)
36
+ .map(item => item.id.replace('CASE-', '#'))
37
+
38
+ const gapCandidate = candidates.find(candidate => candidate.gap)
39
+ const ranked = candidates.filter(candidate => !candidate.gap)
40
+
41
+ return <div className="flex flex-col gap-3">
42
+ {/* 现象切换:三个不同的现象会得到三份不同的排序 —— 这正是「共享假设空间」的证据。 */}
43
+ {symptoms.length > 1 && <div className="flex flex-wrap gap-1.5">
44
+ {symptoms.map(item => <Button
45
+ key={item.id}
46
+ size="sm"
47
+ variant={item.id === symptomId ? 'default' : 'outline'}
48
+ onClick={() => onSymptomChange(item.id)}
49
+ title={problemBadgeOf(item) ?? '未分类'}
50
+ >{item.problemClass ? PROBLEM_CLASS_LABEL[item.problemClass] : '未分类'} · {item.title}</Button>)}
51
+ </div>}
52
+
53
+ <header className="flex items-start justify-between gap-2">
54
+ <div className="min-w-0">
55
+ <h3 className="text-[12.5px] font-semibold">{symptom?.title ?? symptomId}</h3>
56
+ <p className="mt-0.5 text-[10.5px] leading-snug text-tita-ink-muted">
57
+ 优先分 = 根因占比 ÷ 检查成本 · 样本量 {SAMPLE_SIZE} 次(近 30 天)
58
+ </p>
59
+ </div>
60
+ <Badge tone="blue">{candidates.length + (gapCandidate ? 1 : 0)} 个候选</Badge>
61
+ </header>
62
+
63
+ <ol className="flex flex-col gap-2">
64
+ {ranked.map((candidate, index) => <li key={candidate.hypothesis.id}>
65
+ <Card
66
+ className={cn('gap-2 p-2.5', index === 0 && 'border-tita-primary')}
67
+ style={index === 0 ? { boxShadow: '0 0 0 1px var(--tita-primary-wash)' } : undefined}
68
+ >
69
+ <div className="flex items-start gap-2">
70
+ <span className={cn(
71
+ 'mt-0.5 inline-flex h-[20px] w-[20px] shrink-0 items-center justify-center rounded-full text-[10.5px] font-semibold',
72
+ index === 0 ? 'bg-tita-primary-soft text-tita-primary' : 'bg-tita-canvas text-tita-ink-muted',
73
+ )}>{index + 1}</span>
74
+ <div className="min-w-0 flex-1">
75
+ <button type="button" className="border-0 bg-transparent p-0 text-left font-[inherit] text-[12px] font-semibold hover:underline" onClick={() => onSelectNode(candidate.hypothesis.id)}>
76
+ {candidate.hypothesis.title}
77
+ </button>
78
+ <p className="mt-0.5 text-[10.5px] leading-snug text-tita-ink-muted">{candidate.hypothesis.summary}</p>
79
+ </div>
80
+ <span className={cn('shrink-0 text-[12px] font-semibold', index === 0 ? 'text-tita-primary' : 'text-tita-ink-muted')}>
81
+ {candidate.score.toFixed(1)}
82
+ </span>
83
+ </div>
84
+
85
+ <div className="flex flex-wrap gap-1">
86
+ <Chip>根因占比 {Math.round(candidate.prior * 100)}%</Chip>
87
+ <Chip>成本 {candidate.costMinutes ?? '?'} min</Chip>
88
+ <Chip tone={candidate.checks[0]?.automation === 'auto' ? 'green' : 'amber'}>
89
+ {candidate.checks[0]?.automation === 'auto' ? '自动可执行' : '需人工'}
90
+ </Chip>
91
+ <Chip>{candidate.hypothesis.reuseCount ?? 0} 个案例</Chip>
92
+ </div>
93
+
94
+ {/* 来源归因:把「这次排序被谁影响」摊开,否则锚定是无声的。 */}
95
+ <p className="text-[10px] text-tita-ink-faint">
96
+ 来源:{sourcesOf(candidate.hypothesis.id).join(' · ') || '切面先验'} · 切面 {candidate.hypothesis.failureMode ?? '未标注'}
97
+ </p>
98
+
99
+ <div className="flex flex-col gap-1">
100
+ {candidate.checks.map(check => <div key={check.id} className="flex items-center gap-2 rounded-md bg-tita-surface-muted px-2 py-1">
101
+ <Play size={12} className="shrink-0" style={{ color: 'var(--tita-tone-violet)' }} />
102
+ <button type="button" className="min-w-0 flex-1 truncate border-0 bg-transparent p-0 text-left font-mono text-[10.5px] hover:underline" onClick={() => onSelectNode(check.id)}>
103
+ {check.id} {check.title}
104
+ </button>
105
+ <Button size="sm" variant="outline" onClick={() => onSelectNode(check.id)}>
106
+ {check.automation === 'auto' ? '执行' : '派单'}
107
+ </Button>
108
+ </div>)}
109
+ </div>
110
+ </Card>
111
+ </li>)}
112
+ </ol>
113
+
114
+ {/* 空区候选:有假设、无检查。它不是「其他」,是「这张图还没建完」。 */}
115
+ {gapCandidate && <Card className="gap-1.5 border-dashed p-2.5">
116
+ <div className="flex items-center gap-2">
117
+ <Lightbulb size={14} className="shrink-0" style={{ color: 'var(--tita-tone-amber)' }} />
118
+ <button type="button" className="min-w-0 flex-1 border-0 bg-transparent p-0 text-left font-[inherit] text-[12px] font-semibold hover:underline" onClick={() => onSelectNode(gapCandidate.hypothesis.id)}>
119
+ {gapCandidate.hypothesis.title}
120
+ </button>
121
+ <Badge tone="warning">空区 · 无检查</Badge>
122
+ </div>
123
+ <p className="text-[10.5px] leading-snug text-tita-ink-muted">
124
+ {gapCandidate.hypothesis.summary}
125
+ </p>
126
+ <p className="text-[10px] text-tita-ink-faint">
127
+ 它拿不出任何可执行的检查,所以进不了排序 —— 图谱视图里的红虚环就是它。
128
+ </p>
129
+ </Card>}
130
+
131
+ <Card className="gap-1.5 border-dashed p-2.5">
132
+ <div className="flex items-center gap-2">
133
+ <CircleHelp size={14} className="shrink-0" style={{ color: 'var(--tita-tone-amber)' }} />
134
+ <span className="flex-1 text-[12px] font-semibold">未归类(假设空间之外)</span>
135
+ <Button size="sm">登记新假设</Button>
136
+ </div>
137
+ <p className="text-[10.5px] leading-snug text-tita-ink-muted">
138
+ 近 30 天 8 次 · 占比 20% —— 它是重量抽象的触发器,不是兜底话术。
139
+ </p>
140
+ </Card>
141
+
142
+ {/* 路径条:一次点击写回,否定边与误导边同样是要写回的内容。 */}
143
+ <Card className="gap-2 p-2.5">
144
+ <div className="flex items-center gap-1.5 text-[10px] tracking-wide text-tita-ink-muted">
145
+ <PenLine size={12} />本次路径
146
+ </div>
147
+ <div className="flex flex-wrap items-center gap-1">
148
+ {['S1', 'H1', 'T1.1', 'E1', 'C1'].map((id, index, all) => <span key={id} className="flex items-center gap-1">
149
+ <span className="rounded-md border border-tita-line bg-tita-surface-muted px-1.5 py-0.5 font-mono text-[10px]">{id}</span>
150
+ {index < all.length - 1 && <span className="text-[10px] text-tita-ink-faint">→</span>}
151
+ </span>)}
152
+ <span className="flex items-center gap-1">
153
+ <span className="text-[10px] text-tita-ink-faint">+</span>
154
+ <span
155
+ className="flex items-center gap-1 rounded-md border border-dashed px-1.5 py-0.5 font-mono text-[10px] text-tita-danger"
156
+ style={{ borderColor: 'var(--ddk-danger-line)', background: 'var(--ddk-danger-tint)' }}
157
+ >
158
+ <CircleX size={10} />H7
159
+ </span>
160
+ </span>
161
+ </div>
162
+ <p className="text-[10px] text-tita-ink-faint">
163
+ <TriangleAlert size={10} className="mr-1 inline align-[-1px]" />
164
+ 被否定,记为误导边 —— 否定也是要写回的内容。
165
+ </p>
166
+ <Button className="w-full" size="sm">写回图谱(记录本次路径)</Button>
167
+ </Card>
168
+ </div>
169
+ }
170
+
171
+ function Chip({ children, tone }: { children: ReactNode; tone?: 'green' | 'amber' }): ReactNode {
172
+ const toneStyle = tone === 'green'
173
+ ? { borderColor: 'var(--tita-tone-green-line)', background: 'var(--tita-tone-green-tint)', color: 'var(--tita-tone-green)' }
174
+ : tone === 'amber'
175
+ ? { borderColor: 'var(--tita-tone-amber-line)', background: 'var(--tita-tone-amber-tint)', color: 'var(--tita-tone-amber)' }
176
+ : undefined
177
+ return <span
178
+ className={cn(
179
+ 'rounded-md border px-1.5 py-0.5 text-[10px]',
180
+ !tone && 'border-tita-line bg-tita-surface-muted text-tita-ink-muted',
181
+ )}
182
+ style={toneStyle}
183
+ >{children}</span>
184
+ }
@@ -0,0 +1,403 @@
1
+ import {
2
+ ChevronDown, CircleHelp, Maximize2, Minimize2, RotateCcw, Search, SlidersHorizontal, X,
3
+ } from '@tita-data-agent/ui/icons'
4
+ import type { ReactNode } from 'react'
5
+ import { Badge, Button, Input, cn } from '@tita-data-agent/ui/components'
6
+ import {
7
+ EDGE_LEGEND, type GraphLayout,
8
+ } from './graph-options.ts'
9
+ import {
10
+ FAILURE_MODE_STYLE, FILTERABLE_KINDS, NODE_KIND_LABEL, PROBLEM_CLASSES, PROBLEM_CLASS_LABEL,
11
+ symptomCountByProblemClass,
12
+ type AttributionNode, type AttributionNodeKind, type AttributionStats, type FailureMode, type ProblemClassId,
13
+ } from './model.ts'
14
+
15
+ /**
16
+ * 图谱四周的浮层组件。
17
+ *
18
+ * 这一层只做**呈现**:所有状态和判定都在 `AttributionTab` / `model.ts` 里,组件本身
19
+ * 不读数据、不算分数、不改状态。这样「浮层放在哪个角」这种纯排版决定可以随时改,
20
+ * 而不会牵动任何业务逻辑 —— 而这恰恰是这一轮改动的主要内容。
21
+ *
22
+ * 每条浮层都走同一个面板原语 `ChromePanel`:半透明白底 + 细描边 + 轻阴影。
23
+ * 之所以不各写一套边框,是因为它们都盖在画布上,**任何一条比别的更重都会把注意力
24
+ * 从图上偷走**。统一是这里唯一正确的风格决定。
25
+ *
26
+ * 半透明底与 tone 墨色走行内 `style`:`bg-tita-surface/95`、`text-tita-tone-*` 这类
27
+ * 工具类在 packages/ui 的构建产物里不存在(工具类只按「源码里出现过的类」生成,而
28
+ * 插件不能改 packages/ui),所以这里直接给共享令牌。阴影仍是字面量(见仓库约定)。
29
+ */
30
+ export function ChromePanel({ className, children }: { className?: string; children: ReactNode }): ReactNode {
31
+ return <div
32
+ className={cn(
33
+ 'rounded-[10px] border border-tita-line shadow-[0_2px_10px_rgb(15_23_42_/_0.08)] backdrop-blur-[2px]',
34
+ className,
35
+ )}
36
+ style={{ background: 'color-mix(in srgb, var(--tita-surface) 95%, transparent)' }}
37
+ >{children}</div>
38
+ }
39
+
40
+ /** 浮层里的小按钮:比标准 `sm` 再矮 4px,否则一条 380px 宽的浮层会被按钮撑到两行。 */
41
+ const MINI = 'h-[26px] min-h-[26px] px-2 text-[11px] font-[600]'
42
+
43
+ /** 可多选的切面 chip。选中态用描边而不是填色:填色会把失败模式的语义色盖掉。 */
44
+ export function Chip({ active, color, onClick, children, title }: {
45
+ active: boolean
46
+ color?: string
47
+ onClick: () => void
48
+ children: ReactNode
49
+ title?: string
50
+ }): ReactNode {
51
+ return <button
52
+ type="button"
53
+ onClick={onClick}
54
+ aria-pressed={active}
55
+ title={title}
56
+ className={cn(
57
+ 'rounded-md border px-1.5 py-0.5 text-[10px] leading-[15px]',
58
+ active
59
+ // 选中态必须**显式**给边框色:没有 preflight 时 <button> 的 border-color 会退回
60
+ // 浏览器的 ButtonBorder(近黑),于是「选中」看起来比「未选中」还脏。
61
+ ? 'border-tita-primary bg-tita-primary-soft font-medium text-tita-primary'
62
+ : 'border-tita-line bg-tita-surface text-tita-ink-muted hover:border-tita-line-strong',
63
+ )}
64
+ style={active && color ? { borderColor: color, color } : undefined}
65
+ >{children}</button>
66
+ }
67
+
68
+ /* ───────────────────────────────────────────────── 左上:分层 + 类型 ── */
69
+
70
+ /**
71
+ * 左上角工具条:**布局**(这张图怎么摆)与**类型切面**(看哪些节点)。
72
+ *
73
+ * 两件事放在一起是因为它们都是「改变我看到的这张图」,而失败模式/文本筛选是
74
+ * 「在图里找东西」—— 后者留在底部,前者贴住左上,动线上就不互相打架。
75
+ *
76
+ * 宽度钉在 240px:它是**这一行左侧能拿到的全部预算**(380px 侧栏减内边距、
77
+ * 再减右上角那对图标按钮)。不写成自适应是因为自适应在窄容器里会先压扁
78
+ * 「力导向 / 分层」这两个按钮,正好是最不该被压的两个。
79
+ */
80
+ export function ToolRail({ layout, onLayout, onReset, kinds, onToggleKind, problems, onToggleProblem, symptomCounts, hint, onCollapse }: {
81
+ layout: GraphLayout
82
+ onLayout: (layout: GraphLayout) => void
83
+ onReset: () => void
84
+ kinds: ReadonlySet<AttributionNodeKind>
85
+ onToggleKind: (kind: AttributionNodeKind) => void
86
+ /** 问题大类切面。它排在类型之前 —— 「哪类问题」比「哪些节点」更靠前。 */
87
+ problems: ReadonlySet<ProblemClassId>
88
+ onToggleProblem: (id: ProblemClassId) => void
89
+ symptomCounts: Record<ProblemClassId, number>
90
+ hint: string
91
+ onCollapse: () => void
92
+ }): ReactNode {
93
+ return <ChromePanel className="flex w-[min(240px,100%)] flex-col gap-1.5 p-2">
94
+ <div className="flex items-center gap-1">
95
+ <span className="mr-0.5 text-[10px] font-[650] text-tita-ink-faint">布局</span>
96
+ {([['force', '力导向'], ['layered', '分层']] as Array<[GraphLayout, string]>).map(([id, label]) =>
97
+ <Button
98
+ key={id}
99
+ size="sm"
100
+ variant={layout === id ? 'default' : 'outline'}
101
+ className={MINI}
102
+ onClick={() => onLayout(id)}
103
+ >{label}</Button>)}
104
+ <Button size="icon" variant="outline" className="ml-auto h-[26px] w-[26px] min-w-[26px]" title="重新布局" aria-label="重新布局" onClick={onReset}>
105
+ <RotateCcw size={12} />
106
+ </Button>
107
+ <Button size="icon" variant="ghost" className="h-[26px] w-[26px] min-w-[26px]" title="收起工具条" aria-label="收起工具条" onClick={onCollapse}>
108
+ <ChevronDown size={12} />
109
+ </Button>
110
+ </div>
111
+
112
+ <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
113
+ <span className="shrink-0 pt-0.5 font-[650] text-tita-ink-faint">问题</span>
114
+ <div className="flex flex-wrap gap-1">
115
+ {PROBLEM_CLASSES.map(item => <Chip
116
+ key={item.id}
117
+ active={problems.has(item.id)}
118
+ title={`${item.cue} · ${symptomCounts[item.id]} 个现象`}
119
+ onClick={() => onToggleProblem(item.id)}
120
+ >{item.label}<span className="ml-1 font-mono text-[9.5px] opacity-70">{symptomCounts[item.id]}</span></Chip>)}
121
+ </div>
122
+ </div>
123
+
124
+ <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
125
+ <span className="shrink-0 pt-0.5 font-[650] text-tita-ink-faint">类型</span>
126
+ <div className="flex flex-wrap gap-1">
127
+ {FILTERABLE_KINDS.map(kind => <Chip key={kind} active={kinds.has(kind)} onClick={() => onToggleKind(kind)}>
128
+ {NODE_KIND_LABEL[kind]}
129
+ </Chip>)}
130
+ </div>
131
+ </div>
132
+
133
+ <p className="text-[9.5px] leading-snug text-tita-ink-faint">{hint}</p>
134
+ </ChromePanel>
135
+ }
136
+
137
+ /** 收起来之后留下的那颗药丸。收起不等于隐藏 —— 数字还在上面。 */
138
+ export function ToolPill({ layout, kinds, problems, onClick }: {
139
+ layout: GraphLayout
140
+ kinds: ReadonlySet<AttributionNodeKind>
141
+ problems: ReadonlySet<ProblemClassId>
142
+ onClick: () => void
143
+ }): ReactNode {
144
+ const active = kinds.size + problems.size
145
+ return <ChromePanel className="p-[3px]">
146
+ <Button size="sm" variant="ghost" className={MINI} onClick={onClick} title="展开问题大类、布局与类型筛选">
147
+ <SlidersHorizontal size={12} />
148
+ {layout === 'force' ? '力导向' : '分层'}
149
+ {active > 0 && <span className="rounded-[4px] bg-tita-primary-soft px-1 font-mono text-[9.5px] text-tita-primary">{active}</span>}
150
+ </Button>
151
+ </ChromePanel>
152
+ }
153
+
154
+ /* ───────────────────────────────────────────────── 右上:图例 + 全屏 ── */
155
+
156
+ /**
157
+ * 右上角的一对图标按钮:展开图例、进出全屏。
158
+ *
159
+ * 为什么图例不直接摊开在右上角:**380px 的一行放不下两块面板**(工具条内容
160
+ * 约 240px,图例配色至少 200px)。硬挤的结果是两块都变形 —— 所以这里只留
161
+ * 「图例」这一个入口,展开的正文落到下一行右侧(`GraphStage` 的 `side` 槽),
162
+ * 那里是整行宽度,放得下 9 种边和 4 条说明。
163
+ *
164
+ * 这也是「图例在右上角」的正确读法:**入口在右上角,正文跟着入口展开**,
165
+ * 而不是把正文钉死在角上、盖住半张图。
166
+ */
167
+ export function LegendToggle({ open, onToggle, full, onToggleFull, showLegend = true }: {
168
+ open: boolean
169
+ onToggle: () => void
170
+ full: boolean
171
+ onToggleFull: () => void
172
+ /**
173
+ * 案例 / 排查视图里没有图可读,图例就没有对象。
174
+ *
175
+ * 但**全屏按钮必须留下来**:全屏是「这一整块(含三个视图)占满视口」,
176
+ * 换个视图就丢掉出口,是这一轮最不该犯的错。
177
+ */
178
+ showLegend?: boolean
179
+ }): ReactNode {
180
+ return <ChromePanel className="flex items-center gap-1 p-[3px]">
181
+ {showLegend && <Button
182
+ size="sm"
183
+ variant={open ? 'default' : 'ghost'}
184
+ className="h-[22px] min-h-[22px] gap-1 px-1.5 text-[10.5px]"
185
+ aria-label={open ? '隐藏图例' : '显示图例'}
186
+ aria-expanded={open}
187
+ title={open ? '隐藏图例' : '显示图例:配色、九种边、空区与僵尸边'}
188
+ onClick={onToggle}
189
+ ><CircleHelp size={12} />图例</Button>}
190
+ <Button
191
+ size="icon"
192
+ variant="outline"
193
+ className="h-[22px] w-[22px] min-w-[22px]"
194
+ aria-label={full ? '退出全屏' : '全屏查看'}
195
+ title={full ? '退出全屏(Esc)' : '全屏查看:整个视口都用来放这块内容'}
196
+ onClick={onToggleFull}
197
+ >{full ? <Minimize2 size={12} /> : <Maximize2 size={12} />}</Button>
198
+ </ChromePanel>
199
+ }
200
+
201
+ /**
202
+ * 图例正文:**整块由 `open` 控制**。
203
+ *
204
+ * 不做「配色常显、说明可折」的分层 —— 那样关掉图例时角上仍留着一排颜色块,
205
+ * 用户会以为「图例已经开着」,而真正说明含义的那半仍然是藏着的。要么整块在,
206
+ * 要么整块不在,状态才可读。
207
+ *
208
+ * **两个分组必须分开写。** 这里的五种颜色**全部是原因侧**(失败模式,挂在假设上),
209
+ * 一份只画颜色的图例会让人以为它在说「问题有哪几类」—— 这正是最常被看错的一处。
210
+ * 所以原因侧明写「失败模式 · 原因侧」,问题侧(现象)单独一段、并且把
211
+ * 「现象节点有几个、有没有分类」如实报出来:**分类还没建的地方要看得见,而不是留白。**
212
+ */
213
+ export function LegendBody({ stats }: { stats: AttributionStats }): ReactNode {
214
+ return <ChromePanel className="flex flex-col gap-1.5 p-2">
215
+ <div className="text-[9.5px] leading-[14px] text-tita-ink-faint">
216
+ <span className="font-[650] text-tita-ink-muted">失败模式</span> · 原因侧(挂在假设上)
217
+ </div>
218
+ <div className="flex flex-wrap gap-1">
219
+ {Object.entries(FAILURE_MODE_STYLE).map(([key, tone]) => <span
220
+ key={key}
221
+ className="flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] leading-[15px]"
222
+ style={{ borderColor: tone.stroke, background: tone.fill, color: tone.text }}
223
+ >
224
+ <span className="inline-block h-2 w-2 rounded-full" style={{ background: tone.stroke }} />{tone.label}
225
+ </span>)}
226
+ </div>
227
+
228
+ <div className="flex flex-col gap-1 border-t border-tita-line pt-1.5">
229
+ <div className="text-[9.5px] leading-[14px] text-tita-ink-faint">
230
+ <span className="font-[650] text-tita-ink-muted">问题</span> · 现象侧({stats.symptoms} 个现象节点 / {stats.problemClasses} 个大类)
231
+ </div>
232
+ <div className="flex flex-wrap gap-1">
233
+ {PROBLEM_CLASSES.map(item => <span
234
+ key={item.id}
235
+ className="rounded-md border border-tita-line bg-tita-surface-muted px-1.5 py-0.5 text-[10px] leading-[15px] text-tita-ink-muted"
236
+ title={item.cue}
237
+ >{item.label}</span>)}
238
+ </div>
239
+ <p className="text-[9.5px] leading-relaxed text-tita-ink-muted">
240
+ 问题大类刻意不给颜色:颜色通道已经被失败模式占用。问题侧走文字 —— 现象节点下方的
241
+ 徽标写着它属于哪个大类,左上的「问题」切面按大类收窄,并把它的检查一起带出来。
242
+ </p>
243
+ </div>
244
+
245
+ <div className="flex flex-col gap-1.5 border-t border-tita-line pt-1.5">
246
+ <div className="flex flex-wrap gap-x-2 gap-y-1">
247
+ {EDGE_LEGEND.map(edge => <span key={edge.kind} className="flex items-center gap-1 text-[9.5px] text-tita-ink-muted">
248
+ <svg width="16" height="6" aria-hidden="true"><line
249
+ x1="0" y1="3" x2="16" y2="3"
250
+ stroke={edge.stroke} strokeWidth={edge.lineWidth}
251
+ strokeDasharray={edge.lineDash?.join(' ')}
252
+ /></svg>{edge.label}
253
+ </span>)}
254
+ </div>
255
+ <ul className="flex flex-col gap-0.5 text-[9.5px] leading-relaxed text-tita-ink-muted">
256
+ <li>圆的大小 = 复用次数;圆里是节点 id,标题挂在圆下方</li>
257
+ <li><span className="text-tita-danger">红色虚环 + 空区徽标</span> = 有假设但一条检查都没有(当前 {stats.gaps} 个)</li>
258
+ <li><span className="text-tita-ink-faint">灰色虚线</span> = 僵尸边,90 天未走(当前 {stats.zombies} 条)</li>
259
+ <li>实例层默认折叠 —— 它随每次排查线性增长,展开会把类型层挤成小点</li>
260
+ </ul>
261
+ </div>
262
+ </ChromePanel>
263
+ }
264
+
265
+ /* ───────────────────────────────────────────── 底部中间:其余筛选 ── */
266
+
267
+ /**
268
+ * 底部中间的筛选条:文本、失败模式、邻居展开、清除。
269
+ *
270
+ * 放在底部而不是顶部,是因为它是**读图之后的动作**:先看到形状,再收窄。
271
+ * 而顶部的分层与类型是「决定这张图怎么画」,顺序相反。
272
+ */
273
+ export function FilterBar({ query, onQuery, modes, onToggleMode, includeNeighbors, onIncludeNeighbors, filtering, onClear, hit, total }: {
274
+ query: string
275
+ onQuery: (value: string) => void
276
+ modes: ReadonlySet<FailureMode>
277
+ onToggleMode: (mode: FailureMode) => void
278
+ includeNeighbors: boolean
279
+ onIncludeNeighbors: (value: boolean) => void
280
+ filtering: boolean
281
+ onClear: () => void
282
+ hit: number
283
+ total: number
284
+ }): ReactNode {
285
+ return <ChromePanel className="flex w-[min(520px,100%)] flex-col gap-1.5 p-2">
286
+ <div className="flex items-center gap-1.5">
287
+ <div className="relative min-w-0 flex-1">
288
+ <Search size={12} className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-tita-ink-faint" />
289
+ <Input
290
+ value={query}
291
+ onChange={event => onQuery(event.target.value)}
292
+ placeholder="筛选节点:id / 标题 / 判据…"
293
+ className="h-[26px] min-h-[26px] pl-6 text-[11px]"
294
+ />
295
+ </div>
296
+ <span className="shrink-0 text-[10px] tabular-nums text-tita-ink-faint">命中 {hit}/{total}</span>
297
+ {filtering && <Button size="sm" variant="ghost" className={MINI} onClick={onClear}><X size={11} />清除</Button>}
298
+ </div>
299
+
300
+ <div className="flex items-start gap-1.5 text-[10px] text-tita-ink-muted">
301
+ <span className="shrink-0 pt-0.5 font-[650] text-tita-ink-faint">切面</span>
302
+ <div className="flex flex-wrap gap-1">
303
+ {Object.entries(FAILURE_MODE_STYLE).map(([key, tone]) => <Chip
304
+ key={key}
305
+ active={modes.has(key as FailureMode)}
306
+ color={tone.stroke}
307
+ onClick={() => onToggleMode(key as FailureMode)}
308
+ >{tone.label}</Chip>)}
309
+ </div>
310
+ <Chip active={includeNeighbors} onClick={() => onIncludeNeighbors(!includeNeighbors)} title="只留命中节点会让图散成孤点">
311
+ 保留 1 跳邻居
312
+ </Chip>
313
+ </div>
314
+ </ChromePanel>
315
+ }
316
+
317
+ /* ───────────────────────────────────────────────── 右下:统计 ── */
318
+
319
+ /**
320
+ * 右下角统计。
321
+ *
322
+ * 只放**随筛选变化**的数:它回答的是「我现在看的这张图有多大、缺多少」。
323
+ * 图例里已经写了空区/僵尸的含义,这里只给数,不重复解释。
324
+ *
325
+ * 数据来源徽标(`05_diagnosis` / `seed 数据`)也挂在这里:它和这些数一样是
326
+ * 「这份内容从哪来」的事实,而且它必须始终可见 —— 看到的是真数据还是兜底样例,
327
+ * 不能靠猜。
328
+ */
329
+ export function StatsStrip({ stats, source, problems }: {
330
+ stats: AttributionStats
331
+ source: string
332
+ /** 编译 / 迁移过程中发现的问题。空数组表示一切正常。 */
333
+ problems: string[]
334
+ }): ReactNode {
335
+ return <ChromePanel className="flex flex-wrap items-center gap-x-2 gap-y-0.5 px-2 py-1">
336
+ <Stat label="类型" value={stats.typeNodes} />
337
+ <Divider />
338
+ <Stat label="引用" value={stats.referenceNodes} />
339
+ <Divider />
340
+ <Stat label="复用" value={stats.reuseTotal} />
341
+ <Divider />
342
+ <Stat label="空区" value={stats.gaps} tone={stats.gaps > 0 ? 'warning' : undefined} />
343
+ <Divider />
344
+ <Stat label="僵尸边" value={stats.zombies} tone={stats.zombies > 0 ? 'muted' : undefined} />
345
+ {problems.length > 0 && <span
346
+ className="shrink-0 rounded-[4px] border px-1 text-[9.5px] text-tita-danger"
347
+ style={{ borderColor: 'var(--ddk-danger-line)', background: 'var(--ddk-danger-tint)' }}
348
+ title={problems.join('\n')}
349
+ >{problems.length} 个问题</span>}
350
+ <span
351
+ className="ml-auto shrink-0 rounded-[4px] bg-tita-surface-muted px-1 font-mono text-[9.5px] text-tita-ink-faint"
352
+ title={SOURCE_HINT[source] ?? SOURCE_HINT['seed 数据']}
353
+ >{source}</span>
354
+ </ChromePanel>
355
+ }
356
+
357
+ /**
358
+ * 数据来源的说明。
359
+ *
360
+ * 这里必须能分清三种形态,因为它们意味着不同的可信度:卡片是当前真相,
361
+ * `05_diagnosis` 是**旧格式**(迁移没能无损生效),`seed 数据` 是宿主没就绪时的兜底示例。
362
+ * 一句笼统的「05_diagnosis」会把它们混成一种,而这正是最不该让人猜的地方。
363
+ */
364
+ const SOURCE_HINT: Record<string, string> = {
365
+ '02_wiki/diagnosis': '来自 wiki 卡片(诊断卡片是当前唯一的真相)',
366
+ '05_diagnosis(旧格式)': '旧格式 jsonl:迁移会丢知识,因此仍按原样读',
367
+ '05_diagnosis': '来自 05_diagnosis 目录',
368
+ 'seed 数据': '宿主未就绪,当前展示的是内置示例',
369
+ }
370
+
371
+ function Divider(): ReactNode {
372
+ return <span aria-hidden="true" className="h-3 w-px shrink-0 bg-tita-line" />
373
+ }
374
+
375
+ function Stat({ label, value, tone }: { label: string; value: number; tone?: 'warning' | 'muted' }): ReactNode {
376
+ return <span className="flex shrink-0 items-baseline gap-1" title={label}>
377
+ <span className="text-[9.5px] text-tita-ink-faint">{label}</span>
378
+ <span
379
+ className={cn('text-[12px] font-[700] tabular-nums', !tone && 'text-tita-ink')}
380
+ style={tone ? { color: tone === 'warning' ? 'var(--tita-tone-amber)' : 'var(--tita-ink-faint)' } : undefined}
381
+ >{value}</span>
382
+ </span>
383
+ }
384
+
385
+ /* ───────────────────────────────────────────── 选中节点详情卡 ── */
386
+
387
+ export function NodeDetailCard({ node }: { node: AttributionNode }): ReactNode {
388
+ return <ChromePanel className="flex flex-col gap-1.5 p-2">
389
+ <div className="flex items-start justify-between gap-2">
390
+ <span className="text-[11.5px] font-semibold leading-snug">{node.title}</span>
391
+ <span className="shrink-0 font-mono text-[9.5px] text-tita-ink-faint">{node.id}</span>
392
+ </div>
393
+ {node.summary && <p className="text-[10.5px] leading-relaxed text-tita-ink-muted">{node.summary}</p>}
394
+ <div className="flex flex-wrap gap-1">
395
+ <Badge tone="neutral">{NODE_KIND_LABEL[node.kind]}</Badge>
396
+ {node.failureMode && <Badge tone="violet">{FAILURE_MODE_STYLE[node.failureMode].label}</Badge>}
397
+ {node.reuseCount !== undefined && <Badge tone="neutral">复用 {node.reuseCount}</Badge>}
398
+ {node.costMinutes !== undefined && <Badge tone={node.automation === 'auto' ? 'success' : 'warning'}>
399
+ {node.costMinutes} min · {node.automation === 'auto' ? '自动' : '人工'}
400
+ </Badge>}
401
+ </div>
402
+ </ChromePanel>
403
+ }