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
@@ -223,7 +223,12 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
223
223
  * 存储位置要 `agentRoot`、构建设置要 `resources` 的解析值 —— 都在同一份响应里。
224
224
  */
225
225
  const knowledgeSnapshot = useScope(settings?.knowledge)
226
- const { layout } = useAgentLayout(knowledgeSnapshot.revision ?? 0)
226
+ /**
227
+ * `error` / `reload` 必须**真的被用掉**(U4):只取 `layout` 的话,一次失败的
228
+ * `GET /knowledge/layout` 会让「数据位置」那一块永远停在「正在读取数据目录…」——
229
+ * 既不报错,也没有重试的路。两者都交给渲染路径字段的那一块。
230
+ */
231
+ const { layout, error: layoutError, reload: reloadLayout } = useAgentLayout(knowledgeSnapshot.revision ?? 0)
227
232
  const environment = useKnowledgeEnvironment(visible, knowledgeSnapshot.revision ?? 0)
228
233
 
229
234
  /**
@@ -344,9 +349,16 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
344
349
  <div className={cn(styles.content, (activeTab === 'attribution' || activeTab === 'overview') && styles.contentStage)}>
345
350
 
346
351
  {/* 一级 tab 已经说明「现在是哪一页」,这里不再重复一遍标题与说明。
347
- 只有真的出问题时才占一行:读取失败 / 读取中。 */}
352
+ 只有真的出问题时才占一行:读取失败 / 读取中。
353
+ 失败时**必须给一条重试的路**(A5):这一栏的取数只由 `[visible, refreshStats]`
354
+ 触发,而 `visible` 在内部各视图之间一直是 true —— 一次瞬时失败会让
355
+ 「数据读取失败」和空计数一直挂在那里,除非重挂载整个面板。 */}
348
356
  {(statsError || statsLoading) && <p className={styles.viewStatus} role="status">
349
- {statsError ? <><AlertCircle size={13} />数据读取失败:{statsError}</> : '正在读取知识库统计…'}
357
+ {statsError
358
+ ? <><AlertCircle size={13} />数据读取失败:{statsError}
359
+ <Button variant="ghost" size="sm" disabled={statsLoading} onClick={() => void refreshStats()}>重新读取</Button>
360
+ </>
361
+ : '正在读取知识库统计…'}
350
362
  </p>}
351
363
 
352
364
  {notice && <Card className={cn(styles.notice, notice.tone === 'error' ? styles.noticeError : styles.noticeSuccess)} role="status">
@@ -469,6 +481,8 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
469
481
  showAdvanced={showAdvancedSettings}
470
482
  agentRoot={layout?.agentRoot}
471
483
  session={remote?.session}
484
+ layoutError={layoutError}
485
+ onRetryLayout={reloadLayout}
472
486
  {...(node === 'build' && layout?.resources ? { resolved: layout.resources } : {})}
473
487
  />)}
474
488
  </div>
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * 三条纪律:
9
9
  *
10
- * 1. **不重新取数**。两份列表的数据本来就整份在手(`visibleDocuments` / `visibleCards`),
11
- * 分页只切渲染 —— 它不是一个接口概念,是版式概念。所以这里没有加载态。
10
+ * 1. **不重新取数**(卡片列表成立)。两份列表的数据本来整份在手(`visibleDocuments` /
11
+ * `visibleCards`),分页只切渲染。**文件列表是例外**:它走服务端分页(`offset`),
12
+ * 因为「取 500 份再本地切页」会让超过 500 份的库从第 501 份起不可达。
12
13
  * 2. **页码在筛选变化时回到第 1 页**。停在第 5 页而结果只剩 1 页时,用户看到的是一片空白,
13
14
  * 而空白与「没有结果」在界面上长得一样。`usePage` 的第二个参数(筛选 key)管这件事。
14
15
  * 3. **只有一页时不渲染**。一页装得下的列表底下挂一行「第 1 / 1 页」是纯噪音。
