dsh-data-domain-knowledge 0.1.0-alpha.9 → 0.1.1-alpha.2

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 (438) hide show
  1. package/README.md +118 -567
  2. package/cordis.patch.yml +5 -418
  3. package/eval/README.md +19 -8
  4. package/eval/benchmark-extraction-format.mjs +20 -0
  5. package/eval/evaluate-retrieval.mjs +17 -8
  6. package/eval/queries.example.jsonl +1 -0
  7. package/lib/client.js +4181 -3239
  8. package/lib/contract-envelope.d.ts +91 -0
  9. package/lib/contract-envelope.js +106 -0
  10. package/lib/contract-layout.d.ts +130 -0
  11. package/lib/contract-layout.js +138 -0
  12. package/lib/contract-refs.d.ts +39 -0
  13. package/lib/contract-refs.js +9 -0
  14. package/lib/contract-version.d.ts +311 -0
  15. package/lib/contract-version.js +270 -0
  16. package/lib/contract-wire.d.ts +147 -0
  17. package/lib/contract-wire.js +99 -0
  18. package/lib/contracts.d.ts +12 -0
  19. package/lib/contracts.js +12 -0
  20. package/lib/doc-page.js +2 -2
  21. package/lib/pages.js +3329 -1296
  22. package/lib/types/agent-behavior-rules.d.ts +10 -0
  23. package/lib/types/agent-behavior-rules.d.ts.map +1 -0
  24. package/lib/types/agent-behavior-rules.js +28 -0
  25. package/lib/types/agent-behavior-rules.js.map +1 -0
  26. package/lib/types/axis-registry.d.ts +37 -5
  27. package/lib/types/axis-registry.d.ts.map +1 -1
  28. package/lib/types/axis-registry.js +234 -26
  29. package/lib/types/axis-registry.js.map +1 -1
  30. package/lib/types/card-organize.d.ts +27 -12
  31. package/lib/types/card-organize.d.ts.map +1 -1
  32. package/lib/types/card-organize.js +58 -73
  33. package/lib/types/card-organize.js.map +1 -1
  34. package/lib/types/compiler/embedding-identity.d.ts +3 -0
  35. package/lib/types/compiler/embedding-identity.d.ts.map +1 -0
  36. package/lib/types/compiler/embedding-identity.js +24 -0
  37. package/lib/types/compiler/embedding-identity.js.map +1 -0
  38. package/lib/types/compiler/snapshot.d.ts +16 -0
  39. package/lib/types/compiler/snapshot.d.ts.map +1 -0
  40. package/lib/types/compiler/snapshot.js +2 -0
  41. package/lib/types/compiler/snapshot.js.map +1 -0
  42. package/lib/types/config-schema.d.ts +6 -3
  43. package/lib/types/config-schema.d.ts.map +1 -1
  44. package/lib/types/config-schema.js +6 -2
  45. package/lib/types/config-schema.js.map +1 -1
  46. package/lib/types/convert/host.d.ts +6 -5
  47. package/lib/types/convert/host.d.ts.map +1 -1
  48. package/lib/types/convert/host.js +58 -18
  49. package/lib/types/convert/host.js.map +1 -1
  50. package/lib/types/convert/ingest.d.ts +10 -1
  51. package/lib/types/convert/ingest.d.ts.map +1 -1
  52. package/lib/types/convert/ingest.js +68 -9
  53. package/lib/types/convert/ingest.js.map +1 -1
  54. package/lib/types/derive-contract.d.ts +3 -0
  55. package/lib/types/derive-contract.d.ts.map +1 -0
  56. package/lib/types/derive-contract.js +15 -0
  57. package/lib/types/derive-contract.js.map +1 -0
  58. package/lib/types/diagnosis/cards.d.ts +7 -3
  59. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  60. package/lib/types/diagnosis/cards.js +13 -8
  61. package/lib/types/diagnosis/cards.js.map +1 -1
  62. package/lib/types/diagnosis/run.d.ts +4 -13
  63. package/lib/types/diagnosis/run.d.ts.map +1 -1
  64. package/lib/types/diagnosis/run.js +26 -16
  65. package/lib/types/diagnosis/run.js.map +1 -1
  66. package/lib/types/diagnosis/schema.js +2 -2
  67. package/lib/types/diagnosis/schema.js.map +1 -1
  68. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  69. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  70. package/lib/types/diagnosis/seed-source.js +5 -4
  71. package/lib/types/diagnosis/seed-source.js.map +1 -1
  72. package/lib/types/diagnosis/seed.d.ts +4 -4
  73. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  74. package/lib/types/diagnosis/seed.js +6 -8
  75. package/lib/types/diagnosis/seed.js.map +1 -1
  76. package/lib/types/diagnosis/store.d.ts +24 -18
  77. package/lib/types/diagnosis/store.d.ts.map +1 -1
  78. package/lib/types/diagnosis/store.js +130 -65
  79. package/lib/types/diagnosis/store.js.map +1 -1
  80. package/lib/types/diagnosis/types.d.ts +47 -15
  81. package/lib/types/diagnosis/types.d.ts.map +1 -1
  82. package/lib/types/diagnosis/types.js.map +1 -1
  83. package/lib/types/environment/dsh-python.d.ts +77 -0
  84. package/lib/types/environment/dsh-python.d.ts.map +1 -0
  85. package/lib/types/environment/dsh-python.js +139 -0
  86. package/lib/types/environment/dsh-python.js.map +1 -0
  87. package/lib/types/environment/install.d.ts +15 -0
  88. package/lib/types/environment/install.d.ts.map +1 -0
  89. package/lib/types/environment/install.js +297 -0
  90. package/lib/types/environment/install.js.map +1 -0
  91. package/lib/types/environment/manifest.d.ts +208 -0
  92. package/lib/types/environment/manifest.d.ts.map +1 -0
  93. package/lib/types/environment/manifest.js +26 -0
  94. package/lib/types/environment/manifest.js.map +1 -0
  95. package/lib/types/environment/model-catalog.d.ts +223 -0
  96. package/lib/types/environment/model-catalog.d.ts.map +1 -0
  97. package/lib/types/environment/model-catalog.js +129 -0
  98. package/lib/types/environment/model-catalog.js.map +1 -0
  99. package/lib/types/environment/process.d.ts +10 -0
  100. package/lib/types/environment/process.d.ts.map +1 -0
  101. package/lib/types/environment/process.js +54 -0
  102. package/lib/types/environment/process.js.map +1 -0
  103. package/lib/types/environment/selection.d.ts +9 -0
  104. package/lib/types/environment/selection.d.ts.map +1 -0
  105. package/lib/types/environment/selection.js +29 -0
  106. package/lib/types/environment/selection.js.map +1 -0
  107. package/lib/types/environment/service.d.ts +64 -0
  108. package/lib/types/environment/service.d.ts.map +1 -0
  109. package/lib/types/environment/service.js +424 -0
  110. package/lib/types/environment/service.js.map +1 -0
  111. package/lib/types/environment/types.d.ts +56 -0
  112. package/lib/types/environment/types.d.ts.map +1 -0
  113. package/lib/types/environment/types.js +2 -0
  114. package/lib/types/environment/types.js.map +1 -0
  115. package/lib/types/events.d.ts +76 -0
  116. package/lib/types/events.d.ts.map +1 -0
  117. package/lib/types/events.js +52 -0
  118. package/lib/types/events.js.map +1 -0
  119. package/lib/types/extract-contract.d.ts +3 -0
  120. package/lib/types/extract-contract.d.ts.map +1 -0
  121. package/lib/types/extract-contract.js +41 -0
  122. package/lib/types/extract-contract.js.map +1 -0
  123. package/lib/types/extract-model.d.ts +5 -0
  124. package/lib/types/extract-model.d.ts.map +1 -1
  125. package/lib/types/extract-model.js.map +1 -1
  126. package/lib/types/extract-table-contract.d.ts +3 -0
  127. package/lib/types/extract-table-contract.d.ts.map +1 -0
  128. package/lib/types/extract-table-contract.js +27 -0
  129. package/lib/types/extract-table-contract.js.map +1 -0
  130. package/lib/types/extract-table.d.ts +8 -0
  131. package/lib/types/extract-table.d.ts.map +1 -0
  132. package/lib/types/extract-table.js +158 -0
  133. package/lib/types/extract-table.js.map +1 -0
  134. package/lib/types/extraction-runs.d.ts +30 -6
  135. package/lib/types/extraction-runs.d.ts.map +1 -1
  136. package/lib/types/extraction-runs.js +69 -8
  137. package/lib/types/extraction-runs.js.map +1 -1
  138. package/lib/types/fs-atomic.d.ts +0 -7
  139. package/lib/types/fs-atomic.d.ts.map +1 -1
  140. package/lib/types/fs-atomic.js +17 -2
  141. package/lib/types/fs-atomic.js.map +1 -1
  142. package/lib/types/images.d.ts +26 -0
  143. package/lib/types/images.d.ts.map +1 -0
  144. package/lib/types/images.js +196 -0
  145. package/lib/types/images.js.map +1 -0
  146. package/lib/types/index.d.ts +2 -2
  147. package/lib/types/index.d.ts.map +1 -1
  148. package/lib/types/index.js +598 -209
  149. package/lib/types/index.js.map +1 -1
  150. package/lib/types/intent/engine.d.ts +12 -4
  151. package/lib/types/intent/engine.d.ts.map +1 -1
  152. package/lib/types/intent/engine.js +43 -5
  153. package/lib/types/intent/engine.js.map +1 -1
  154. package/lib/types/intent/host.d.ts +5 -8
  155. package/lib/types/intent/host.d.ts.map +1 -1
  156. package/lib/types/intent/host.js +56 -11
  157. package/lib/types/intent/host.js.map +1 -1
  158. package/lib/types/intent/scope.d.ts +25 -0
  159. package/lib/types/intent/scope.d.ts.map +1 -0
  160. package/lib/types/intent/scope.js +116 -0
  161. package/lib/types/intent/scope.js.map +1 -0
  162. package/lib/types/intent/tools.d.ts +24 -20
  163. package/lib/types/intent/tools.d.ts.map +1 -1
  164. package/lib/types/intent/tools.js +65 -23
  165. package/lib/types/intent/tools.js.map +1 -1
  166. package/lib/types/intent/types.d.ts +11 -4
  167. package/lib/types/intent/types.d.ts.map +1 -1
  168. package/lib/types/knowledge-bases.d.ts +11 -5
  169. package/lib/types/knowledge-bases.d.ts.map +1 -1
  170. package/lib/types/knowledge-bases.js +52 -20
  171. package/lib/types/knowledge-bases.js.map +1 -1
  172. package/lib/types/knowledge-contract.d.ts +32 -0
  173. package/lib/types/knowledge-contract.d.ts.map +1 -0
  174. package/lib/types/knowledge-contract.js +116 -0
  175. package/lib/types/knowledge-contract.js.map +1 -0
  176. package/lib/types/knowledge.d.ts +31 -9
  177. package/lib/types/knowledge.d.ts.map +1 -1
  178. package/lib/types/knowledge.js +137 -60
  179. package/lib/types/knowledge.js.map +1 -1
  180. package/lib/types/layout.d.ts +10 -29
  181. package/lib/types/layout.d.ts.map +1 -1
  182. package/lib/types/layout.js +17 -36
  183. package/lib/types/layout.js.map +1 -1
  184. package/lib/types/materialize-snapshot.d.ts +49 -0
  185. package/lib/types/materialize-snapshot.d.ts.map +1 -0
  186. package/lib/types/materialize-snapshot.js +2 -0
  187. package/lib/types/materialize-snapshot.js.map +1 -0
  188. package/lib/types/migrate.d.ts +11 -0
  189. package/lib/types/migrate.d.ts.map +1 -1
  190. package/lib/types/migrate.js +83 -2
  191. package/lib/types/migrate.js.map +1 -1
  192. package/lib/types/model-text.d.ts +58 -1
  193. package/lib/types/model-text.d.ts.map +1 -1
  194. package/lib/types/model-text.js +85 -30
  195. package/lib/types/model-text.js.map +1 -1
  196. package/lib/types/oplog.d.ts.map +1 -1
  197. package/lib/types/oplog.js +7 -0
  198. package/lib/types/oplog.js.map +1 -1
  199. package/lib/types/organize-contract.d.ts +18 -0
  200. package/lib/types/organize-contract.d.ts.map +1 -0
  201. package/lib/types/organize-contract.js +82 -0
  202. package/lib/types/organize-contract.js.map +1 -0
  203. package/lib/types/pipeline/engine.d.ts +66 -0
  204. package/lib/types/pipeline/engine.d.ts.map +1 -0
  205. package/lib/types/pipeline/engine.js +352 -0
  206. package/lib/types/pipeline/engine.js.map +1 -0
  207. package/lib/types/pipeline/store.d.ts +73 -0
  208. package/lib/types/pipeline/store.d.ts.map +1 -0
  209. package/lib/types/pipeline/store.js +143 -0
  210. package/lib/types/pipeline/store.js.map +1 -0
  211. package/lib/types/pipeline/types.d.ts +102 -0
  212. package/lib/types/pipeline/types.d.ts.map +1 -0
  213. package/lib/types/pipeline/types.js +5 -0
  214. package/lib/types/pipeline/types.js.map +1 -0
  215. package/lib/types/present.d.ts +2 -0
  216. package/lib/types/present.d.ts.map +1 -1
  217. package/lib/types/present.js +9 -4
  218. package/lib/types/present.js.map +1 -1
  219. package/lib/types/preset-ids.d.ts +4 -0
  220. package/lib/types/preset-ids.d.ts.map +1 -0
  221. package/lib/types/preset-ids.js +6 -0
  222. package/lib/types/preset-ids.js.map +1 -0
  223. package/lib/types/query-analysis.d.ts +13 -1
  224. package/lib/types/query-analysis.d.ts.map +1 -1
  225. package/lib/types/query-analysis.js +62 -6
  226. package/lib/types/query-analysis.js.map +1 -1
  227. package/lib/types/recall-rank.d.ts +3 -3
  228. package/lib/types/recall-rank.d.ts.map +1 -1
  229. package/lib/types/recall-rank.js +4 -4
  230. package/lib/types/recall-rank.js.map +1 -1
  231. package/lib/types/retention.d.ts +39 -0
  232. package/lib/types/retention.d.ts.map +1 -0
  233. package/lib/types/retention.js +42 -0
  234. package/lib/types/retention.js.map +1 -0
  235. package/lib/types/retrieval/vector-index.d.ts +32 -0
  236. package/lib/types/retrieval/vector-index.d.ts.map +1 -0
  237. package/lib/types/retrieval/vector-index.js +15 -0
  238. package/lib/types/retrieval/vector-index.js.map +1 -0
  239. package/lib/types/retrieval/visibility.d.ts +6 -0
  240. package/lib/types/retrieval/visibility.d.ts.map +1 -0
  241. package/lib/types/retrieval/visibility.js +11 -0
  242. package/lib/types/retrieval/visibility.js.map +1 -0
  243. package/lib/types/runtime.d.ts +272 -27
  244. package/lib/types/runtime.d.ts.map +1 -1
  245. package/lib/types/runtime.js +2751 -537
  246. package/lib/types/runtime.js.map +1 -1
  247. package/lib/types/settings-schema.d.ts +17 -15
  248. package/lib/types/settings-schema.d.ts.map +1 -1
  249. package/lib/types/settings-schema.js +50 -47
  250. package/lib/types/settings-schema.js.map +1 -1
  251. package/lib/types/settings-volatile.d.ts +4 -4
  252. package/lib/types/settings-volatile.js +4 -4
  253. package/lib/types/source-evidence.d.ts +14 -0
  254. package/lib/types/source-evidence.d.ts.map +1 -0
  255. package/lib/types/source-evidence.js +36 -0
  256. package/lib/types/source-evidence.js.map +1 -0
  257. package/lib/types/store/axis-references.d.ts +5 -0
  258. package/lib/types/store/axis-references.d.ts.map +1 -0
  259. package/lib/types/store/axis-references.js +106 -0
  260. package/lib/types/store/axis-references.js.map +1 -0
  261. package/lib/types/store/derived.d.ts +12 -1
  262. package/lib/types/store/derived.d.ts.map +1 -1
  263. package/lib/types/store/derived.js +71 -12
  264. package/lib/types/store/derived.js.map +1 -1
  265. package/lib/types/store/knowledge-db.d.ts +31 -1
  266. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  267. package/lib/types/store/knowledge-db.js +252 -18
  268. package/lib/types/store/knowledge-db.js.map +1 -1
  269. package/lib/types/store/migrate.d.ts.map +1 -1
  270. package/lib/types/store/migrate.js +17 -6
  271. package/lib/types/store/migrate.js.map +1 -1
  272. package/lib/types/trajectory-types.d.ts +172 -0
  273. package/lib/types/trajectory-types.d.ts.map +1 -0
  274. package/lib/types/trajectory-types.js +36 -0
  275. package/lib/types/trajectory-types.js.map +1 -0
  276. package/lib/types/types.d.ts +59 -27
  277. package/lib/types/types.d.ts.map +1 -1
  278. package/lib/types/types.js +6 -15
  279. package/lib/types/types.js.map +1 -1
  280. package/lib/types/wiki-finalize.d.ts +16 -0
  281. package/lib/types/wiki-finalize.d.ts.map +1 -1
  282. package/lib/types/wiki-finalize.js +26 -0
  283. package/lib/types/wiki-finalize.js.map +1 -1
  284. package/lib/types/wiki-materialize.d.ts +4 -0
  285. package/lib/types/wiki-materialize.d.ts.map +1 -1
  286. package/lib/types/wiki-materialize.js +21 -7
  287. package/lib/types/wiki-materialize.js.map +1 -1
  288. package/lib/types/wiki-trial-types.d.ts +87 -0
  289. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  290. package/lib/types/wiki-trial-types.js +0 -16
  291. package/lib/types/wiki-trial-types.js.map +1 -1
  292. package/lib/types/wiki-trial.d.ts +40 -16
  293. package/lib/types/wiki-trial.d.ts.map +1 -1
  294. package/lib/types/wiki-trial.js +337 -224
  295. package/lib/types/wiki-trial.js.map +1 -1
  296. package/package.json +29 -20
  297. package/python/prepare-models.py +19 -0
  298. package/python/requirements.txt +8 -6
  299. package/scripts/doctor.mjs +13 -19
  300. package/scripts/environment.mjs +29 -0
  301. package/scripts/setup-embedding.sh +15 -5
  302. package/src/agent-behavior-rules.ts +37 -0
  303. package/src/axis-registry.ts +234 -27
  304. package/src/card-organize.ts +69 -75
  305. package/src/client/AgentBehaviorRulesCard.tsx +65 -0
  306. package/src/client/AttributionEventGuide.tsx +41 -39
  307. package/src/client/AxisRegistrySection.tsx +204 -164
  308. package/src/client/CompileDialogs.tsx +44 -29
  309. package/src/client/DeleteDocumentDialog.tsx +36 -5
  310. package/src/client/DirectoryPicker.tsx +0 -10
  311. package/src/client/EnvironmentSetup.tsx +243 -0
  312. package/src/client/ExpansionGraph.tsx +13 -7
  313. package/src/client/ExtractionTasksPage.tsx +578 -0
  314. package/src/client/GuideArchitectureCanvas.tsx +470 -0
  315. package/src/client/ImportFilesDialog.tsx +2 -13
  316. package/src/client/IntentLogic.tsx +2 -2
  317. package/src/client/IntentTab.tsx +118 -47
  318. package/src/client/KnowledgeBaseTab.tsx +277 -197
  319. package/src/client/KnowledgeLayerSection.tsx +24 -6
  320. package/src/client/KnowledgePanel.tsx +58 -93
  321. package/src/client/KnowledgeSettingsDrawer.tsx +90 -0
  322. package/src/client/ListPager.tsx +15 -5
  323. package/src/client/MaterializeTrajectoryView.tsx +124 -0
  324. package/src/client/NodeSettings.tsx +210 -89
  325. package/src/client/NormalizeTrajectoryView.tsx +136 -0
  326. package/src/client/PipelineNotice.tsx +26 -0
  327. package/src/client/PreviewSections.tsx +118 -51
  328. package/src/client/RecallPanel.tsx +89 -85
  329. package/src/client/TabGuide.tsx +159 -184
  330. package/src/client/TrialReportView.tsx +391 -214
  331. package/src/client/TypeDefinitionDrawer.tsx +150 -23
  332. package/src/client/WikiCardPreview.tsx +62 -16
  333. package/src/client/WikiFlowPanel.tsx +4 -4
  334. package/src/client/arch/NodeLogicPanel.tsx +3 -4
  335. package/src/client/arch/model.ts +6 -9
  336. package/src/client/architecture.tsx +23 -33
  337. package/src/client/attribution/AttributionGraph.tsx +10 -6
  338. package/src/client/attribution/AttributionTab.tsx +136 -84
  339. package/src/client/attribution/CaseView.tsx +3 -1
  340. package/src/client/attribution/EventView.tsx +108 -0
  341. package/src/client/attribution/RunView.tsx +2 -2
  342. package/src/client/attribution/StageChrome.tsx +34 -3
  343. package/src/client/attribution/ViewSwitch.tsx +20 -5
  344. package/src/client/attribution/graph-options.ts +47 -39
  345. package/src/client/attribution/model.ts +17 -10
  346. package/src/client/config-scope.ts +3 -3
  347. package/src/client/graph/palette.ts +165 -0
  348. package/src/client/index.ts +49 -42
  349. package/src/client/pages.css +459 -135
  350. package/src/client/pages.tsx +37 -9
  351. package/src/client/problem-domains.ts +1 -1
  352. package/src/client/session-remote.ts +9 -0
  353. package/src/client/styles.css +205 -62
  354. package/src/client/wiki-trial.ts +44 -3
  355. package/src/compiler/embedding-identity.ts +21 -0
  356. package/src/compiler/snapshot.ts +17 -0
  357. package/src/config-schema.ts +8 -3
  358. package/src/convert/host.ts +56 -21
  359. package/src/convert/ingest.ts +74 -10
  360. package/src/derive-contract.ts +14 -0
  361. package/src/diagnosis/cards.ts +13 -8
  362. package/src/diagnosis/run.ts +27 -17
  363. package/src/diagnosis/schema.ts +2 -2
  364. package/src/diagnosis/seed-source.ts +5 -4
  365. package/src/diagnosis/seed.ts +6 -8
  366. package/src/diagnosis/store.ts +138 -69
  367. package/src/diagnosis/types.ts +34 -15
  368. package/src/environment/dsh-python.ts +156 -0
  369. package/src/environment/install.ts +244 -0
  370. package/src/environment/manifest.ts +28 -0
  371. package/src/environment/model-catalog.ts +147 -0
  372. package/src/environment/process.ts +54 -0
  373. package/src/environment/selection.ts +29 -0
  374. package/src/environment/service.ts +348 -0
  375. package/src/environment/types.ts +45 -0
  376. package/src/events.ts +108 -0
  377. package/src/extract-contract.ts +42 -0
  378. package/src/extract-model.ts +10 -1
  379. package/src/extract-table-contract.ts +26 -0
  380. package/src/extract-table.ts +132 -0
  381. package/src/extraction-runs.ts +70 -11
  382. package/src/fs-atomic.ts +19 -2
  383. package/src/images.ts +203 -0
  384. package/src/index.ts +553 -205
  385. package/src/intent/README.md +8 -6
  386. package/src/intent/engine.ts +44 -6
  387. package/src/intent/host.ts +59 -18
  388. package/src/intent/scope.ts +125 -0
  389. package/src/intent/tools.ts +70 -28
  390. package/src/intent/types.ts +11 -4
  391. package/src/knowledge-bases.ts +56 -20
  392. package/src/knowledge-contract.ts +114 -0
  393. package/src/knowledge.ts +138 -69
  394. package/src/layout.ts +17 -37
  395. package/src/materialize-snapshot.ts +46 -0
  396. package/src/migrate.ts +75 -2
  397. package/src/model-text.ts +109 -4
  398. package/src/oplog.ts +7 -0
  399. package/src/organize-contract.ts +83 -0
  400. package/src/pipeline/engine.ts +323 -0
  401. package/src/pipeline/store.ts +147 -0
  402. package/src/pipeline/types.ts +93 -0
  403. package/src/present.ts +10 -4
  404. package/src/preset-ids.ts +6 -0
  405. package/src/python/embedding_worker.py +66 -2
  406. package/src/query-analysis.ts +68 -8
  407. package/src/recall-rank.ts +5 -5
  408. package/src/retention.ts +76 -0
  409. package/src/retrieval/vector-index.ts +45 -0
  410. package/src/retrieval/visibility.ts +14 -0
  411. package/src/runtime.ts +2708 -567
  412. package/src/settings-schema.ts +11 -8
  413. package/src/settings-volatile.ts +4 -4
  414. package/src/source-evidence.ts +37 -0
  415. package/src/store/axis-references.ts +96 -0
  416. package/src/store/derived.ts +70 -12
  417. package/src/store/knowledge-db.ts +248 -17
  418. package/src/store/migrate.ts +16 -6
  419. package/src/trajectory-types.ts +142 -0
  420. package/src/types.ts +61 -35
  421. package/src/wiki-finalize.ts +28 -0
  422. package/src/wiki-materialize.ts +25 -6
  423. package/src/wiki-trial-types.ts +83 -1
  424. package/src/wiki-trial.ts +357 -185
  425. package/lib/types/intent/preset-tools.d.ts +0 -37
  426. package/lib/types/intent/preset-tools.d.ts.map +0 -1
  427. package/lib/types/intent/preset-tools.js +0 -45
  428. package/lib/types/intent/preset-tools.js.map +0 -1
  429. package/preset/tita-data-agent/agent.cordis.yml +0 -394
  430. package/preset/tita-data-agent/preset.yml +0 -3
  431. package/scripts/install-agent-preset.mjs +0 -92
  432. package/scripts/sync-agent-preset.mjs +0 -188
  433. package/src/client/AgentPresetCard.tsx +0 -247
  434. package/src/client/agent-preset.ts +0 -121
  435. package/src/client/convert/ConvertPanel.tsx +0 -583
  436. package/src/client/convert/styles.css +0 -138
  437. package/src/client/convert/styles.ts +0 -49
  438. package/src/intent/preset-tools.ts +0 -48
