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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/README.md +89 -56
  2. package/cordis.patch.yml +36 -129
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +3262 -4142
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1147 -364
  7. package/lib/types/axis-registry.d.ts +8 -1
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +62 -7
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/convert/host.d.ts.map +1 -1
  12. package/lib/types/convert/host.js +3 -0
  13. package/lib/types/convert/host.js.map +1 -1
  14. package/lib/types/convert/ingest.d.ts +9 -0
  15. package/lib/types/convert/ingest.d.ts.map +1 -1
  16. package/lib/types/convert/ingest.js +68 -9
  17. package/lib/types/convert/ingest.js.map +1 -1
  18. package/lib/types/diagnosis/cards.d.ts +7 -3
  19. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  20. package/lib/types/diagnosis/cards.js +13 -8
  21. package/lib/types/diagnosis/cards.js.map +1 -1
  22. package/lib/types/diagnosis/run.d.ts +4 -13
  23. package/lib/types/diagnosis/run.d.ts.map +1 -1
  24. package/lib/types/diagnosis/run.js +26 -16
  25. package/lib/types/diagnosis/run.js.map +1 -1
  26. package/lib/types/diagnosis/schema.js +2 -2
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +5 -4
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +4 -4
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +6 -8
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +18 -17
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +94 -66
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +10 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js.map +1 -1
  43. package/lib/types/environment/dsh-python.d.ts +77 -0
  44. package/lib/types/environment/dsh-python.d.ts.map +1 -0
  45. package/lib/types/environment/dsh-python.js +139 -0
  46. package/lib/types/environment/dsh-python.js.map +1 -0
  47. package/lib/types/environment/install.d.ts +9 -1
  48. package/lib/types/environment/install.d.ts.map +1 -1
  49. package/lib/types/environment/install.js +149 -24
  50. package/lib/types/environment/install.js.map +1 -1
  51. package/lib/types/environment/model-catalog.d.ts +34 -2
  52. package/lib/types/environment/model-catalog.d.ts.map +1 -1
  53. package/lib/types/environment/model-catalog.js +40 -2
  54. package/lib/types/environment/model-catalog.js.map +1 -1
  55. package/lib/types/environment/service.d.ts +13 -3
  56. package/lib/types/environment/service.d.ts.map +1 -1
  57. package/lib/types/environment/service.js +127 -23
  58. package/lib/types/environment/service.js.map +1 -1
  59. package/lib/types/environment/types.d.ts +9 -0
  60. package/lib/types/environment/types.d.ts.map +1 -1
  61. package/lib/types/events.d.ts +2 -0
  62. package/lib/types/events.d.ts.map +1 -1
  63. package/lib/types/events.js +13 -0
  64. package/lib/types/events.js.map +1 -1
  65. package/lib/types/extract-contract.d.ts +1 -1
  66. package/lib/types/extract-contract.d.ts.map +1 -1
  67. package/lib/types/extract-contract.js +34 -31
  68. package/lib/types/extract-contract.js.map +1 -1
  69. package/lib/types/extract-model.d.ts +2 -0
  70. package/lib/types/extract-model.d.ts.map +1 -1
  71. package/lib/types/extract-model.js.map +1 -1
  72. package/lib/types/extract-table-contract.d.ts +3 -0
  73. package/lib/types/extract-table-contract.d.ts.map +1 -0
  74. package/lib/types/extract-table-contract.js +30 -0
  75. package/lib/types/extract-table-contract.js.map +1 -0
  76. package/lib/types/extract-table.d.ts +8 -0
  77. package/lib/types/extract-table.d.ts.map +1 -0
  78. package/lib/types/extract-table.js +151 -0
  79. package/lib/types/extract-table.js.map +1 -0
  80. package/lib/types/extraction-runs.d.ts +30 -6
  81. package/lib/types/extraction-runs.d.ts.map +1 -1
  82. package/lib/types/extraction-runs.js +69 -8
  83. package/lib/types/extraction-runs.js.map +1 -1
  84. package/lib/types/fs-atomic.d.ts +0 -7
  85. package/lib/types/fs-atomic.d.ts.map +1 -1
  86. package/lib/types/fs-atomic.js +17 -2
  87. package/lib/types/fs-atomic.js.map +1 -1
  88. package/lib/types/images.d.ts +1 -1
  89. package/lib/types/images.d.ts.map +1 -1
  90. package/lib/types/images.js +2 -2
  91. package/lib/types/images.js.map +1 -1
  92. package/lib/types/index.d.ts.map +1 -1
  93. package/lib/types/index.js +232 -50
  94. package/lib/types/index.js.map +1 -1
  95. package/lib/types/intent/scope.d.ts.map +1 -1
  96. package/lib/types/intent/scope.js +1 -0
  97. package/lib/types/intent/scope.js.map +1 -1
  98. package/lib/types/intent/tools.d.ts +19 -0
  99. package/lib/types/intent/tools.d.ts.map +1 -1
  100. package/lib/types/intent/tools.js +37 -1
  101. package/lib/types/intent/tools.js.map +1 -1
  102. package/lib/types/knowledge-bases.d.ts.map +1 -1
  103. package/lib/types/knowledge-bases.js +5 -16
  104. package/lib/types/knowledge-bases.js.map +1 -1
  105. package/lib/types/knowledge.d.ts +3 -0
  106. package/lib/types/knowledge.d.ts.map +1 -1
  107. package/lib/types/knowledge.js +21 -5
  108. package/lib/types/knowledge.js.map +1 -1
  109. package/lib/types/materialize-snapshot.d.ts +49 -0
  110. package/lib/types/materialize-snapshot.d.ts.map +1 -0
  111. package/lib/types/materialize-snapshot.js +2 -0
  112. package/lib/types/materialize-snapshot.js.map +1 -0
  113. package/lib/types/model-text.d.ts +1 -1
  114. package/lib/types/model-text.d.ts.map +1 -1
  115. package/lib/types/model-text.js +5 -4
  116. package/lib/types/model-text.js.map +1 -1
  117. package/lib/types/oplog.d.ts.map +1 -1
  118. package/lib/types/oplog.js +7 -0
  119. package/lib/types/oplog.js.map +1 -1
  120. package/lib/types/pipeline/engine.d.ts +60 -0
  121. package/lib/types/pipeline/engine.d.ts.map +1 -0
  122. package/lib/types/pipeline/engine.js +338 -0
  123. package/lib/types/pipeline/engine.js.map +1 -0
  124. package/lib/types/pipeline/store.d.ts +73 -0
  125. package/lib/types/pipeline/store.d.ts.map +1 -0
  126. package/lib/types/pipeline/store.js +143 -0
  127. package/lib/types/pipeline/store.js.map +1 -0
  128. package/lib/types/pipeline/types.d.ts +101 -0
  129. package/lib/types/pipeline/types.d.ts.map +1 -0
  130. package/lib/types/pipeline/types.js +5 -0
  131. package/lib/types/pipeline/types.js.map +1 -0
  132. package/lib/types/present.d.ts +2 -0
  133. package/lib/types/present.d.ts.map +1 -1
  134. package/lib/types/present.js +9 -4
  135. package/lib/types/present.js.map +1 -1
  136. package/lib/types/retention.d.ts +39 -0
  137. package/lib/types/retention.d.ts.map +1 -0
  138. package/lib/types/retention.js +42 -0
  139. package/lib/types/retention.js.map +1 -0
  140. package/lib/types/runtime.d.ts +115 -5
  141. package/lib/types/runtime.d.ts.map +1 -1
  142. package/lib/types/runtime.js +862 -132
  143. package/lib/types/runtime.js.map +1 -1
  144. package/lib/types/source-evidence.d.ts +9 -1
  145. package/lib/types/source-evidence.d.ts.map +1 -1
  146. package/lib/types/source-evidence.js +13 -5
  147. package/lib/types/source-evidence.js.map +1 -1
  148. package/lib/types/store/axis-references.js +3 -3
  149. package/lib/types/store/axis-references.js.map +1 -1
  150. package/lib/types/store/derived.d.ts +1 -1
  151. package/lib/types/store/derived.d.ts.map +1 -1
  152. package/lib/types/store/derived.js +41 -7
  153. package/lib/types/store/derived.js.map +1 -1
  154. package/lib/types/store/knowledge-db.d.ts +18 -0
  155. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  156. package/lib/types/store/knowledge-db.js +111 -6
  157. package/lib/types/store/knowledge-db.js.map +1 -1
  158. package/lib/types/store/migrate.js +1 -1
  159. package/lib/types/store/migrate.js.map +1 -1
  160. package/lib/types/trajectory-types.d.ts +172 -0
  161. package/lib/types/trajectory-types.d.ts.map +1 -0
  162. package/lib/types/trajectory-types.js +36 -0
  163. package/lib/types/trajectory-types.js.map +1 -0
  164. package/lib/types/types.d.ts +3 -4
  165. package/lib/types/types.d.ts.map +1 -1
  166. package/lib/types/types.js.map +1 -1
  167. package/lib/types/wiki-finalize.d.ts +16 -0
  168. package/lib/types/wiki-finalize.d.ts.map +1 -1
  169. package/lib/types/wiki-finalize.js +26 -0
  170. package/lib/types/wiki-finalize.js.map +1 -1
  171. package/lib/types/wiki-materialize.d.ts.map +1 -1
  172. package/lib/types/wiki-materialize.js +3 -15
  173. package/lib/types/wiki-materialize.js.map +1 -1
  174. package/lib/types/wiki-trial-types.d.ts +3 -0
  175. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  176. package/lib/types/wiki-trial.d.ts +2 -0
  177. package/lib/types/wiki-trial.d.ts.map +1 -1
  178. package/lib/types/wiki-trial.js +31 -7
  179. package/lib/types/wiki-trial.js.map +1 -1
  180. package/package.json +6 -3
  181. package/preset/tita-data-agent/agent.cordis.yml +36 -129
  182. package/src/axis-registry.ts +57 -9
  183. package/src/client/AttributionEventGuide.tsx +10 -9
  184. package/src/client/CompileDialogs.tsx +10 -2
  185. package/src/client/EnvironmentSetup.tsx +76 -16
  186. package/src/client/ExpansionGraph.tsx +13 -7
  187. package/src/client/ExtractionTasksPage.tsx +543 -156
  188. package/src/client/ImportFilesDialog.tsx +2 -13
  189. package/src/client/IntentTab.tsx +3 -1
  190. package/src/client/KnowledgeBaseTab.tsx +139 -122
  191. package/src/client/KnowledgeLayerSection.tsx +19 -2
  192. package/src/client/KnowledgePanel.tsx +17 -3
  193. package/src/client/ListPager.tsx +15 -5
  194. package/src/client/MaterializeTrajectoryView.tsx +124 -0
  195. package/src/client/NodeSettings.tsx +90 -25
  196. package/src/client/NormalizeTrajectoryView.tsx +136 -0
  197. package/src/client/PipelineNotice.tsx +26 -0
  198. package/src/client/RecallPanel.tsx +55 -25
  199. package/src/client/TabGuide.tsx +4 -4
  200. package/src/client/TrialReportView.tsx +61 -16
  201. package/src/client/TypeDefinitionDrawer.tsx +33 -9
  202. package/src/client/WikiCardPreview.tsx +43 -10
  203. package/src/client/WikiFlowPanel.tsx +4 -4
  204. package/src/client/architecture.tsx +1 -1
  205. package/src/client/attribution/AttributionTab.tsx +37 -36
  206. package/src/client/attribution/CaseView.tsx +3 -1
  207. package/src/client/attribution/EventView.tsx +14 -4
  208. package/src/client/attribution/RunView.tsx +2 -2
  209. package/src/client/attribution/ViewSwitch.tsx +20 -5
  210. package/src/client/attribution/graph-options.ts +38 -31
  211. package/src/client/attribution/model.ts +16 -9
  212. package/src/client/graph/palette.ts +165 -0
  213. package/src/client/pages.css +224 -28
  214. package/src/client/pages.tsx +33 -11
  215. package/src/client/styles.css +61 -6
  216. package/src/client/wiki-trial.ts +30 -1
  217. package/src/convert/host.ts +3 -0
  218. package/src/convert/ingest.ts +73 -9
  219. package/src/diagnosis/cards.ts +13 -8
  220. package/src/diagnosis/run.ts +27 -17
  221. package/src/diagnosis/schema.ts +2 -2
  222. package/src/diagnosis/seed-source.ts +5 -4
  223. package/src/diagnosis/seed.ts +6 -8
  224. package/src/diagnosis/store.ts +99 -69
  225. package/src/diagnosis/types.ts +10 -16
  226. package/src/environment/dsh-python.ts +156 -0
  227. package/src/environment/install.ts +134 -19
  228. package/src/environment/model-catalog.ts +45 -3
  229. package/src/environment/service.ts +111 -23
  230. package/src/environment/types.ts +6 -0
  231. package/src/events.ts +12 -0
  232. package/src/extract-contract.ts +6 -3
  233. package/src/extract-model.ts +2 -0
  234. package/src/extract-table-contract.ts +29 -0
  235. package/src/extract-table.ts +126 -0
  236. package/src/extraction-runs.ts +70 -11
  237. package/src/fs-atomic.ts +19 -2
  238. package/src/images.ts +2 -2
  239. package/src/index.ts +207 -72
  240. package/src/intent/scope.ts +1 -0
  241. package/src/intent/tools.ts +38 -1
  242. package/src/knowledge-bases.ts +4 -12
  243. package/src/knowledge.ts +24 -4
  244. package/src/materialize-snapshot.ts +46 -0
  245. package/src/model-text.ts +5 -3
  246. package/src/oplog.ts +7 -0
  247. package/src/pipeline/engine.ts +312 -0
  248. package/src/pipeline/store.ts +147 -0
  249. package/src/pipeline/types.ts +92 -0
  250. package/src/present.ts +10 -4
  251. package/src/retention.ts +76 -0
  252. package/src/runtime.ts +853 -131
  253. package/src/source-evidence.ts +12 -5
  254. package/src/store/axis-references.ts +3 -3
  255. package/src/store/derived.ts +40 -7
  256. package/src/store/knowledge-db.ts +111 -5
  257. package/src/store/migrate.ts +1 -1
  258. package/src/trajectory-types.ts +142 -0
  259. package/src/types.ts +3 -4
  260. package/src/wiki-finalize.ts +28 -0
  261. package/src/wiki-materialize.ts +3 -14
  262. package/src/wiki-trial-types.ts +3 -0
  263. package/src/wiki-trial.ts +26 -7
  264. package/src/client/convert/ConvertPanel.tsx +0 -583
  265. package/src/client/convert/styles.css +0 -138
  266. package/src/client/convert/styles.ts +0 -49
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './docum
32
32
  import type { ModelCall } from './extract-model.js'