@@ -71,11 +72,20 @@ export interface ListPagerProps {
71
72
  export function ListPager({ page, pageCount, total, onChange }: ListPagerProps): ReactNode {
72
73
  // 一页装得下就不显示:分页器只在「真的有下一页」时才有意义。
73
74
  if (total <= PAGE_SIZE) return null
75
+ /**
76
+ * 夹紧页码 —— **不能直接信 `page`**。
77
+ *
78
+ * 卡片抽屉那份列表是服务端分页的,总数可能在抽屉开着的时候变小;那时不夹紧就会在屏幕上
79
+ * 出现「第 3 / 2 页」这种自相矛盾的读数,而且两个按钮都还是可点的。标签与按钮边界
80
+ * 必须用同一个夹紧后的数。
81
+ */
82
+ const safePageCount = Math.max(1, Math.trunc(pageCount) || 1)
83
+ const current = Math.min(Math.max(1, Math.trunc(page) || 1), safePageCount)
74
84
  return <nav className={styles.pager} aria-label="分页">
75
85
  <span className={styles.pagerCount}>
76
- 共 {total} 条 · 第 <b>{page}</b> / {pageCount} 页
86
+ 共 {total} 条 · 第 <b>{current}</b> / {safePageCount} 页
77
87
  </span>
78
- <Button variant="outline" size="sm" disabled={page <= 1} onClick={() => onChange(page - 1)}>上一页</Button>
79
- <Button variant="outline" size="sm" disabled={page >= pageCount} onClick={() => onChange(page + 1)}>下一页</Button>
88
+ <Button variant="outline" size="sm" disabled={current <= 1} onClick={() => onChange(current - 1)}>上一页</Button>
89
+ <Button variant="outline" size="sm" disabled={current >= safePageCount} onClick={() => onChange(current + 1)}>下一页</Button>
80
90
  </nav>
81
91
  }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 物化轨迹的渲染(2026-10-18)。
3
+ *
4
+ * 物化这一段的产物就是卡片本身,所以「卡里有什么」在 WikiCard 那一栏已经查得到。
5
+ * 这一页回答的是**另外**三个问题:
6
+ *
7
+ * 1. **这一次对这张卡做了什么**(新建 / 更新 / 停用),以及是按哪个分组指纹做的。
8
+ * 2. **正文是怎么排出来的**(模型组织,还是机械拼装;每节几条)。
9
+ * 3. **哪些知识进了这张卡、哪几张卡被链进来、为什么**。
10
+ *
11
+ * 第 3 条是改提示词之后最想看的东西:「这次正文里链到的那张卡,和上次是不是同一张」——
12
+ * 这在前一个版本里没有任何地方能回答。
13
+ */
14
+
15
+ import { type ReactNode } from 'react'
16
+ import { TriangleAlert } from '@tita-agent/ui/icons'
17
+ import { styles } from './styles.ts'
18
+ import type { MaterializeTrajectoryReport } from '../trajectory-types.js'
19
+
20
+ const ROW_CAP = 200
21
+
22
+ const ACTION_LABEL: Record<MaterializeTrajectoryReport['action'], string> = {
23
+ created: '新建卡片',
24
+ updated: '更新卡片',
25
+ withdrawn: '停用卡片',
26
+ }
27
+
28
+ const REASON_LABEL: Record<string, string> = {
29
+ coordinate: '同一坐标',
30
+ relation: '关系边',
31
+ source: '共同来源',
32
+ }
33
+
34
+ export function MaterializeTrajectory({ report }: { report: MaterializeTrajectoryReport }): ReactNode {
35
+ const card = report.card
36
+ const rows = report.rows ?? []
37
+ const related = report.related ?? []
38
+ return <div className={styles.trialReport}>
39
+ <section className={styles.trialOverview} aria-labelledby="materialize-overview-title">
40
+ <div className={styles.trialSectionHead}>
41
+ <h2 id="materialize-overview-title">结果概览</h2>
42
+ <span>{ACTION_LABEL[report.action] ?? report.action}</span>
43
+ </div>
44
+ <dl className={styles.trialOverviewStats}>
45
+ <div><dt>知识</dt><dd>{card?.knowledge ?? 0}<small>进这张卡</small></dd></div>
46
+ <div><dt>Event</dt><dd>{card?.events ?? 0}<small>同坐标引用</small></dd></div>
47
+ <div><dt>图</dt><dd>{card?.images ?? 0}<small>跟着原文走</small></dd></div>
48
+ <div><dt>小节</dt><dd>{card?.sections?.length ?? 0}<small>正文结构</small></dd></div>
49
+ <div><dt>正文字数</dt><dd>{(card?.characters ?? 0).toLocaleString('zh-CN')}<small>字符</small></dd></div>
50
+ </dl>
51
+ <p className={styles.trialOverviewTitle}>
52
+ <strong>{card?.title || report.group?.title || '(没有标题)'}</strong>
53
+ {/* 坐标在库里是**三个 id**,不是名字 —— 所以标出这是 id,别让它读成卡名。 */}
54
+ <span>坐标 id:{report.group?.coordinate || '无'} · 状态 {card?.status ?? '未知'}</span>
55
+ </p>
56
+ </section>
57
+
58
+ {card?.id === undefined && <p className={styles.trialError} role="alert">
59
+ <TriangleAlert size={12} />这条轨迹没有卡片身份,本次没有写入任何卡片。
60
+ </p>}
61
+
62
+ {report.warnings.length > 0 && <section className={styles.trialSection} aria-labelledby="materialize-warnings-title">
63
+ <div className={styles.trialSectionHead}>
64
+ <h2 id="materialize-warnings-title">警告</h2>
65
+ <span>{report.warnings.length} 条</span>
66
+ </div>
67
+ <ul className={styles.trialCandidates}>{report.warnings.map((warning, index) => <li key={index}>
68
+ {/* 不加装饰图标:这一段就叫「警告」,一条黄三角再标一次是同一句话说两遍。
69
+ 而且 `trialCandidates > li` 是上下两行的栅格 —— 图标单占一行会让正文掉到第三行。 */}
70
+ <span className={styles.trialCandName}>{warning}</span>
71
+ </li>)}</ul>
72
+ </section>}
73
+
74
+ <section className={styles.trialSection} aria-labelledby="materialize-sections-title">
75
+ <div className={styles.trialSectionHead}>
76
+ <h2 id="materialize-sections-title">正文结构</h2>
77
+ <span>{report.organize?.used ? `模型组织${report.organize.origin === 'fallback' ? '(本轮退回确定性布局)' : ''}` : '机械拼装(本库未开卡片组织)'}</span>
78
+ </div>
79
+ {card?.sections && card.sections.length > 0
80
+ ? <ul className={styles.trialItems}>{card.sections.map(section => <li key={section.label} className={styles.trialItem}>
81
+ <span className={styles.trialItemRef}>{section.label}</span>
82
+ <span className={styles.trialItemArrow} aria-hidden="true">·</span>
83
+ <span className={styles.trialItemValue}>{section.count} 条</span>
84
+ </li>)}</ul>
85
+ : <p className={styles.trialEmpty}>
86
+ {report.action === 'withdrawn' ? '停用不改正文,所以没有分节。' : '这张卡没有分节(正文里的知识全部平铺)。'}
87
+ </p>}
88
+ </section>
89
+
90
+ <section className={styles.trialSection} aria-labelledby="materialize-knowledge-title">
91
+ <div className={styles.trialSectionHead}>
92
+ <h2 id="materialize-knowledge-title">进卡的知识</h2>
93
+ <span>{rows.length} 条</span>
94
+ </div>
95
+ {rows.length === 0
96
+ ? <p className={styles.trialEmpty}>{report.action === 'withdrawn' ? '停用不涉及知识。' : '这一组没有知识。'}</p>
97
+ : <ul className={styles.trialOutputList}>{rows.slice(0, ROW_CAP).map(row => <li key={row.id} className={styles.trialOutputItem}>
98
+ <div className={styles.trialCandHead}>
99
+ <span className={styles.trialCandName}>{row.text}</span>
100
+ <span className={styles.trialCandMeta}>{row.id}</span>
101
+ </div>
102
+ <div className={styles.trialOutputMeta}>{row.sources} 条出处</div>
103
+ </li>)}</ul>}
104
+ {rows.length > ROW_CAP && <p className={styles.trialMore}>另有 {rows.length - ROW_CAP} 条未展开。</p>}
105
+ </section>
106
+
107
+ <section className={styles.trialSection} aria-labelledby="materialize-related-title">
108
+ <div className={styles.trialSectionHead}>
109
+ <h2 id="materialize-related-title">相关卡片与来源</h2>
110
+ <span>正文里的链接目标必须是库里真实存在的卡</span>
111
+ </div>
112
+ {related.length > 0 && <ul className={styles.trialCandidates}>{related.map((item, index) => <li key={`${item.title}-${index}`}>
113
+ <strong>{item.title}</strong><span>{REASON_LABEL[item.reason] ?? item.reason}</span>
114
+ </li>)}</ul>}
115
+ <dl className={styles.trialRows}>
116
+ <div className={styles.trialRow}><dt>来源</dt><dd>{report.sources.length > 0 ? report.sources.join('、') : '—'}</dd></div>
117
+ <div className={styles.trialRow}><dt>分组键</dt><dd>{report.group?.key || '—'}</dd></div>
118
+ <div className={styles.trialRow}><dt>分组指纹</dt><dd>{report.group?.fingerprint || '—'}</dd></div>
119
+ {report.organize?.layoutHash && <div className={styles.trialRow}><dt>布局指纹</dt><dd>{report.organize.layoutHash}</dd></div>}
120
+ <div className={styles.trialRow}><dt>模型</dt><dd>{report.model?.model ? `${report.model.provider ? `${report.model.provider} / ` : ''}${report.model.model}${report.model.reasoningEffort ? ` · ${report.model.reasoningEffort}` : ''}` : '未记录'}</dd></div>
121
+ </dl>
122
+ </section>
123
+ </div>
124
+ }
@@ -87,10 +87,10 @@ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
87
87
  // 0025:`compile` 设置组已撤。卡片编译模板是系统固定契约,不是用户设置项。
88
88
  build: {
89
89
  scope: 'knowledge',
90
- title: '向量模型',
90
+ title: '向量运行设置',
91
91
  fields: [
92
92
  { key: 'embeddingProvider', label: '向量来源', kind: 'select', fallback: 'wemm-python', options: [{ value: 'wemm-python', label: '本地模型' }, { value: 'ollama', label: 'Ollama' }, { value: 'none', label: '关闭向量' }] },
93
- { key: 'embeddingModel', label: '模型路径或名称', kind: 'text', fallback: '', hint: '本地模型留空时自动选择;Ollama 填模型名称。' },
93
+ { key: 'embeddingModel', label: 'Ollama 模型名称', kind: 'text', fallback: '', hint: '填写 Ollama 中已安装的模型名称。', visibleWhen: { key: 'embeddingProvider', value: 'ollama' } },
94
94
  { key: 'embeddingBaseUrl', label: 'Ollama 地址', kind: 'text', fallback: 'http://127.0.0.1:11434', visibleWhen: { key: 'embeddingProvider', value: 'ollama' } },
95
95
  { key: 'embeddingDevice', label: '运行设备', kind: 'select', fallback: 'auto', advanced: true, options: [{ value: 'auto', label: '自动' }, { value: 'mps', label: 'Apple MPS' }, { value: 'cpu', label: 'CPU' }] },
96
96
  { key: 'embeddingDimension', label: '向量维度', kind: 'number', fallback: 512, min: 1, step: 1, advanced: true, hint: '必须与所选模型输出维度一致;修改后需要重建向量索引。' },
@@ -107,7 +107,6 @@ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
107
107
  { key: 'keywordThreshold', label: '关键词阈值', kind: 'number', fallback: 0.3, min: 0, max: 1, step: 0.01, advanced: true, hint: '0–1 的归一化 BM25 分数;适合过滤弱命中,不能单独判断整批结果是否相关。' },
108
108
  { key: 'vectorThreshold', label: '向量阈值', kind: 'number', fallback: 0.5, min: -1, max: 1, step: 0.01, advanced: true, hint: '原始余弦分数。默认 0.5 是当前本地模型与语料的起点,换模型后应按召回诊断校准。' },
109
109
  { key: 'rerankThreshold', label: '重排最低分', kind: 'number', fallback: 0.2, min: -10, max: 10, step: 0.1, advanced: true },
110
- { key: 'rerankModel', label: '模型路径或名称', kind: 'text', fallback: '', advanced: true },
111
110
  { key: 'rerankTimeoutMs', label: '重排超时(毫秒)', kind: 'number', fallback: 60000, min: 1000, max: 600000, step: 1000, advanced: true },
112
111
  { key: 'contextMaxChars', label: '上下文字符上限', kind: 'number', fallback: 20000, min: 0, max: 1000000, step: 1000, hint: '0 表示不限', advanced: true },
113
112
  ],
@@ -165,9 +164,36 @@ function readField(spec: FieldSpec, value: Record<string, unknown> | undefined):
165
164
  return String(raw)
166
165
  }
167
166
 
167
+ /**
168
+ * 一个数字字段的草稿能不能落盘。返回 `null` = 能,否则是人话的原因。
169
+ *
170
+ * 这里挡的是两个具体的坏值,它们都不是「用户想存这个数」,而是**那一瞬间的输入状态**:
171
+ *
172
+ * - `Number('') === 0`:清空「向量维度」会把 0 写进设置,而 0 是被 `min` 挡掉的非法值;
173
+ * - `Number('abc') === NaN`:`JSON.stringify(NaN)` 是 `null`,存下去是一份坏设置。
174
+ *
175
+ * 所以非法文本**留在草稿里**(人看到自己敲的东西、右面有一句「值无效」),只是不写。
176
+ * 这比「敲一半就被改写」温和:中间态是人打字时的一个正常阶段,不是错误。
177
+ */
178
+ function fieldErrorOf(spec: FieldSpec, value: string | boolean): string | undefined {
179
+ if (spec.kind !== 'number') return undefined
180
+ const text = String(value).trim()
181
+ if (text === '') return '值无效:请填写一个数字'
182
+ const parsed = Number(text)
183
+ if (!Number.isFinite(parsed)) return '值无效:请填写一个数字'
184
+ if (spec.min !== undefined && parsed < spec.min) return `值无效:不能小于 ${spec.min}`
185
+ if (spec.max !== undefined && parsed > spec.max) return `值无效:不能大于 ${spec.max}`
186
+ return undefined
187
+ }
188
+
189
+ /** 数字字段只在**合法**时才写。非法时返回 `undefined`,上层跳过这一次写入。 */
168
190
  function writeField(spec: FieldSpec, draft: string | boolean): unknown {
169
191
  if (spec.kind === 'toggle') return Boolean(draft)
170
- if (spec.kind === 'number') return Number(draft)
192
+ if (spec.kind === 'number') {
193
+ const error = fieldErrorOf(spec, draft)
194
+ if (error !== undefined) return undefined
195
+ return Number(String(draft).trim())
196
+ }
171
197
  if (spec.list) return String(draft).split(',').map(item => item.trim()).filter(Boolean)
172
198
  return String(draft)
173
199
  }
@@ -178,6 +204,7 @@ function needsDebounce(spec: FieldSpec): boolean {
178
204
  }
179
205
 
180
206
  function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean; resolved?: string }) {
207
+ const invalid = fieldErrorOf(spec, value)
181
208
  return <Field label={spec.label} hint={spec.hint}>
182
209
  <Input
183
210
  type={spec.kind === 'number' ? 'number' : 'text'}
@@ -189,6 +216,8 @@ function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldS
189
216
  {...(resolved ? { placeholder: resolved } : {})}
190
217
  onChange={event => onChange(event.target.value)}
191
218
  />
219
+ {/* 非法草稿**留在输入框里**,只在旁边说一句「值无效、不会写」—— 见 `fieldErrorOf`。 */}
220
+ {invalid && <span className="tita-settingInvalid" role="status">{invalid}</span>}
192
221
  </Field>
193
222
  }