package/src/index.ts CHANGED
@@ -1,17 +1,21 @@
1
+ import { embeddingModel } from './environment/model-catalog.js'
1
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
- import { createReadStream, existsSync } from 'node:fs'
3
+ import { createReadStream } from 'node:fs'
3
4
  import { readFile, stat } from 'node:fs/promises'
4
5
  import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path'
5
6
  import { fileURLToPath } from 'node:url'
6
7
  import type { Context } from '@deepseek-ai/cordis'
8
+ import type {} from '@deepseek-ai/dsh-agent'
9
+ import type {} from '@deepseek-ai/dsh-system-prompt'
7
10
  import z from '@deepseek-ai/schemastery'
8
11
  import { defineTool, type PreToolDecision, type ToolRunContext } from '@deepseek-ai/dsh-tools'
9
- import type { LineageContext } from 'dsh-data-contracts/refs'
12
+ import type { LineageContext } from '#contracts'
10
13
  import { operationId, type OperationLog } from './oplog.js'
11
14
  import { AXIS_GUIDANCE_KINDS, type AxisGuidanceKind } from './axis-registry.js'
12
15
  import type { OperationDetail } from './types.js'
13
16
  import {
14
- DEFAULT_KNOWLEDGE_CONFIG, type CardStatus, type Envelope, type KnowledgeBase, type KnowledgeConfig,
17
+ DEFAULT_COMPILE_MODEL, DEFAULT_KNOWLEDGE_CONFIG, type CardStatus, type CompileModelSelection,
18
+ type Envelope, type KnowledgeBase, type KnowledgeConfig,
15
19
  type KnowledgeStatus, type RecallMode, type SchemaType,
16
20
  } from './types.js'