33
33
  import { runWikiTrial, trialSummary } from './wiki-trial.js'
34
34
  import { extractionRunId } from './extraction-runs.js'
35
+ import type { PipelineStage } from './pipeline/types.js'
35
36
  import {
36
37
  agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot,
37
38
  memoryRoot, presetSourceDir, resourceRegistry, resourceRoot, type AgentLayout,
@@ -474,7 +475,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
474
475
  }
475
476
  const provider = configured.mode === 'fixed' ? configured.provider : defaultProvider
476
477
  const model = configured.mode === 'fixed' ? configured.model : defaultModel
477
- const effectiveReasoningEffort = configured.mode === 'default' ? reasoningEffort : undefined
478
+ const effectiveReasoningEffort = configured.reasoningEffort || (configured.mode === 'default' ? reasoningEffort : undefined)
478
479
  if (!provider || !model) {
479
480
  return { why: `编译模型没有选好(provider=${provider ?? '空'} / model=${model ?? '空'})` }
480
481
  }
@@ -482,7 +483,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
482
483
  provider,
483
484
  model,
484
485
  ...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
485
- call: async ({ system, prompt, sessionId, onUsage }) => {
486
+ call: async ({ system, prompt, sessionId, onUsage, onDelta }) => {
486
487
  /**
487
488
  * 先走 DSH 的公开解析接口,再开始发 prompt。这样固定模型过期、默认模型失效时,
488
489
  * 编译会在模型调用边界明确失败,而不是落成一次看似成功的规则抽取。
@@ -491,7 +492,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
491
492
  try {
492
493
  await llm.resolveCallConfig({
493
494
  provider, model,
494
- ...(configured.mode === 'default' && reasoningEffort ? { reasoningEffort } : {}),
495
+ ...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
495
496
  })
496
497
  } catch (cause) {
497
498
  const detail = cause instanceof Error ? cause.message : String(cause)
@@ -513,13 +514,13 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
513
514
  // 而不是留空 —— 留空会在 OpenCode 路由上 400。
514
515
  sessionId: sessionId ?? 'knowledge:default',
515
516
  })
516
- return collectModelText(llm.stream(request) as AsyncIterable<never>, onUsage)
517
+ return collectModelText(llm.stream(request) as AsyncIterable<never>, onUsage, onDelta)
517
518
  },
518
519
  }
519
520
  }
520
521
  let environment: KnowledgeEnvironment
521
522
  const runtimeOptions = (): KnowledgeRuntimeOptions => ({
522
- environmentGuard: () => environment.requireReady(),
523
+ environmentGuard: kind => environment.requireReady(kind),
523
524
  // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
524
525
  resourceRoot: resourceRoot(config.agentRoot),
525
526
  // 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
@@ -556,7 +557,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
556
557
  // 0044:非默认库的 runtime 也要拿到模型接缝。只传 `config` 不够 —— 默认库走上面
557
558
  // 那个 `new KnowledgeRuntime(... runtimeOptions())`,别的库走注册表这条支路,
558
559
  // 两边必须都带上,否则「新建一个库 → 导入文档」静默变成规则抽取。
559
- runtimeOptions: () => ({ resourceRoot: resourceRoot(config.agentRoot), modelSeam: modelSeamFor, environmentGuard: () => environment.requireReady() }),
560
+ runtimeOptions: () => ({ resourceRoot: resourceRoot(config.agentRoot), modelSeam: modelSeamFor, environmentGuard: kind => environment.requireReady(kind) }),
560
561
  })
561
562
  compileModelForBase = id => bases.compileModelOf(id)
562
563
  runtime.baseId = bases.defaultId
@@ -566,12 +567,35 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
566
567
  snapshot: () => ({ config: runtime.config, paths: runtime.resourcePaths() }),
567
568
  probe: kind => runtime.environmentProbe(kind),
568
569
  reload: () => { runtime.reloadResources(); bases.reloadResources() },
570
+ activateSelection: async () => {
571
+ const keys = ['embeddingProvider', 'embeddingModel', 'rerankProvider', 'rerankModel'] as const
572
+ const previous = Object.fromEntries(keys.map(key => [key, config[key]]))
573
+ const managed = { embeddingProvider: 'wemm-python', embeddingModel: '', rerankProvider: 'bert-python', rerankModel: '' }
574
+ const write = async (values: Record<string, unknown>): Promise<void> => {
575
+ const group = pluginConfig.knowledge as unknown as Record<string, unknown> | undefined
576
+ if (!group) throw new Error('模型设置不可写')
577
+ for (const key of keys) {
578
+ const reference = group[key] as { write?: (value: unknown) => unknown } | undefined
579
+ if (reference && typeof reference.write === 'function') await reference.write(values[key])
580
+ else group[key] = values[key]
581
+ }
582
+ config = normalizeKnowledge(pluginConfig.knowledge)
583
+ runtime.updateConfig(config); bases.updateRuntimeConfig(config)
584
+ }
585
+ try { await write(managed) } catch (cause) { await write(previous); throw cause }
586
+ return () => {
587
+ const current = normalizeKnowledge(pluginConfig.knowledge)
588
+ // A later manual setting change belongs to the user and must survive rollback.
589
+ const restored = Object.fromEntries(keys.map(key => [key, current[key] === managed[key] ? previous[key] : current[key]]))
590
+ return write(restored)
591
+ }
592
+ },
569
593
  })
570
- // Startup checks imports and resource files; model inference runs on demand.
571
- void environment.check(false).catch(() => undefined)
594
+ // Reuse a matching persisted probe after restart; only stale or missing reports need a resource check.
595
+ if (environment.view().report.state !== 'ready') void environment.check(false).catch(() => undefined)
572
596
 
573
- type CompileModelCatalogItem = { provider: string; id: string; name: string; description?: string; available: boolean; current?: boolean }
574
- const compileModelCatalog = async (kbId: string): Promise<{
597
+ 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 } }
598
+ const compileModelCatalog = async (kbId: string, queryModelRoute: { provider?: string; model?: string } = {}): Promise<{
575
599
  selection: CompileModelSelection
576
600
  default: { provider?: string; model?: string; reasoningEffort?: string; available: boolean }
577
601
  models: CompileModelCatalogItem[]
@@ -582,8 +606,10 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
582
606
  const defaultRoute = service?.currentSelection?.() ?? {}
583
607
  const llm = ctx.get('llm') as {
584
608
  listModels?: (provider: string) => Promise<Array<{ id: string; name?: string; description?: string }>>
609
+ listProviders?: () => Array<{ id: string; name: string }>
610
+ resolveModelInfo?: (provider: string, model: string) => Promise<{ reasoning?: CompileModelCatalogItem['reasoning'] }>
585
611
  } | undefined
586
- const providers = [...new Set([defaultRoute.provider, compileModelForBase(kbId).provider].filter((value): value is string => Boolean(value)))]
612
+ const providers = [...new Set([...(llm?.listProviders?.() ?? []).map(provider => provider.id), defaultRoute.provider, compileModelForBase(kbId).provider].filter((value): value is string => Boolean(value)))]
587
613
  const models: CompileModelCatalogItem[] = []
588
614
  const failures: string[] = []
589
615
  if (!llm?.listModels) {
@@ -610,6 +636,17 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
610
636
  }
611
637
 
612
638
  const selection = compileModelForBase(kbId)
639
+ const effortProvider = selection.mode === 'fixed' ? selection.provider : defaultRoute.provider
640
+ const effortModel = selection.mode === 'fixed' ? selection.model : defaultRoute.model
641
+ const requestedProvider = queryModelRoute.provider || effortProvider
642
+ const requestedModel = queryModelRoute.model || effortModel
643
+ if (requestedProvider && requestedModel && llm?.resolveModelInfo) {
644
+ const item = models.find(model => model.provider === requestedProvider && model.id === requestedModel)
645
+ if (item) {
646
+ try { item.reasoning = (await llm.resolveModelInfo(requestedProvider, requestedModel)).reasoning }
647
+ catch (cause) { failures.push(`思考强度目录:${cause instanceof Error ? cause.message : String(cause)}`) }
648
+ }
649
+ }
613
650
  const selected = selection.mode === 'fixed' && selection.provider && selection.model
614
651
  ? models.find(item => item.provider === selection.provider && item.id === selection.model)
615
652
  : undefined
@@ -777,23 +814,56 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
777
814
  applyIntent(ctx, { intentRoot: () => intentRoot(config.agentRoot) })
778
815
 
779
816
  /**
780
- * 需要本轮用户审批的知识写工具。`images.publish` 只写入本地素材目录,按用户要求免审批。
817
+ * 需要本轮用户审批的知识写工具。`image_publish` 只写入本地素材目录,按用户要求免审批。
781
818
  *
782
819
  * 0044 换过一批名字:`write_knowledge_card`(只接受已确认提议)退役了,因为正文现在
783
820
  * 只读、卡片由编译直接写入;`list_card_proposals` 也退役了,因为待办队列不存在了。
784
821
  * 新增的两个是**入库之后**的动作:加备注、review 抬分。
785
822
  */
786
- const writeTools = new Set(['add_source', 'import_sources', 'propose_wiki_cards', 'annotate_wiki_card', 'review_wiki_card', 'set_knowledge_status', 'rebuild_knowledge', 'record_feedback'])
823
+ ctx.tools.register(tool('run_knowledge_pipeline', 'Run one independent knowledge stage. Import only saves sources by default. Stages: extract Knowledge/Event, normalize existing results, materialize selected complete card groups, build retrieval indexes, or all. Returns a durable background task; inspect it with get_knowledge_pipeline.', {
824
+ stage: { type: 'string', required: true, enum: ['extract', 'normalize', 'materialize', 'build', 'all'] },
825
+ kb: { type: 'string' }, source_ids: { type: 'array', description: 'Optional explicit source IDs; omit for all sources. An empty selection is rejected.' },
826
+ 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.' },
827
+ }, async args => withRuntime(args.kb, async target => {
828
+ const pipeline = await target.pipeline()
829
+ const { task } = pipeline.start(args.stage as PipelineStage, { sourceIds: args.source_ids, groupKeys: args.group_keys, force: args.force === true })
830
+ return success({ task, url: `/knowledge/pages/tasks?kb=${encodeURIComponent(target.baseId)}&task=${encodeURIComponent(task.id)}` })
831
+ })))
832
+ 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.', {
833
+ kb: { type: 'string' }, task_id: { type: 'string' }, after: { type: 'integer' },
834
+ }, async args => withRuntime(args.kb, async (target): Promise<Envelope<Record<string, unknown>>> => {
835
+ const pipeline = await target.pipeline()
836
+ return args.task_id
837
+ ? success({ task: pipeline.store.task(args.task_id) ?? null, ...pipeline.store.events(args.task_id, Math.max(0, args.after ?? 0)) })
838
+ : success({ status: await target.pipelineStatus(), ...pipeline.store.tasks() })
839
+ })))
840
+ 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'])
787
841
  ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
