dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.15

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 (266) hide show
  1. package/README.md +89 -56
  2. package/cordis.patch.yml +36 -129
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +3262 -4142
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1147 -364
  7. package/lib/types/axis-registry.d.ts +8 -1
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +62 -7
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/convert/host.d.ts.map +1 -1
  12. package/lib/types/convert/host.js +3 -0
  13. package/lib/types/convert/host.js.map +1 -1
  14. package/lib/types/convert/ingest.d.ts +9 -0
  15. package/lib/types/convert/ingest.d.ts.map +1 -1
  16. package/lib/types/convert/ingest.js +68 -9
  17. package/lib/types/convert/ingest.js.map +1 -1
  18. package/lib/types/diagnosis/cards.d.ts +7 -3
  19. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  20. package/lib/types/diagnosis/cards.js +13 -8
  21. package/lib/types/diagnosis/cards.js.map +1 -1
  22. package/lib/types/diagnosis/run.d.ts +4 -13
  23. package/lib/types/diagnosis/run.d.ts.map +1 -1
  24. package/lib/types/diagnosis/run.js +26 -16
  25. package/lib/types/diagnosis/run.js.map +1 -1
  26. package/lib/types/diagnosis/schema.js +2 -2
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +5 -4
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +4 -4
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +6 -8
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +18 -17
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +94 -66
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +10 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js.map +1 -1
  43. package/lib/types/environment/dsh-python.d.ts +77 -0
  44. package/lib/types/environment/dsh-python.d.ts.map +1 -0
  45. package/lib/types/environment/dsh-python.js +139 -0
  46. package/lib/types/environment/dsh-python.js.map +1 -0
  47. package/lib/types/environment/install.d.ts +9 -1
  48. package/lib/types/environment/install.d.ts.map +1 -1
  49. package/lib/types/environment/install.js +149 -24
  50. package/lib/types/environment/install.js.map +1 -1
  51. package/lib/types/environment/model-catalog.d.ts +34 -2
  52. package/lib/types/environment/model-catalog.d.ts.map +1 -1
  53. package/lib/types/environment/model-catalog.js +40 -2
  54. package/lib/types/environment/model-catalog.js.map +1 -1
  55. package/lib/types/environment/service.d.ts +13 -3
  56. package/lib/types/environment/service.d.ts.map +1 -1
  57. package/lib/types/environment/service.js +127 -23
  58. package/lib/types/environment/service.js.map +1 -1
  59. package/lib/types/environment/types.d.ts +9 -0
  60. package/lib/types/environment/types.d.ts.map +1 -1
  61. package/lib/types/events.d.ts +2 -0
  62. package/lib/types/events.d.ts.map +1 -1
  63. package/lib/types/events.js +13 -0
  64. package/lib/types/events.js.map +1 -1
  65. package/lib/types/extract-contract.d.ts +1 -1
  66. package/lib/types/extract-contract.d.ts.map +1 -1
  67. package/lib/types/extract-contract.js +34 -31
  68. package/lib/types/extract-contract.js.map +1 -1
  69. package/lib/types/extract-model.d.ts +2 -0
  70. package/lib/types/extract-model.d.ts.map +1 -1
  71. package/lib/types/extract-model.js.map +1 -1
  72. package/lib/types/extract-table-contract.d.ts +3 -0
  73. package/lib/types/extract-table-contract.d.ts.map +1 -0
  74. package/lib/types/extract-table-contract.js +30 -0
  75. package/lib/types/extract-table-contract.js.map +1 -0
  76. package/lib/types/extract-table.d.ts +8 -0
  77. package/lib/types/extract-table.d.ts.map +1 -0
  78. package/lib/types/extract-table.js +151 -0
  79. package/lib/types/extract-table.js.map +1 -0
  80. package/lib/types/extraction-runs.d.ts +30 -6
  81. package/lib/types/extraction-runs.d.ts.map +1 -1
  82. package/lib/types/extraction-runs.js +69 -8
  83. package/lib/types/extraction-runs.js.map +1 -1
  84. package/lib/types/fs-atomic.d.ts +0 -7
  85. package/lib/types/fs-atomic.d.ts.map +1 -1
  86. package/lib/types/fs-atomic.js +17 -2
  87. package/lib/types/fs-atomic.js.map +1 -1
  88. package/lib/types/images.d.ts +1 -1
  89. package/lib/types/images.d.ts.map +1 -1
  90. package/lib/types/images.js +2 -2
  91. package/lib/types/images.js.map +1 -1
  92. package/lib/types/index.d.ts.map +1 -1
  93. package/lib/types/index.js +232 -50
  94. package/lib/types/index.js.map +1 -1
  95. package/lib/types/intent/scope.d.ts.map +1 -1
  96. package/lib/types/intent/scope.js +1 -0
  97. package/lib/types/intent/scope.js.map +1 -1
  98. package/lib/types/intent/tools.d.ts +19 -0
  99. package/lib/types/intent/tools.d.ts.map +1 -1
  100. package/lib/types/intent/tools.js +37 -1
  101. package/lib/types/intent/tools.js.map +1 -1
  102. package/lib/types/knowledge-bases.d.ts.map +1 -1
  103. package/lib/types/knowledge-bases.js +5 -16
  104. package/lib/types/knowledge-bases.js.map +1 -1
  105. package/lib/types/knowledge.d.ts +3 -0
  106. package/lib/types/knowledge.d.ts.map +1 -1
  107. package/lib/types/knowledge.js +21 -5
  108. package/lib/types/knowledge.js.map +1 -1
  109. package/lib/types/materialize-snapshot.d.ts +49 -0
  110. package/lib/types/materialize-snapshot.d.ts.map +1 -0
  111. package/lib/types/materialize-snapshot.js +2 -0
  112. package/lib/types/materialize-snapshot.js.map +1 -0
  113. package/lib/types/model-text.d.ts +1 -1
  114. package/lib/types/model-text.d.ts.map +1 -1
  115. package/lib/types/model-text.js +5 -4
  116. package/lib/types/model-text.js.map +1 -1
  117. package/lib/types/oplog.d.ts.map +1 -1
  118. package/lib/types/oplog.js +7 -0
  119. package/lib/types/oplog.js.map +1 -1
  120. package/lib/types/pipeline/engine.d.ts +60 -0
  121. package/lib/types/pipeline/engine.d.ts.map +1 -0
  122. package/lib/types/pipeline/engine.js +338 -0
  123. package/lib/types/pipeline/engine.js.map +1 -0
  124. package/lib/types/pipeline/store.d.ts +73 -0
  125. package/lib/types/pipeline/store.d.ts.map +1 -0
  126. package/lib/types/pipeline/store.js +143 -0
  127. package/lib/types/pipeline/store.js.map +1 -0
  128. package/lib/types/pipeline/types.d.ts +101 -0
  129. package/lib/types/pipeline/types.d.ts.map +1 -0
  130. package/lib/types/pipeline/types.js +5 -0
  131. package/lib/types/pipeline/types.js.map +1 -0
  132. package/lib/types/present.d.ts +2 -0
  133. package/lib/types/present.d.ts.map +1 -1
  134. package/lib/types/present.js +9 -4
  135. package/lib/types/present.js.map +1 -1
  136. package/lib/types/retention.d.ts +39 -0
  137. package/lib/types/retention.d.ts.map +1 -0
  138. package/lib/types/retention.js +42 -0
  139. package/lib/types/retention.js.map +1 -0
  140. package/lib/types/runtime.d.ts +115 -5
  141. package/lib/types/runtime.d.ts.map +1 -1
  142. package/lib/types/runtime.js +862 -132
  143. package/lib/types/runtime.js.map +1 -1
  144. package/lib/types/source-evidence.d.ts +9 -1
  145. package/lib/types/source-evidence.d.ts.map +1 -1
  146. package/lib/types/source-evidence.js +13 -5
  147. package/lib/types/source-evidence.js.map +1 -1
  148. package/lib/types/store/axis-references.js +3 -3
  149. package/lib/types/store/axis-references.js.map +1 -1
  150. package/lib/types/store/derived.d.ts +1 -1
  151. package/lib/types/store/derived.d.ts.map +1 -1
  152. package/lib/types/store/derived.js +41 -7
  153. package/lib/types/store/derived.js.map +1 -1
  154. package/lib/types/store/knowledge-db.d.ts +18 -0
  155. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  156. package/lib/types/store/knowledge-db.js +111 -6
  157. package/lib/types/store/knowledge-db.js.map +1 -1
  158. package/lib/types/store/migrate.js +1 -1
  159. package/lib/types/store/migrate.js.map +1 -1
  160. package/lib/types/trajectory-types.d.ts +172 -0
  161. package/lib/types/trajectory-types.d.ts.map +1 -0
  162. package/lib/types/trajectory-types.js +36 -0
  163. package/lib/types/trajectory-types.js.map +1 -0
  164. package/lib/types/types.d.ts +3 -4
  165. package/lib/types/types.d.ts.map +1 -1
  166. package/lib/types/types.js.map +1 -1
  167. package/lib/types/wiki-finalize.d.ts +16 -0
  168. package/lib/types/wiki-finalize.d.ts.map +1 -1
  169. package/lib/types/wiki-finalize.js +26 -0
  170. package/lib/types/wiki-finalize.js.map +1 -1
  171. package/lib/types/wiki-materialize.d.ts.map +1 -1
  172. package/lib/types/wiki-materialize.js +3 -15
  173. package/lib/types/wiki-materialize.js.map +1 -1
  174. package/lib/types/wiki-trial-types.d.ts +3 -0
  175. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  176. package/lib/types/wiki-trial.d.ts +2 -0
  177. package/lib/types/wiki-trial.d.ts.map +1 -1
  178. package/lib/types/wiki-trial.js +31 -7
  179. package/lib/types/wiki-trial.js.map +1 -1
  180. package/package.json +6 -3
  181. package/preset/tita-data-agent/agent.cordis.yml +36 -129
  182. package/src/axis-registry.ts +57 -9
  183. package/src/client/AttributionEventGuide.tsx +10 -9
  184. package/src/client/CompileDialogs.tsx +10 -2
  185. package/src/client/EnvironmentSetup.tsx +76 -16
  186. package/src/client/ExpansionGraph.tsx +13 -7
  187. package/src/client/ExtractionTasksPage.tsx +543 -156
  188. package/src/client/ImportFilesDialog.tsx +2 -13
  189. package/src/client/IntentTab.tsx +3 -1
  190. package/src/client/KnowledgeBaseTab.tsx +139 -122
  191. package/src/client/KnowledgeLayerSection.tsx +19 -2
  192. package/src/client/KnowledgePanel.tsx +17 -3
  193. package/src/client/ListPager.tsx +15 -5
  194. package/src/client/MaterializeTrajectoryView.tsx +124 -0
  195. package/src/client/NodeSettings.tsx +90 -25
  196. package/src/client/NormalizeTrajectoryView.tsx +136 -0
  197. package/src/client/PipelineNotice.tsx +26 -0
  198. package/src/client/RecallPanel.tsx +55 -25
  199. package/src/client/TabGuide.tsx +4 -4
  200. package/src/client/TrialReportView.tsx +61 -16
  201. package/src/client/TypeDefinitionDrawer.tsx +33 -9
  202. package/src/client/WikiCardPreview.tsx +43 -10
  203. package/src/client/WikiFlowPanel.tsx +4 -4
  204. package/src/client/architecture.tsx +1 -1
  205. package/src/client/attribution/AttributionTab.tsx +37 -36
  206. package/src/client/attribution/CaseView.tsx +3 -1
  207. package/src/client/attribution/EventView.tsx +14 -4
  208. package/src/client/attribution/RunView.tsx +2 -2
  209. package/src/client/attribution/ViewSwitch.tsx +20 -5
  210. package/src/client/attribution/graph-options.ts +38 -31
  211. package/src/client/attribution/model.ts +16 -9
  212. package/src/client/graph/palette.ts +165 -0
  213. package/src/client/pages.css +224 -28
  214. package/src/client/pages.tsx +33 -11
  215. package/src/client/styles.css +61 -6
  216. package/src/client/wiki-trial.ts +30 -1
  217. package/src/convert/host.ts +3 -0
  218. package/src/convert/ingest.ts +73 -9
  219. package/src/diagnosis/cards.ts +13 -8
  220. package/src/diagnosis/run.ts +27 -17
  221. package/src/diagnosis/schema.ts +2 -2
  222. package/src/diagnosis/seed-source.ts +5 -4
  223. package/src/diagnosis/seed.ts +6 -8
  224. package/src/diagnosis/store.ts +99 -69
  225. package/src/diagnosis/types.ts +10 -16
  226. package/src/environment/dsh-python.ts +156 -0
  227. package/src/environment/install.ts +134 -19
  228. package/src/environment/model-catalog.ts +45 -3
  229. package/src/environment/service.ts +111 -23
  230. package/src/environment/types.ts +6 -0
  231. package/src/events.ts +12 -0
  232. package/src/extract-contract.ts +6 -3
  233. package/src/extract-model.ts +2 -0
  234. package/src/extract-table-contract.ts +29 -0
  235. package/src/extract-table.ts +126 -0
  236. package/src/extraction-runs.ts +70 -11
  237. package/src/fs-atomic.ts +19 -2
  238. package/src/images.ts +2 -2
  239. package/src/index.ts +207 -72
  240. package/src/intent/scope.ts +1 -0
  241. package/src/intent/tools.ts +38 -1
  242. package/src/knowledge-bases.ts +4 -12
  243. package/src/knowledge.ts +24 -4
  244. package/src/materialize-snapshot.ts +46 -0
  245. package/src/model-text.ts +5 -3
  246. package/src/oplog.ts +7 -0
  247. package/src/pipeline/engine.ts +312 -0
  248. package/src/pipeline/store.ts +147 -0
  249. package/src/pipeline/types.ts +92 -0
  250. package/src/present.ts +10 -4
  251. package/src/retention.ts +76 -0
  252. package/src/runtime.ts +853 -131
  253. package/src/source-evidence.ts +12 -5
  254. package/src/store/axis-references.ts +3 -3
  255. package/src/store/derived.ts +40 -7
  256. package/src/store/knowledge-db.ts +111 -5
  257. package/src/store/migrate.ts +1 -1
  258. package/src/trajectory-types.ts +142 -0
  259. package/src/types.ts +3 -4
  260. package/src/wiki-finalize.ts +28 -0
  261. package/src/wiki-materialize.ts +3 -14
  262. package/src/wiki-trial-types.ts +3 -0
  263. package/src/wiki-trial.ts +26 -7
  264. package/src/client/convert/ConvertPanel.tsx +0 -583
  265. package/src/client/convert/styles.css +0 -138
  266. package/src/client/convert/styles.ts +0 -49