17
21
  import {
@@ -26,16 +30,17 @@ import {
26
30
  type ModelSeam,
27
31
  } from './runtime.js'
28
32
  import { unwrapGroup } from './settings-volatile.js'
33
+ import { behaviorPromptForPreset } from './agent-behavior-rules.js'
34
+ import { KnowledgeEnvironment } from './environment/service.js'
29
35
  import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './document-page.js'
30
36
  import type { ModelCall } from './extract-model.js'
31
37
  import { runWikiTrial, trialSummary } from './wiki-trial.js'
32
38
  import { extractionRunId } from './extraction-runs.js'
39
+ import type { PipelineStage } from './pipeline/types.js'
33
40
  import {
34
- agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot, legacyDefaultKnowledgeRoot,
35
- memoryRoot, presetSourceDir, resourceRegistry, resourceRoot, type AgentLayout,
41
+ agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot,
42
+ memoryRoot, resourceRegistry, resourceRoot, type AgentLayout,
36
43
  } from './layout.js'
37
- import { migrateLegacyKnowledgeTree, migrateLegacyMemory, migrateLegacyResources, type MigrationOutcome } from './migrate.js'
38
- import { legacyResourceHome } from './types.js'
39
44
  import {
40
45
  PickerError, createDirectory, listDirectory, pickDirectory, pickerKind, pickerOf,
41
46
  } from './directory-picker.js'
@@ -44,6 +49,7 @@ import type { DiagnosisRunRecord } from './diagnosis/types.js'
44
49
  import { KNOWLEDGE_SETTINGS_SCHEMA, SETTINGS_NAMESPACE } from './settings-schema.js'
45
50
  import { DEFAULT_KB_ID, KnowledgeBases } from './knowledge-bases.js'
46
51
  import type { KnowledgeQuery } from './knowledge.js'
52
+ import { ImagePublishError, publishKnowledgeImage } from './images.js'
47
53
  import {
48
54
  applyConvert, CONVERT_SETTINGS_NAMESPACE, CONVERT_SETTINGS_SCHEMA, MOUNT_PATH as CONVERT_MOUNT_PATH,
49
55
  type ConvertHost,
@@ -53,7 +59,9 @@ import {
53
59
  applyIntent, INTENT_MOUNT_PATH, INTENT_SERVICE,
54
60
  type IntentHost,
55
61
  type IntentLineageService,
62
+ type IntentService,
56
63
  } from './intent/host.js'
64
+ import { registerIntentTools } from './intent/tools.js'
57
65
 
58
66
  export * from './types.js'
59
67
  export * from './runtime.js'
@@ -68,14 +76,14 @@ export const name = 'dsh-data-domain-knowledge'
68
76
  * `settings` 在 0.1.7 之后**不再需要**:设置已经是本 entry 的 config
69
77
  * (见下面的 `Config`),`settings.installSection` 那条路已经整个删除。
70
78
  */
71
- export const inject = ['tools', 'webServer'] as const
79
+ export const inject = ['tools', 'webServer', 'systemPrompt'] as const
72
80
  // schema 与命名空间搬到了 `settings-schema.ts`(可测)。这里再导出一次,保持
73
81
  // `dsh-data-domain-knowledge` 的既有出口不变。
74
82
  export { KNOWLEDGE_SETTINGS_SCHEMA, SETTINGS_NAMESPACE }
75
83
  export { CONVERT_SETTINGS_SCHEMA }
76
84
  /**
77
85
  * 组合 schema 与它的取值形态住在 `./config-schema.js`:那里**没有工具层依赖**,
78
- * 所以 `tests/settings-schema.test.mjs` 能 import 它并把两组设置解析一遍(装载器
86
+ * 所以 `tests/settings-schema.test.mjs` 能 import 它并把设置解析一遍(装载器
79
87
  * 仍然要从**本模块**读到 `Config`,所以这里 import 之后再 re-export)。
80
88
  *
81
89
  * 0.1.7 删掉了「设置命名空间 + `settings.installSection`」这一整套 API,改成
@@ -84,7 +92,7 @@ export { CONVERT_SETTINGS_SCHEMA }
84
92
  * 客户端按**路径**寻址(例如 `{op:'set', path:['knowledge','recallTopK']}`)。
85
93
  */
86
94
  import { Config, type KnowledgePluginConfig } from './config-schema.js'
87
- import { buildModelRequest, collectModelText } from './model-text.js'
95
+ import { buildModelRequest, callModelWithRetry, collectModelText } from './model-text.js'
88
96
  import {
89
97
  addSourceCallView, addSourceMetaOf, addSourceResultMarkdown,
90
98
  importCallView, importMetaOf, importResultMarkdown,
@@ -263,7 +271,7 @@ function escapeHtml(value: string): string {
263
271
  /**
264
272
  * 独立整页的 HTML 外壳。
265
273
  *
266
- * 与 Notebook 的整页同一个形状,而且**只做三件事**:标题、挂载点、把自己那个 bundle
274
+ * 与知识库原文整页相同的挂载形状,而且**只做三件事**:标题、挂载点、把自己那个 bundle
267
275
  * 拉起来。正文不在外壳里 —— 页面自己去 `/knowledge/documents/:docId/content` 一页一页地取,
268
276
  * 所以这份 HTML 的大小与文档大小无关。
269
277
  */
@@ -372,29 +380,23 @@ function normalizeKnowledge(next: Partial<KnowledgeConfig> | undefined): Knowled
372
380
  return {
373
381
  ...DEFAULT_KNOWLEDGE_CONFIG,
374
382
  ...plain,
375
- agentRoot: typeof plain.agentRoot === 'string' && plain.agentRoot.trim() ? plain.agentRoot : defaultAgentRoot(),
376
- rootDir: typeof plain.rootDir === 'string' ? plain.rootDir : '',
383
+ // 根目录是 DSH 下的 Tita Agent 专属目录,不再是用户可切换的设置。
384
+ // 旧值即使还存在于宿主配置中,也不参与路径解析。
385
+ agentRoot: defaultAgentRoot(),
386
+ // 仅保留字段形状,避免旧配置无法解析;新逻辑不读取旧知识树。
387
+ rootDir: '',
388
+ // 产品检索固定使用多路径融合。旧版单路选择仍可解析,但不再改变运行行为。
389
+ recallMode: 'hybrid',
377
390
  }
378
391
  }
379
392
 
380
393
 
381
394
  export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): void {
382
- /**
383
- * 0029:配置里现在有两个路径字段,但只有 `agentRoot` 是「设置」。
384
- *
385
- * `rootDir` 是旧布局的知识树根,只作为迁移来源存在。初始值给空串而不是旧默认值
386
- * ——迁移的来源由下面的 `legacySource()` 决定,它还要考虑「没设过 rootDir 的
387
- * 用户,旧树在旧默认位置」这一种情况。
388
- *
389
- * 0.1.7:值来自本 entry 的 `config`(`apply` 的第二个参数),不再是
390
- * `settings.installSection` 推回来的命名空间。这里先 `normalizeKnowledge` 补齐,
391
- * 之后的迁移与 runtime 都只认这一份配置。
392
- */
393
395
  let config: KnowledgeConfig = normalizeKnowledge(pluginConfig.knowledge)
394
396
  /**
395
397
  * entry config 的另外两组,**按需求值**。
396
398
  *
397
- * 为什么是函数而不是先解包成一个值:0.1.7 的设置字段全标了 volatile,schema
399
+ * 为什么是函数而不是先解包成一个值:`convert` 的可编辑字段标了 volatile,schema
398
400
  * 解析出来是引用;解包一次就是一张快照,而 volatile 改动**不会重载插件**
399
401
  * (loader 只就地改引用、再发一个 `loader/volatile-update`)。函数让下游每次
400
402
  * 「重灌」都从同一个引用重新读值,改设置才真的会生效。
@@ -409,38 +411,16 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
409
411
  * `applyIntent`)。
410
412
  */
411
413
  let convertHost: ConvertHost | undefined
412
- /** 知识树迁移的结论。`/knowledge/layout` 会带上它,界面据此如实交代数据搬没搬。 */
413
- let migration: MigrationOutcome = { status: 'skipped', reason: '尚未解析设置' }
414
- /** 家当(模型 / Python 环境 / 注册表)迁移的结论。 */
415
- let resourceMigration: MigrationOutcome = { status: 'skipped', reason: '尚未解析设置' }
416
- /** 记忆段(知识树 → `<agentRoot>/memory/<库 id>`)迁移的结论(0032)。 */
417
- let memoryMigration: MigrationOutcome = { status: 'skipped', reason: '尚未解析设置' }
418
414
  /**
419
415
  * 初始化失败的原因(如果有)。字段名保持 `settingsError`:`/knowledge/layout`
420
416
  * 与面板上的告警都按这个名字读,改名的收益不如改语义的成本高。
421
417
  *
422
418
  * 旧的理由是「设置分区注册失败会被静默吞掉」(v0.7.5 的
423
419
  * `z.string().min(1).default('')` 让整个命名空间注册不上)。0.1.7 之后 schema
424
- * 错误会在装载 entry 时就抛、插件直接加载失败,不再是静默的;但**迁移**仍可能
425
- * 因为一个权限问题失败,而那条失败不该让插件起不来,所以这里依旧接住并上报。
420
+ * 错误会在装载 entry 时就抛、插件直接加载失败,不再是静默的;这里仍保留错误
421
+ * 视图,便于报告布局初始化失败。
426
422
  */
427
423
  let settingsError: string | undefined
428
- /**
429
- * 迁移只该发生一次 —— 实际含义是「每次 apply 最多一次」。
430
- *
431
- * 0.1.7 下改设置会让插件**整个重载**,apply 因此会再跑一遍。用「已经跑过」而不是
432
- * 「结果如何」做闸门,是为了不让一个失败的迁移在重载时被反复重试。
433
- */
434
- let migrationDone = false
435
-
436
- /**
437
- * 迁移的旧位置来源。
438
- *
439
- * 显式设过 `rootDir` 就用它;否则退回旧默认位置(`DSH_KNOWLEDGE_ROOT` 或
440
- * `~/Desktop/titaagent/domain_knowledge`)。判据交给 `migrateLegacyKnowledgeTree`,
441
- * 这里只负责「谁可能是旧树」。
442
- */
443
- const legacySource = (): string => config.rootDir.trim() || legacyDefaultKnowledgeRoot()
444
424
 
445
425
  /**
446
426
  * #3b:把 **DSH 的默认模型**包成运行时认识的纯文本调用。
@@ -449,10 +429,13 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
449
429
  * DSH 自己的 `agentDefaultModel`(设置里那一项)。插件再存一份,就会出现「设置里显示
450
430
  * A、实际调的是 B」—— 0029 想收拢的正是这类两处说法。
451
431
  *
452
- * 取不到 `llm` 或默认模型时返回 undefined,链路**退回纯规则抽取**:模型是增强,
453
- * 不是前提。
432
+ * 取不到 `llm` 或所选模型时返回原因,链路**退回纯规则抽取**:模型是增强,不是前提。
433
+ * 选择“跟随默认”时才读取 `agentDefaultModel`;固定模型不依赖默认模型服务。
454
434
  */
455
- const modelSeamFor = (): ModelSeam => {
435
+ /** 注册表在默认 runtime 之后才构造;用一个可替换的读取器避开构造期 TDZ。 */
436
+ let compileModelForBase: (id: string) => CompileModelSelection = () => ({ ...DEFAULT_COMPILE_MODEL })
437
+
438
+ const modelSeamFor = (kbId = DEFAULT_KB_ID): ModelSeam => {
456
439
  /**
457
440
  * **只能用 `ctx.get(name)`,不能用属性访问。**
458
441
  *
@@ -464,8 +447,11 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
464
447
  *
465
448
  * 本机探针(cordis 4.0.4)实测:`ctx.get` 能看见后来才注册的服务,属性访问抛异常。
466
449
  */
467
- const llm = ctx.get('llm') as
468
- { stream(options: Record<string, unknown>): AsyncIterable<{ type?: string; text?: string }> } | undefined
450
+ const llm = ctx.get('llm') as {
451
+ stream(options: Record<string, unknown>): AsyncIterable<{ type?: string; text?: string }>
452
+ resolveCallConfig?: (config: Record<string, unknown>) => Promise<unknown>
453
+ listModels?: (provider: string) => Promise<Array<{ id: string; name?: string; description?: string }>>
454
+ } | undefined
469
455
  if (!llm) return { why: '当前上下文里取不到 `llm` 服务(模型调用入口)' }
470
456
 
471
457
  /**
@@ -477,16 +463,48 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
477
463
  * 所以那个类自己也是用 `this.config.provider.get()` 读的。第一版就是按普通属性读的,
478
464
  * 结果是永远拿到 undefined —— 模型接缝静默失效。
479
465
  */
480
- const service = ctx.get('agentDefaultModel') as
481
- { currentSelection?: () => { provider?: string; model?: string; reasoningEffort?: string } } | undefined
482
- if (!service) return { why: '当前上下文里取不到 `agentDefaultModel` 服务 —— 默认模型那一行可能没挂上' }
483
- const route = service.currentSelection?.()
484
- if (!route?.provider || !route?.model) {
485
- return { why: `默认模型没有选好(provider=${route?.provider ?? '空'} / model=${route?.model ?? '空'})` }
466
+ const configured = compileModelForBase(kbId)
467
+ let defaultProvider: string | undefined
468
+ let defaultModel: string | undefined
469
+ let reasoningEffort: string | undefined
470
+ if (configured.mode === 'default') {
471
+ const service = ctx.get('agentDefaultModel') as
472
+ { currentSelection?: () => { provider?: string; model?: string; reasoningEffort?: string } } | undefined
473
+ if (!service) return { why: '当前上下文里取不到 `agentDefaultModel` 服务 —— 默认模型那一行可能没挂上' }
474
+ const route = service.currentSelection?.()
475
+ defaultProvider = route?.provider
476
+ defaultModel = route?.model
477
+ reasoningEffort = route?.reasoningEffort
478
+ if (!defaultProvider || !defaultModel) {
479
+ return { why: `默认模型没有选好(provider=${defaultProvider ?? '空'} / model=${defaultModel ?? '空'})` }
480
+ }
481
+ }
482
+ const provider = configured.mode === 'fixed' ? configured.provider : defaultProvider
483
+ const model = configured.mode === 'fixed' ? configured.model : defaultModel
484
+ const effectiveReasoningEffort = configured.reasoningEffort || (configured.mode === 'default' ? reasoningEffort : undefined)
485
+ if (!provider || !model) {
486
+ return { why: `编译模型没有选好(provider=${provider ?? '空'} / model=${model ?? '空'})` }
486
487
  }
487
- const { provider, model, reasoningEffort } = route
488
488
  return {
489
- call: async ({ system, prompt, sessionId }) => {
489
+ provider,
490
+ model,
491
+ ...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
492
+ call: async ({ system, prompt, sessionId, onUsage, onDelta }) => {
493
+ /**
494
+ * 先走 DSH 的公开解析接口,再开始发 prompt。这样固定模型过期、默认模型失效时,
495
+ * 编译会在模型调用边界明确失败,而不是落成一次看似成功的规则抽取。
496
+ */
497
+ if (llm.resolveCallConfig) {
498
+ try {
499
+ await llm.resolveCallConfig({
500
+ provider, model,
501
+ ...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
502
+ })
503
+ } catch (cause) {
504
+ const detail = cause instanceof Error ? cause.message : String(cause)
505
+ throw new Error(`编译模型不可用(${provider}/${model}):${detail}`)
506
+ }
507
+ }
490
508
  /**
491
509
  * 请求形状与收流都交给 `model-text.ts`(0044 实机验证第三~五批)。
492
510
  *
@@ -497,16 +515,34 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
497
515
  * (OpenCode 系路由回 400 `MissingSessionID`)。
498
516
  */
499
517
  const request = buildModelRequest({
500
- provider, model, reasoningEffort, system, prompt,
518
+ provider, model, reasoningEffort: effectiveReasoningEffort, system, prompt,
501
519
  // 运行时按知识库给;接缝被单独调用时(测试)退回一个稳定值,
502
520
  // 而不是留空 —— 留空会在 OpenCode 路由上 400。
503
521
  sessionId: sessionId ?? 'knowledge:default',
504
522
  })
505
- return collectModelText(llm.stream(request) as AsyncIterable<never>)
523
+ /**
524
+ * 流在**半路**结束要重发,不要一次失败就作废整份文档。
525
+ *
526
+ * 知识抽取是本插件里最长的一次模型调用(实机:约四万输出 token、两三分钟),
527
+ * 而上游网关 / 本机代理 / 账号额度任何一处抖一下,流就会在没给
528
+ * `finish_reason` 的情况下结束。适配器把这件事报成
529
+ * `TRANSPORT: Stream ended without finish_reason`,过去一次抖动就等于
530
+ * 「这份文档抽取失败」,人在界面上看到的是「全部失败」。
531
+ *
532
+ * 重发用的是**同一个请求**:提示词、sessionId、reasoningEffort 全不变,
533
+ * 所以缓存亲和仍然成立。不可重试的错误(凭据、请求体、截断、用户取消)
534
+ * 由 `isRetryableModelFailure` 放行,不在错的地方多等两分钟。
535
+ */
536
+ return callModelWithRetry(
537
+ onDeltaOfAttempt => collectModelText(llm.stream(request) as AsyncIterable<never>, onUsage, onDeltaOfAttempt),
538
+ { ...(onDelta ? { onDelta } : {}) },
539
+ )
506
540
  },
507
541
  }
508
542
  }
543
+ let environment: KnowledgeEnvironment
509
544
  const runtimeOptions = (): KnowledgeRuntimeOptions => ({
545
+ environmentGuard: kind => environment.requireReady(kind),
510
546
  // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
511
547
  resourceRoot: resourceRoot(config.agentRoot),
512
548
  // 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
@@ -543,10 +579,119 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
543
579
  // 0044:非默认库的 runtime 也要拿到模型接缝。只传 `config` 不够 —— 默认库走上面
544
580
  // 那个 `new KnowledgeRuntime(... runtimeOptions())`,别的库走注册表这条支路,
545
581
  // 两边必须都带上,否则「新建一个库 → 导入文档」静默变成规则抽取。
546
- runtimeOptions: () => ({ modelSeam: modelSeamFor }),
582
+ runtimeOptions: () => ({ resourceRoot: resourceRoot(config.agentRoot), modelSeam: modelSeamFor, environmentGuard: kind => environment.requireReady(kind) }),
547
583
  })
584
+ compileModelForBase = id => bases.compileModelOf(id)
548
585
  runtime.baseId = bases.defaultId
549
586
  activeKnowledgeBases = bases
587
+ environment = new KnowledgeEnvironment({
588
+ resourceRoot: resourceRoot(config.agentRoot),
589
+ snapshot: () => ({ config: runtime.config, paths: runtime.resourcePaths() }),
590
+ probe: kind => runtime.environmentProbe(kind),
591
+ compileStatus: () => runtime.compileModelStatus(),
592
+ reload: () => { runtime.reloadResources(); bases.reloadResources() },
593
+ activateSelection: async selection => {
594
+ const keys = ['embeddingProvider', 'embeddingModel', 'embeddingDimension', 'rerankProvider', 'rerankModel'] as const
595
+ const previous = Object.fromEntries(keys.map(key => [key, config[key]]))
596
+ const managed = { embeddingProvider: 'wemm-python', embeddingModel: '', embeddingDimension: embeddingModel(selection.embedding).dimension, rerankProvider: 'bert-python', rerankModel: '' }
597
+ const write = async (values: Record<string, unknown>): Promise<void> => {
598
+ const group = pluginConfig.knowledge as unknown as Record<string, unknown> | undefined
599
+ if (!group) throw new Error('模型设置不可写')
600
+ for (const key of keys) {
601
+ const reference = group[key] as { write?: (value: unknown) => unknown } | undefined
602
+ if (reference && typeof reference.write === 'function') await reference.write(values[key])
603
+ else group[key] = values[key]
604
+ }
605
+ config = normalizeKnowledge(pluginConfig.knowledge)
606
+ runtime.updateConfig(config); bases.updateRuntimeConfig(config)
607
+ }
608
+ try { await write(managed) } catch (cause) { await write(previous); throw cause }
609
+ return () => {
610
+ const current = normalizeKnowledge(pluginConfig.knowledge)
611
+ // A later manual setting change belongs to the user and must survive rollback.
612
+ const restored = Object.fromEntries(keys.map(key => [key, current[key] === managed[key] ? previous[key] : current[key]]))
613
+ return write(restored)
614
+ }
615
+ },
616
+ })
617
+ // Reuse a matching persisted probe after restart; only stale or missing reports need a resource check.
618
+ if (environment.view().report.state !== 'ready') void environment.check(false).catch(() => undefined)
619
+
620
+ type CompileModelCatalogItem = { provider: string; id: string; name: string; description?: string; available: boolean; current?: boolean; reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }
621
+ const compileModelCatalog = async (kbId: string, queryModelRoute: { provider?: string; model?: string } = {}): Promise<{
622
+ selection: CompileModelSelection
623
+ default: { provider?: string; model?: string; reasoningEffort?: string; available: boolean }
624
+ models: CompileModelCatalogItem[]
625
+ why?: string
626
+ }> => {
627
+ const service = ctx.get('agentDefaultModel') as
628
+ { currentSelection?: () => { provider?: string; model?: string; reasoningEffort?: string } } | undefined
629
+ const defaultRoute = service?.currentSelection?.() ?? {}
630
+ const llm = ctx.get('llm') as {
631
+ listModels?: (provider: string) => Promise<Array<{ id: string; name?: string; description?: string }>>
632
+ listProviders?: () => Array<{ id: string; name: string }>
633
+ resolveModelInfo?: (provider: string, model: string) => Promise<{ reasoning?: CompileModelCatalogItem['reasoning'] }>
634
+ } | undefined
635
+ const providers = [...new Set([...(llm?.listProviders?.() ?? []).map(provider => provider.id), defaultRoute.provider, compileModelForBase(kbId).provider].filter((value): value is string => Boolean(value)))]
636
+ const models: CompileModelCatalogItem[] = []
637
+ const failures: string[] = []
638
+ if (!llm?.listModels) {
639
+ failures.push('当前上下文没有 DSH 模型目录服务')
640
+ } else {
641
+ for (const provider of providers) {
642
+ try {
643
+ const entries = await llm.listModels(provider)
644
+ for (const entry of entries) {
645
+ if (!entry?.id) continue
646
+ models.push({
647
+ provider,
648
+ id: entry.id,
649
+ name: entry.name?.trim() || entry.id,
650
+ ...(entry.description ? { description: entry.description } : {}),
651
+ available: true,
652
+ ...(entry.id === defaultRoute.model && provider === defaultRoute.provider ? { current: true } : {}),
653
+ })
654
+ }
655
+ } catch (cause) {
656
+ failures.push(`${provider}: ${cause instanceof Error ? cause.message : String(cause)}`)
657
+ }
658
+ }
659
+ }
660
+
661
+ const selection = compileModelForBase(kbId)
662
+ const effortProvider = selection.mode === 'fixed' ? selection.provider : defaultRoute.provider
663
+ const effortModel = selection.mode === 'fixed' ? selection.model : defaultRoute.model
664
+ const requestedProvider = queryModelRoute.provider || effortProvider
665
+ const requestedModel = queryModelRoute.model || effortModel
666
+ if (requestedProvider && requestedModel && llm?.resolveModelInfo) {
667
+ const item = models.find(model => model.provider === requestedProvider && model.id === requestedModel)
668
+ if (item) {
669
+ try { item.reasoning = (await llm.resolveModelInfo(requestedProvider, requestedModel)).reasoning }
670
+ catch (cause) { failures.push(`思考强度目录:${cause instanceof Error ? cause.message : String(cause)}`) }
671
+ }
672
+ }
673
+ const selected = selection.mode === 'fixed' && selection.provider && selection.model
674
+ ? models.find(item => item.provider === selection.provider && item.id === selection.model)
675
+ : undefined
676
+ if (selection.mode === 'fixed' && selection.provider && selection.model && !selected) {
677
+ models.unshift({ provider: selection.provider, id: selection.model, name: selection.model, available: false, current: true })
678
+ }
679
+ const defaultAvailable = Boolean(
680
+ defaultRoute.provider && defaultRoute.model
681
+ && models.some(item => item.provider === defaultRoute.provider && item.id === defaultRoute.model && item.available),
682
+ )
683
+ return {
684
+ selection,
685
+ default: {
686
+ ...(defaultRoute.provider ? { provider: defaultRoute.provider } : {}),
687
+ ...(defaultRoute.model ? { model: defaultRoute.model } : {}),
688
+ ...(defaultRoute.reasoningEffort ? { reasoningEffort: defaultRoute.reasoningEffort } : {}),
689
+ available: defaultAvailable,
690
+ },
691
+ models,
692
+ ...(failures.length > 0 ? { why: failures.join(';') } : {}),
693
+ }
694
+ }
550
695
 
551
696
  const kbIdOf = (value: unknown): string => {
552
697
  if (value === undefined || value === null) return bases.defaultId
@@ -559,7 +704,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
559
704
  *
560
705
  * `Entry._commitVolatile()` 只调 `updateVolatile()` 就地改引用,再发一个
561
706
  * `loader/volatile-update` 事件。所以「设置存下了但没生效」这件事只能在这里兜住 ——
562
- * 设置 schema 里的字段全部标了 volatile(见 `settings-volatile.ts`),这两件事必须成对。
707
+ * 设置 schema 里的可编辑字段标了 volatile(见 `settings-volatile.ts`),这两件事必须成对。
563
708
  *
564
709
  * 就地更新之所以够用,是因为 `pluginConfig` 就是宿主那份被改过的对象:重新
565
710
  * `normalizeKnowledge` 一下就能拿到新值。
@@ -579,6 +724,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
579
724
  const previousAgentRoot = config.agentRoot
580
725
  config = normalizeKnowledge(pluginConfig.knowledge)
581
726
  runtime.updateConfig(config)
727
+ bases.updateRuntimeConfig(config)
582
728
  /**
583
729
  * 转换设置挂在同一个 entry 上,volatile 事件需要让转换宿主重新读取。
584
730
  */
@@ -613,66 +759,12 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
613
759
  }
614
760
 
615
761
  /**
616
- * 给每个知识库搬记忆段(0032)。
617
- *
618
- * 结论只保留**最后一个非 skipped 的**:默认库才是用户实际在用的那个,而面板上
619
- * 一行位置有限。真有多个库搬失败时,`/knowledge/layout` 的 `resourceMigration`
620
- * 与知识库列表里的路径仍然能对上账。
621
- */
622
- const migrateAllMemory = (): MigrationOutcome => {
623
- const bases = activeKnowledgeBases
624
- if (!bases) return { status: 'skipped', reason: '知识库注册表尚未就绪' }
625
- let outcome: MigrationOutcome = { status: 'skipped', reason: '每个知识库的记忆都在新位置' }
626
- const roots = bases.list()
627
- const defaultBase = roots.find(item => item.id === bases.defaultId)
628
- // 默认库的根由配置推出来,可能还没出现在注册表里(首启);补上它。
629
- const plan = defaultBase ? roots : [{ id: bases.defaultId, rootDir: defaultBaseRoot() }, ...roots]
630
- for (const base of plan) {
631
- try {
632
- const moved = migrateLegacyMemory({ agentRoot: config.agentRoot, kbRoot: base.rootDir, kbId: base.id })
633
- if (moved.status !== 'skipped') outcome = moved
634
- } catch (cause) {
635
- outcome = {
636
- status: 'failed',
637
- from: join(base.rootDir, '04_memory'),
638
- to: memoryRoot(config.agentRoot, base.id),
639
- reason: cause instanceof Error ? cause.message : String(cause),
640
- }
641
- }
642
- }
643
- return outcome
644
- }
645
-
646
- /**
647
- * 初始化:迁移 → 布局 → 构造 runtime。
762
+ * 初始化:只创建新布局,再构造 runtime。
648
763
  *
649
- * 顺序是**硬要求**:配置已由外层的 `normalizeKnowledge` 补齐,这里先迁移(迁移要读
650
- * 新的 `agentRoot` 与旧的 `rootDir`),最后才构造 runtime。反过来的话,runtime 会在
651
- * 数据搬完之前就指着新路径,于是「刚启动时知识库是空的」——而那正是最容易让人以为
652
- * 数据丢了的时刻。
653
- *
654
- * 0.1.7:这段以前挂在 `settings.installSection` 的 `setSource`/`onChange` 回调里,
655
- * 因为配置要等设置层推回来。现在配置是 `apply` 的入参,所以直接在 apply 时跑一次;
656
- * 改设置会让插件**整个重载**,于是它会随重载再跑,由 `migrationDone` 挡住重复迁移。
764
+ * 旧版的 `rootDir`、旧 Agent 根路径和旧家当目录都不再参与启动流程。这样新安装的
765
+ * 数据路径是确定的,也不会因为启动而改动用户现有目录。
657
766
  */
658
767
  const initialize = (): void => {
659
- if (!migrationDone) {
660
- migrationDone = true
661
- // 顺序:先把知识树搬进新根路径,再把家当搬进来。家当要在 runtime 构造**之前**
662
- // 到位 —— 否则模型与解释器会先按旧位置解析一次,那一轮跑的是旧环境。
663
- migration = migrateLegacyKnowledgeTree({ agentRoot: config.agentRoot, legacyRoot: legacySource() })
664
- resourceMigration = migrateLegacyResources({ agentRoot: config.agentRoot, legacyHome: legacyResourceHome() })
665
- /**
666
- * 0032:记忆段从知识树搬出去。**每个知识库各搬一次** —— 它按库分叉,而
667
- * 非默认库的 runtime 是懒建的(`runtimeOf`),懒建里做迁移会把「打开一个库」
668
- * 变成一次可能的搬盘。放在这里,顺序与另外两个迁移一致:搬完才构造 runtime。
669
- *
670
- * 逐个 try:一个库搬不动不该让其余的也不搬,更不该让插件起不来。
671
- */
672
- memoryMigration = migrateAllMemory()
673
- // 注册表可能刚被搬进来:让它按新位置重新读一遍。
674
- void bases.repoint()
675
- }
676
768
  ensureAgentLayout(config.agentRoot)
677
769
  runtime.close()
678
770
  runtime = new KnowledgeRuntime(defaultBaseRoot(), config, runtimeOptions())
@@ -684,9 +776,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
684
776
  /**
685
777
  * 初始化失败必须留下痕迹。
686
778
  *
687
- * 迁移会因为权限、占用、半个快照这类原因失败;而它抛出的异常如果没人接,就只是
688
- * 一次没人看见的失败 —— 面板照常显示,数据却没搬。抓住它、记下来、让
689
- * `/knowledge/layout` 报出去,界面于是能说「为什么不对」,而不是只显示症状。
779
+ * 创建新布局或构造 runtime 会因为权限、占用等原因失败;而它抛出的异常如果没人接,
780
+ * 就只是一次没人看见的失败。抓住它、记下来、让 `/knowledge/layout` 报出去,
781
+ * 界面于是能说「为什么不对」,而不是只显示症状。
690
782
  *
691
783
  * 注意这里**不吞配置错误**:schema 不合法的 config 在装载 entry 时就已经让插件
692
784
  * 加载失败了(那是响亮的)。这里接住的只是初始化期的数据问题。
@@ -726,45 +818,122 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
726
818
  // 0.1.7:转换的设置不再走命名空间,随本 entry 的 config 一起下来。
727
819
  // 给函数而不是值:字段是 volatile 引用,解包一次就是快照(见 `convertGroup`)。
728
820
  config: convertGroup,
729
- // 0019: a delivery can target one knowledge base (the panel imports into the
730
- // base the user selected). Unknown/absent id falls back to the default base
731
- // instead of failing the ingest.
732
- resolveTarget: (kbId?: string) => {
733
- const target = kbId ? runtimeOf(kbId) : undefined
734
- return target ? { runtime: target, knowledgeRoot: target.rootDir } : { runtime, knowledgeRoot: runtime.rootDir }
821
+ // 0019: an omitted id selects the default base in the host. An explicit id
822
+ // must resolve here; otherwise ingestion fails instead of writing to a
823
+ // different knowledge base while reporting the requested id.
824
+ resolveTarget: (kbId: string) => {
825
+ const target = runtimeOf(kbId)
826
+ return target ? { runtime: target, knowledgeRoot: target.rootDir } : undefined
735
827
  },
736
828
  })
737
829
 
738
830
  // 意图记录模块 (issue 0020). The 意图 view was already one of this plugin's
739
831
  // five panel tabs, but its Host lived in a second mount unit; 0020 folds it in
740
832
  // so the declared information architecture and the code layout agree again.
741
- // It keeps the `intent_record` / `intent_store_view` tools and the `/intent/**` read-only surface,
833
+ // It keeps the `intent_record` / `intent_store_view` tools and `/intent/**` audit endpoints,
742
834
  // and — critically — the `titaDataAgentIntent` Cordis service that `data-table`
743
835
  // resolves by name.
744
836
  // 意图只保留固定的会话审计契约;可编辑词表不再属于插件配置。
745
- applyIntent(ctx, { intentRoot: () => intentRoot(config.agentRoot) })
837
+ applyIntent(ctx, {
838
+ intentRoot: () => intentRoot(config.agentRoot),
839
+ })
840
+ // `applyIntent` provides this service from the same plugin fiber. During
841
+ // `apply()`, that fiber is still starting, so Cordis's default strict lookup
842
+ // hides the just-provided service until startup completes. The non-strict
843
+ // lookup is safe here because `applyIntent` ran immediately above.
844
+ const intentService = ctx.get(INTENT_SERVICE, false) as IntentService
845
+ /**
846
+ * `intent_*` 工具在 Host 层注册,使标准模式可以调用它们;工具执行端只接受
847
+ * standard 会话,其他 preset 不会写入或读取意图记录。
848
+ */
849
+ ctx.effect(
850
+ () => registerIntentTools(ctx as unknown as { tools: { register(definition: unknown): unknown } }, intentService),
851
+ 'tita knowledge: global intent tools',
852
+ )
853
+
854
+ /**
855
+ * 规则正文由当前会话 preset 决定;设置里的 volatile 引用在每次组装提示词时解包,
856
+ * 因此开关保存后立即生效,不需要重建 Agent 会话。
857
+ */
858
+ ctx.effect(() => ctx.systemPrompt.section({
859
+ name: 'dsh-data-domain-knowledge:behavior-rules',
860
+ order: ctx.systemPrompt.getSectionOrder('TOOLS_SDK') + 1,
861
+ text: ({ agent }) => {
862
+ return behaviorPromptForPreset(
863
+ agent?.session?.header?.agentPreset,
864
+ unwrapGroup<{ enabled?: boolean }>(pluginConfig.behaviorRules).enabled !== false,
865
+ )
866
+ },
867
+ }), 'tita knowledge: agent behavior rules')
746
868
 
747
869
  /**
748
- * 需要本轮用户审批的写工具。
870
+ * 需要本轮用户审批的知识写工具。`image_publish` 只写入本地素材目录,按用户要求免审批。
749
871
  *
750
872
  * 0044 换过一批名字:`write_knowledge_card`(只接受已确认提议)退役了,因为正文现在
751
873
  * 只读、卡片由编译直接写入;`list_card_proposals` 也退役了,因为待办队列不存在了。
752
874
  * 新增的两个是**入库之后**的动作:加备注、review 抬分。
753
875
  */
754
- const writeTools = new Set(['add_source', 'import_sources', 'propose_wiki_cards', 'annotate_wiki_card', 'review_wiki_card', 'set_knowledge_status', 'rebuild_knowledge', 'record_feedback'])
876
+ ctx.tools.register(tool('run_knowledge_pipeline', 'Run one independent knowledge stage. Import only saves sources by default. Stages: extract Fact/Procedure/Case, normalize axes, derive Procedure from Case, materialize Wiki Fact/Wiki Way, build section indexes, or all. Returns a durable background task; inspect it with get_knowledge_pipeline.', {
877
+ stage: { type: 'string', required: true, enum: ['extract', 'normalize', 'derive', 'materialize', 'build', 'all'] },
878
+ kb: { type: 'string' }, source_ids: { type: 'array', description: 'Optional explicit source IDs; omit for all sources. An empty selection is rejected.' },
879
+ group_keys: { type: 'array', description: 'Optional full card group keys returned by get_knowledge_pipeline.' }, force: { type: 'boolean', description: 'Explicitly rerun the selected stage despite an unchanged checkpoint.' },
880
+ }, async args => withRuntime(args.kb, async target => {
881
+ const pipeline = await target.pipeline()
882
+ const { task } = pipeline.start(args.stage as PipelineStage, { sourceIds: args.source_ids, groupKeys: args.group_keys, force: args.force === true })
883
+ return success({ task, url: `/knowledge/pages/tasks?kb=${encodeURIComponent(target.baseId)}&task=${encodeURIComponent(task.id)}` })
884
+ })))
885
+ ctx.tools.register(tool('get_knowledge_pipeline', 'Read pending stages, materializable card groups, persistent task history, or incremental public model output. Use the returned next cursor as after on the next call.', {
886
+ kb: { type: 'string' }, task_id: { type: 'string' }, after: { type: 'integer' },
887
+ }, async args => withRuntime(args.kb, async (target): Promise<Envelope<Record<string, unknown>>> => {
888
+ const pipeline = await target.pipeline()
889
+ return args.task_id
890
+ ? success({ task: pipeline.store.task(args.task_id) ?? null, ...pipeline.store.events(args.task_id, Math.max(0, args.after ?? 0)) })
891
+ : success({ status: await target.pipelineStatus(), ...pipeline.store.tasks() })
892
+ })))
893
+ const writeTools = new Set(['add_source', 'import_sources', 'propose_wiki_cards', 'annotate_wiki_card', 'review_wiki_card', 'set_knowledge_status', 'rebuild_knowledge', 'run_knowledge_pipeline', 'record_feedback'])
755
894
  ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
756
895
  if (writeTools.has(exec.name)) return Promise.resolve({ kind: 'ask', reason: '该操作会写入 Tita Knowledge Store,需要本轮用户审批。' })
757
896
  return next()
758
897
  })
759
- ctx.tools.register(tool('recall_knowledge', 'Recall compiled knowledge through keyword and vector candidate routes, rerank wiki sections, then return the top wiki cards with their matched sections and provenance.', { query: { type: 'string', required: true }, limit: { type: 'integer' }, top_k: { type: 'integer' }, top_n: { type: 'integer' }, rerank: { type: 'boolean' }, context_max_chars: { type: 'integer', description: '这次召回的上下文字符上限;省略则用设置里的值(默认 20000,0 = 不限)。超出部分落在系统临时目录,响应里的 budget.overflow_path 就是它的绝对路径 —— 需要更多信息时直接读那个文件。' }, include_problems: { type: 'boolean' }, mode: { type: 'string', enum: ['fts', 'vector', 'hybrid'] }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, parent_subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.recall(args.query, args.top_k ?? args.limit, args.include_problems ?? false, args.mode as RecallMode | undefined, lineageOf(args, exec), { contextTopN: args.top_n, rerank: args.rerank, ...(typeof args.context_max_chars === 'number' ? { contextMaxChars: args.context_max_chars } : {}) })), {
898
+ ctx.tools.register(tool('recall_knowledge', 'Recall Wiki Fact and Wiki Way through H2 title and body search. Use the current subquestion as query when seeking a Procedure. Return complete Wiki files with conditions and evidence. Use get_knowledge_cases for historical Case evidence; a past root cause does not establish the current root cause.', { query: { type: 'string', required: true }, limit: { type: 'integer' }, top_k: { type: 'integer' }, top_n: { type: 'integer' }, rerank: { type: 'boolean' }, context_max_chars: { type: 'integer', description: '这次召回的上下文字符上限;省略则用设置里的值(默认 20000,0 = 不限)。超出部分落在系统临时目录,响应里的 budget.overflow_path 就是它的绝对路径 —— 需要更多信息时直接读那个文件。' }, include_problems: { type: 'boolean' }, mode: { type: 'string', enum: ['fts', 'vector', 'hybrid'] }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, parent_subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.recall(args.query, args.top_k ?? args.limit, args.include_problems ?? false, args.mode as RecallMode | undefined, lineageOf(args, exec), { contextTopN: args.top_n, rerank: args.rerank, ...(typeof args.context_max_chars === 'number' ? { contextMaxChars: args.context_max_chars } : {}) })), {
760
899
  call: recallCallView,
761
900
  // 查询词从参数来,命中从结果来 —— 两者拼成一张能看懂的账本。
762
901
  meta: (args, value) => recallMetaOf({ ...dataOf(value), query: args?.query }),
763
902
  result: resultViewOf(recallResultMarkdown),
764
903
  }))
904
+ ctx.tools.register(tool('get_knowledge_cases', 'Read historical Case records referenced by Wiki metadata.case_refs. Timelines are evidence from a past instance, not proof of the present diagnosis.', {
905
+ case_ids: { type: 'array', required: true, description: 'Case IDs from Wiki metadata.case_refs; maximum 50 IDs.' },
906
+ version: { type: 'integer', description: 'Optional historical Case version from metadata.case_refs; requires exactly one case ID.' },
907
+ kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' },
908
+ run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' },
909
+ }, async args => withRuntime(args.kb, async target => {
910
+ const ids = [...new Set((Array.isArray(args.case_ids) ? args.case_ids : [])
911
+ .filter((id): id is string => typeof id === 'string' && id.trim() !== '').map(id => id.trim()))]
912
+ if (ids.length === 0) return failure({ cases: [], missing_ids: [] }, 'CASE_IDS_REQUIRED', '请传入 Wiki 的 case_refs 中的 Case ID。', false)
913
+ if (ids.length > 50) return failure({ cases: [], missing_ids: [] }, 'CASE_IDS_LIMIT', '一次最多读取 50 个 Case ID。', false)
914
+ if (args.version !== undefined && (ids.length !== 1 || !Number.isInteger(args.version) || args.version < 1)) return failure({ cases: [], missing_ids: ids }, 'CASE_VERSION_INVALID', '读取历史版本时,请传一个 Case ID 和正整数 version。', false)
915
+ const records = await Promise.all(ids.map(id => target.readCase(id, args.version)))
916
+ return success({ cases: records.filter(record => record !== undefined), missing_ids: ids.filter((_, index) => records[index] === undefined) })
917
+ })))
918
+ ctx.tools.register(tool('get_knowledge_events', 'Read Event records referenced by recalled WikiCards. Pass event IDs from card metadata.event_refs; the result includes full timelines, evidence, outcomes, and missing fields.', {
919
+ event_ids: { type: 'array', required: true, description: 'Event IDs from WikiCard metadata.event_refs; maximum 50 IDs.' },
920
+ kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' },
921
+ run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' },
922
+ }, async args => withRuntime(args.kb, async target => {
923
+ const ids = [...new Set((Array.isArray(args.event_ids) ? args.event_ids : [])
924
+ .filter((id): id is string => typeof id === 'string' && id.trim() !== '').map(id => id.trim()))]
925
+ if (ids.length === 0) return failure({ events: [], missing_ids: [] }, 'EVENT_IDS_REQUIRED', '请传入 WikiCard 的 event_refs 中的 Event ID。', false)
926
+ if (ids.length > 50) return failure({ events: [], missing_ids: [] }, 'EVENT_IDS_LIMIT', '一次最多读取 50 个 Event ID;请分批读取该 WikiCard 的 event_refs。', false)
927
+ const byId = new Map((await target.loadEvents()).map(event => [event.id, event]))
928
+ const events = ids.flatMap(id => {
929
+ const event = byId.get(id)
930
+ return event ? [event] : []
931
+ })
932
+ return success({ events, missing_ids: ids.filter(id => !byId.has(id)) })
933
+ })))
765
934
  ctx.tools.register(tool('check_knowledge_embeddings', 'Inspect the local embedding provider and binary/int8 vector index without loading the model.', { kb: { type: 'string' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => target.embeddingHealth())))
766
935
  ctx.tools.register(tool('query_knowledge_graph', 'Query entities, neighbors, paths, lists and relation types in the compiled knowledge graph.', { action: { type: 'string', enum: ['entity', 'neighbors', 'path', 'list', 'types'], required: true }, entity_id: { type: 'string' }, id: { type: 'string' }, entity_type: { type: 'string' }, from_id: { type: 'string' }, to_id: { type: 'string' }, type: { type: 'string' }, status: { type: 'string' }, include_problem: { type: 'boolean' }, limit: { type: 'integer' }, depth: { type: 'integer' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, parent_subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.graphQuery(args.action, args.entity_id ?? args.id, args.entity_type ?? args.type, args.depth ?? 1, { fromId: args.from_id, toId: args.to_id, status: args.status, includeProblem: args.include_problem ?? false, limit: args.limit ?? 50 }, lineageOf(args, exec)))))
767
- ctx.tools.register(tool('query_source_truth', 'Read SourceRaw (原始可追溯文档) with access audit. Returns the top max_chars characters (default and hard max 40000) shared across the matched sources. Each source carries `returnedChars`, `totalChars` and `offset` (where this slice starts inside that source), and the envelope carries `offset` and `truncated`. When truncated is true the content is INCOMPLETE — call again with `offset` set to the number of characters you have already read in this pass (the running sum of `returnedChars`), and keep paging until `truncated` is false. Do not answer from a partial document.', { query: { type: 'string', required: true }, limit: { type: 'integer' }, max_chars: { type: 'integer' }, offset: { type: 'integer' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.sourceTruth(args.query, args.limit ?? 20, args.max_chars ?? 40000, lineageOf(args, exec), args.offset ?? 0))))
936
+ ctx.tools.register(tool('query_source_truth', 'Read SourceRaw (原始可追溯文档) with access audit. Each call returns up to `limit` matched sources and `max_chars` total characters. If `hasMoreInCurrentPage` is true, continue with `offset += returnedChars`. When it is false and `hasMoreSources` is true, continue with `source_offset = nextSourceOffset` and reset `offset` to 0. `truncated` stays true until both the current source page and all matching source pages are complete. Do not answer from partial content.', { query: { type: 'string', required: true }, limit: { type: 'integer' }, max_chars: { type: 'integer' }, offset: { type: 'integer' }, source_offset: { type: 'integer', description: '来源分页游标;当前页字符已读完后才使用 nextSourceOffset,并将 offset 重置为 0' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.sourceTruth(args.query, args.limit ?? 20, args.max_chars ?? 40000, lineageOf(args, exec), args.offset ?? 0, args.source_offset ?? 0))))
768
937
  /**
769
938
  * 知识库 id 的**发现入口**。
770
939
  *
@@ -792,6 +961,29 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
792
961
  })),
793
962
  })))
794
963
 
964
+ const imagePublishParameters = {
965
+ file_path: { type: 'string', description: '宿主可读取的本地图片文件路径;与 content_base64 二选一' },
966
+ content_base64: { type: 'string', description: '图片的标准 Base64,或 data:image/...;base64,...;与 file_path 二选一' },
967
+ filename: { type: 'string', description: '可选文件名;默认使用 file_path 的文件名,Base64 输入默认 image-<hash>.<ext>' },
968
+ mime_type: { type: 'string', description: '可选 MIME 类型;必须与图片二进制内容一致' },
969
+ kb: { type: 'string', description: '知识库 id;省略时写入默认库' },
970
+ }
971
+ const publishImage = async (args: Args) => withRuntime(args.kb, async target => {
972
+ try {
973
+ return success(await publishKnowledgeImage(target.rootDir, target.baseId, {
974
+ filePath: args.file_path,
975
+ contentBase64: args.content_base64,
976
+ filename: args.filename,
977
+ mimeType: args.mime_type,
978
+ }))
979
+ } catch (cause) {
980
+ if (cause instanceof ImagePublishError) return failure({}, cause.code, cause.message, false, cause.suggestion)
981
+ return failure({}, 'IMAGE_PUBLISH_FAILED', `图片写入失败:${cause instanceof Error ? cause.message : String(cause)}`, true)
982
+ }
983
+ })
984
+ const imagePublishDescription = 'Publish one local image into a knowledge base assets bucket without user approval. Pass exactly one of `file_path` or `content_base64` (standard Base64 or a data URI); optional `filename` overrides the stored name, and optional `mime_type` must match the file bytes. Supports PNG, JPEG, GIF, WebP, AVIF, BMP and SVG up to 20 MiB. Returns the local asset URL and Markdown image reference.'
985
+ ctx.tools.register(tool('image_publish', imagePublishDescription, imagePublishParameters, publishImage))
986
+
795
987
  ctx.tools.register(tool('add_source', 'Add or update a SourceRaw document (原始可追溯文档). DSH requests one user approval before execution.', { title: { type: 'string' }, content: { type: 'string', required: true }, external_id: { type: 'string' }, metadata: { type: 'json' }, source_id: { type: 'string' }, allow_duplicate_title: { type: 'boolean', description: '默认 false:标题与已有来源重复时拒收(SOURCE_TITLE_DUPLICATED),要求改标题。传 true 才允许并存。' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.addSource({ title: args.title, content: args.content, externalId: args.external_id, metadata: args.metadata, sourceId: args.source_id, ...(args.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}), lineage: lineageOf(args, exec) })), {
796
988
  call: addSourceCallView,
797
989
  meta: (args, value) => addSourceMetaOf(args, dataOf(value)),
@@ -811,7 +1003,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
811
1003
  * `compile` 默认开:批量导入的意图几乎总是「进来就能用」。关掉它的场景是「先全部
812
1004
  * 落盘、稍后统一编译」——那时卡片还没生成,工具返回里会明说 `compile: null`。
813
1005
  */
814
- ctx.tools.register(tool('import_sources', 'Import many SourceRaw documents and compile them into Wiki cards in ONE call (one approval, one compile, per-source failures reported instead of aborting the batch). Use this instead of calling add_source repeatedly. DSH requests one user approval before execution.', { sources: { type: 'array', required: true, description: `来源数组,每项 { title?, content, external_id?, metadata?, allow_duplicate_title? };一次最多 ${MAX_IMPORT_SOURCES} 份,超了请分批。标题与已有来源重复的那一份会被拒(SOURCE_TITLE_DUPLICATED),逐条报出来而不是整批失败` }, compile: { type: 'boolean', description: '默认 true:写入后立刻编译成 WikiCard。false 表示只落盘' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.importSources({ sources: (Array.isArray(args.sources) ? args.sources : []).map(item => ({ ...(item?.title ? { title: item.title } : {}), content: item?.content, ...(item?.external_id ? { externalId: item.external_id } : {}), ...(item?.metadata ? { metadata: item.metadata } : {}), ...(item?.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}) })), ...(args.compile === false ? { compile: false } : {}), lineage: lineageOf(args, exec) })), { call: importCallView, meta: (_args, value) => importMetaOf(dataOf(value)), result: resultViewOf(importResultMarkdown) }))
1006
+ ctx.tools.register(tool('import_sources', 'Import many SourceRaw documents in one call. By default only stores sources. Explicit compile:true runs extract, normalize, materialize, and index publication for the imported documents. Per-source import failures are reported without aborting the batch. DSH requests one user approval before execution.', { sources: { type: 'array', required: true, description: `来源数组,每项 { title?, content, external_id?, metadata?, allow_duplicate_title? };一次最多 ${MAX_IMPORT_SOURCES} 份,超了请分批。标题与已有来源重复的那一份会被拒(SOURCE_TITLE_DUPLICATED),逐条报出来而不是整批失败` }, compile: { type: 'boolean', description: '默认 false:只保存来源;显式 true 才运行完整抽取、归一与物化' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.importSources({ sources: (Array.isArray(args.sources) ? args.sources : []).map(item => ({ ...(item?.title ? { title: item.title } : {}), content: item?.content, ...(item?.external_id ? { externalId: item.external_id } : {}), ...(item?.metadata ? { metadata: item.metadata } : {}), ...(item?.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}) })), compile: args.compile === true, lineage: lineageOf(args, exec) })), { call: importCallView, meta: (_args, value) => importMetaOf(dataOf(value)), result: resultViewOf(importResultMarkdown) }))
815
1007
  /**
816
1008
  * `propose_wiki_cards` —— 把一批卡片**直接写进 `02_wiki`**(工具名不变,语义在 0044 改了)。
817
1009
  *
@@ -824,9 +1016,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
824
1016
  * 这个工具服务于另一条路:Agent 手里已经有结构化的卡片草案(例如从一次排查里读出来的),
825
1017
  * 直接写进来,不必再走一遍抽取。
826
1018
  */
827
- ctx.tools.register(tool('propose_wiki_cards', 'Write Wiki cards compiled from a SourceRaw directly into 02_wiki. The confirmation gate was retired in 0044: cards land immediately and are marked unreviewed, so a human reviews them AFTER entry (review raises the card score; the body itself is read-only). Use the system fixed card template and coordinate entity identity. DSH requests one user approval before execution.', { source_id: { type: 'string', required: true, description: '这批卡片引用的那条 SourceRaw 的 source_id(先 add_source)' }, proposals: { type: 'array', required: true, description: '卡片数组,每项 { action?, entity_type, entity_id, title, summary?, content, source_refs?, metadata? };content 使用系统固定卡片模板' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.writeCards({
1019
+ ctx.tools.register(tool('propose_wiki_cards', 'Write Wiki cards compiled from a SourceRaw directly into 02_wiki. The confirmation gate was retired in 0044: cards land immediately and are marked unreviewed, so a human reviews them AFTER entry (review raises the card score; the body itself is read-only). Use the system fixed card template and coordinate entity identity. Existing cards are safely merged by appending new content and unioning source references; the only supported explicit action is `merge`. DSH requests one user approval before execution.', { source_id: { type: 'string', required: true, description: '这批卡片引用的那条 SourceRaw 的 source_id(先 add_source)' }, proposals: { type: 'array', required: true, description: '卡片数组,每项 { entity_type, entity_id, title, summary?, content, source_refs?, metadata? };content 使用系统固定卡片模板。不要传 action;兼容旧调用只接受 action=merge。' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.writeCards({
828
1020
  sourceId: args.source_id,
829
- cards: (Array.isArray(args.proposals) ? args.proposals : []).map(item => ({ action: item?.action, entityType: item?.entity_type, entityId: item?.entity_id, title: item?.title, summary: item?.summary, content: item?.content, sourceRefs: item?.source_refs, metadata: item?.metadata })),
1021
+ cards: (Array.isArray(args.proposals) ? args.proposals : []).map(item => ({ ...(item?.action ? { action: item.action } : {}), entityType: item?.entity_type, entityId: item?.entity_id, title: item?.title, summary: item?.summary, content: item?.content, sourceRefs: item?.source_refs, metadata: item?.metadata })),
830
1022
  lineage: lineageOf(args, exec),
831
1023
  })), {
832
1024
  call: writeCardsCallView,
@@ -842,7 +1034,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
842
1034
  * 有效评分压到 `ANOMALY_SCORE` —— 异常卡片照样召回得到,只是排在后面,
843
1035
  * 而不是被藏起来(藏起来就没人会去修它)。
844
1036
  */
845
- ctx.tools.register(tool('annotate_wiki_card', 'Attach a human annotation to a Wiki card, or remove one. Wiki bodies are read-only — this is how a person records a judgement about a card ("this number disagrees with the source", "these two definitions conflict"). kind=anomaly additionally lowers the card score for recall ranking while keeping it retrievable. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, entity_type: { type: 'string' }, kind: { type: 'string', enum: ['note', 'anomaly'], description: '默认 note;anomaly 表示异常点' }, note: { type: 'string', description: '备注正文;撤销一条时省略并给 remove_id' }, author: { type: 'string' }, source_ref: { type: 'string', description: '这条备注针对哪一份来源(可选)' }, remove_id: { type: 'string', description: '要撤销的备注 id' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.annotateCard({
1037
+ ctx.tools.register(tool('annotate_wiki_card', 'Attach a human annotation to a Wiki card, or remove one. Wiki bodies are read-only — this is how a person records a judgement about a card ("this number disagrees with the source", "these two definitions conflict"). kind=anomaly additionally lowers the card score for recall ranking while keeping it retrievable. Provide `note` to add, or `remove_id` to delete; do not provide both. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, entity_type: { type: 'string' }, kind: { type: 'string', enum: ['note', 'anomaly'], description: '默认 note;anomaly 表示异常点' }, note: { type: 'string', description: '备注正文;添加备注时提供' }, author: { type: 'string' }, source_ref: { type: 'string', description: '这条备注针对哪一份来源(可选)' }, remove_id: { type: 'string', description: '要撤销的备注 id;删除时只传此字段,不要同时传 note' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.annotateCard({
846
1038
  entityId: args.entity_id,
847
1039
  ...(args.entity_type ? { entityType: args.entity_type as SchemaType } : {}),
848
1040
  ...(args.kind ? { kind: args.kind as 'note' | 'anomaly' } : {}),
@@ -867,7 +1059,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
867
1059
  lineage: lineageOf(args, exec),
868
1060
  }))))
869
1061
  ctx.tools.register(tool('set_knowledge_status', 'Change a Wiki card status. This status belongs to the Card lifecycle; it does not change Knowledge truth status. problem requires problem_note and recovery must pass through unreviewed. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, status: { type: 'string', enum: ['unreviewed', 'draft', 'reviewed', 'active', 'deprecated', 'problem'], required: true }, problem_note: { type: 'string' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.setStatus(args.entity_id, args.status as CardStatus, args.problem_note, lineageOf(args, exec)))))
870
- ctx.tools.register(tool('rebuild_knowledge', 'Rebuild schema/wiki/compiled knowledge indexes. DSH requests one user approval before execution.', { only: { type: 'string', enum: ['all', 'schema', 'wiki', 'embeddings'] }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.rebuild(args.only ?? 'all', lineageOf(args, exec))), {
1062
+ ctx.tools.register(tool('rebuild_knowledge', 'Rebuild schema/wiki/compiled knowledge indexes from saved cards without document extraction. Use run_knowledge_pipeline for extract, normalize, materialize, or the full chain. DSH requests one user approval before execution.', { only: { type: 'string', enum: ['all', 'schema', 'wiki', 'embeddings'] }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.rebuild(args.only ?? 'all', lineageOf(args, exec), { extract: false })), {
871
1063
  call: rebuildCallView,
872
1064
  meta: (args, value) => rebuildMetaOf(args?.only ?? 'all', dataOf(value)),
873
1065
  result: resultViewOf(rebuildResultMarkdown),
@@ -912,7 +1104,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
912
1104
  * - `canLoadSample` 是空态的开关 —— 这个库还什么都没有时才为真。
913
1105
  */
914
1106
  const diagnosisPayload = (snapshot: Awaited<ReturnType<DiagnosisStore['snapshot']>>): Record<string, unknown> => ({
915
- graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
1107
+ graph: snapshot.graph, cases: snapshot.cases, events: snapshot.events, issues: snapshot.issues,
916
1108
  seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source,
917
1109
  canLoadSample: snapshot.canLoadSample, problems: snapshot.problems,
918
1110
  })
@@ -931,17 +1123,41 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
931
1123
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
932
1124
  const swept = await resolved.runtime.sweepOrphans()
933
1125
  if (swept === null) return sendEnvelope(res, failure({}, 'KNOWLEDGE_UNAVAILABLE', '这个部署上没有 Knowledge 库', false))
934
- const { knowledge, cards } = swept
935
- const total = knowledge.knowledgeDeleted + cards.deleted
1126
+ const { knowledge, cards, events } = swept
1127
+ const total = knowledge.knowledgeDeleted + cards.deleted + events.deleted + events.updated + swept.assets.files
936
1128
  return send(res, 200, success(swept, total === 0
937
- ? ['没有孤儿:每条知识的出处都还在库里。']
938
- : [`清掉了 ${knowledge.knowledgeDeleted} 条没有出处的知识与 ${cards.deleted} 张没有依据的卡片。`]))
1129
+ ? ['没有孤儿:Knowledge、Event 和 WikiCard 的来源都还在库里。']
1130
+ : [`清掉了 ${knowledge.knowledgeDeleted} 条无来源 Knowledge、${events.deleted} 个无来源 Event(更新 ${events.updated} 个共享 Event)、${cards.deleted} 张无依据 WikiCard。`]))
1131
+ } },
1132
+ /**
1133
+ * DSH 模型目录(知识库编译设置)。编译模型不是插件再维护一套供应商列表,
1134
+ * 而是每次从 DSH 的 `llm.listModels()` 读取;这样固定模型下拉框与真正可调用的
1135
+ * 模型来源保持一致,默认模型失效时也能把状态说清楚。
1136
+ */
1137
+ { kind: 'exact' as const, path: '/knowledge/models', handler: async (req: IncomingMessage, res: ServerResponse) => {
1138
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
1139
+ const url = query(req)
1140
+ const id = kbIdOf(url.searchParams.get('kb'))
1141
+ if (!bases.get(id)) return sendEnvelope(res, failure({}, 'BASE_NOT_FOUND', `知识库不存在: ${id}`, true, '先 GET /knowledge/bases 拿到可用 id。'))
1142
+ return send(res, 200, success(await compileModelCatalog(id, { provider: url.searchParams.get('provider') ?? undefined, model: url.searchParams.get('model') ?? undefined })))
939
1143
  } },
940
1144
  { kind: 'exact' as const, path: '/knowledge/stats', handler: async (req: IncomingMessage, res: ServerResponse) => {
941
1145
  const resolved = resolveBase(query(req).searchParams.get('kb'))
942
1146
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
943
1147
  return send(res, 200, success(await resolved.runtime.stats()))
944
1148
  } },
1149
+ { kind: 'exact' as const, path: '/knowledge/cases', handler: async (req: IncomingMessage, res: ServerResponse) => {
1150
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', 'Case 只接受 GET'))
1151
+ const resolved = resolveBase(query(req).searchParams.get('kb'))
1152
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1153
+ return send(res, 200, success({ cases: await resolved.runtime.loadEvents() }))
1154
+ } },
1155
+ { kind: 'exact' as const, path: '/knowledge/events', handler: async (req: IncomingMessage, res: ServerResponse) => {
1156
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', 'Event 只接受 GET'))
1157
+ const resolved = resolveBase(query(req).searchParams.get('kb'))
1158
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1159
+ return send(res, 200, success({ events: await resolved.runtime.diagnosisStore().listEvents() }))
1160
+ } },
945
1161
  { kind: 'exact' as const, path: '/knowledge/search', handler: async (req: IncomingMessage, res: ServerResponse) => {
946
1162
  const url = query(req)
947
1163
  const resolved = resolveBase(url.searchParams.get('kb'))
@@ -966,6 +1182,19 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
966
1182
  },
967
1183
  ))
968
1184
  } },
1185
+ { kind: 'exact' as const, path: '/knowledge/environment', handler: async (req: IncomingMessage, res: ServerResponse) => {
1186
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '只支持 GET'))
1187
+ return send(res, 200, success(environment.view()))
1188
+ } },
1189
+ { kind: 'exact' as const, path: '/knowledge/environment/prepare', handler: async (req: IncomingMessage, res: ServerResponse) => {
1190
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '只支持 POST'))
1191
+ try {
1192
+ const body = await readBody(req, 4000) as { action?: string; selection?: unknown } | null
1193
+ if (body?.action === 'cancel') { environment.cancel(); return send(res, 200, success(environment.view())) }
1194
+ if (body?.action !== 'check' && body?.action !== 'install') return send(res, 400, failure({}, 'ACTION_INVALID', 'action 必须是 check、install 或 cancel', false))
1195
+ return send(res, 200, success(environment.start(body.action, body.selection)))
1196
+ } catch (cause) { return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause))) }
1197
+ } },
969
1198
  { kind: 'exact' as const, path: '/knowledge/embedding/health', handler: async (req: IncomingMessage, res: ServerResponse) => {
970
1199
  const resolved = resolveBase(query(req).searchParams.get('kb'))
971
1200
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
@@ -1005,6 +1234,72 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1005
1234
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1006
1235
  return send(res, 200, success({ progress: resolved.runtime.compileProgressView(url.searchParams.get('runId') ?? undefined) }))
1007
1236
  } },
1237
+ { kind: 'exact' as const, path: '/knowledge/pipeline', handler: async (req: IncomingMessage, res: ServerResponse) => {
1238
+ try {
1239
+ const body = req.method === 'POST' ? await readBody(req, 100_000) : {}
1240
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1241
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1242
+ if (req.method === 'GET') return send(res, 200, success(await resolved.runtime.pipelineStatus()))
1243
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '知识处理只接受 GET 或 POST'))
1244
+ const pipeline = await resolved.runtime.pipeline()
1245
+ const started = pipeline.start(body.stage as PipelineStage, { sourceIds: body.sourceIds, groupKeys: body.groupKeys, force: body.force === true })
1246
+ return send(res, 202, success({ task: started.task }))
1247
+ } catch (cause) { return send(res, 400, envelopeError('PIPELINE_INVALID', cause instanceof Error ? cause.message : String(cause))) }
1248
+ } },
1249
+ { kind: 'exact' as const, path: '/knowledge/pipeline-tasks', handler: async (req: IncomingMessage, res: ServerResponse) => {
1250
+ try {
1251
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '任务历史只接受 GET'))
1252
+ const url = query(req)
1253
+ const resolved = resolveBase(url.searchParams.get('kb'))
1254
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1255
+ const pipeline = await resolved.runtime.pipeline()
1256
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit')) || 20))
1257
+ const offset = Math.max(0, Number(url.searchParams.get('offset')) || 0)
1258
+ return send(res, 200, success({ ...pipeline.store.tasks(limit, offset), extractions: await resolved.runtime.listExtractionRuns({ limit, offset }) }))
1259
+ } catch (cause) { return send(res, 400, envelopeError('PIPELINE_HISTORY_FAILED', cause instanceof Error ? cause.message : String(cause))) }
1260
+ } },
1261
+ { kind: 'exact' as const, path: '/knowledge/pipeline-task', handler: async (req: IncomingMessage, res: ServerResponse) => {
1262
+ try {
1263
+ const url = query(req)
1264
+ const body = req.method === 'POST' ? await readBody(req, 20_000) : {}
1265
+ const resolved = resolveBase(body.kb ?? url.searchParams.get('kb'))
1266
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1267
+ const pipeline = await resolved.runtime.pipeline()
1268
+ const id = typeof body.id === 'string' ? body.id : url.searchParams.get('id') ?? ''
1269
+ const task = pipeline.store.task(id)
1270
+ if (!task) return send(res, 404, envelopeError('PIPELINE_NOT_FOUND', '任务不存在'))
1271
+ if (req.method === 'GET') return send(res, 200, success({ task, ...pipeline.store.events(id, Math.max(0, Number(url.searchParams.get('after')) || 0)) }))
1272
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '任务只接受 GET 或 POST'))
1273
+ if (body.action === 'retry') return send(res, 202, success({ task: pipeline.start(task.stage, { ...task.selection, force: false }).task }))
1274
+ if (!['pause', 'resume', 'cancel'].includes(body.action)) throw new Error('不支持的任务操作')
1275
+ return send(res, 200, success({ task: pipeline.control(id, body.action) }))
1276
+ } catch (cause) { return send(res, 409, envelopeError('PIPELINE_TASK_REJECTED', cause instanceof Error ? cause.message : String(cause))) }
1277
+ } },
1278
+ { kind: 'exact' as const, path: '/knowledge/extraction-tasks', handler: async (req: IncomingMessage, res: ServerResponse) => {
1279
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '抽取任务只接受 GET'))
1280
+ const resolved = resolveBase(query(req).searchParams.get('kb'))
1281
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1282
+ return send(res, 200, success({ tasks: resolved.runtime.listCompileTasks() }))
1283
+ } },
1284
+ { kind: 'exact' as const, path: '/knowledge/extraction-task', handler: async (req: IncomingMessage, res: ServerResponse) => {
1285
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '抽取任务控制只接受 POST'))
1286
+ try {
1287
+ const body = await readBody(req, 20_000)
1288
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1289
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1290
+ const runId = typeof body.run_id === 'string' ? body.run_id.trim() : ''
1291
+ const action = body.action
1292
+ if (!runId || (action !== 'pause' && action !== 'resume' && action !== 'cancel')) {
1293
+ return send(res, 400, envelopeError('REQUEST_INVALID', '需要 run_id 和 pause / resume / cancel 操作'))
1294
+ }
1295
+ const result = resolved.runtime.controlCompileTask(runId, action)
1296
+ return result.ok
1297
+ ? send(res, 200, success({ task: result.task }))
1298
+ : send(res, 409, envelopeError('TASK_CONTROL_REJECTED', result.why ?? '任务状态不支持此操作'))
1299
+ } catch (cause) {
1300
+ return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
1301
+ }
1302
+ } },
1008
1303
  /**
1009
1304
  * 把一个知识库**导出成目录形态**(0062 v2 §3.7 末)。
1010
1305
  *
@@ -1043,7 +1338,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1043
1338
  const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1044
1339
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1045
1340
  return send(res, 202, success(resolved.runtime.startCompile(body.only ?? 'all', lineageOf(body), {
1046
- ...(typeof body.extract === 'boolean' ? { extract: body.extract } : {}),
1341
+ extract: body.extract === true,
1047
1342
  ...(body.forceExtract === true ? { forceExtract: true } : {}),
1048
1343
  })))
1049
1344
  } catch (cause) {
@@ -1070,7 +1365,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1070
1365
  ...(item?.external_id ? { externalId: String(item.external_id) } : {}),
1071
1366
  ...(item?.metadata && typeof item.metadata === 'object' ? { metadata: item.metadata as Record<string, unknown> } : {}),
1072
1367
  })),
1073
- ...(body.compile === false ? { compile: false } : {}),
1368
+ compile: body.compile === true,
1074
1369
  lineage: lineageOf(body),
1075
1370
  }))
1076
1371
  } catch (cause) {
@@ -1095,6 +1390,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1095
1390
  Number(url.searchParams.get('max_chars') ?? 40000) || 40000,
1096
1391
  lineageFromUrl(url),
1097
1392
  Number(url.searchParams.get('offset') ?? 0) || 0,
1393
+ Number(url.searchParams.get('source_offset') ?? 0) || 0,
1098
1394
  ))
1099
1395
  } catch (cause) {
1100
1396
  return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
@@ -1162,7 +1458,8 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1162
1458
  * 任何一条轨迹 —— 「这个实体是哪次抽出来的」就答不上了。
1163
1459
  */
1164
1460
  const runId = extractionRunId()
1165
- const seam = modelSeamFor()
1461
+ const seam = modelSeamFor(resolved.runtime.baseId)
1462
+ const draft = await (await resolved.runtime.axisRegistry())?.draft()
1166
1463
  const axesBlock = await resolved.runtime.axisPromptBlockView()
1167
1464
  const note = resolved.runtime.extractionNote()
1168
1465
  const report = await runWikiTrial({
@@ -1170,14 +1467,17 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1170
1467
  text,
1171
1468
  ...(docId ? { docId } : {}),
1172
1469
  ...(typeof body.subject === 'string' && body.subject.trim() ? { subject: body.subject.trim() } : {}),
1173
- /**
1174
- * Resolve 第 2 步的**真实入口**:交给轴注册表。没有注册表时它返回 null,
1175
- * 报告里如实写 `no-registry` —— 不假装查过。
1176
- */
1177
- resolveAxis: (kind, mention, parentMention, extra) => resolved.runtime.resolveAxisMention({
1178
- kind, mention, parentMention, runId,
1179
- ...(extra?.preferMention ? { preferMention: extra.preferMention } : {}),
1180
- ...(extra?.description ? { description: extra.description } : {}),
1470
+ /** 与编译相同:先查注册表,再把有候选的坐标合并成一次模型补判。 */
1471
+ resolveAxisSet: axes => resolved.runtime.resolveTrialAxisSet({
1472
+ registry: draft?.registry, axes,
1473
+ axesBlock,
1474
+ runId,
1475
+ call: seam.call,
1476
+ modelRequest: {
1477
+ ...(seam.provider ? { provider: seam.provider } : {}),
1478
+ ...(seam.model ? { model: seam.model } : {}),
1479
+ ...(seam.reasoningEffort ? { reasoningEffort: seam.reasoningEffort } : {}),
1480
+ },
1181
1481
  }),
1182
1482
  /**
1183
1483
  * 已有坐标(0068)。**与真编译下发同一份** —— 试跑看到的世界与真编译看到的
@@ -1185,9 +1485,14 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1185
1485
  */
1186
1486
  ...(axesBlock === undefined ? {} : { axes: axesBlock }),
1187
1487
  ...(note ? { note } : {}),
1488
+ modelRequest: {
1489
+ ...(seam.provider ? { provider: seam.provider } : {}),
1490
+ ...(seam.model ? { model: seam.model } : {}),
1491
+ ...(seam.reasoningEffort ? { reasoningEffort: seam.reasoningEffort } : {}),
1492
+ },
1188
1493
  ...('call' in seam ? { call: seam.call } : {}),
1189
1494
  ...('why' in seam ? { callWhy: seam.why } : {}),
1190
- })
1495
+ }).finally(() => draft?.close())
1191
1496
  const summary = trialSummary(report)
1192
1497
 
1193
1498
  /**
@@ -1203,24 +1508,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1203
1508
  lineage: lineageOf(body),
1204
1509
  })
1205
1510
 
1206
- /**
1207
- * 把这次抽出来的 Knowledge 收进真源(0063 §2)。
1208
- *
1209
- * **只在跑一份已入库的文档时收。** 理由有两条:来源要指得回一份**真实的**
1210
- * 文档(临时正文的 `documentId` 是一个标题字符串,存下来就是悬空引用);
1211
- * 而「这条知识是哪份文档说的」正是它可验证的前提。
1212
- *
1213
- * **只收引文核对得上的那些。** 引文对不上原文 = 模型改写了,那条判断不是从
1214
- * 原文推出来的(§5.7 的引文核验)。把它当知识存进真源,等于让一句编出来的话
1215
- * 看起来像有出处 —— 而报告里已经把它报成例外了,那里才是它该待的地方。
1216
- */
1217
- const ingest = docId
1218
- ? await resolved.runtime.ingestTrialKnowledge({
1219
- runId, knowledge: report.knowledge,
1220
- objects: report.objects, actions: report.actions, scenes: report.scenes,
1221
- })
1222
- : { saved: 0, merged: 0, skipped: 0 }
1223
- return send(res, 200, success({ ...report, summary, run, ingest }))
1511
+ return send(res, 200, success({ ...report, summary, run }))
1224
1512
  } catch (cause) {
1225
1513
  return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
1226
1514
  }
@@ -1266,7 +1554,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1266
1554
  * 必须分开:`get()` 给 `''`,缺席给 `null`,这个区别正好用得上。
1267
1555
  */
1268
1556
  const status = url.searchParams.get('status')
1557
+ const kind = url.searchParams.get('kind')
1269
1558
  const filter: KnowledgeQuery = {
1559
+ ...(kind === 'fact' || kind === 'procedure' ? { kind } : {}),
1270
1560
  ...(status === 'candidate' || status === 'active' || status === 'disputed' || status === 'deprecated' ? { status } : {}),
1271
1561
  ...(url.searchParams.get('run_id') === null ? {} : { runId: url.searchParams.get('run_id')! }),
1272
1562
  ...(url.searchParams.get('object_id') === null ? {} : { objectId: url.searchParams.get('object_id')! }),
@@ -1352,22 +1642,40 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1352
1642
  const kind = kindParam === 'object' || kindParam === 'action' || kindParam === 'scene' ? kindParam : undefined
1353
1643
  const status = statusParam === 'candidate' || statusParam === 'active' || statusParam === 'deprecated' || statusParam === 'merged' ? statusParam : undefined
1354
1644
  if (req.method === 'GET') {
1355
- const [entities, stats] = await Promise.all([
1645
+ const [entities, stats, relations] = await Promise.all([
1356
1646
  resolved.runtime.listAxisEntities(kind, status),
1357
1647
  resolved.runtime.axisStats(),
1648
+ resolved.runtime.listAxisRelations(),
1358
1649
  ])
1359
- if (entities === null || stats === null) return send(res, 200, success({ entities: [], stats: null, guidance: {}, available: false }))
1650
+ if (entities === null || stats === null || relations === null) return send(res, 200, success({ entities: [], stats: null, guidance: {}, relations: [], available: false }))
1360
1651
  /*
1361
1652
  * 指导随实体一起给(0068)。分开一条路由要多一次往返,而两样东西永远一起显示 ——
1362
1653
  * 一条指导离开它挂着的那个实体就没有意义。
1363
1654
  */
1364
1655
  const guidance = await resolved.runtime.axisGuidanceMap()
1365
- return send(res, 200, success({ entities, stats, guidance, available: true }))
1656
+ return send(res, 200, success({ entities, stats, guidance, relations, available: true }))
1366
1657
  }
1367
1658
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '轴注册表只接受 GET 或 POST'))
1368
1659
  const body = await readBody(req)
1369
1660
  const id = typeof body.id === 'string' ? body.id.trim() : ''
1370
1661
  const action = typeof body.action === 'string' ? body.action.trim() : ''
1662
+ if (action === 'relation-add') {
1663
+ const fromId = typeof body.from_id === 'string' ? body.from_id.trim() : ''
1664
+ const toId = typeof body.to_id === 'string' ? body.to_id.trim() : ''
1665
+ const relationType = typeof body.relation_type === 'string' ? body.relation_type.trim() : 'related_to'
1666
+ const evidence = typeof body.evidence === 'string' ? body.evidence : ''
1667
+ if (!fromId || !toId) return send(res, 400, envelopeError('REQUEST_INVALID', '添加相关对象关系需要 from_id 与 to_id'))
1668
+ if (relationType !== 'related_to') return send(res, 400, envelopeError('REQUEST_INVALID', '当前只支持 related_to 对象关系'))
1669
+ const relation = await resolved.runtime.addAxisRelation(fromId, toId, evidence)
1670
+ if (relation === null) return sendEnvelope(res, failure({}, 'AXIS_RELATION_INVALID', '关系无效:两个对象必须存在、未合并且不能是同一个对象', false))
1671
+ return send(res, 200, success({ relation }))
1672
+ }
1673
+ if (action === 'relation-remove') {
1674
+ const relationId = typeof body.relation_id === 'string' ? body.relation_id.trim() : ''
1675
+ if (!relationId) return send(res, 400, envelopeError('REQUEST_INVALID', '移除关系需要 relation_id'))
1676
+ const removed = await resolved.runtime.removeAxisRelation(relationId)
1677
+ return send(res, 200, success({ removed }))
1678
+ }
1371
1679
  if (action === 'merge') {
1372
1680
  const into = typeof body.into === 'string' ? body.into.trim() : ''
1373
1681
  if (!id || !into) return send(res, 400, envelopeError('REQUEST_INVALID', '并两个实体要同时给 id 与 into'))
@@ -1430,7 +1738,10 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1430
1738
  return send(res, 400, envelopeError('REQUEST_INVALID', `不认识的 action: ${action || '(空)'}`))
1431
1739
  } },
1432
1740
  /**
1433
- * 抽取轨迹(`extraction_run`):按文档列、按 id 读全文。
1741
+ * 轨迹(`extraction_run`):按文档或阶段列、按 id 读全文。
1742
+ *
1743
+ * 2026-10-18 起三段共用这张表(`stage` 列:`extract` / `normalize` / `materialize`)。
1744
+ * `stage` 省略 = 三段一起 —— 面板里那份「这份文档的轨迹」就是这么读的。
1434
1745
  *
1435
1746
  * 列表**不返回报告本体** —— 一条报告几十到几百 KB,翻一页列表不该按全文付费。
1436
1747
  * 点开某一条走 `/knowledge/extraction-run?id=…`(单独一条路由,不是路径参数:
@@ -1442,8 +1753,11 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1442
1753
  const resolved = resolveBase(url.searchParams.get('kb'))
1443
1754
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1444
1755
  const docId = url.searchParams.get('doc_id')
1756
+ const stageParam = url.searchParams.get('stage')
1757
+ const stage = stageParam === 'extract' || stageParam === 'normalize' || stageParam === 'materialize' ? stageParam : undefined
1445
1758
  const page = await resolved.runtime.listExtractionRuns({
1446
1759
  ...(docId === null ? {} : { docId }),
1760
+ ...(stage === undefined ? {} : { stage }),
1447
1761
  limit: clampInt(url.searchParams.get('limit'), 20, 1, 100),
1448
1762
  offset: clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER),
1449
1763
  })
@@ -1463,7 +1777,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1463
1777
  } },
1464
1778
  // ---------------------------------------------------------- 目录选择(0029)
1465
1779
  /**
1466
- * 根路径的**只读**视图:四段布局、两次迁移的结论、家当解析结果、宿主提供的是哪种选择交互。
1780
+ * 根路径的**只读**视图:新布局、家当解析结果、宿主提供的是哪种选择交互。
1467
1781
  *
1468
1782
  * 设置面板要显示「你这个根路径下会长出哪几段」,而那是服务端算出来的
1469
1783
  * (`layout.ts` 是唯一真源),所以不能让前端自己拼字符串。
@@ -1471,22 +1785,13 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1471
1785
  { kind: 'exact' as const, path: '/knowledge/layout', handler: async (req: IncomingMessage, res: ServerResponse) => {
1472
1786
  const dirs = agentLayout(config.agentRoot)
1473
1787
  const resources = runtime.resourcePaths()
1474
- // preset 的真源目录。它随插件分发,所以路径由插件自己算 —— 客户端不该拼这个
1475
- // 字符串,也不该猜包装在哪。`exists` 是给界面用的:路径不在时如实说「这个安装
1476
- // 里没有 preset 真源」,而不是给一个点了弹空目录的按钮。
1477
- const presetDir = presetSourceDir()
1478
1788
  return send(res, 200, success({
1479
1789
  ...dirs,
1480
- presetDir,
1481
- presetDirExists: existsSync(presetDir),
1482
- migration,
1483
- resourceMigration,
1484
- memoryMigration,
1485
1790
  // 三个「家当」路径**解析后**的值。设置面板用它显示「自动」到底指向哪里 ——
1486
1791
  // 配置里是空串(= 自动),但用户要看到的是真实路径。
1487
1792
  resources,
1488
- // 家当有没有真的落在 `resource/` 下(0029 的目标)。`resource` 是家当目录本身,
1489
- // 用它做前缀判断:不在里面的就是「还在旧位置」或「是一个裸的 HuggingFace 仓库名」。
1793
+ // 家当有没有真的落在 `resource/` 下。`resource` 是家当目录本身,
1794
+ // 用它做前缀判断:不在里面的可能是一个裸的 HuggingFace 仓库名。
1490
1795
  resourcesOutsideResource: outsideResource(resources, dirs.resource),
1491
1796
  // 设置分区没注册上时的原因。有它,「设置服务不可用」才不是一句空话。
1492
1797
  ...(settingsError ? { settingsError } : {}),
@@ -1559,10 +1864,8 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1559
1864
  return send(res, 200, success({ cases: (await storeOf(resolved.runtime).snapshot()).cases }))
1560
1865
  } },
1561
1866
  /**
1562
- * 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并写入一张案例卡片。
1563
- *
1564
- * 0044:卡片不再只提议 —— 它直接入库并标成 `unreviewed`。响应给 `sourceId` 与
1565
- * `entityId`,界面据此说清「记录留下了,卡片也建了,等人 review」。
1867
+ * 一次排查结束只落一条 SourceRaw。Case 视图从记录派生;Knowledge / Event 的抽取、
1868
+ * 归一和 WikiCard 物化由知识处理阶段分别执行。
1566
1869
  */
1567
1870
  { kind: 'exact' as const, path: '/diagnosis/run', handler: async (req: IncomingMessage, res: ServerResponse) => {
1568
1871
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
@@ -1575,9 +1878,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1575
1878
  }
1576
1879
  const outcome = await storeOf(resolved.runtime).recordRun(record)
1577
1880
  if (outcome.problems.length > 0 && outcome.sourceId === undefined) {
1578
- return send(res, 200, failure({ sourceId: '', entityId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
1881
+ return send(res, 200, failure({ sourceId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
1579
1882
  }
1580
- return send(res, 200, success({ sourceId: outcome.sourceId ?? '', entityId: outcome.entityId ?? '' }, outcome.problems))
1883
+ return send(res, 200, success({ sourceId: outcome.sourceId ?? '' }, outcome.problems))
1581
1884
  } },
1582
1885
  /**
1583
1886
  * 入库之后的两个人工动作(0044):加备注、review 抬分。
@@ -1661,6 +1964,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1661
1964
  * ```
1662
1965
  * /knowledge/pages/trial?kb=<知识库 id> 抽取试跑
1663
1966
  * /knowledge/pages/run?kb=<库 id>&run=<轨迹 id> 一次抽取的完整报告
1967
+ * /knowledge/pages/tasks?kb=<知识库 id> 抽取任务
1664
1968
  * /knowledge/pages/help/compile 编译链路
1665
1969
  * /knowledge/pages/help/intent 意图的逻辑
1666
1970
  * /knowledge/pages/help/overview?node=... 五个 Tab 的方案说明
@@ -1687,6 +1991,8 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1687
1991
  ? '抽取试跑'
1688
1992
  : rest[1] === 'run'
1689
1993
  ? '抽取轨迹'
1994
+ : rest[1] === 'tasks'
1995
+ ? '抽取任务'
1690
1996
  : rest[1] === 'help' && rest[2] === 'intent'
1691
1997
  ? '意图的逻辑'
1692
1998
  : rest[1] === 'help' && rest[2] === 'compile'
@@ -1722,9 +2028,30 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1722
2028
  if (rest.length === 2) {
1723
2029
  if (method === 'PATCH') {
1724
2030
  const body = await readBody(req)
2031
+ if (body.compileModel && typeof body.compileModel === 'object') {
2032
+ const selection = body.compileModel as CompileModelSelection
2033
+ const catalog = await compileModelCatalog(baseId)
2034
+ const provider = selection.mode === 'fixed' ? selection.provider : catalog.default.provider
2035
+ const model = selection.mode === 'fixed' ? selection.model : catalog.default.model
2036
+ if (selection.mode !== 'fixed' && selection.mode !== 'default') return send(res, 400, envelopeError('MODEL_INVALID', '请选择跟随默认或固定模型'))
2037
+ if (selection.mode === 'fixed' && (!provider || !model)) return send(res, 400, envelopeError('MODEL_INVALID', '固定模型需要 provider 和 model'))
2038
+ const llm = ctx.get('llm')
2039
+ if (provider && model && llm?.resolveCallConfig) {
2040
+ try {
2041
+ const info = selection.reasoningEffort ? await llm.resolveModelInfo(provider, model) : undefined
2042
+ const effort = info?.reasoning?.efforts.find(item => item.id === selection.reasoningEffort)
2043
+ if (selection.reasoningEffort && !effort) throw new Error('所选模型不支持这个思考强度,请重新选择')
2044
+ await llm.resolveCallConfig({ provider, model, ...(effort ? { reasoningEffort: effort.id } : {}) })
2045
+ }
2046
+ catch (cause) { return send(res, 400, envelopeError('MODEL_INVALID', cause instanceof Error ? cause.message : String(cause))) }
2047
+ } else if (selection.reasoningEffort) return send(res, 400, envelopeError('MODEL_INVALID', '模型不可用,无法校验思考强度'))
2048
+ }
1725
2049
  const updated = await bases.update(baseId, {
1726
2050
  name: body.name, description: body.description,
1727
2051
  extractionNote: typeof body.extractionNote === 'string' ? body.extractionNote : undefined,
2052
+ compileModel: body.compileModel && typeof body.compileModel === 'object'
2053
+ ? body.compileModel
2054
+ : undefined,
1728
2055
  })
1729
2056
  if (!updated.ok) return sendEnvelope(res, updated)
1730
2057
  return send(res, 200, success({ base: updated.data }, updated.warnings))
@@ -1858,8 +2185,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1858
2185
 
1859
2186
  // ----------------------------------------------- /knowledge/cards
1860
2187
  // wiki 卡片详情与版本(0025)。列表走上面的 exact 路由;这里只处理
1861
- // `/cards/:entityId` 及其 `/versions`、`/revert` 子路径 —— 卡片和
1862
- // SourceRaw 一样有历史,用户点开一张卡片要能看到它被改过几次。
2188
+ // `/cards/:entityId` 与 `/versions` 子路径 —— 卡片保留只读历史,供查看和比较。
1863
2189
  if (rest[0] === 'cards' && rest.length >= 2) {
1864
2190
  const kbParam = url.searchParams.get('kb')
1865
2191
  const resolved = resolveBase(kbParam)
@@ -1915,12 +2241,14 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1915
2241
  }
1916
2242
  if (rest.length === 4 && rest[2] === 'versions') {
1917
2243
  if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
1918
- const version = await resolved.runtime.readCardVersion(entityId, rest[3]!, entityType)
2244
+ const previewChars = clampInt(url.searchParams.get('preview_chars'), VERSION_DIFF_CHARS, 0, VERSION_DIFF_CHARS)
2245
+ const version = await resolved.runtime.readCardVersion(entityId, rest[3]!, entityType, previewChars)
1919
2246
  if (version === undefined) return sendEnvelope(res, failure({}, 'VERSION_NOT_FOUND', `版本不存在: ${rest[3]}`, true))
1920
2247
  return send(res, 200, success({ version }))
1921
2248
  }
1922
2249
  if (rest.length === 3 && rest[2] === 'revert') {
1923
2250
  if (method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
2251
+ // 兼容旧客户端的路径;运行时会明确拒绝恢复 WikiCard。
1924
2252
  const body = await readBody(req)
1925
2253
  const reverted = await resolved.runtime.revertCard(entityId, String(body.versionId ?? ''), entityType)
1926
2254
  if (!reverted.ok) return sendEnvelope(res, reverted)
@@ -1939,6 +2267,26 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1939
2267
  let documents = await resolved.runtime.listDocuments()
1940
2268
  const search = (url.searchParams.get('query') ?? '').trim().toLocaleLowerCase()
1941
2269
  if (search) documents = documents.filter(item => `${item.docId}\n${item.title}\n${item.externalId}`.toLocaleLowerCase().includes(search))
2270
+ /**
2271
+ * 文件类型的计数对着**搜索之后、类型筛选之前**的集合算。
2272
+ *
2273
+ * 下拉要能切换类型,所以每个类型的数量必须是「当前搜索下有多少份」,
2274
+ * 而不是「当前这一页里有多少份」—— 后者会让选项随翻页变来变去。
2275
+ */
2276
+ const extCounts: Record<string, number> = {}
2277
+ for (const item of documents) {
2278
+ const key = item.ext || '—'
2279
+ extCounts[key] = (extCounts[key] ?? 0) + 1
2280
+ }
2281
+ /**
2282
+ * 文件类型筛选**在服务端**做(0070)。
2283
+ *
2284
+ * 原来是「客户端取前 500 份、再在这一页里筛」:超过 500 份的库,第 501 份起
2285
+ * 在面板上根本不可达,而表头把这种截断显示成「筛出 500 / 600 个」——
2286
+ * 读起来像用户自己开了筛选。既然这里已经支持 `offset`,筛选就该和分页同层。
2287
+ */
2288
+ const ext = (url.searchParams.get('ext') ?? '').trim()
2289
+ if (ext) documents = documents.filter(item => item.ext === ext)
1942
2290
  const total = documents.length
1943
2291
  const offset = clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
1944
2292
  const limit = clampInt(url.searchParams.get('limit'), 50, 1, 500)
@@ -1949,7 +2297,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1949
2297
  * 两件事。放成并列的一张表,前端按 `docId` 查,文档类型不用为审计字段让步。
1950
2298
  */
1951
2299
  const extractionRuns = await resolved.runtime.extractionRunCounts()
1952
- return send(res, 200, success({ documents: documents.slice(offset, offset + limit), total, extractionRuns }))
2300
+ return send(res, 200, success({ documents: documents.slice(offset, offset + limit), total, extCounts, extractionRuns }))
1953
2301
  }
1954
2302
  const docId = rest[1]!
1955
2303
  const resolved = resolveBase(kbParam)
@@ -2067,6 +2415,6 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
2067
2415
  } },
2068
2416
  ]
2069
2417
  const disposers = routes.map(register)
2070
- return () => { runtime.close(); for (const dispose of disposers) void dispose() }
2418
+ return () => { environment.close(); runtime.close(); bases.closeAll(); for (const dispose of disposers) void dispose() }
2071
2419
  })
2072
2420
  }