788
842
  if (writeTools.has(exec.name)) return Promise.resolve({ kind: 'ask', reason: '该操作会写入 Tita Knowledge Store,需要本轮用户审批。' })
789
843
  return next()
790
844
  })
791
- 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 } : {}) })), {
845
+ ctx.tools.register(tool('recall_knowledge', 'Recall compiled Knowledge through keyword and vector candidate routes, rerank WikiCard sections, then return cards with matched sections, provenance, and Event reference IDs. Use get_knowledge_events to read referenced Events.', { 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 } : {}) })), {
792
846
  call: recallCallView,
793
847
  // 查询词从参数来,命中从结果来 —— 两者拼成一张能看懂的账本。
794
848
  meta: (args, value) => recallMetaOf({ ...dataOf(value), query: args?.query }),
795
849
  result: resultViewOf(recallResultMarkdown),
796
850
  }))
851
+ 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.', {
852
+ event_ids: { type: 'array', required: true, description: 'Event IDs from WikiCard metadata.event_refs; maximum 50 IDs.' },
853
+ kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' },
854
+ run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' },
855
+ }, async args => withRuntime(args.kb, async target => {
856
+ const ids = [...new Set((Array.isArray(args.event_ids) ? args.event_ids : [])
857
+ .filter((id): id is string => typeof id === 'string' && id.trim() !== '').map(id => id.trim()))]
858
+ if (ids.length === 0) return failure({ events: [], missing_ids: [] }, 'EVENT_IDS_REQUIRED', '请传入 WikiCard 的 event_refs 中的 Event ID。', false)
859
+ if (ids.length > 50) return failure({ events: [], missing_ids: [] }, 'EVENT_IDS_LIMIT', '一次最多读取 50 个 Event ID;请分批读取该 WikiCard 的 event_refs。', false)
860
+ const byId = new Map((await target.loadEvents()).map(event => [event.id, event]))
861
+ const events = ids.flatMap(id => {
862
+ const event = byId.get(id)
863
+ return event ? [event] : []
864
+ })
865
+ return success({ events, missing_ids: ids.filter(id => !byId.has(id)) })
866
+ })))
797
867
  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())))