@@ -2,8 +2,8 @@ import {
2
2
  CircleDot, Database, GitBranch, History, Sparkles, Stethoscope, X,
3
3
  } from '@tita-agent/ui/icons'
4
4
  import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
5
- import { createPortal } from 'react-dom'
6
5
  import { Button } from '@tita-agent/ui/components'
6
+ import { Dialog, DialogContent, DialogTitle } from '@tita-agent/ui/overlays'
7
7
  import { AttributionGraph } from './AttributionGraph.tsx'
8
8
  import { CaseView } from './CaseView.tsx'
9
9
  import { EventView } from './EventView.tsx'
@@ -37,8 +37,9 @@ interface DiagnosisPayload {
37
37
 
38
38
  /** 服务端的三种来源 → 徽标上那句人话。 */
39
39
  function sourceLabelOf(source: string | undefined): string {
40
- if (source === 'cards') return '02_wiki/diagnosis'
41
- if (source === 'jsonl') return '05_diagnosis(旧格式)'
40
+ if (source === 'cards') return '可复用诊断卡片'
41
+ if (source === 'records') return 'Event 与排查记录'
42
+ if (source === 'jsonl') return '旧格式历史快照'
42
43
  return '还没有排查知识'
43
44
  }
44
45
 
@@ -224,10 +225,9 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
224
225
  }, [visible, reloadToken, applyPayload])