194
223
 
@@ -231,11 +260,15 @@ function ToggleField({ spec, value, onChange, disabled }: { spec: FieldSpec; val
231
260
  /**
232
261
  * 固定的数据目录:只读展示实际路径,打开动作交给 DSH 的 session 能力。
233
262
  */
234
- function StorageLocationField({ label, path, session, onError }: {
263
+ function StorageLocationField({ label, path, session, onError, loadError, onRetry }: {
235
264
  label: string
236
265
  path?: string
237
266
  session?: AgentPresetRemote['session']
238
267
  onError: (message: string | null) => void
268
+ /** `/knowledge/layout` 的读取失败。它发生在这个字段上,所以报在这个字段旁边。 */
269
+ loadError?: string | null
270
+ /** 重读一次布局。不给人这条路的话,一次瞬时失败会永久停在一句占位文字上。 */
271
+ onRetry?: () => void
239
272
  }): ReactNode {
240
273
  const [opening, setOpening] = useState(false)
241
274
 
@@ -259,19 +292,26 @@ function StorageLocationField({ label, path, session, onError }: {
259
292
 
260
293
  return <Field as="div" label={label}>
261
294
  <div className="tita-pickerRow">
262
- <Input readOnly value={path ?? ''} placeholder="正在读取数据目录…" aria-label={label} />
295
+ {/* 读不到布局时占位文字要跟着变:原来的「正在读取数据目录…」在失败之后
296
+ 是一句**永远为真**的话,而它掩盖了「已经不读了」这个事实。 */}
297
+ <Input readOnly value={path ?? ''} placeholder={loadError ? '未能读取数据目录' : '正在读取数据目录…'} aria-label={label} />
263
298
  <Button variant="outline" onClick={() => void open()} disabled={!path || opening}>
264
299
  {opening ? <LoaderCircle size={14} className="tita-spin" /> : <FolderOpen size={14} />}
265
300
  {opening ? '正在打开…' : '打开目录'}
266
301
  </Button>
267
302
  </div>
303
+ {loadError && <p className="tita-settingsRetry" role="alert">
304
+ <AlertCircle size={13} />
305
+ <span>读取数据目录失败:{loadError}</span>
306
+ {onRetry && <Button variant="ghost" size="sm" onClick={onRetry}>重新读取</Button>}
307
+ </p>}
268
308
  </Field>
269
309
  }
270
310
 
271
311
  /** 写入状态。`saved` 只短暂出现,`error` 留到下一次改动。 */
272
312
  type SaveState = 'idle' | 'saving' | 'saved' | 'error'
273
313
 
274
- export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolved = {}, highlight = false, showAdvanced = false }: {
314
+ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolved = {}, highlight = false, showAdvanced = false, layoutError = null, onRetryLayout }: {
275
315
  node: SettingsNode
276
316
  knowledge?: SettingsScopeLike
277
317
  /** `/knowledge/layout` 返回的固定 Agent 根目录。 */
@@ -284,6 +324,10 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
284
324
  highlight?: boolean
285
325
  /** Show the model and retrieval tuning fields. */
286
326
  showAdvanced?: boolean
327
+ /** `/knowledge/layout` 读取失败时的原话;只有「数据位置」那一块用得到。 */
328
+ layoutError?: string | null
329
+ /** 重读一次布局。 */
330
+ onRetryLayout?: () => void
287
331
  }): ReactNode {
288
332
  const spec = NODE_SETTINGS[node]
289
333
  const scope = knowledge
@@ -291,11 +335,18 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
291
335
  const [draft, setDraft] = useState<Record<string, string | boolean>>({})
292
336
  const [saveState, setSaveState] = useState<SaveState>('idle')
293
337
  const [error, setError] = useState<string | null>(null)
294
- /** 待落地的防抖写入,按字段键索引。 */
295
- const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>())
338
+ /**
339
+ * 待落地的防抖写入,按字段键索引。
340
+ *
341
+ * 除了计时器,它还记着**那次要写的值**:卸载时那个 `setTimeout` 不会再回来,
342
+ * 而草稿 state 在那一刻不一定已经渲染过(`setDraft` 是异步的)—— 从 timer 里读
343
+ * 值才是「用户最后敲进去的东西」的唯一可靠来源。
344
+ */
345
+ const pending = useRef(new Map<string, { timer: ReturnType<typeof setTimeout>; field: FieldSpec; value: string | boolean }>())
296
346
  /** 正在进行的写入数。用它挡住「写完 → revision 变 → 重置草稿」把输入抹掉。 */
297
347
  const inflight = useRef(0)
298
348
  const savedTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
349
+ const disabled = !state.writable || state.status !== 'ready'
299
350
 
300
351
  useEffect(() => {
301
352
  // 有写入在飞的时候不重置草稿:那一刻 `state.value` 还是旧值,重置会把用户刚
@@ -304,21 +355,16 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
304
355
  setDraft(Object.fromEntries(spec.fields.map(field => [field.key, readField(field, state.value)])))
305
356
  }, [node, state.revision, state.value])
306
357
 
307
- // 卸载时把还没落地的防抖写入丢掉并清计时器:一个已经离开的面板不该再写设置。
308
- useEffect(() => () => {
309
- for (const timer of timers.current.values()) clearTimeout(timer)
310
- timers.current.clear()
311
- if (savedTimer.current) clearTimeout(savedTimer.current)
312
- }, [])
313
-
314
- const disabled = !state.writable || state.status !== 'ready'
315
-
316
358
  const persist = useCallback(async (field: FieldSpec, raw: string | boolean): Promise<void> => {
317
359
  if (!scope || disabled) return
360
+ const value = writeField(field, raw)
361
+ // 非法草稿跳过这一次写入(见 `fieldErrorOf`)。它不是失败,也不该报错 ——
362
+ // 只是「这一笔现在还不能写」,草稿留在输入框里等人把它改对。
363
+ if (value === undefined) return
318
364
  inflight.current += 1
319
365
  setSaveState('saving'); setError(null)
320
366
  try {
321
- await scope.set(field.key, writeField(field, raw))
367
+ await scope.set(field.key, value)
322
368
  setSaveState('saved')
323
369
  if (savedTimer.current) clearTimeout(savedTimer.current)
324
370
  // 「已保存」只是一个确认,不是一条状态:两秒后收回,免得它常驻成一个
@@ -332,18 +378,35 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
332
378
  }
333
379
  }, [scope, disabled])
334
380
 
381
+ // 卸载时把还没落地的防抖写入**冲出去**,而不是丢掉:用户敲完立刻按 Esc 是常规操作,
382
+ // 而丢掉的表现是「输入框里的值过一会儿自己变回去了」,中间连一个保存指示都没有。
383
+ // 冲出去就是替那一个 `setTimeout` 把事做完。卸载后 `persist` 里的 setState 会被
384
+ // React 忽略(它已经不在树上了),写入本身照旧发出去。
385
+ useEffect(() => () => {
386
+ for (const { timer, field, value } of pending.current.values()) {
387
+ clearTimeout(timer)
388
+ void persist(field, value)
389
+ }
390
+ pending.current.clear()
391
+ if (savedTimer.current) clearTimeout(savedTimer.current)
392
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
393
+
335
394
  const update = (field: FieldSpec, value: string | boolean): void => {
336
395
  setDraft(current => ({ ...current, [field.key]: value }))
337
396
  if (!needsDebounce(field)) {
338
397
  void persist(field, value)
339
398
  return
340
399
  }
341
- const existing = timers.current.get(field.key)
342
- if (existing) clearTimeout(existing)
343
- timers.current.set(field.key, setTimeout(() => {
344
- timers.current.delete(field.key)
345
- void persist(field, value)
346
- }, 600))
400
+ const existing = pending.current.get(field.key)
401
+ if (existing) clearTimeout(existing.timer)
402
+ pending.current.set(field.key, {
403
+ field,
404
+ value,
405
+ timer: setTimeout(() => {
406
+ pending.current.delete(field.key)
407
+ void persist(field, value)
408
+ }, 600),
409
+ })
347
410
  }
348
411
 
349
412
  const meta = state.status === 'loading'
@@ -399,6 +462,8 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
399
462
  path={agentRoot}
400
463
  session={session}
401
464
  onError={setError}
465
+ loadError={layoutError}
466
+ {...(onRetryLayout ? { onRetry: onRetryLayout } : {})}
402
467
  />
403
468
  : <TextField
404
469
  key={field.key}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * 归一轨迹的渲染(2026-10-18)。
3
+ *
4
+ * 归一这一段原来只留下两个东西:任务里一段会滚走的流式输出,和最终写进事实表的那些行。
5
+ * 中间那一步 ——「模型给的这些名字里,哪些命中了已有实体、哪些新建了候选、哪些因为不唯一
6
+ * 没猜」—— 跑完就查不到了。而它恰好是坐标错了之后唯一能复盘的东西:卡名错了,根在这里。
7
+ *
8
+ * 所以这一页只回答三个问题,按这个顺序:
9
+ *
10
+ * 1. **这次归一动了多少**(提及数、命中/新建/歧义、写进多少条事实)。
11
+ * 2. **每个名字落到了哪里**(名字 → 实体 id,以及凭什么)。
12
+ * 3. **写进去的事实长什么样**(正文、坐标、几条出处、哪几条还悬着)。
13
+ *
14
+ * 样式复用抽取报告那一套(`.tita-trial*`):三份轨迹是同一件事的三种粒度,
15
+ * 长得不一样会让人以为是三个不同的功能。
16
+ */
17
+
18
+ import { type ReactNode } from 'react'
19
+ import { TriangleAlert } from '@tita-agent/ui/icons'
20
+ import { cn } from '@tita-agent/ui/components'
21
+ import { styles } from './styles.ts'
22
+ import type { NormalizeTrajectoryReport } from '../trajectory-types.js'
23
+
24
+ /** 名字最多摊这么多行。再多就该看总体结论,不该在页面上滚。 */
25
+ const AXIS_CAP = 200
26
+ const FACT_CAP = 200
27
+
28
+ const OUTCOME_LABEL: Record<string, string> = {
29
+ matched: '命中已有',
30
+ new: '新建候选',
31
+ ambiguous: '不唯一,没猜',
32
+ 'no-registry': '只做文档内归一',
33
+ }
34
+
35
+ export function NormalizeTrajectory({ report }: { report: NormalizeTrajectoryReport }): ReactNode {
36
+ const axes = report.axes ?? []
37
+ const rows = report.rows ?? []
38
+ const counts = report.counts ?? { matched: 0, created: 0, ambiguous: 0, mechanical: 0 }
39
+ const modelOk = report.model?.ok !== false
40
+ return <div className={styles.trialReport}>
41
+ <section className={styles.trialOverview} aria-labelledby="normalize-overview-title">
42
+ <div className={styles.trialSectionHead}>
43
+ <h2 id="normalize-overview-title">结果概览</h2>
44
+ <span>{report.knowledge?.unresolved ? `${report.knowledge.unresolved} 条坐标未决` : modelOk ? '已完成' : '需检查'}</span>
45
+ </div>
46
+ <dl className={styles.trialOverviewStats}>
47
+ <div><dt>坐标提及</dt><dd>{report.mentions ?? axes.length}</dd></div>
48
+ <div><dt>命中 / 新建</dt><dd>{counts.matched} / {counts.created}<small>已有实体 / 候选</small></dd></div>
49
+ <div><dt>歧义未决</dt><dd>{counts.ambiguous}<small>不猜,留给人</small></dd></div>
50
+ <div><dt>写入 Knowledge</dt><dd>{report.knowledge?.saved ?? 0}<small>另并入 {report.knowledge?.merged ?? 0} 条</small></dd></div>
51
+ <div><dt>写入 Event</dt><dd>{report.events?.saved ?? 0}<small>另并入 {report.events?.merged ?? 0} 个</small></dd></div>
52
+ </dl>
53
+ <p className={styles.trialOverviewTitle}>
54
+ <strong>{report.source?.title || '(来源已删除)'}</strong>
55
+ <span>v{report.source?.version ?? '?'} · 依据抽取记录 {report.extractionId || '—'}</span>
56
+ </p>
57
+ </section>
58
+
59
+ {!modelOk && <p className={styles.trialError} role="alert">
60
+ <TriangleAlert size={12} />{report.model?.why ?? '归一没有跑通'} · 这一份文档的事实没有写完。
61
+ </p>}
62
+
63
+ {report.warnings.length > 0 && <section className={styles.trialSection} aria-labelledby="normalize-warnings-title">
64
+ <div className={styles.trialSectionHead}>
65
+ <h2 id="normalize-warnings-title">警告</h2>
66
+ <span>{report.warnings.length} 条</span>
67
+ </div>
68
+ <ul className={styles.trialCandidates}>{report.warnings.map((warning, index) => <li key={index}>
69
+ {/* 不加装饰图标:这一段就叫「警告」,一条黄三角再标一次是同一句话说两遍。
70
+ 而且 `trialCandidates > li` 是上下两行的栅格 —— 图标单占一行会让正文掉到第三行。 */}
71
+ <span className={styles.trialCandName}>{warning}</span>
72
+ </li>)}</ul>
73
+ </section>}
74
+
75
+ <section className={styles.trialSection} aria-labelledby="normalize-axes-title">
76
+ <div className={styles.trialSectionHead}>
77
+ <h2 id="normalize-axes-title">坐标解析</h2>
78
+ <span>{axes.length} 个名字{counts.mechanical > 0 ? ` · ${counts.mechanical} 个只做了机械归一` : ''}</span>
79
+ </div>
80
+ {axes.length === 0
81
+ ? <p className={styles.trialEmpty}>这次归一没有解析任何坐标名。</p>
82
+ : <ul className={styles.trialItems}>{axes.slice(0, AXIS_CAP).map((axis, index) => <li
83
+ key={`${axis.kind}-${axis.name}-${index}`}
84
+ className={axis.outcome === 'matched' ? styles.trialItem : `${styles.trialItem} ${styles.trialItemWarn}`}
85
+ >
86
+ <span className={styles.trialItemRef}>{axis.name}</span>
87
+ <span className={styles.trialItemArrow} aria-hidden="true">→</span>
88
+ <span className={styles.trialItemValue}>
89
+ {(OUTCOME_LABEL[axis.outcome] ?? axis.outcome)}{axis.id ? ` ${axis.id}` : ''}
90
+ </span>
91
+ <span className={styles.trialItemNote}>
92
+ {KIND_LABEL[axis.kind] ?? axis.kind}{axis.how ? ` · ${axis.how}` : ''}{axis.parentMention ? ` · 作用域:${axis.parentMention}` : ''}
93
+ </span>
94
+ </li>)}</ul>}
95
+ {axes.length > AXIS_CAP && <p className={styles.trialItemsMore}>另有 {axes.length - AXIS_CAP} 个名字未展开。</p>}
96
+ </section>
97
+
98
+ <section className={styles.trialSection} aria-labelledby="normalize-facts-title">
99
+ <div className={styles.trialSectionHead}>
100
+ <h2 id="normalize-facts-title">写入的事实</h2>
101
+ <span>{rows.length} 条{report.knowledge?.unresolved ? ` · ${report.knowledge.unresolved} 条坐标未决` : ''}</span>
102
+ </div>
103
+ {rows.length === 0
104
+ ? <p className={styles.trialEmpty}>这次归一没有写入 Knowledge。</p>
105
+ : <ul className={styles.trialOutputList}>{rows.slice(0, FACT_CAP).map((row, index) => <li key={index} className={styles.trialOutputItem}>
106
+ <div className={styles.trialCandHead}>
107
+ <span className={cn(styles.trialCandName, row.unresolved && styles.trialCandBad)}>{row.text}</span>
108
+ </div>
109
+ <div className={styles.trialOutputMeta}>
110
+ {row.coordinate} · {row.sources} 条出处
111
+ {row.unresolved && <strong>坐标未决</strong>}
112
+ </div>
113
+ </li>)}</ul>}
114
+ {rows.length > FACT_CAP && <p className={styles.trialMore}>另有 {rows.length - FACT_CAP} 条未展开。</p>}
115
+ </section>
116
+
117
+ <section className={styles.trialSection} aria-labelledby="normalize-fingerprint-title">
118
+ <div className={styles.trialSectionHead}>
119
+ <h2 id="normalize-fingerprint-title">续跑信息</h2>
120
+ <span>指纹没变,下一次归一会直接复用这一份</span>
121
+ </div>
122
+ <dl className={styles.trialRows}>
123
+ <div className={styles.trialRow}><dt>归一指纹</dt><dd>{report.fingerprint || '—'}</dd></div>
124
+ <div className={styles.trialRow}>
125
+ <dt>已下发坐标</dt>
126
+ <dd>{report.axesContext === undefined
127
+ ? '这次没有下发(没接注册表,或库里还没有实体)'
128
+ : `${report.axesContext.injected} / ${report.axesContext.totals} 条${report.axesContext.truncated > 0 ? ` · 未下发 ${report.axesContext.truncated} 条` : ''}${report.axesContext.selection === 'relevant' ? ' · 当前文档相关候选' : report.axesContext.selection === 'full' ? ' · 全集' : ''}`}</dd>
129
+ </div>
130
+ <div className={styles.trialRow}><dt>模型</dt><dd>{report.model?.model ? `${report.model.provider ? `${report.model.provider} / ` : ''}${report.model.model}${report.model.reasoningEffort ? ` · ${report.model.reasoningEffort}` : ''}` : '未记录'}</dd></div>
131
+ </dl>
132
+ </section>
133
+ </div>
134
+ }
135
+
136
+ const KIND_LABEL: Record<string, string> = { object: '对象', action: '动作', scene: '场景' }
@@ -0,0 +1,26 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Button } from '@tita-agent/ui/components'
3
+ import type { PipelineStatus } from '../pipeline/types.js'
4
+ import { requestJson } from './request.ts'
5
+
6
+ export function PipelineNotice({ kb, revision, onOpen }: { kb: string; revision?: string; onOpen: () => void }) {
7
+ const [status, setStatus] = useState<PipelineStatus>()
8
+ useEffect(() => {
9
+ let active = true
10
+ const refresh = (): void => { void requestJson<PipelineStatus>(`/knowledge/pipeline?kb=${encodeURIComponent(kb)}`).then(result => { if (active && result.data) setStatus(result.data) }).catch(() => undefined) }
11
+ refresh()
12
+ window.addEventListener('focus', refresh)
13
+ return () => { active = false; window.removeEventListener('focus', refresh) }
14
+ }, [kb, revision])
15
+ if (!status) return null
16
+ const pending = status.pendingNew + status.pendingUpdated + status.pendingWithdrawn
17
+ const parts = [
18
+ status.sources.filter(source => source.state === 'pending' || source.state === 'outdated').length,
19
+ status.sources.filter(source => source.state === 'extracted').length,
20
+ ]
21
+ if (!parts.some(Boolean) && !pending && !status.indexPending) return null
22
+ return <div className="mx-3 flex min-w-0 items-center gap-2 border-b border-tita-line py-2 text-xs text-tita-ink-muted" role="status">
23
+ <span className="min-w-0 flex-1 leading-relaxed">{[parts[0] ? `${parts[0]} 份待抽取` : '', parts[1] ? `${parts[1]} 份待归一` : '', pending ? `${pending} 张 WikiCard 待物化` : '', status.indexPending ? '索引待更新' : ''].filter(Boolean).join(' · ')}</span>
24
+ <Button size="sm" variant="ghost" onClick={onOpen}>处理</Button>
25
+ </div>
26
+ }