798
868
  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)))))
799
869
  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))))
@@ -824,30 +894,28 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
824
894
  })),
825
895
  })))
826
896
 
827
- ctx.tools.register(tool(
828
- 'images.publish',
829
- '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.',
830
- {
831
- file_path: { type: 'string', description: '宿主可读取的本地图片文件路径;与 content_base64 二选一' },
832
- content_base64: { type: 'string', description: '图片的标准 Base64,或 data:image/...;base64,...;与 file_path 二选一' },
833
- filename: { type: 'string', description: '可选文件名;默认使用 file_path 的文件名,Base64 输入默认 image-<hash>.<ext>' },
834
- mime_type: { type: 'string', description: '可选 MIME 类型;必须与图片二进制内容一致' },
835
- kb: { type: 'string', description: '知识库 id;省略时写入默认库' },
836
- },
837
- async args => withRuntime(args.kb, async target => {
838
- try {
839
- return success(await publishKnowledgeImage(target.rootDir, target.baseId, {
840
- filePath: args.file_path,
841
- contentBase64: args.content_base64,
842
- filename: args.filename,
843
- mimeType: args.mime_type,
844
- }))
845
- } catch (cause) {
846
- if (cause instanceof ImagePublishError) return failure({}, cause.code, cause.message, false, cause.suggestion)
847
- return failure({}, 'IMAGE_PUBLISH_FAILED', `图片写入失败:${cause instanceof Error ? cause.message : String(cause)}`, true)
848
- }
849
- }),
850
- ))
897
+ const imagePublishParameters = {
898
+ file_path: { type: 'string', description: '宿主可读取的本地图片文件路径;与 content_base64 二选一' },
899
+ content_base64: { type: 'string', description: '图片的标准 Base64,或 data:image/...;base64,...;与 file_path 二选一' },
900
+ filename: { type: 'string', description: '可选文件名;默认使用 file_path 的文件名,Base64 输入默认 image-<hash>.<ext>' },
901
+ mime_type: { type: 'string', description: '可选 MIME 类型;必须与图片二进制内容一致' },
902
+ kb: { type: 'string', description: '知识库 id;省略时写入默认库' },
903
+ }
904
+ const publishImage = async (args: Args) => withRuntime(args.kb, async target => {
905
+ try {
906
+ return success(await publishKnowledgeImage(target.rootDir, target.baseId, {
907
+ filePath: args.file_path,
908
+ contentBase64: args.content_base64,
909
+ filename: args.filename,
910
+ mimeType: args.mime_type,
911
+ }))
912
+ } catch (cause) {
913
+ if (cause instanceof ImagePublishError) return failure({}, cause.code, cause.message, false, cause.suggestion)
914
+ return failure({}, 'IMAGE_PUBLISH_FAILED', `图片写入失败:${cause instanceof Error ? cause.message : String(cause)}`, true)
915
+ }
916
+ })
917
+ 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.'
918
+ ctx.tools.register(tool('image_publish', imagePublishDescription, imagePublishParameters, publishImage))
851
919
 