225
226
 
226
227
  /**
227
- * 载入内置示例:**显式的一次写**,写进的是这个库自己的 `01_source_raw/` 与 `02_wiki/`。
228
+ * 载入内置示例:**显式的一次写**,将来源记录与可复用诊断卡写入当前知识库。
228
229
  *
229
- * 载完重新读一次,让图、案例、来源徽标全部换成真实数据 —— 载入之后它们就是普通卡片,
230
- * 可以照常改、照常删,所以说明里必须把这件事讲清楚。
230
+ * 载完重新读一次,让图、只读排查视图、来源徽标全部换成当前库的数据。
231
231
  */
232
232
  const loadSample = async (): Promise<void> => {
233
233
  setSampleBusy(true); setSampleError(null)
@@ -236,7 +236,7 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
236
236
  const body = await response.json() as { ok?: boolean; data?: DiagnosisPayload; error?: { message?: string } }
237
237
  if (!response.ok || body.ok === false) throw new Error(body.error?.message ?? `HTTP ${response.status}`)
238
238
  applyPayload(body.data ?? {})
239
- setSampleNotice('已载入内置示例:4 份来源写进 01_source_raw/,26 张卡片写进 02_wiki/diagnosis/。它们就是普通来源与卡片,可以照常改或删。')
239
+ setSampleNotice('已载入内置示例:7 条来源记录、23 张可复用诊断卡;3 条排查记录以只读案例视图呈现。')
240
240
  } catch (error) {
241
241
  setSampleError(error instanceof Error ? error.message : String(error))
242
242
  } finally {
@@ -245,22 +245,21 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
245
245
  }
246
246
 
247
247
  /**
248
- * 全屏期间锁住宿主页面的滚动。
248
+ * 全屏期间的滚动锁。
249
249
  *
250
250
  * 只把浮层铺满视口是不够的:滚轮落在浮层的非画布区域上时,事件会冒泡给宿主,
251
251
  * 背后的页面照样滚。锁 `body` 是这里唯一简单可靠的做法(宿主用的是文档级滚动),
252
252
  * 退出时按原值还原而不是写死 `''`。
253
+ *
254
+ * 焦点**不在这里管**:全屏那一层现在是共享的 Radix `Dialog`,它自带焦点移入、
255
+ * 焦点圈定与关闭后还原。原来这一层只有 Escape + 滚动锁 —— 焦点留在面板里,Tab
256
+ * 一路走到背后的按钮上,而背景既没有 inert 也没有读屏的边界。
253
257
  */
254
258
  useEffect(() => {
255
259
  if (!full) return
256
260
  const previous = document.body.style.overflow
257
261
  document.body.style.overflow = 'hidden'
258
- const onKeyDown = (event: KeyboardEvent): void => { if (event.key === 'Escape') setFull(false) }
259
- window.addEventListener('keydown', onKeyDown)
260
- return () => {
261
- document.body.style.overflow = previous
262
- window.removeEventListener('keydown', onKeyDown)
263
- }
262
+ return () => { document.body.style.overflow = previous }
264
263
  }, [full])
265
264
 
266
265
  const filter = useMemo(
@@ -636,12 +635,21 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
636
635
  }
637
636
 