852
920
  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) })), {
853
921
  call: addSourceCallView,
@@ -868,7 +936,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
868
936
  * `compile` 默认开:批量导入的意图几乎总是「进来就能用」。关掉它的场景是「先全部
869
937
  * 落盘、稍后统一编译」——那时卡片还没生成,工具返回里会明说 `compile: null`。
870
938
  */
871
- 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) }))
939
+ 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) }))
872
940
  /**
873
941
  * `propose_wiki_cards` —— 把一批卡片**直接写进 `02_wiki`**(工具名不变,语义在 0044 改了)。
874
942
  *
@@ -924,7 +992,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
924
992
  lineage: lineageOf(args, exec),
925
993
  }))))
926
994
  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)))))
927
- 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))), {
995
+ 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 })), {
928
996
  call: rebuildCallView,
929
997
  meta: (args, value) => rebuildMetaOf(args?.only ?? 'all', dataOf(value)),
930
998
  result: resultViewOf(rebuildResultMarkdown),
@@ -1004,7 +1072,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1004
1072
  const url = query(req)
1005
1073
  const id = kbIdOf(url.searchParams.get('kb'))
1006
1074
  if (!bases.get(id)) return sendEnvelope(res, failure({}, 'BASE_NOT_FOUND', `知识库不存在: ${id}`, true, '先 GET /knowledge/bases 拿到可用 id。'))
1007
- return send(res, 200, success(await compileModelCatalog(id)))
1075
+ return send(res, 200, success(await compileModelCatalog(id, { provider: url.searchParams.get('provider') ?? undefined, model: url.searchParams.get('model') ?? undefined })))
1008
1076
  } },
1009
1077
  { kind: 'exact' as const, path: '/knowledge/stats', handler: async (req: IncomingMessage, res: ServerResponse) => {
1010
1078
  const resolved = resolveBase(query(req).searchParams.get('kb'))
@@ -1093,6 +1161,47 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1093
1161
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1094
1162
  return send(res, 200, success({ progress: resolved.runtime.compileProgressView(url.searchParams.get('runId') ?? undefined) }))
1095
1163
  } },
1164
+ { kind: 'exact' as const, path: '/knowledge/pipeline', handler: async (req: IncomingMessage, res: ServerResponse) => {
1165
+ try {
1166
+ const body = req.method === 'POST' ? await readBody(req, 100_000) : {}
1167
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1168
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1169
+ if (req.method === 'GET') return send(res, 200, success(await resolved.runtime.pipelineStatus()))
1170
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '知识处理只接受 GET 或 POST'))
1171
+ const pipeline = await resolved.runtime.pipeline()
1172
+ const started = pipeline.start(body.stage as PipelineStage, { sourceIds: body.sourceIds, groupKeys: body.groupKeys, force: body.force === true })
1173
+ return send(res, 202, success({ task: started.task }))
1174
+ } catch (cause) { return send(res, 400, envelopeError('PIPELINE_INVALID', cause instanceof Error ? cause.message : String(cause))) }
1175
+ } },
1176
+ { kind: 'exact' as const, path: '/knowledge/pipeline-tasks', handler: async (req: IncomingMessage, res: ServerResponse) => {
1177
+ try {
1178
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '任务历史只接受 GET'))
1179
+ const url = query(req)
1180
+ const resolved = resolveBase(url.searchParams.get('kb'))
1181
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1182
+ const pipeline = await resolved.runtime.pipeline()
1183
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit')) || 20))
1184
+ const offset = Math.max(0, Number(url.searchParams.get('offset')) || 0)
1185
+ return send(res, 200, success({ ...pipeline.store.tasks(limit, offset), extractions: await resolved.runtime.listExtractionRuns({ limit, offset }) }))
1186
+ } catch (cause) { return send(res, 400, envelopeError('PIPELINE_HISTORY_FAILED', cause instanceof Error ? cause.message : String(cause))) }
1187
+ } },
1188
+ { kind: 'exact' as const, path: '/knowledge/pipeline-task', handler: async (req: IncomingMessage, res: ServerResponse) => {
1189
+ try {
1190
+ const url = query(req)
1191
+ const body = req.method === 'POST' ? await readBody(req, 20_000) : {}
1192
+ const resolved = resolveBase(body.kb ?? url.searchParams.get('kb'))
1193
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1194
+ const pipeline = await resolved.runtime.pipeline()
1195
+ const id = typeof body.id === 'string' ? body.id : url.searchParams.get('id') ?? ''
1196
+ const task = pipeline.store.task(id)
1197
+ if (!task) return send(res, 404, envelopeError('PIPELINE_NOT_FOUND', '任务不存在'))
1198
+ if (req.method === 'GET') return send(res, 200, success({ task, ...pipeline.store.events(id, Math.max(0, Number(url.searchParams.get('after')) || 0)) }))
1199
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '任务只接受 GET 或 POST'))
1200
+ if (body.action === 'retry') return send(res, 202, success({ task: pipeline.start(task.stage, { ...task.selection, force: false }).task }))
1201
+ if (!['pause', 'resume', 'cancel'].includes(body.action)) throw new Error('不支持的任务操作')
1202
+ return send(res, 200, success({ task: pipeline.control(id, body.action) }))
1203
+ } catch (cause) { return send(res, 409, envelopeError('PIPELINE_TASK_REJECTED', cause instanceof Error ? cause.message : String(cause))) }
1204
+ } },
1096
1205
  { kind: 'exact' as const, path: '/knowledge/extraction-tasks', handler: async (req: IncomingMessage, res: ServerResponse) => {
1097
1206
  if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '抽取任务只接受 GET'))
1098
1207
  const resolved = resolveBase(query(req).searchParams.get('kb'))
@@ -1156,7 +1265,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1156
1265
  const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1157
1266
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1158
1267
  return send(res, 202, success(resolved.runtime.startCompile(body.only ?? 'all', lineageOf(body), {
1159
- ...(typeof body.extract === 'boolean' ? { extract: body.extract } : {}),
1268
+ extract: body.extract === true,
1160
1269
  ...(body.forceExtract === true ? { forceExtract: true } : {}),
1161
1270
  })))
1162
1271
  } catch (cause) {
@@ -1183,7 +1292,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1183
1292
  ...(item?.external_id ? { externalId: String(item.external_id) } : {}),
1184
1293
  ...(item?.metadata && typeof item.metadata === 'object' ? { metadata: item.metadata as Record<string, unknown> } : {}),
1185
1294
  })),
1186
- ...(body.compile === false ? { compile: false } : {}),
1295
+ compile: body.compile === true,
1187
1296
  lineage: lineageOf(body),
1188
1297
  }))
1189
1298
  } catch (cause) {
@@ -1276,7 +1385,8 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1276
1385
  * 任何一条轨迹 —— 「这个实体是哪次抽出来的」就答不上了。
1277
1386
  */
1278
1387
  const runId = extractionRunId()
1279
- const seam = modelSeamFor()
1388
+ const seam = modelSeamFor(resolved.runtime.baseId)
1389
+ const draft = await (await resolved.runtime.axisRegistry())?.draft()
1280
1390
  const axesBlock = await resolved.runtime.axisPromptBlockView()
1281
1391
  const note = resolved.runtime.extractionNote()
1282
1392
  const report = await runWikiTrial({
@@ -1286,7 +1396,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1286
1396
  ...(typeof body.subject === 'string' && body.subject.trim() ? { subject: body.subject.trim() } : {}),
1287
1397
  /** 与编译相同:先查注册表,再把有候选的坐标合并成一次模型补判。 */
1288
1398
  resolveAxisSet: axes => resolved.runtime.resolveTrialAxisSet({
1289
- axes,
1399
+ registry: draft?.registry, axes,
1290
1400
  axesBlock,
1291
1401
  runId,
1292
1402
  call: seam.call,
@@ -1309,7 +1419,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1309
1419
  },
1310
1420
  ...('call' in seam ? { call: seam.call } : {}),
1311
1421
  ...('why' in seam ? { callWhy: seam.why } : {}),
1312
- })
1422
+ }).finally(() => draft?.close())
1313
1423
  const summary = trialSummary(report)
1314
1424
 