638
637
  if (full) {
639
- return createPortal(
640
- <div
641
- role="dialog"
642
- aria-modal="true"
643
- aria-label="智能归因全屏视图"
644
- className="fixed inset-0 z-[900] flex flex-col gap-2 overscroll-contain bg-tita-surface-muted p-3"
638
+ /*
639
+ * 全屏 = 共享 `Dialog` 铺满视口,而不是自己写一层 `role="dialog"`。
640
+ *
641
+ * 为什么必须借这一层:自写的那一层只有 Escape + 滚动锁,焦点既不进也不出 ——
642
+ * 打开全屏之后 Tab 会一路走到背后面板的按钮上,读屏也读不出「这是一个模态」
643
+ * (没有 inert,背景照旧可读)。Radix 给的是焦点圈定、初始焦点、关闭后还原,
644
+ * 以及 Escape / 点遮罩两种关闭。
645
+ *
646
+ * 尺寸与底色用 className 覆盖(`cn` 走的是 tailwind-merge,后面的同名工具类赢):
647
+ * 默认那张 420px 的板子在这里要变成整个视口,而 `bg-tita-surface-muted` /
648
+ * `bg-tita-surface` 是同一组里的类,所以底色也跟着换。
649
+ */
650
+ return <Dialog open onOpenChange={next => { if (!next) setFull(false) }}>
651
+ <DialogContent
652
+ className="inset-0 top-0 left-0 z-[900] flex h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 flex-col gap-2 overscroll-contain rounded-none border-0 bg-tita-surface-muted p-3 shadow-none"
645
653
  >
646
654
  {/* 全屏只有一行页头:标题 + 页面切换 + 当前页说明 + 出口。全屏是「同一块内容
647
655
  换成整个视口」,所以这一行就是面板里那两行(页头 + 切换行)压成一行。 */}
@@ -652,7 +660,10 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
652
660
  >
653
661
  <Stethoscope size={14} />
654
662
  </span>
655
- <h2 className="shrink-0 text-[14px] font-[750] tracking-[-0.02em]">智能归因</h2>
663
+ {/* 可见的 h2 **就是**这一层的标题:`asChild` 让它带上 Radix 要求的可达名,
664
+ 不必再写一份收起来的副标题(插件没有自建 Tailwind,`sr-only` 在这里
665
+ 并不可用 —— 用了就是一条永远不生效的类名)。 */}
666
+ <DialogTitle asChild><h2 className="shrink-0 text-[14px] font-[750] tracking-[-0.02em]">智能归因</h2></DialogTitle>
656
667
  {switcher}
657
668
  <p className="min-w-0 flex-1 truncate text-[11px] text-tita-ink-muted">{viewHint}</p>
658
669
  <Button
@@ -663,9 +674,8 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
663
674
  ><X size={12} />退出全屏(Esc)</Button>
664
675
  </header>
665
676
  {stage}
666
- </div>,
667
- document.body,
668
- )
677
+ </DialogContent>
678
+ </Dialog>
669
679
  }
670
680
 
671
681
  return <div className="flex min-h-0 flex-1 flex-col gap-1.5">
@@ -677,12 +687,8 @@ export function AttributionTab({ visible, activeQuestion, onOpenKnowledge }: {
677
687
  >
678
688
  <Sparkles size={12} className="mt-0.5 shrink-0" />
679
689
  <span className="min-w-0 flex-1">{sampleNotice}</span>
680
- <button
681
- type="button"
682
- aria-label="关闭提示"
683
- className="shrink-0 rounded-sm focus-visible:outline-[3px] focus-visible:outline-offset-1 focus-visible:outline-tita-focus-ring"
684
- onClick={() => setSampleNotice(null)}
685
- ><X size={12} /></button>
690
+ {/* 共享的 ghost 图标按钮:原来的手写 `<button>` 命中区只有 12px,焦点圈还是自己写的一条。 */}
691
+ <Button variant="ghost" size="icon" className="shrink-0" aria-label="关闭提示" title="关闭提示" onClick={() => setSampleNotice(null)}><X size={13} /></Button>
686
692
  </div>}
687
693
  {helpError && <AttributionHelpNotice message={helpError} onDismiss={() => setHelpError(null)} />}
688
694
  <div className="flex min-w-0 items-center gap-2">
@@ -721,12 +727,7 @@ function AttributionHelpNotice({ message, onDismiss }: { message: string; onDism
721
727
  className="flex items-start gap-1.5 rounded-lg border border-tita-line bg-tita-surface-muted p-2 text-[10.5px] leading-relaxed text-tita-danger"
722
728
  >
723
729
  <span className="min-w-0 flex-1">{message}</span>
724
- <button
725
- type="button"
726
- aria-label="关闭提示"
727
- className="shrink-0 rounded-sm focus-visible:outline-[3px] focus-visible:outline-offset-1 focus-visible:outline-tita-focus-ring"
728
- onClick={onDismiss}
729
- ><X size={12} /></button>
730
+ <Button variant="ghost" size="icon" className="shrink-0" aria-label="关闭提示" title="关闭提示" onClick={onDismiss}><X size={13} /></Button>
730
731
  </div>
731
732
  }
732
733
 
@@ -46,7 +46,9 @@ export function CaseView({ cases, activeId, onActiveChange }: {
46
46
  <h3 className="text-[13px] font-semibold leading-snug">{active.title}</h3>
47
47
  <p className="mt-0.5 text-[10.5px] text-tita-ink-muted">{active.id} · 耗时 {active.durationMinutes} min</p>
48
48
  </div>
49
- <Badge tone="success">已确认根因</Badge>
49
+ <Badge tone={active.rootCauseStatus === 'unconfirmed' ? 'warning' : 'success'}>
50
+ {active.rootCauseStatus === 'unconfirmed' ? '根因未确认' : '已确认根因'}
51
+ </Badge>
50
52
  </header>
51
53
 
52
54
  {/* ── wiki 段 ── */}
@@ -1,5 +1,5 @@
1
1
  import { CircleDot, FileText, GitBranch, Search } from '@tita-agent/ui/icons'
2
- import type { ReactNode } from 'react'
2
+ import { useEffect, type ReactNode } from 'react'
3
3
  import { Badge, Card } from '@tita-agent/ui/components'
4
4
  import type { AttributionEvent } from './model.ts'
5
5
 
@@ -15,7 +15,12 @@ function identityLabel(event: AttributionEvent): string {
15
15
  return '来源内独立记录'
16
16
  }
17
17
 
18
- export function EventView({ events }: { events: AttributionEvent[] }): ReactNode {
18
+ export function EventView({ events, focusEventId }: { events: AttributionEvent[]; focusEventId?: string }): ReactNode {
19
+ useEffect(() => {
20
+ if (!focusEventId) return
21
+ const target = document.getElementById(`knowledge-event-${focusEventId}`)
22
+ target?.scrollIntoView({ block: 'center', behavior: 'smooth' })
23
+ }, [events, focusEventId])
19
24
  if (events.length === 0) return <p className="py-5 text-center text-[11px] text-tita-ink-muted">还没有从文档中提取 Event。</p>
20
25
 
21
26
  const groups = new Map<string, { label: string; events: AttributionEvent[] }>()
@@ -40,7 +45,12 @@ export function EventView({ events }: { events: AttributionEvent[] }): ReactNode
40
45
  <GitBranch size={12} className="mt-0.5 shrink-0 text-tita-primary" />
41
46
  {group.label}
42
47
  </h3>
43
- {group.events.map(event => <Card key={event.id} className="gap-2 p-2.5">
48
+ {group.events.map(event => <article
49
+ key={event.id}
50
+ id={`knowledge-event-${event.id}`}
51
+ className={event.id === focusEventId ? 'rounded-lg ring-2 ring-tita-primary ring-offset-2 ring-offset-tita-surface' : undefined}
52
+ aria-label={event.id === focusEventId ? `已定位 Event:${event.title}` : undefined}
53
+ ><Card className="gap-2 p-2.5">
44
54
  <header className="flex items-start gap-2">
45
55
  <div className="min-w-0 flex-1">
46
56
  <h4 className="text-[12px] font-semibold leading-snug text-tita-ink">{event.title}</h4>
@@ -92,7 +102,7 @@ export function EventView({ events }: { events: AttributionEvent[] }): ReactNode
92
102
  : <span className="text-[9.5px] text-tita-ink-faint">暂无可显示的引文</span>}
93
103
  {event.missing.length > 0 && <p className="text-[9.5px] leading-relaxed text-tita-warning">待补:{event.missing.join('、')}</p>}
94
104
  </footer>
95
- </Card>)}
105
+ </Card></article>)}
96
106
  </section>)}
97
107
  </div>
98
108
  }
@@ -177,9 +177,9 @@ export function RunView({ data, cases, symptoms, symptomId, onSymptomChange, onS
177
177
  不是一次进行中的排查 —— 没有「本次跑了哪些检查点、看到什么」的现场状态,
178
178
  按一个按钮只能编出一份路径来,而那正好是这个产品最不该做的事。
179
179
  真实的写回发生在排查进行时:Agent 调 `POST /diagnosis/run` 落一条
180
- `agent_diagnosis_case` 的 SourceRaw,顺手提一张案例卡片等人确认。
180
+ `agent_diagnosis_case` 的 SourceRaw;Case 由这条记录只读派生,Knowledge / Event 由独立链路抽取和归一。
181
181
  */}
182
- <Button className="w-full" size="sm" disabled title="写回发生在排查进行时:Agent 用 POST /diagnosis/run 落一条诊断日志并提议案例卡片。这一屏是只读的复盘视图。">
182
+ <Button className="w-full" size="sm" disabled title="Agent 在排查结束时用 POST /diagnosis/run 落 SourceRaw;Case 只读派生,Knowledge / Event 由知识链路处理。">
183
183
  写回由排查进行时完成(这一屏只读)
184
184
  </Button>
185
185
  </Card>
@@ -20,6 +20,20 @@ export interface ViewSwitchItem<T extends string> {
20
20
  * 而分段的轨道 + 会滑动的白色胶囊把「同一组、选一个」直接画了出来 ——
21
21
  * 位置本身就是状态,不需要读颜色。
22
22
  *
23
+ * ## 语义是 `group` + `aria-pressed`,不是 tablist
24
+ *
25
+ * 它原来是 `role="tablist"` / `role="tab"` / `aria-selected` / roving `tabIndex`。
26
+ * 那是**冒充**:这一组只换内容,被换掉的那一块渲染在树的别处(调用方自己的
27
+ * `stage`/`subView` 分支),没有 `tabpanel`、没有 `aria-controls`、也没有 id。
28
+ * 读屏会念出「选项卡,4 个中的第 2 个」,然后按 tab 语义去找一个不存在的面板 ——
29
+ * 那不是「少写了一层」,那是把一句假话说给不会看到的用户听。
30
+ *
31
+ * 要把它改成真的 tab 就得让被换的内容成为这一组的 `tabpanel` 并互相对上 id:那要求
32
+ * 调用方把舞台搬进这个组件,或者把 id 一路传出去。**这一组承载的是「同一个视图的几种
33
+ * 画法」,不是「几个并列的内容面板」** —— 所以诚实的做法是退掉 tab 语义:一组
34
+ * 互斥按钮(`group` + `aria-pressed`),键盘用方向键在同一组里走。这一层没有丢
35
+ * 可达性,丢掉的是一句假的角色声明。
36
+ *
23
37
  * 滑块的位移用纯 CSS 算:轨道是 `auto-cols-fr` 的等宽栅格,滑块宽度取
24
38
  * `(100% - 内边距) / 页数`,位移取 `index × 100%`(百分比是相对滑块自身宽度)。
25
39
  * 这样**不需要测量 DOM**,也就不存在「测量时机不对导致滑块错位」这一类 bug ——
@@ -54,20 +68,20 @@ export function ViewSwitch<T extends string>({ items, value, onChange, ariaLabel
54
68
  if (!target) return
55
69
  onChange(target.id)
56
70
  // 焦点必须跟着走,否则键盘用户的下一次左右键会从旧位置继续算。
57
- const nodes = event.currentTarget.querySelectorAll<HTMLElement>('[role="tab"]')
71
+ const nodes = event.currentTarget.querySelectorAll<HTMLElement>('button')
58
72
  nodes[next]?.focus()
59
73
  }
60
74
 
61
75
  return <div className={cn('flex min-w-0 flex-col gap-1', className)}>
62
76
  <div
63
- role="tablist"
77
+ role="group"
64
78
  aria-label={ariaLabel}
65
79
  onKeyDown={onKeyDown}
66
80
  // 轨道刻意与面板自己的标签条(`.tita-tabsList`)用同一组颜色与圆角:
67
81
  // 同屏出现两个分段控件时,它们要么明显是同一套,要么明显是两套 —— 差一点点最难看。
68
82
  className="relative inline-grid grid-flow-col auto-cols-fr self-start rounded-[9px] border border-tita-line bg-tita-surface-muted p-[3px]"
69
83
  >
70
- {/* 滑块:装饰性元素,语义由 `aria-selected` 承载。 */}
84
+ {/* 滑块:装饰性元素,语义由 `aria-pressed` 承载。 */}
71
85
  <span
72
86
  aria-hidden="true"
73
87
  className="pointer-events-none absolute bottom-[3px] left-[3px] top-[3px] rounded-[7px] bg-tita-surface shadow-[0_1px_3px_rgb(15_23_42_/_0.16),inset_0_0_0_1px_var(--tita-primary-shadow)] transition-transform duration-200 ease-[cubic-bezier(0.22,0.61,0.36,1)]"
@@ -79,8 +93,9 @@ export function ViewSwitch<T extends string>({ items, value, onChange, ariaLabel
79
93
  return <button
80
94
  key={item.id}
81
95
  type="button"
82
- role="tab"
83
- aria-selected={on}
96
+ aria-pressed={on}
97
+ // 整个控件是一个 tab 停靠点,组内用方向键走 —— 与 shadcn 的 Tabs 同一条纪律,
98
+ // 只是角色换成了诚实的 `group`。
84
99
  tabIndex={on ? 0 : -1}
85
100
  data-state={on ? 'active' : 'inactive'}
86
101
  onClick={() => onChange(item.id)}
@@ -5,6 +5,11 @@ import {
5
5
  visibleEdges, visibleNodes,
6
6
  type AttributionEdge, type AttributionGraphData, type AttributionNode, type AttributionEdgeKind,
7
7
  } from './model.ts'
8
+ import {
9
+ BADGE_AUTO_INK, BADGE_GAP_STROKE, BADGE_INK, BADGE_MANUAL_INK, BADGE_PLATE, BADGE_PLATE_STROKE,
10
+ BADGE_PROBLEM_INK, BADGE_PROBLEM_STROKE, EDGE_COUNT_INK, EDGE_STROKE, EDGE_ZOMBIE_INK,
11
+ GAP_STROKE, LABEL_PLATE, SELECTION_RING, ZOMBIE_STROKE,
12
+ } from '../graph/palette.ts'
8
13
 
9
14
  /**
10
15
  * 我的模型 → G6 v5 的数据与配置。
@@ -47,19 +52,19 @@ const FONT = '"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-
47
52
 
48
53
  /** 十种边各自的外观。层内边实线、跨层边点线,`refutes` 一律红色虚线(一等公民)。 */
49
54
  const EDGE_STYLE: Record<AttributionEdgeKind, { stroke: string; lineWidth: number; lineDash?: number[] }> = {
50
- may_be_caused_by: { stroke: '#93b4f8', lineWidth: 1.6 },
51
- verified_by: { stroke: '#6b54cf', lineWidth: 1.3 },
52
- categorized_as: { stroke: '#c9b8ff', lineWidth: 1.2, lineDash: [5, 4] },
55
+ may_be_caused_by: { stroke: EDGE_STROKE.may_be_caused_by, lineWidth: 1.6 },
56
+ verified_by: { stroke: EDGE_STROKE.verified_by, lineWidth: 1.3 },
57
+ categorized_as: { stroke: EDGE_STROKE.categorized_as, lineWidth: 1.2, lineDash: [5, 4] },
53
58
  // `queries`(检查点 → 资产)与 `indexed_from`(资产 → 假设)是两条边,两端与回答的
54
59
  // 问题都不同,所以外观也要能分开:同色同线型的话,图上分不清「这条假设落在哪张表上」
55
60
  // 与「这条检查要去查哪张表」。
56
- queries: { stroke: '#9ec2ff', lineWidth: 1.3, lineDash: [1, 3] },
57
- produces: { stroke: '#b96d14', lineWidth: 1.2, lineDash: [2, 3] },
58
- supports: { stroke: '#12875f', lineWidth: 1.4 },
59
- refutes: { stroke: '#b42318', lineWidth: 1.6, lineDash: [5, 4] },
60
- confirmed_as: { stroke: '#0f766e', lineWidth: 1.6 },
61
- instance_of: { stroke: '#b96d14', lineWidth: 1.2, lineDash: [2, 3] },
62
- indexed_from: { stroke: '#9ec2ff', lineWidth: 1.2, lineDash: [2, 3] },
61
+ queries: { stroke: EDGE_STROKE.queries, lineWidth: 1.3, lineDash: [1, 3] },
62
+ produces: { stroke: EDGE_STROKE.produces, lineWidth: 1.2, lineDash: [2, 3] },
63
+ supports: { stroke: EDGE_STROKE.supports, lineWidth: 1.4 },
64
+ refutes: { stroke: EDGE_STROKE.refutes, lineWidth: 1.6, lineDash: [5, 4] },
65
+ confirmed_as: { stroke: EDGE_STROKE.confirmed_as, lineWidth: 1.6 },
66
+ instance_of: { stroke: EDGE_STROKE.instance_of, lineWidth: 1.2, lineDash: [2, 3] },
67
+ indexed_from: { stroke: EDGE_STROKE.indexed_from, lineWidth: 1.2, lineDash: [2, 3] },
63
68
  }
64
69
 
65
70
  /** 边的中文读法。**十种必须全有** —— 图例是通过遍历这个表生成的,漏一种就是漏一段语义。 */
@@ -90,15 +95,17 @@ export const EDGE_LEGEND: Array<{ kind: AttributionEdgeKind; label: string; stro
90
95
  ...EDGE_STYLE[kind],
91
96
  }))
92
97
 
93
- const ZOMBIE_STYLE = { stroke: '#9aa4b2', lineWidth: 1.2, lineDash: [5, 4] }
94
- const GAP_STROKE = '#b42318'
98
+ // 画布侧的颜色全部来自 `graph/palette.ts`:canvas 读不到 CSS 变量(那条理由写在那个
99
+ // 模块头上),所以值只能以字面量给,但**只给一份**。三处各写一份的代价是同一个
100
+ // 「中性文字」在三处慢慢漂成三个灰度,而没有任何一处能看出它们本该相同。
101
+ const ZOMBIE_STYLE = { stroke: ZOMBIE_STROKE, lineWidth: 1.2, lineDash: [5, 4] }
95
102
 
96
103
  const BADGE_BASE = {
97
- fill: '#748091',
104
+ fill: BADGE_INK,
98
105
  fontSize: 9,
99
106
  fontFamily: FONT,
100
- backgroundFill: '#ffffff',
101
- backgroundStroke: '#e5e9f0',
107
+ backgroundFill: BADGE_PLATE,
108
+ backgroundStroke: BADGE_PLATE_STROKE,
102
109
  backgroundLineWidth: 1,
103
110
  backgroundRadius: 6,
104
111
  // 动线视角里徽标画在**胶囊内部**(见 `laneNodeStyleFor`),所以它越矮越好:
@@ -162,8 +169,8 @@ export function nodeStyleFor(
162
169
  lineDash: isGap ? [4, 3] : undefined,
163
170
  zIndex: node.layer === 'instance' ? 0 : 1,
164
171
  // 圆里放 id(直径由复用次数决定,中文标题塞不进);胶囊放标题。
165
- // 案例例外:它的标题来自案例卡片、不含 id,而案例要靠 id 才能在复盘里被指认 ——
166
- // 所以由画布把 id 拼回去,标题本身保持单一份来源。
172
+ // Case 是排查 SourceRaw 的只读投影,不是 WikiCard。视图用症状标题;画布补上稳定的
173
+ // Case id,方便在复盘里指认这次排查。
167
174
  labelText: shape === 'circle' ? node.id : node.kind === 'case' ? `${node.id} ${node.title}` : node.title,
168
175
  labelPlacement: 'center',
169
176
  labelFill: palette.text,
@@ -179,12 +186,12 @@ export function nodeStyleFor(
179
186
  if (shape === 'rect') style.radius = 8
180
187
 
181
188
  const badges: Array<Record<string, unknown>> = []
182
- const gapBadge = { ...BADGE_BASE, text: '空区 · 无检查', placement: 'top', offsetY: -10, fill: GAP_STROKE, backgroundStroke: '#f0b7ad' }
189
+ const gapBadge = { ...BADGE_BASE, text: '空区 · 无检查', placement: 'top', offsetY: -10, fill: GAP_STROKE, backgroundStroke: BADGE_GAP_STROKE }
183
190
  if (node.kind === 'symptom') {
184
191
  // 问题侧的层次必须能在**画布上**读到,而不是只能靠筛选器猜。
185
192
  // 用文字徽标而不是颜色:颜色通道已经被失败模式占满(见 model.ts 的 PROBLEM_CLASS_ORDER)。
186
193
  const problem = problemBadgeOf(node)
187
- if (problem) badges.push({ ...BADGE_BASE, text: problem, placement: 'bottom', offsetY: 12, fill: '#96580f', backgroundStroke: '#f2ddb8' })
194
+ if (problem) badges.push({ ...BADGE_BASE, text: problem, placement: 'bottom', offsetY: 12, fill: BADGE_PROBLEM_INK, backgroundStroke: BADGE_PROBLEM_STROKE })
188
195
  } else if (node.kind === 'hypothesis') {
189
196
  badges.push({ ...BADGE_BASE, text: node.title, placement: 'bottom', offsetY: 12, fill: palette.text, backgroundStroke: palette.stroke })
190
197
  // 空区徽标必须和「复用 N」**互斥地**占用顶部位置,而不是写成 else-if ——
@@ -192,7 +199,7 @@ export function nodeStyleFor(
192
199
  // 而空区恰恰总是假设节点。
193
200
  badges.push(isGap ? gapBadge : { ...BADGE_BASE, text: `复用 ${node.reuseCount ?? 0}`, placement: 'top', offsetY: -10 })
194
201
  } else if (node.kind === 'check_point') {
195
- badges.push({ ...BADGE_BASE, text: `${node.costMinutes ?? '?'} min · ${node.automation === 'auto' ? '自动' : '人工'}`, placement: 'bottom', offsetY: 10, fill: node.automation === 'auto' ? '#12875f' : '#b96d14' })
202
+ badges.push({ ...BADGE_BASE, text: `${node.costMinutes ?? '?'} min · ${node.automation === 'auto' ? '自动' : '人工'}`, placement: 'bottom', offsetY: 10, fill: node.automation === 'auto' ? BADGE_AUTO_INK : BADGE_MANUAL_INK })
196
203
  } else if (isGap) {
197
204
  badges.push(gapBadge)
198
205
  }
@@ -269,18 +276,18 @@ export function laneNodeStyleFor(node: AttributionNode, gapIds: ReadonlySet<stri
269
276
  // 都堆上去的话,44px 高的胶囊会被三个徽标切成碎片。
270
277
  if (node.kind === 'symptom') {
271
278
  const problem = problemBadgeOf(node)
272
- if (problem) badges.push({ ...BADGE_BASE, text: problem, placement: 'bottom', offsetY: BADGE_GAP, fill: '#96580f', backgroundStroke: '#f2ddb8' })
279
+ if (problem) badges.push({ ...BADGE_BASE, text: problem, placement: 'bottom', offsetY: BADGE_GAP, fill: BADGE_PROBLEM_INK, backgroundStroke: BADGE_PROBLEM_STROKE })
273
280
  } else if (node.kind === 'hypothesis') {
274
281
  badges.push(isGap
275
- ? { ...BADGE_BASE, text: '空区 · 无检查点', placement: 'bottom', offsetY: BADGE_GAP, fill: GAP_STROKE, backgroundStroke: '#f0b7ad' }
282
+ ? { ...BADGE_BASE, text: '空区 · 无检查点', placement: 'bottom', offsetY: BADGE_GAP, fill: GAP_STROKE, backgroundStroke: BADGE_GAP_STROKE }
276
283
  : { ...BADGE_BASE, text: `复用 ${node.reuseCount ?? 0}`, placement: 'bottom', offsetY: BADGE_GAP, fill: palette.text, backgroundStroke: palette.stroke })
277
284
  } else if (node.kind === 'check_point') {
278
- badges.push({ ...BADGE_BASE, text: `${node.costMinutes ?? '?'} min · ${node.automation === 'auto' ? '自动' : '人工'}`, placement: 'bottom', offsetY: BADGE_GAP, fill: node.automation === 'auto' ? '#12875f' : '#b96d14' })
285
+ badges.push({ ...BADGE_BASE, text: `${node.costMinutes ?? '?'} min · ${node.automation === 'auto' ? '自动' : '人工'}`, placement: 'bottom', offsetY: BADGE_GAP, fill: node.automation === 'auto' ? BADGE_AUTO_INK : BADGE_MANUAL_INK })
279
286
  } else if (node.kind === 'evidence') {
280
287
  const verdict = node.verdict === 'refutes' ? '否定' : node.verdict === 'supports' ? '支持' : '给不出结论'
281
- badges.push({ ...BADGE_BASE, text: verdict, placement: 'bottom', offsetY: BADGE_GAP, fill: node.verdict === 'refutes' ? GAP_STROKE : '#12875f' })
288
+ badges.push({ ...BADGE_BASE, text: verdict, placement: 'bottom', offsetY: BADGE_GAP, fill: node.verdict === 'refutes' ? GAP_STROKE : BADGE_AUTO_INK })
282
289
  } else if (isGap) {
283
- badges.push({ ...BADGE_BASE, text: '空区 · 无检查点', placement: 'bottom', offsetY: BADGE_GAP, fill: GAP_STROKE, backgroundStroke: '#f0b7ad' })
290
+ badges.push({ ...BADGE_BASE, text: '空区 · 无检查点', placement: 'bottom', offsetY: BADGE_GAP, fill: GAP_STROKE, backgroundStroke: BADGE_GAP_STROKE })
284
291
  }
285
292
  if (badges.length > 0) style.badges = badges
286
293
  if (options.dimmed) {
@@ -314,7 +321,7 @@ export function edgeStyleFor(edge: AttributionEdge, options: { labels?: boolean
314
321
  labelAutoRotate: false,
315
322
  labelFontFamily: FONT,
316
323
  labelBackground: true,
317
- labelBackgroundFill: '#f6f8fb',
324
+ labelBackgroundFill: LABEL_PLATE,
318
325
  labelBackgroundRadius: 4,
319
326
  labelPadding: [2, 4, 2, 4],
320
327
  }
@@ -324,11 +331,11 @@ export function edgeStyleFor(edge: AttributionEdge, options: { labels?: boolean
324
331
  if (zombie) {
325
332
  style.labelText = `僵尸边 ${edge.staleDays} 天未走`
326
333
  style.labelFontSize = 9
327
- style.labelFill = '#9aa4b2'
334
+ style.labelFill = EDGE_ZOMBIE_INK
328
335
  } else if (edge.count !== undefined) {
329
336
  style.labelText = `${edge.count} 例`
330
337
  style.labelFontSize = 9
331
- style.labelFill = '#b96d14'
338
+ style.labelFill = EDGE_COUNT_INK
332
339
  }
333
340
  return style
334
341
  }
@@ -876,8 +883,8 @@ export function buildGraphOptions(
876
883
  layout: layoutOf(layout, rankdir),
877
884
  node: {
878
885
  state: {
879
- selected: { lineWidth: 3, stroke: '#2563eb' },
880
- active: { lineWidth: 2.6, stroke: '#2563eb' },
886
+ selected: { lineWidth: 3, stroke: SELECTION_RING },
887
+ active: { lineWidth: 2.6, stroke: SELECTION_RING },
881
888
  // 「其余节点透明度降到 0.35」(§7.4 第 9 条)。状态的样式值走 `var()` 共享
882
889
  // 令牌,`DIM_OPACITY` 是它的数值孪生 —— 节点数据在纯函数里算出来时也要用同一
883
890
  // 个数(node:test 里读不到 CSS),两处不同步就会一半亮一半暗。
@@ -889,7 +896,7 @@ export function buildGraphOptions(
889
896
  type: line ? 'polyline' : 'quadratic',
890
897
  style: line ? { radius: 6 } : { curveOffset: 22 },
891
898
  state: {
892
- selected: { lineWidth: 2.4, stroke: '#2563eb' },
899
+ selected: { lineWidth: 2.4, stroke: SELECTION_RING },
893
900
  dim: { strokeOpacity: DIM_OPACITY, labelOpacity: DIM_OPACITY },
894
901
  },
895
902
  },
@@ -8,6 +8,10 @@ import type {
8
8
  AttributionEdge, AttributionEdgeKind, AttributionGraphData, AttributionNode, AttributionNodeKind,
9
9
  AttributionStats, FailureReason, NodeLayer,
10
10
  } from '../../diagnosis/types.js'
11
+ import {
12
+ FAILURE_REASON_FILL, FAILURE_REASON_STROKE, FAILURE_REASON_TEXT,
13
+ NEUTRAL_NODE, NODE_CHECK_POINT, NODE_REFERENCE_FILL, NODE_REFERENCE_STROKE, NODE_REFERENCE_TEXT, NODE_SYMPTOM,
14
+ } from '../graph/palette.ts'
11
15
 
12
16
  export { ZOMBIE_EDGE_DAYS, PROBLEM_CLASSES, PROBLEM_CLASS_LABEL, problemClassIds, problemSubclassOf }
13
17
  export type {
@@ -35,13 +39,16 @@ export const LAYER_TONES: Record<NodeLayer, 'blue' | 'violet' | 'amber'> = {
35
39
  /**
36
40
  * 失败原因 → 颜色。分类在图谱视图里是**着色**而不是分区,所以它只影响一眼扫过的
37
41
  * 分组感,不影响节点位置 —— 位置由布局按边的方向决定。
42
+ *
43
+ * 五个色值来自 `graph/palette.ts`(画布色只有那一份)。这里只是把它们按
44
+ * `FailureReason` 装配起来,标签留在本地 —— 标签是文案,不是调色板。
38
45
  */
39
46
  export const FAILURE_REASON_STYLE: Record<FailureReason, { label: string; fill: string; stroke: string; text: string }> = {
40
- 'upstream-missing': { label: '上游未产出', fill: '#eef4ff', stroke: '#2563eb', text: '#1d4ed8' },
41
- 'spec-change': { label: '口径变更', fill: '#f3f0ff', stroke: '#6b54cf', text: '#5b45be' },
42
- 'data-processing': { label: '数据处理', fill: '#eafaf2', stroke: '#12875f', text: '#0f6b4d' },
43
- consumption: { label: '消费与展示', fill: '#fff7ea', stroke: '#b96d14', text: '#96580f' },
44
- permission: { label: '权限与过滤', fill: '#fdecea', stroke: '#b42318', text: '#98200f' },
47
+ 'upstream-missing': { label: '上游未产出', fill: FAILURE_REASON_FILL['upstream-missing'], stroke: FAILURE_REASON_STROKE['upstream-missing'], text: FAILURE_REASON_TEXT['upstream-missing'] },
48
+ 'spec-change': { label: '口径变更', fill: FAILURE_REASON_FILL['spec-change'], stroke: FAILURE_REASON_STROKE['spec-change'], text: FAILURE_REASON_TEXT['spec-change'] },
49
+ 'data-processing': { label: '数据处理', fill: FAILURE_REASON_FILL['data-processing'], stroke: FAILURE_REASON_STROKE['data-processing'], text: FAILURE_REASON_TEXT['data-processing'] },
50
+ consumption: { label: '消费与展示', fill: FAILURE_REASON_FILL.consumption, stroke: FAILURE_REASON_STROKE.consumption, text: FAILURE_REASON_TEXT.consumption },
51
+ permission: { label: '权限与过滤', fill: FAILURE_REASON_FILL.permission, stroke: FAILURE_REASON_STROKE.permission, text: FAILURE_REASON_TEXT.permission },
45
52
  }
46
53
 
47
54
  /**
@@ -76,7 +83,7 @@ export function symptomCountByProblemClass(nodes: AttributionNode[]): Record<Pro
76
83
  }
77
84
 
78
85
  /** 无失败模式的节点(现象、资产、检查)用中性色。 */
79
- export const NEUTRAL_STYLE = { fill: '#ffffff', stroke: '#d6dce6', text: '#748091' }
86
+ export const NEUTRAL_STYLE = { fill: NEUTRAL_NODE.fill, stroke: NEUTRAL_NODE.stroke, text: NEUTRAL_NODE.text }
80
87
 
81
88
  export const NODE_KIND_LABEL: Record<AttributionNodeKind, string> = {
82
89
  symptom: '现象',
@@ -564,9 +571,9 @@ export function nodeShapeOf(node: AttributionNode): 'circle' | 'rect' {
564
571
  /** 节点的取色:假设跟着失败模式走,其余走层色。 */
565
572
  export function nodeStyleOf(node: AttributionNode): { fill: string; stroke: string; text: string } {
566
573
  if (node.failureReason) return FAILURE_REASON_STYLE[node.failureReason]
567
- if (node.layer === 'reference') return { fill: '#eef4ff', stroke: '#9ec2ff', text: '#1d4ed8' }
568
- if (node.kind === 'symptom') return { fill: '#fff7ea', stroke: '#b96d14', text: '#96580f' }
569
- if (node.kind === 'check_point') return { fill: '#f5f1ff', stroke: '#c9b8ff', text: '#5b45be' }
574
+ if (node.layer === 'reference') return { fill: NODE_REFERENCE_FILL, stroke: NODE_REFERENCE_STROKE, text: NODE_REFERENCE_TEXT }
575
+ if (node.kind === 'symptom') return { fill: NODE_SYMPTOM.fill, stroke: NODE_SYMPTOM.stroke, text: NODE_SYMPTOM.text }
576
+ if (node.kind === 'check_point') return { fill: NODE_CHECK_POINT.fill, stroke: NODE_CHECK_POINT.stroke, text: NODE_CHECK_POINT.text }
570
577
  return NEUTRAL_STYLE
571
578
  }
572
579