1315
1425
  /**
@@ -1325,24 +1435,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1325
1435
  lineage: lineageOf(body),
1326
1436
  })
1327
1437
 
1328
- /**
1329
- * 把这次抽出来的 Knowledge 收进真源(0063 §2)。
1330
- *
1331
- * **只在跑一份已入库的文档时收。** 理由有两条:来源要指得回一份**真实的**
1332
- * 文档(临时正文的 `documentId` 是一个标题字符串,存下来就是悬空引用);
1333
- * 而「这条知识是哪份文档说的」正是它可验证的前提。
1334
- *
1335
- * **只收引文核对得上的那些。** 引文对不上原文 = 模型改写了,那条判断不是从
1336
- * 原文推出来的(§5.7 的引文核验)。把它当知识存进真源,等于让一句编出来的话
1337
- * 看起来像有出处 —— 而报告里已经把它报成例外了,那里才是它该待的地方。
1338
- */
1339
- const ingest = docId
1340
- ? await resolved.runtime.ingestTrialKnowledge({
1341
- runId, knowledge: report.knowledge,
1342
- objects: report.objects, actions: report.actions, scenes: report.scenes,
1343
- })
1344
- : { saved: 0, merged: 0, skipped: 0 }
1345
- return send(res, 200, success({ ...report, summary, run, ingest }))
1438
+ return send(res, 200, success({ ...report, summary, run }))
1346
1439
  } catch (cause) {
1347
1440
  return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
1348
1441
  }
@@ -1570,7 +1663,10 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1570
1663
  return send(res, 400, envelopeError('REQUEST_INVALID', `不认识的 action: ${action || '(空)'}`))
1571
1664
  } },
1572
1665
  /**
1573
- * 抽取轨迹(`extraction_run`):按文档列、按 id 读全文。
1666
+ * 轨迹(`extraction_run`):按文档或阶段列、按 id 读全文。
1667
+ *
1668
+ * 2026-10-18 起三段共用这张表(`stage` 列:`extract` / `normalize` / `materialize`)。
1669
+ * `stage` 省略 = 三段一起 —— 面板里那份「这份文档的轨迹」就是这么读的。
1574
1670
  *
1575
1671
  * 列表**不返回报告本体** —— 一条报告几十到几百 KB,翻一页列表不该按全文付费。
1576
1672
  * 点开某一条走 `/knowledge/extraction-run?id=…`(单独一条路由,不是路径参数:
@@ -1582,8 +1678,11 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1582
1678
  const resolved = resolveBase(url.searchParams.get('kb'))
1583
1679
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1584
1680
  const docId = url.searchParams.get('doc_id')
1681
+ const stageParam = url.searchParams.get('stage')
1682
+ const stage = stageParam === 'extract' || stageParam === 'normalize' || stageParam === 'materialize' ? stageParam : undefined
1585
1683
  const page = await resolved.runtime.listExtractionRuns({
1586
1684
  ...(docId === null ? {} : { docId }),
1685
+ ...(stage === undefined ? {} : { stage }),
1587
1686
  limit: clampInt(url.searchParams.get('limit'), 20, 1, 100),
1588
1687
  offset: clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER),
1589
1688
  })
@@ -1696,10 +1795,8 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1696
1795
  return send(res, 200, success({ cases: (await storeOf(resolved.runtime).snapshot()).cases }))
1697
1796
  } },
1698
1797
  /**
1699
- * 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并写入一张案例卡片。
1700
- *
1701
- * 0044:卡片不再只提议 —— 它直接入库并标成 `unreviewed`。响应给 `sourceId` 与
1702
- * `entityId`,界面据此说清「记录留下了,卡片也建了,等人 review」。
1798
+ * 一次排查结束只落一条 SourceRaw。Case 视图从记录派生;Knowledge / Event 的抽取、
1799
+ * 归一和 WikiCard 物化由知识处理阶段分别执行。
1703
1800
  */
1704
1801
  { kind: 'exact' as const, path: '/diagnosis/run', handler: async (req: IncomingMessage, res: ServerResponse) => {
1705
1802
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
@@ -1712,9 +1809,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1712
1809
  }
1713
1810
  const outcome = await storeOf(resolved.runtime).recordRun(record)
1714
1811
  if (outcome.problems.length > 0 && outcome.sourceId === undefined) {
1715
- return send(res, 200, failure({ sourceId: '', entityId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
1812
+ return send(res, 200, failure({ sourceId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
1716
1813
  }
1717
- return send(res, 200, success({ sourceId: outcome.sourceId ?? '', entityId: outcome.entityId ?? '' }, outcome.problems))
1814
+ return send(res, 200, success({ sourceId: outcome.sourceId ?? '' }, outcome.problems))
1718
1815
  } },
1719
1816
  /**
1720
1817
  * 入库之后的两个人工动作(0044):加备注、review 抬分。
@@ -1862,6 +1959,24 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1862
1959
  if (rest.length === 2) {
1863
1960
  if (method === 'PATCH') {
1864
1961
  const body = await readBody(req)
1962
+ if (body.compileModel && typeof body.compileModel === 'object') {
1963
+ const selection = body.compileModel as CompileModelSelection
1964
+ const catalog = await compileModelCatalog(baseId)
1965
+ const provider = selection.mode === 'fixed' ? selection.provider : catalog.default.provider
1966
+ const model = selection.mode === 'fixed' ? selection.model : catalog.default.model
1967
+ if (selection.mode !== 'fixed' && selection.mode !== 'default') return send(res, 400, envelopeError('MODEL_INVALID', '请选择跟随默认或固定模型'))
1968
+ if (selection.mode === 'fixed' && (!provider || !model)) return send(res, 400, envelopeError('MODEL_INVALID', '固定模型需要 provider 和 model'))
1969
+ const llm = ctx.get('llm')
1970
+ if (provider && model && llm?.resolveCallConfig) {
1971
+ try {
1972
+ const info = selection.reasoningEffort ? await llm.resolveModelInfo(provider, model) : undefined
1973
+ const effort = info?.reasoning?.efforts.find(item => item.id === selection.reasoningEffort)
1974
+ if (selection.reasoningEffort && !effort) throw new Error('所选模型不支持这个思考强度,请重新选择')
1975
+ await llm.resolveCallConfig({ provider, model, ...(effort ? { reasoningEffort: effort.id } : {}) })
1976
+ }
1977
+ catch (cause) { return send(res, 400, envelopeError('MODEL_INVALID', cause instanceof Error ? cause.message : String(cause))) }
1978
+ } else if (selection.reasoningEffort) return send(res, 400, envelopeError('MODEL_INVALID', '模型不可用,无法校验思考强度'))
1979
+ }
1865
1980
  const updated = await bases.update(baseId, {
1866
1981
  name: body.name, description: body.description,
1867
1982
  extractionNote: typeof body.extractionNote === 'string' ? body.extractionNote : undefined,
@@ -2083,6 +2198,26 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
2083
2198
  let documents = await resolved.runtime.listDocuments()
2084
2199
  const search = (url.searchParams.get('query') ?? '').trim().toLocaleLowerCase()
2085
2200
  if (search) documents = documents.filter(item => `${item.docId}\n${item.title}\n${item.externalId}`.toLocaleLowerCase().includes(search))
2201
+ /**
2202
+ * 文件类型的计数对着**搜索之后、类型筛选之前**的集合算。
2203
+ *
2204
+ * 下拉要能切换类型,所以每个类型的数量必须是「当前搜索下有多少份」,
2205
+ * 而不是「当前这一页里有多少份」—— 后者会让选项随翻页变来变去。
2206
+ */
2207
+ const extCounts: Record<string, number> = {}
2208
+ for (const item of documents) {
2209
+ const key = item.ext || '—'
2210
+ extCounts[key] = (extCounts[key] ?? 0) + 1
2211
+ }
2212
+ /**
2213
+ * 文件类型筛选**在服务端**做(0070)。
2214
+ *
2215
+ * 原来是「客户端取前 500 份、再在这一页里筛」:超过 500 份的库,第 501 份起
2216
+ * 在面板上根本不可达,而表头把这种截断显示成「筛出 500 / 600 个」——
2217
+ * 读起来像用户自己开了筛选。既然这里已经支持 `offset`,筛选就该和分页同层。
2218
+ */
2219
+ const ext = (url.searchParams.get('ext') ?? '').trim()
2220
+ if (ext) documents = documents.filter(item => item.ext === ext)
2086
2221
  const total = documents.length
2087
2222
  const offset = clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER)
2088
2223
  const limit = clampInt(url.searchParams.get('limit'), 50, 1, 500)
@@ -2093,7 +2228,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
2093
2228
  * 两件事。放成并列的一张表,前端按 `docId` 查,文档类型不用为审计字段让步。
2094
2229
  */
2095
2230
  const extractionRuns = await resolved.runtime.extractionRunCounts()
2096
- return send(res, 200, success({ documents: documents.slice(offset, offset + limit), total, extractionRuns }))
2231
+ return send(res, 200, success({ documents: documents.slice(offset, offset + limit), total, extCounts, extractionRuns }))
2097
2232
  }
2098
2233
  const docId = rest[1]!
2099
2234
  const resolved = resolveBase(kbParam)
@@ -48,6 +48,7 @@ const DATA_PREDICATE_IDS = new Set([
48
48
  /** 发生实际数据工作的工具。意图工具本身不在这里,避免自调用激活门控。 */
49
49
  const DATA_TOOL_NAMES = new Set([
50
50
  'recall_knowledge',
51
+ 'get_knowledge_events',
51
52
  'query_knowledge_graph',
52
53
  'query_source_truth',
53
54
  'list_knowledge_bases',
@@ -91,6 +91,40 @@ export function sessionKeyOf(exec?: ToolRunContext): string | undefined {
91
91
  return typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : undefined
92
92
  }
93
93
 
94
+ /**
95
+ * 这个会话是不是**子代理**。
96
+ *
97
+ * 子代理会加入父会话的组合,所以它会继承到 `intent_*` 两个工具。而意图记录描述的是
98
+ * 「用户在跟 Agent 要什么」:子代理做的是被拆出来的一个片段,它记一条只会污染任务树。
99
+ *
100
+ * 这条规矩原来写在 preset 的 delegation 行上(`toolFilter: { deny: [...] }`)。DSH 0.1.8
101
+ * 起 `toolFilter`(与 `persona`)**不再被 `dsh-tool-subagent` 接受** —— 带上它的 preset
102
+ * 直接挂载失败,整个组合都不会生效。平台给的替代是「在 Agent Preset 里配好工具,再用
103
+ * `agentPreset` 选中它」,而那**只对 spawn provider 有效**:fork / codex / claude-code
104
+ * 的子会话按定义继承父会话的组合,没有选择余地(`inheritsParentContext` 直接拒绝)。
105
+ *
106
+ * 所以判据搬进工具本身:看会话头,与「谁把子会话拉起来的」无关。四个 provider 一视同仁,
107
+ * 也不会再出现「新增一个 delegation 行忘了 deny」这种漏法。
108
+ *
109
+ * 判据取两个字段:`origin === 'subagent'` 是产品分类,`delegationDepth > 0` 是递归深度
110
+ * (持久化过,重启后仍然成立)。任一条成立就算子会话。
111
+ */
112
+ export function isSubagentSession(exec?: ToolRunContext): boolean {
113
+ const header = exec?.agent?.session?.header
114
+ if (!header) return false
115
+ if (header.origin === 'subagent') return true
116
+ return typeof header.delegationDepth === 'number' && header.delegationDepth > 0
117
+ }
118
+
119
+ /** 子代理调这两个工具时统一回这一条:说清为什么,并给出该走的路。 */
120
+ function subagentRefusal(): unknown {
121
+ return envelopeFailure({ sessionKey: null }, {
122
+ code: 'SUBAGENT_NOT_RECORDED',
123
+ message: '子代理不记录意图:意图描述的是用户与主 Agent 的交互。把要记的内容写进返回结果,由主 Agent 记录。',
124
+ retryable: false,
125
+ })
126
+ }
127
+
94
128
  /**
95
129
  * 注册两个 `intent_*` 工具,返回一个把两者一起撤掉的 disposer。
96
130
  *
@@ -100,11 +134,14 @@ export function sessionKeyOf(exec?: ToolRunContext): string | undefined {
100
134
  * 返回 disposer。
101
135
  */
102
136
  export function registerIntentTools(ctx: { tools: ToolRegistryLike }, host: IntentToolHost): () => void {
103
- const disposeRecord: unknown = ctx.tools.register(jsonTool('intent_record', host.routingPrompt(), INTENT_RECORD_PARAMS, (args, exec) => host.record(args, exec)))
137
+ const disposeRecord: unknown = ctx.tools.register(jsonTool('intent_record', host.routingPrompt(), INTENT_RECORD_PARAMS, (args, exec) => (
138
+ isSubagentSession(exec) ? subagentRefusal() : host.record(args, exec)
139
+ )))
104
140
  const disposeView: unknown = ctx.tools.register(jsonTool(
105
141
  'intent_store_view', 'View recent intent records and the active task ids for the current session.',
106
142
  { include_prompt: { type: 'boolean' } },
107
143
  (args: { include_prompt?: boolean }, exec) => {
144
+ if (isSubagentSession(exec)) return subagentRefusal()
108
145
  const sessionKey = sessionKeyOf(exec)
109
146
  if (!sessionKey) {
110
147
  return envelopeFailure({ sessionKey: null }, {