dsh-data-domain-knowledge 0.1.1-alpha.2 → 0.1.1-alpha.4

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 (252) hide show
  1. package/README.md +33 -16
  2. package/lib/client.js +1042 -2097
  3. package/lib/contract-refs.d.ts +0 -1
  4. package/lib/doc-page.js +1 -1
  5. package/lib/pages.js +2489 -1060
  6. package/lib/types/agent-behavior-rules.d.ts.map +1 -1
  7. package/lib/types/agent-behavior-rules.js +3 -2
  8. package/lib/types/agent-behavior-rules.js.map +1 -1
  9. package/lib/types/axis-contract.js +1 -1
  10. package/lib/types/axis-contract.js.map +1 -1
  11. package/lib/types/card-organize.d.ts +2 -1
  12. package/lib/types/card-organize.d.ts.map +1 -1
  13. package/lib/types/card-organize.js +5 -3
  14. package/lib/types/card-organize.js.map +1 -1
  15. package/lib/types/config-schema.d.ts +1 -1
  16. package/lib/types/config-schema.js +1 -1
  17. package/lib/types/convert/host.d.ts +2 -3
  18. package/lib/types/convert/host.d.ts.map +1 -1
  19. package/lib/types/convert/host.js +11 -2
  20. package/lib/types/convert/host.js.map +1 -1
  21. package/lib/types/convert/ingest.d.ts +14 -20
  22. package/lib/types/convert/ingest.d.ts.map +1 -1
  23. package/lib/types/convert/ingest.js +8 -5
  24. package/lib/types/convert/ingest.js.map +1 -1
  25. package/lib/types/diagnosis/store.d.ts +2 -2
  26. package/lib/types/diagnosis/store.d.ts.map +1 -1
  27. package/lib/types/diagnosis/store.js +10 -2
  28. package/lib/types/diagnosis/store.js.map +1 -1
  29. package/lib/types/diagnosis/types.d.ts +1 -1
  30. package/lib/types/diagnosis/types.js +1 -1
  31. package/lib/types/extract-model.d.ts +3 -0
  32. package/lib/types/extract-model.d.ts.map +1 -1
  33. package/lib/types/extract-model.js.map +1 -1
  34. package/lib/types/images.js +1 -1
  35. package/lib/types/index.d.ts +0 -4
  36. package/lib/types/index.d.ts.map +1 -1
  37. package/lib/types/index.js +99 -91
  38. package/lib/types/index.js.map +1 -1
  39. package/lib/types/knowledge-bases.d.ts +3 -0
  40. package/lib/types/knowledge-bases.d.ts.map +1 -1
  41. package/lib/types/knowledge-bases.js +22 -2
  42. package/lib/types/knowledge-bases.js.map +1 -1
  43. package/lib/types/knowledge-contract.d.ts +6 -0
  44. package/lib/types/knowledge-contract.d.ts.map +1 -1
  45. package/lib/types/knowledge-contract.js +6 -2
  46. package/lib/types/knowledge-contract.js.map +1 -1
  47. package/lib/types/layout.d.ts +9 -26
  48. package/lib/types/layout.d.ts.map +1 -1
  49. package/lib/types/layout.js +10 -28
  50. package/lib/types/layout.js.map +1 -1
  51. package/lib/types/materialize-snapshot.d.ts +5 -0
  52. package/lib/types/materialize-snapshot.d.ts.map +1 -1
  53. package/lib/types/migrate.d.ts +3 -1
  54. package/lib/types/migrate.d.ts.map +1 -1
  55. package/lib/types/migrate.js +3 -1
  56. package/lib/types/migrate.js.map +1 -1
  57. package/lib/types/model-text.d.ts +7 -0
  58. package/lib/types/model-text.d.ts.map +1 -1
  59. package/lib/types/model-text.js +31 -9
  60. package/lib/types/model-text.js.map +1 -1
  61. package/lib/types/organize-contract.d.ts.map +1 -1
  62. package/lib/types/organize-contract.js +2 -1
  63. package/lib/types/organize-contract.js.map +1 -1
  64. package/lib/types/pipeline/engine.d.ts +28 -4
  65. package/lib/types/pipeline/engine.d.ts.map +1 -1
  66. package/lib/types/pipeline/engine.js +68 -15
  67. package/lib/types/pipeline/engine.js.map +1 -1
  68. package/lib/types/pipeline/progress.d.ts +12 -0
  69. package/lib/types/pipeline/progress.d.ts.map +1 -0
  70. package/lib/types/pipeline/progress.js +37 -0
  71. package/lib/types/pipeline/progress.js.map +1 -0
  72. package/lib/types/pipeline/resumable-extract.d.ts +45 -0
  73. package/lib/types/pipeline/resumable-extract.d.ts.map +1 -0
  74. package/lib/types/pipeline/resumable-extract.js +262 -0
  75. package/lib/types/pipeline/resumable-extract.js.map +1 -0
  76. package/lib/types/pipeline/store.d.ts +8 -2
  77. package/lib/types/pipeline/store.d.ts.map +1 -1
  78. package/lib/types/pipeline/store.js +27 -5
  79. package/lib/types/pipeline/store.js.map +1 -1
  80. package/lib/types/pipeline/types.d.ts +56 -0
  81. package/lib/types/pipeline/types.d.ts.map +1 -1
  82. package/lib/types/pipeline/types.js +18 -1
  83. package/lib/types/pipeline/types.js.map +1 -1
  84. package/lib/types/present.d.ts +1 -1
  85. package/lib/types/present.js +1 -1
  86. package/lib/types/preset-ids.d.ts +0 -1
  87. package/lib/types/preset-ids.d.ts.map +1 -1
  88. package/lib/types/preset-ids.js +0 -3
  89. package/lib/types/preset-ids.js.map +1 -1
  90. package/lib/types/query-analysis.d.ts +0 -5
  91. package/lib/types/query-analysis.d.ts.map +1 -1
  92. package/lib/types/query-analysis.js +0 -16
  93. package/lib/types/query-analysis.js.map +1 -1
  94. package/lib/types/recall-delivery.d.ts +36 -0
  95. package/lib/types/recall-delivery.d.ts.map +1 -0
  96. package/lib/types/recall-delivery.js +16 -0
  97. package/lib/types/recall-delivery.js.map +1 -0
  98. package/lib/types/retrieval/scope.d.ts +17 -0
  99. package/lib/types/retrieval/scope.d.ts.map +1 -0
  100. package/lib/types/retrieval/scope.js +11 -0
  101. package/lib/types/retrieval/scope.js.map +1 -0
  102. package/lib/types/runtime.d.ts +75 -14
  103. package/lib/types/runtime.d.ts.map +1 -1
  104. package/lib/types/runtime.js +454 -198
  105. package/lib/types/runtime.js.map +1 -1
  106. package/lib/types/settings-volatile.d.ts +0 -7
  107. package/lib/types/settings-volatile.d.ts.map +1 -1
  108. package/lib/types/settings-volatile.js +0 -7
  109. package/lib/types/settings-volatile.js.map +1 -1
  110. package/lib/types/source-identity.d.ts +34 -0
  111. package/lib/types/source-identity.d.ts.map +1 -0
  112. package/lib/types/source-identity.js +85 -0
  113. package/lib/types/source-identity.js.map +1 -0
  114. package/lib/types/source-read.d.ts +18 -0
  115. package/lib/types/source-read.d.ts.map +1 -0
  116. package/lib/types/source-read.js +49 -0
  117. package/lib/types/source-read.js.map +1 -0
  118. package/lib/types/store/axis-references.d.ts.map +1 -1
  119. package/lib/types/store/axis-references.js +5 -3
  120. package/lib/types/store/axis-references.js.map +1 -1
  121. package/lib/types/store/knowledge-db.d.ts +5 -29
  122. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  123. package/lib/types/store/knowledge-db.js +57 -2
  124. package/lib/types/store/knowledge-db.js.map +1 -1
  125. package/lib/types/store/source-identity.d.ts +8 -0
  126. package/lib/types/store/source-identity.d.ts.map +1 -0
  127. package/lib/types/store/source-identity.js +56 -0
  128. package/lib/types/store/source-identity.js.map +1 -0
  129. package/lib/types/types.d.ts +46 -5
  130. package/lib/types/types.d.ts.map +1 -1
  131. package/lib/types/types.js +6 -3
  132. package/lib/types/types.js.map +1 -1
  133. package/lib/types/wiki-availability.d.ts +11 -0
  134. package/lib/types/wiki-availability.d.ts.map +1 -0
  135. package/lib/types/wiki-availability.js +39 -0
  136. package/lib/types/wiki-availability.js.map +1 -0
  137. package/lib/types/wiki-finalize.d.ts +4 -2
  138. package/lib/types/wiki-finalize.d.ts.map +1 -1
  139. package/lib/types/wiki-finalize.js +9 -27
  140. package/lib/types/wiki-finalize.js.map +1 -1
  141. package/lib/types/wiki-links.d.ts +35 -0
  142. package/lib/types/wiki-links.d.ts.map +1 -0
  143. package/lib/types/wiki-links.js +129 -0
  144. package/lib/types/wiki-links.js.map +1 -0
  145. package/lib/types/wiki-trial-types.d.ts +3 -0
  146. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  147. package/lib/types/wiki-trial.d.ts +3 -3
  148. package/lib/types/wiki-trial.d.ts.map +1 -1
  149. package/lib/types/wiki-trial.js +14 -3
  150. package/lib/types/wiki-trial.js.map +1 -1
  151. package/package.json +16 -16
  152. package/scripts/doctor.mjs +0 -7
  153. package/scripts/setup-embedding.sh +0 -0
  154. package/src/agent-behavior-rules.ts +3 -2
  155. package/src/axis-contract.ts +1 -1
  156. package/src/card-organize.ts +5 -3
  157. package/src/client/AttributionEventGuide.tsx +3 -3
  158. package/src/client/AxisMatrix.tsx +86 -19
  159. package/src/client/AxisRegistrySection.tsx +1 -1
  160. package/src/client/CompileSettingsView.tsx +473 -0
  161. package/src/client/DirectoryPicker.tsx +0 -2
  162. package/src/client/ExtractionTasksPage.tsx +557 -423
  163. package/src/client/GuideArchitectureCanvas.tsx +5 -12
  164. package/src/client/ImportFilesDialog.tsx +1 -0
  165. package/src/client/InfoHint.tsx +1 -1
  166. package/src/client/KnowledgeBaseTab.tsx +141 -148
  167. package/src/client/KnowledgePanel.tsx +16 -9
  168. package/src/client/OperationLogTab.tsx +1 -1
  169. package/src/client/PipelineRunView.tsx +173 -0
  170. package/src/client/RecallPanel.tsx +5 -16
  171. package/src/client/TabGuide.tsx +3 -7
  172. package/src/client/WikiFlowPanel.tsx +10 -11
  173. package/src/client/arch/NodeLogicPanel.tsx +3 -3
  174. package/src/client/arch/model.ts +5 -113
  175. package/src/client/architecture.css +1 -1
  176. package/src/client/architecture.tsx +13 -27
  177. package/src/client/attribution/AttributionTab.tsx +141 -10
  178. package/src/client/pages.css +305 -180
  179. package/src/client/pages.tsx +7 -12
  180. package/src/client/styles.css +15 -96
  181. package/src/config-schema.ts +1 -1
  182. package/src/convert/host.ts +13 -5
  183. package/src/convert/ingest.ts +20 -7
  184. package/src/diagnosis/store.ts +7 -2
  185. package/src/diagnosis/types.ts +1 -1
  186. package/src/extract-model.ts +3 -0
  187. package/src/images.ts +1 -1
  188. package/src/index.ts +92 -97
  189. package/src/knowledge-bases.ts +23 -3
  190. package/src/knowledge-contract.ts +7 -2
  191. package/src/layout.ts +10 -32
  192. package/src/materialize-snapshot.ts +1 -0
  193. package/src/migrate.ts +3 -1
  194. package/src/model-text.ts +33 -11
  195. package/src/organize-contract.ts +2 -1
  196. package/src/pipeline/engine.ts +85 -22
  197. package/src/pipeline/progress.ts +40 -0
  198. package/src/pipeline/resumable-extract.ts +233 -0
  199. package/src/pipeline/store.ts +28 -6
  200. package/src/pipeline/types.ts +47 -2
  201. package/src/present.ts +1 -1
  202. package/src/preset-ids.ts +0 -4
  203. package/src/query-analysis.ts +0 -20
  204. package/src/recall-delivery.ts +19 -0
  205. package/src/retrieval/scope.ts +10 -0
  206. package/src/runtime.ts +412 -206
  207. package/src/settings-volatile.ts +0 -7
  208. package/src/source-identity.ts +85 -0
  209. package/src/source-read.ts +47 -0
  210. package/src/store/axis-references.ts +5 -3
  211. package/src/store/knowledge-db.ts +54 -2
  212. package/src/store/source-identity.ts +42 -0
  213. package/src/types.ts +18 -5
  214. package/src/wiki-availability.ts +37 -0
  215. package/src/wiki-finalize.ts +9 -25
  216. package/src/wiki-links.ts +120 -0
  217. package/src/wiki-trial-types.ts +3 -2
  218. package/src/wiki-trial.ts +17 -6
  219. package/lib/types/intent/engine.d.ts +0 -122
  220. package/lib/types/intent/engine.d.ts.map +0 -1
  221. package/lib/types/intent/engine.js +0 -488
  222. package/lib/types/intent/engine.js.map +0 -1
  223. package/lib/types/intent/host.d.ts +0 -68
  224. package/lib/types/intent/host.d.ts.map +0 -1
  225. package/lib/types/intent/host.js +0 -249
  226. package/lib/types/intent/host.js.map +0 -1
  227. package/lib/types/intent/scope.d.ts +0 -25
  228. package/lib/types/intent/scope.d.ts.map +0 -1
  229. package/lib/types/intent/scope.js +0 -116
  230. package/lib/types/intent/scope.js.map +0 -1
  231. package/lib/types/intent/store.d.ts +0 -33
  232. package/lib/types/intent/store.d.ts.map +0 -1
  233. package/lib/types/intent/store.js +0 -239
  234. package/lib/types/intent/store.js.map +0 -1
  235. package/lib/types/intent/tools.d.ts +0 -62
  236. package/lib/types/intent/tools.d.ts.map +0 -1
  237. package/lib/types/intent/tools.js +0 -130
  238. package/lib/types/intent/tools.js.map +0 -1
  239. package/lib/types/intent/types.d.ts +0 -170
  240. package/lib/types/intent/types.d.ts.map +0 -1
  241. package/lib/types/intent/types.js +0 -56
  242. package/lib/types/intent/types.js.map +0 -1
  243. package/src/client/IntentLogic.tsx +0 -192
  244. package/src/client/IntentTab.tsx +0 -343
  245. package/src/client/TypeDefinitionDrawer.tsx +0 -195
  246. package/src/intent/README.md +0 -42
  247. package/src/intent/engine.ts +0 -515
  248. package/src/intent/host.ts +0 -315
  249. package/src/intent/scope.ts +0 -125
  250. package/src/intent/store.ts +0 -218
  251. package/src/intent/tools.ts +0 -154
  252. package/src/intent/types.ts +0 -229
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { newSourceId } from './source-identity.js'
1
2
  import { embeddingModel } from './environment/model-catalog.js'
2
3
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
4
  import { createReadStream } from 'node:fs'
@@ -36,9 +37,9 @@ import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './docum
36
37
  import type { ModelCall } from './extract-model.js'
37
38
  import { runWikiTrial, trialSummary } from './wiki-trial.js'
38
39
  import { extractionRunId } from './extraction-runs.js'
39
- import type { PipelineStage } from './pipeline/types.js'
40
+ import { DEFAULT_EXTRACTION_TIMEOUT_MINUTES, PIPELINE_ACTIVE_STATES, isExtractionTimeoutMinutes, type ExtractionTimeoutMinutes, type PipelineStage } from './pipeline/types.js'
40
41
  import {
41
- agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot,
42
+ agentLayout, defaultAgentRoot, ensureAgentLayout, knowledgeBaseRoot,
42
43
  memoryRoot, resourceRegistry, resourceRoot, type AgentLayout,
43
44
  } from './layout.js'
44
45
  import {
@@ -55,21 +56,11 @@ import {
55
56
  type ConvertHost,
56
57
  } from './convert/host.js'
57
58
  import type { ConvertPluginConfig } from './convert/types.js'
58
- import {
59
- applyIntent, INTENT_MOUNT_PATH, INTENT_SERVICE,
60
- type IntentHost,
61
- type IntentLineageService,
62
- type IntentService,
63
- } from './intent/host.js'
64
- import { registerIntentTools } from './intent/tools.js'
65
59
 
66
60
  export * from './types.js'
67
61
  export * from './runtime.js'
68
62
  export * from './knowledge-bases.js'
69
- export * from './intent/types.js'
70
- export * from './intent/engine.js'
71
63
  export { applyConvert as applyConvertHost, CONVERT_SETTINGS_NAMESPACE, CONVERT_MOUNT_PATH, type ConvertHost }
72
- export { applyIntent as applyIntentHost, INTENT_MOUNT_PATH, INTENT_SERVICE, type IntentHost, type IntentLineageService }
73
64
 
74
65
  export const name = 'dsh-data-domain-knowledge'
75
66
  /**
@@ -93,6 +84,7 @@ export { CONVERT_SETTINGS_SCHEMA }
93
84
  */
94
85
  import { Config, type KnowledgePluginConfig } from './config-schema.js'
95
86
  import { buildModelRequest, callModelWithRetry, collectModelText } from './model-text.js'
87
+ import { agentRecallEnvelope } from './recall-delivery.js'
96
88
  import {
97
89
  addSourceCallView, addSourceMetaOf, addSourceResultMarkdown,
98
90
  importCallView, importMetaOf, importResultMarkdown,
@@ -123,7 +115,7 @@ type WebServerLike = { register(route: { kind: 'exact' | 'prefix'; path: string;
123
115
  * `src/client/KnowledgeToolRow.tsx` 读它渲染专用卡片(值必须是 JSON 可序列化的 ——
124
116
  * 会话日志会整份校验,塞个函数或 Date 会在写日志时炸)。
125
117
  *
126
- * `call` / `result` 是与工具名无关的渲染意图(标题 / 分类 / 一段 Markdown),
118
+ * `call` / `result` 是与工具名无关的渲染参数(标题 / 分类 / 一段 Markdown),
127
119
  * 给终端界面和「没有专用卡片的界面」兜底。三个都是**纯函数**:界面在实时流和会话回放
128
120
  * 两条路径上都会调它们。
129
121
  *
@@ -212,7 +204,7 @@ function lineageOf(args: Args, exec?: ToolRunContext): LineageContext | undefine
212
204
  const sessionId = typeof args.session_key === 'string' && args.session_key.trim() ? args.session_key.trim() : exec?.agent?.session.header.id
213
205
  if (sessionId) lineage.sessionId = sessionId
214
206
  const fields: Array<[keyof LineageContext, string]> = [
215
- ['turnId', 'turn_id'], ['intentId', 'intent_id'], ['taskId', 'task_id'],
207
+ ['turnId', 'turn_id'], ['taskId', 'task_id'],
216
208
  ['subtaskId', 'subtask_id'], ['parentSubtaskId', 'parent_subtask_id'], ['runId', 'run_id'], ['traceId', 'trace_id'], ['domain', 'domain'],
217
209
  ]
218
210
  for (const [target, source] of fields) if (typeof args[source] === 'string' && args[source].trim()) lineage[target] = args[source].trim().slice(0, 160)
@@ -223,7 +215,6 @@ function lineageFromUrl(url: URL): LineageContext | undefined {
223
215
  return lineageOf({
224
216
  session_key: url.searchParams.get('session_key') ?? undefined,
225
217
  turn_id: url.searchParams.get('turn_id') ?? undefined,
226
- intent_id: url.searchParams.get('intent_id') ?? undefined,
227
218
  task_id: url.searchParams.get('task_id') ?? undefined,
228
219
  subtask_id: url.searchParams.get('subtask_id') ?? undefined,
229
220
  parent_subtask_id: url.searchParams.get('parent_subtask_id') ?? undefined,
@@ -407,8 +398,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
407
398
  /**
408
399
  * 这两组的把手,`loader/volatile-update` 时调 `reprocess()` 按新值重灌。
409
400
  *
410
- * 先声明后赋值:事件只在运行时触发,而两组的装配在下面(`applyConvert` /
411
- * `applyIntent`)。
401
+ * 先声明后赋值:事件只在运行时触发,转换宿主在下面的 `applyConvert` 中装配。
412
402
  */
413
403
  let convertHost: ConvertHost | undefined
414
404
  /**
@@ -434,6 +424,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
434
424
  */
435
425
  /** 注册表在默认 runtime 之后才构造;用一个可替换的读取器避开构造期 TDZ。 */
436
426
  let compileModelForBase: (id: string) => CompileModelSelection = () => ({ ...DEFAULT_COMPILE_MODEL })
427
+ let extractionTimeoutForBase: (id: string) => ExtractionTimeoutMinutes = () => DEFAULT_EXTRACTION_TIMEOUT_MINUTES
437
428
 
438
429
  const modelSeamFor = (kbId = DEFAULT_KB_ID): ModelSeam => {
439
430
  /**
@@ -489,7 +480,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
489
480
  provider,
490
481
  model,
491
482
  ...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
492
- call: async ({ system, prompt, sessionId, onUsage, onDelta }) => {
483
+ ...(configured.maxTokens !== undefined ? { maxTokens: configured.maxTokens } : {}),
484
+ call: async ({ system, prompt, sessionId, onUsage, onDelta, signal, retryAttempts, maxTokens }) => {
485
+ const outputLimit = maxTokens ?? configured.maxTokens
493
486
  /**
494
487
  * 先走 DSH 的公开解析接口,再开始发 prompt。这样固定模型过期、默认模型失效时,
495
488
  * 编译会在模型调用边界明确失败,而不是落成一次看似成功的规则抽取。
@@ -499,6 +492,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
499
492
  await llm.resolveCallConfig({
500
493
  provider, model,
501
494
  ...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
495
+ ...(outputLimit !== undefined ? { maxTokens: outputLimit } : {}),
502
496
  })
503
497
  } catch (cause) {
504
498
  const detail = cause instanceof Error ? cause.message : String(cause)
@@ -515,7 +509,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
515
509
  * (OpenCode 系路由回 400 `MissingSessionID`)。
516
510
  */
517
511
  const request = buildModelRequest({
518
- provider, model, reasoningEffort: effectiveReasoningEffort, system, prompt,
512
+ provider, model, reasoningEffort: effectiveReasoningEffort, maxTokens: outputLimit, system, prompt,
519
513
  // 运行时按知识库给;接缝被单独调用时(测试)退回一个稳定值,
520
514
  // 而不是留空 —— 留空会在 OpenCode 路由上 400。
521
515
  sessionId: sessionId ?? 'knowledge:default',
@@ -534,14 +528,15 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
534
528
  * 由 `isRetryableModelFailure` 放行,不在错的地方多等两分钟。
535
529
  */
536
530
  return callModelWithRetry(
537
- onDeltaOfAttempt => collectModelText(llm.stream(request) as AsyncIterable<never>, onUsage, onDeltaOfAttempt),
538
- { ...(onDelta ? { onDelta } : {}) },
531
+ onDeltaOfAttempt => collectModelText(llm.stream({ ...request, ...(signal ? { signal } : {}) }) as AsyncIterable<never>, onUsage, onDeltaOfAttempt),
532
+ { ...(onDelta ? { onDelta } : {}), attempts: retryAttempts, signal },
539
533
  )
540
534
  },
541
535
  }
542
536
  }
543
537
  let environment: KnowledgeEnvironment
544
538
  const runtimeOptions = (): KnowledgeRuntimeOptions => ({
539
+ extractionTimeoutMinutes: () => extractionTimeoutForBase(DEFAULT_KB_ID),
545
540
  environmentGuard: kind => environment.requireReady(kind),
546
541
  // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
547
542
  resourceRoot: resourceRoot(config.agentRoot),
@@ -582,6 +577,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
582
577
  runtimeOptions: () => ({ resourceRoot: resourceRoot(config.agentRoot), modelSeam: modelSeamFor, environmentGuard: kind => environment.requireReady(kind) }),
583
578
  })
584
579
  compileModelForBase = id => bases.compileModelOf(id)
580
+ extractionTimeoutForBase = id => bases.extractionTimeoutMinutesOf(id)
585
581
  runtime.baseId = bases.defaultId
586
582
  activeKnowledgeBases = bases
587
583
  environment = new KnowledgeEnvironment({
@@ -617,7 +613,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
617
613
  // Reuse a matching persisted probe after restart; only stale or missing reports need a resource check.
618
614
  if (environment.view().report.state !== 'ready') void environment.check(false).catch(() => undefined)
619
615
 
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 } }
616
+ type CompileModelCatalogItem = { provider: string; id: string; name: string; description?: string; available: boolean; current?: boolean; defaultMaxTokens?: number; reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }
621
617
  const compileModelCatalog = async (kbId: string, queryModelRoute: { provider?: string; model?: string } = {}): Promise<{
622
618
  selection: CompileModelSelection
623
619
  default: { provider?: string; model?: string; reasoningEffort?: string; available: boolean }
@@ -630,7 +626,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
630
626
  const llm = ctx.get('llm') as {
631
627
  listModels?: (provider: string) => Promise<Array<{ id: string; name?: string; description?: string }>>
632
628
  listProviders?: () => Array<{ id: string; name: string }>
633
- resolveModelInfo?: (provider: string, model: string) => Promise<{ reasoning?: CompileModelCatalogItem['reasoning'] }>
629
+ resolveModelInfo?: (provider: string, model: string) => Promise<{ reasoning?: CompileModelCatalogItem['reasoning']; defaultMaxTokens?: number }>
634
630
  } | undefined
635
631
  const providers = [...new Set([...(llm?.listProviders?.() ?? []).map(provider => provider.id), defaultRoute.provider, compileModelForBase(kbId).provider].filter((value): value is string => Boolean(value)))]
636
632
  const models: CompileModelCatalogItem[] = []
@@ -666,7 +662,11 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
666
662
  if (requestedProvider && requestedModel && llm?.resolveModelInfo) {
667
663
  const item = models.find(model => model.provider === requestedProvider && model.id === requestedModel)
668
664
  if (item) {
669
- try { item.reasoning = (await llm.resolveModelInfo(requestedProvider, requestedModel)).reasoning }
665
+ try {
666
+ const info = await llm.resolveModelInfo(requestedProvider, requestedModel)
667
+ item.reasoning = info.reasoning
668
+ if (info.defaultMaxTokens !== undefined) item.defaultMaxTokens = info.defaultMaxTokens
669
+ }
670
670
  catch (cause) { failures.push(`思考强度目录:${cause instanceof Error ? cause.message : String(cause)}`) }
671
671
  }
672
672
  }
@@ -806,7 +806,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
806
806
  /**
807
807
  * 交付物落在**工作项目**下:`<工作区>/output/<日期>/`,不是 Agent 根路径。
808
808
  *
809
- * 根路径下的四段(知识 / 记忆 / 意图 / 家当)是**这个 Agent 自己的东西**,跨项目
809
+ * 根路径下的几段(知识 / 记忆 / 家当)是**这个 Agent 自己的东西**,跨项目
810
810
  * 累积;交付物是**某个项目的产物**,换一个项目打开就不该再看见。两者判据不同,
811
811
  * 所以落点也不同。
812
812
  *
@@ -827,30 +827,6 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
827
827
  },
828
828
  })
829
829
 
830
- // 意图记录模块 (issue 0020). The 意图 view was already one of this plugin's
831
- // five panel tabs, but its Host lived in a second mount unit; 0020 folds it in
832
- // so the declared information architecture and the code layout agree again.
833
- // It keeps the `intent_record` / `intent_store_view` tools and `/intent/**` audit endpoints,
834
- // and — critically — the `titaDataAgentIntent` Cordis service that `data-table`
835
- // resolves by name.
836
- // 意图只保留固定的会话审计契约;可编辑词表不再属于插件配置。
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
830
  /**
855
831
  * 规则正文由当前会话 preset 决定;设置里的 volatile 引用在每次组装提示词时解包,
856
832
  * 因此开关保存后立即生效,不需要重建 Agent 会话。
@@ -895,45 +871,37 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
895
871
  if (writeTools.has(exec.name)) return Promise.resolve({ kind: 'ask', reason: '该操作会写入 Tita Knowledge Store,需要本轮用户审批。' })
896
872
  return next()
897
873
  })
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 } : {}) })), {
899
- call: recallCallView,
900
- // 查询词从参数来,命中从结果来 —— 两者拼成一张能看懂的账本。
901
- meta: (args, value) => recallMetaOf({ ...dataOf(value), query: args?.query }),
902
- result: resultViewOf(recallResultMarkdown),
874
+ const scopeParameters = { object_id: { type: 'string', description: 'Known Object ID. Omit for all; empty matches empty only.' }, action_id: { type: 'string' }, scene_id: { type: 'string' } }
875
+ const scopeOf = (args: Args) => ({ objectId: args.object_id, actionId: args.action_id, sceneId: args.scene_id, wikiKind: args.wiki_kind })
876
+ ctx.tools.register(tool('recall_knowledge', 'Search H2 sections, rank and deduplicate Wiki files, then deliver complete Wiki Fact / Wiki Way within the budget. Query the current subquestion for methods. top_k counts distinct Wiki candidates; top_n counts delivered hit files. Optional known coordinate IDs and wiki_kind filter every retrieval route before ranking. foundCandidates and contentDelivered distinguish matches from delivered bodies. For budget omissions call read_knowledge_wiki with budget.omitted[].read.args. Scores indicate retrieval relevance, not factual truth. Historical Case does not prove the present root cause.', {
877
+ query: { type: 'string', required: true }, ...scopeParameters, wiki_kind: { type: 'string', enum: ['fact', 'way'] },
878
+ limit: { type: 'integer', description: 'Compatibility alias for top_k.' }, top_k: { type: 'integer', description: 'Distinct Wiki candidates, 1–50.' }, top_n: { type: 'integer', description: 'Maximum complete hit Wiki files to deliver, no more than top_k.' },
879
+ rerank: { type: 'boolean' }, context_max_chars: { type: 'integer', description: 'Complete-context character budget; 0 explicitly disables the limit. Omitted files have stable read references in budget.omitted. overflowPath is a local diagnostic file, not the Agent continuation contract.' },
880
+ 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' },
881
+ }, async (args, exec) => withRuntime(args.kb, async target => agentRecallEnvelope(await target.recall(args.query, args.top_k ?? args.limit, args.include_problems ?? false, args.mode as RecallMode | undefined, lineageOf(args, exec), { ...scopeOf(args), contextTopN: args.top_n, rerank: args.rerank, contextMaxChars: args.context_max_chars }))), {
882
+ call: recallCallView, meta: (args, value) => recallMetaOf({ ...dataOf(value), query: args?.query }), result: resultViewOf(recallResultMarkdown),
903
883
  }))
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
- })))
884
+ ctx.tools.register(tool('read_knowledge_wiki', 'Read a complete Wiki through a stable target or browse an Object directory. Omitted axes mean all; empty axes mean empty only. Directory mode omits content by default. include_content requests complete files within max_chars. If a file does not fit, follow nextRead for explicit body pages, concatenate them and read all conditions before applying a Procedure. expected_version prevents mixing Wiki versions. contentPage is partial unless contentComplete is true. Outgoing links and backlinks are independently bounded by link_limit; advance link_offset using links.nextOffset. Follow only relevant links.', {
885
+ target: { type: 'string' }, ...scopeParameters, object: { type: 'string' }, wiki_kind: { type: 'string', enum: ['fact', 'way'] },
886
+ include_content: { type: 'boolean' }, limit: { type: 'integer' }, offset: { type: 'integer' }, max_chars: { type: 'integer' }, content_offset: { type: 'integer' }, content_max_chars: { type: 'integer' }, expected_version: { type: 'integer' }, link_limit: { type: 'integer' }, link_offset: { type: 'integer' }, kb: { type: 'string' },
887
+ }, async args => withRuntime(args.kb, target => target.readWiki({ ...scopeOf(args), target: args.target, object: args.object, includeContent: args.include_content, limit: args.limit, offset: args.offset, maxChars: args.max_chars, contentOffset: args.content_offset, contentMaxChars: args.content_max_chars, expectedVersion: args.expected_version, linkLimit: args.link_limit, linkOffset: args.link_offset }))))
888
+ ctx.tools.register(tool('get_knowledge_cases', 'Find historical Case by current subquestion or exact coordinates, or read referenced case_ids. Search returns a paged directory with version-pinned nextRead references; ID reads include full records within max_chars. Pass one case_id and its version to read historical evidence. Oversized records use explicit contentPage JSON pages; concatenate and parse before using them. Historical actions, observations and outcomes do not prove the current cause.', {
889
+ case_ids: { type: 'array', description: '1–50 known Case IDs; omit for search.' }, query: { type: 'string' }, ...scopeParameters,
890
+ version: { type: 'integer' }, include_content: { type: 'boolean' }, limit: { type: 'integer' }, offset: { type: 'integer' }, max_chars: { type: 'integer' }, content_offset: { type: 'integer' },
891
+ 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' },
892
+ }, async args => withRuntime(args.kb, target => target.queryCases({ ...scopeOf(args), ids: args.case_ids, version: args.version, query: args.query, includeContent: args.include_content, limit: args.limit, offset: args.offset, maxChars: args.max_chars, contentOffset: args.content_offset }))))
934
893
  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())))
935
894
  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)))))
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))))
895
+ ctx.tools.register(tool('query_source_truth', 'Read a precise Source Raw using source_id plus the evidence version, optional content_hash and [start,end) UTF-16 offsets. Prefer readRef returned by knowledge tools. IDs are opaque and scoped to kb; never infer one from a title, path, hash or external_id. Unknown IDs, unavailable versions and mismatched hashes or quotes fail explicitly; there is no fallback to another source. Exact-read offset is relative to the selected range; follow nextRead to continue. Without source_id, query discovers sources and returns version-pinned readRef for precise follow-up. Discovery uses source_offset for source pages and offset for text pages; advance with returnedChars and nextSourceOffset. Do not apply incomplete evidence.', {
896
+ source_id: { type: 'string' }, version: { type: 'integer' }, content_hash: { type: 'string' }, start: { type: 'integer' }, end: { type: 'integer' }, quote: { type: 'string' }, query: { type: 'string' }, limit: { type: 'integer' }, max_chars: { type: 'integer' }, offset: { type: 'integer' }, source_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' },
897
+ }, async (args, exec) => withRuntime(args.kb, async target => {
898
+ if (args.source_id !== undefined) {
899
+ if (args.query !== undefined || args.source_offset !== undefined) return failure({}, 'SOURCE_SELECTION_CONFLICT', '精确读取 source_id 时不要传 query 或 source_offset。', false)
900
+ return target.readSource({ sourceId: args.source_id, version: args.version, contentHash: args.content_hash, start: args.start, end: args.end, quote: args.quote, maxChars: args.max_chars, offset: args.offset }, lineageOf(args, exec))
901
+ }
902
+ if (typeof args.query !== 'string' || !args.query.trim() || [args.version, args.content_hash, args.start, args.end, args.quote].some(value => value !== undefined)) return failure({}, 'SOURCE_QUERY_REQUIRED', '搜索请传非空 query;版本、哈希和引文范围读取必须提供 source_id。', false)
903
+ return target.sourceTruth(args.query, args.limit ?? 20, args.max_chars ?? 40000, lineageOf(args, exec), args.offset ?? 0, args.source_offset ?? 0)
904
+ })))
937
905
  /**
938
906
  * 知识库 id 的**发现入口**。
939
907
  *
@@ -984,7 +952,12 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
984
952
  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
953
  ctx.tools.register(tool('image_publish', imagePublishDescription, imagePublishParameters, publishImage))
986
954
 
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) })), {
955
+ ctx.tools.register(tool('allocate_source_ids', 'Allocate opaque Source Raw IDs before import. Keep one ID per independent input; reuse it across conversion, transport retries, renames and updates. IDs are not reserved documents until imported. Existing sources keep their current IDs. Never allocate a new ID to update an existing source.', { count: { type: 'integer', description: '1–100, default 1.' } }, async args => {
956
+ const count = args.count ?? 1
957
+ if (!Number.isInteger(count) || count < 1 || count > 100) return failure({ source_ids: [] }, 'SOURCE_COUNT_INVALID', 'count 必须为 1–100。', false)
958
+ return success({ source_ids: Array.from({ length: count }, () => newSourceId()) })
959
+ }))
960
+ ctx.tools.register(tool('add_source', 'Add or update Source Raw. For links/platform documents pass source_url or source_identity: identity lookup reuses the existing source_id, otherwise allocates one. For independent files first use allocate_source_ids and retain the ID across retries. Updates use the known source_id and preferably expected_version from an exact read (0 means create only). Titles, paths, body hashes and arbitrary external_id are not update keys. A preallocated unused ID may resolve to an existing document; always use the returned sourceId. Known IDs with contradictory origin fail. Returns a version-pinned readRef. 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' }, source_url: { type: 'string', description: 'Explicit acquisition URL, preserving query and fragment. Do not copy an unverified link from body text.' }, source_identity: { type: 'json', description: '{system, scope, document_id, document_type?, url?}; native identity requires system, scope and document_id. URL-only identity can use {url, scope?}.' }, expected_version: { type: 'integer' }, 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) => (typeof args.source_id !== 'string' || !args.source_id.trim()) && args.source_url === undefined && args.source_identity === undefined ? failure({ sourceId: '' }, 'SOURCE_ID_REQUIRED', '请先分配 source_id;重试或更新复用已有 ID。', false) : withRuntime(args.kb, target => target.addSource({ title: args.title, content: args.content, externalId: args.external_id, metadata: args.metadata, sourceId: args.source_id, sourceUrl: args.source_url, sourceIdentity: args.source_identity, expectedVersion: args.expected_version, ...(args.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}), lineage: lineageOf(args, exec) })), {
988
961
  call: addSourceCallView,
989
962
  meta: (args, value) => addSourceMetaOf(args, dataOf(value)),
990
963
  result: resultViewOf(addSourceResultMarkdown),
@@ -1000,10 +973,10 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1000
973
  * 两者的差别只在「一次多少份」,落盘路径、分类闸门、编译链路完全共用 ——
1001
974
  * 这个工具的 `execute` 就是循环调 `importSources`,而后者逐条调 `addSource`。
1002
975
  *
1003
- * `compile` 默认开:批量导入的意图几乎总是「进来就能用」。关掉它的场景是「先全部
1004
- * 落盘、稍后统一编译」——那时卡片还没生成,工具返回里会明说 `compile: null`。
976
+ * `compile` 默认关闭:先保存来源,再由调用方决定何时处理。显式 true 才跑全链路;
977
+ * 未触发处理时工具返回 `compile: null`。
1005
978
  */
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) }))
979
+ 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: `来源数组,每项 { source_id?, source_url?, source_identity?, expected_version?, title?, content, external_id?, metadata?, allow_duplicate_title? };链接提供 source_url 或 source_identity,先查来源映射;独立文件先分配 source_id,重试沿用;以后续返回的 sourceId 为准;更新旧来源必须传其 source_id;一次最多 ${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) => Array.isArray(args.sources) && args.sources.some(item => (typeof item?.source_id !== 'string' || !item.source_id.trim()) && item?.source_url === undefined && item?.source_identity === undefined) ? failure({}, 'SOURCE_ID_REQUIRED', '请先为每个输入分配 source_id;重试或更新沿用已有 ID。', false) : withRuntime(args.kb, target => target.importSources({ sources: (Array.isArray(args.sources) ? args.sources : []).map(item => ({ ...(item?.source_id ? { sourceId: item.source_id } : {}), ...(item?.expected_version === undefined ? {} : { expectedVersion: item.expected_version }), ...(item?.source_url === undefined ? {} : { sourceUrl: item.source_url }), ...(item?.source_identity === undefined ? {} : { sourceIdentity: item.source_identity }), ...(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) }))
1007
980
  /**
1008
981
  * `propose_wiki_cards` —— 把一批卡片**直接写进 `02_wiki`**(工具名不变,语义在 0044 改了)。
1009
982
  *
@@ -1146,6 +1119,17 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1146
1119
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1147
1120
  return send(res, 200, success(await resolved.runtime.stats()))
1148
1121
  } },
1122
+ { kind: 'exact' as const, path: '/knowledge/wiki', handler: async (req: IncomingMessage, res: ServerResponse) => {
1123
+ if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', 'Wiki 读取只接受 GET'))
1124
+ const url = query(req)
1125
+ const resolved = resolveBase(url.searchParams.get('kb'))
1126
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1127
+ return sendEnvelope(res, await resolved.runtime.readWiki({ target: url.searchParams.get('target') ?? undefined, objectId: url.searchParams.get('object_id') ?? undefined, object: url.searchParams.get('object') ?? undefined,
1128
+ actionId: url.searchParams.get('action_id') ?? undefined, sceneId: url.searchParams.get('scene_id') ?? undefined, wikiKind: (url.searchParams.get('wiki_kind') ?? undefined) as 'fact' | 'way' | undefined,
1129
+ includeContent: url.searchParams.has('include_content') ? url.searchParams.get('include_content') === 'true' : undefined,
1130
+ ...Object.fromEntries([['max_chars', 'maxChars'], ['content_offset', 'contentOffset'], ['content_max_chars', 'contentMaxChars'], ['expected_version', 'expectedVersion'], ['link_limit', 'linkLimit'], ['link_offset', 'linkOffset']].filter(([key]) => url.searchParams.has(key!)).map(([key, field]) => [field, Number(url.searchParams.get(key!))])),
1131
+ limit: url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : undefined, offset: url.searchParams.has('offset') ? Number(url.searchParams.get('offset')) : undefined }))
1132
+ } },
1149
1133
  { kind: 'exact' as const, path: '/knowledge/cases', handler: async (req: IncomingMessage, res: ServerResponse) => {
1150
1134
  if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', 'Case 只接受 GET'))
1151
1135
  const resolved = resolveBase(query(req).searchParams.get('kb'))
@@ -1173,6 +1157,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1173
1157
  (url.searchParams.get('mode') ?? undefined) as RecallMode | undefined,
1174
1158
  lineageFromUrl(url),
1175
1159
  {
1160
+ ...scopeOf(Object.fromEntries(['object_id', 'action_id', 'scene_id', 'wiki_kind'].filter(key => url.searchParams.has(key)).map(key => [key, url.searchParams.get(key)]))),
1176
1161
  ...(topN === null ? {} : { contextTopN: Number(topN) }),
1177
1162
  ...(rerankParam === null ? {} : { rerank: rerankParam !== 'false' && rerankParam !== '0' }),
1178
1163
  ...(url.searchParams.get('context_max_chars') === null ? {} : { contextMaxChars: Number(url.searchParams.get('context_max_chars')) }),
@@ -1255,7 +1240,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1255
1240
  const pipeline = await resolved.runtime.pipeline()
1256
1241
  const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit')) || 20))
1257
1242
  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 }) }))
1243
+ return send(res, 200, success({ ...pipeline.store.tasks(limit, offset), activeTasks: pipeline.store.activeTasks(), extractions: await resolved.runtime.listExtractionRuns({ limit, offset }) }))
1259
1244
  } catch (cause) { return send(res, 400, envelopeError('PIPELINE_HISTORY_FAILED', cause instanceof Error ? cause.message : String(cause))) }
1260
1245
  } },
1261
1246
  { kind: 'exact' as const, path: '/knowledge/pipeline-task', handler: async (req: IncomingMessage, res: ServerResponse) => {
@@ -1270,6 +1255,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1270
1255
  if (!task) return send(res, 404, envelopeError('PIPELINE_NOT_FOUND', '任务不存在'))
1271
1256
  if (req.method === 'GET') return send(res, 200, success({ task, ...pipeline.store.events(id, Math.max(0, Number(url.searchParams.get('after')) || 0)) }))
1272
1257
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '任务只接受 GET 或 POST'))
1258
+ if (body.action === 'retry' && PIPELINE_ACTIVE_STATES.includes(task.state)) throw new Error('任务仍在运行,请先停止或等待完成')
1273
1259
  if (body.action === 'retry') return send(res, 202, success({ task: pipeline.start(task.stage, { ...task.selection, force: false }).task }))
1274
1260
  if (!['pause', 'resume', 'cancel'].includes(body.action)) throw new Error('不支持的任务操作')
1275
1261
  return send(res, 200, success({ task: pipeline.control(id, body.action) }))
@@ -1362,8 +1348,13 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1362
1348
  sources: (Array.isArray(body.sources) ? body.sources : []).map((item: Record<string, unknown>) => ({
1363
1349
  ...(item?.title ? { title: String(item.title) } : {}),
1364
1350
  content: item?.content as string,
1351
+ ...(item?.source_id ? { sourceId: String(item.source_id) } : {}),
1352
+ ...(item?.source_url === undefined ? {} : { sourceUrl: item.source_url }),
1353
+ ...(item?.source_identity === undefined ? {} : { sourceIdentity: item.source_identity }),
1354
+ ...(item?.expected_version === undefined ? {} : { expectedVersion: item.expected_version }),
1365
1355
  ...(item?.external_id ? { externalId: String(item.external_id) } : {}),
1366
1356
  ...(item?.metadata && typeof item.metadata === 'object' ? { metadata: item.metadata as Record<string, unknown> } : {}),
1357
+ ...(item?.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}),
1367
1358
  })),
1368
1359
  compile: body.compile === true,
1369
1360
  lineage: lineageOf(body),
@@ -1378,12 +1369,16 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1378
1369
  const body = await readBody(req, 1_200_000)
1379
1370
  const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1380
1371
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1381
- return send(res, 200, await resolved.runtime.addSource({ title: body.title, content: body.content, externalId: body.external_id, metadata: body.metadata, sourceId: body.source_id, ...(body.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}), lineage: lineageOf(body) }))
1372
+ return send(res, 200, await resolved.runtime.addSource({ title: body.title, content: body.content, externalId: body.external_id, metadata: body.metadata, sourceId: body.source_id, sourceUrl: body.source_url, sourceIdentity: body.source_identity, expectedVersion: body.expected_version, ...(body.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}), lineage: lineageOf(body) }))
1382
1373
  }
1383
1374
  if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET 或 POST'))
1384
1375
  const url = query(req)
1385
1376
  const resolved = resolveBase(url.searchParams.get('kb'))
1386
1377
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1378
+ if (url.searchParams.has('source_id')) return sendEnvelope(res, await resolved.runtime.readSource({ sourceId: url.searchParams.get('source_id')!,
1379
+ contentHash: url.searchParams.get('content_hash') ?? undefined, quote: url.searchParams.get('quote') ?? undefined,
1380
+ ...Object.fromEntries([['version', 'version'], ['start', 'start'], ['end', 'end'], ['offset', 'offset'], ['max_chars', 'maxChars']].filter(([key]) => url.searchParams.has(key!)).map(([key, field]) => [field, Number(url.searchParams.get(key!))])),
1381
+ }, lineageFromUrl(url)))
1387
1382
  return send(res, 200, await resolved.runtime.sourceTruth(
1388
1383
  url.searchParams.get('query') ?? '',
1389
1384
  Number(url.searchParams.get('limit') ?? 20) || 20,
@@ -1964,9 +1959,8 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1964
1959
  * ```
1965
1960
  * /knowledge/pages/trial?kb=<知识库 id> 抽取试跑
1966
1961
  * /knowledge/pages/run?kb=<库 id>&run=<轨迹 id> 一次抽取的完整报告
1967
- * /knowledge/pages/tasks?kb=<知识库 id> 抽取任务
1962
+ * /knowledge/pages/tasks?kb=<知识库 id> 知识处理与编译设置
1968
1963
  * /knowledge/pages/help/compile 编译链路
1969
- * /knowledge/pages/help/intent 意图的逻辑
1970
1964
  * /knowledge/pages/help/overview?node=... 五个 Tab 的方案说明
1971
1965
  * ```
1972
1966
  *
@@ -1992,9 +1986,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1992
1986
  : rest[1] === 'run'
1993
1987
  ? '抽取轨迹'
1994
1988
  : rest[1] === 'tasks'
1995
- ? '抽取任务'
1996
- : rest[1] === 'help' && rest[2] === 'intent'
1997
- ? '意图的逻辑'
1989
+ ? '知识处理'
1998
1990
  : rest[1] === 'help' && rest[2] === 'compile'
1999
1991
  ? '编译链路'
2000
1992
  : rest[1] === 'help' && rest[2] === 'overview' ? 'Agent 方案说明' : '整页'
@@ -2028,12 +2020,14 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
2028
2020
  if (rest.length === 2) {
2029
2021
  if (method === 'PATCH') {
2030
2022
  const body = await readBody(req)
2023
+ if (body.extractionTimeoutMinutes !== undefined && !isExtractionTimeoutMinutes(body.extractionTimeoutMinutes)) return send(res, 400, envelopeError('EXTRACTION_TIMEOUT_INVALID', '文档抽取时限只能选择 30、60 或 100 分钟,最大 100 分钟。'))
2031
2024
  if (body.compileModel && typeof body.compileModel === 'object') {
2032
2025
  const selection = body.compileModel as CompileModelSelection
2033
2026
  const catalog = await compileModelCatalog(baseId)
2034
2027
  const provider = selection.mode === 'fixed' ? selection.provider : catalog.default.provider
2035
2028
  const model = selection.mode === 'fixed' ? selection.model : catalog.default.model
2036
2029
  if (selection.mode !== 'fixed' && selection.mode !== 'default') return send(res, 400, envelopeError('MODEL_INVALID', '请选择跟随默认或固定模型'))
2030
+ if (selection.maxTokens !== undefined && (!Number.isSafeInteger(selection.maxTokens) || selection.maxTokens <= 0)) return send(res, 400, envelopeError('MODEL_INVALID', '单次最大输出必须是正整数 token;留空表示跟随模型默认。'))
2037
2031
  if (selection.mode === 'fixed' && (!provider || !model)) return send(res, 400, envelopeError('MODEL_INVALID', '固定模型需要 provider 和 model'))
2038
2032
  const llm = ctx.get('llm')
2039
2033
  if (provider && model && llm?.resolveCallConfig) {
@@ -2041,7 +2035,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
2041
2035
  const info = selection.reasoningEffort ? await llm.resolveModelInfo(provider, model) : undefined
2042
2036
  const effort = info?.reasoning?.efforts.find(item => item.id === selection.reasoningEffort)
2043
2037
  if (selection.reasoningEffort && !effort) throw new Error('所选模型不支持这个思考强度,请重新选择')
2044
- await llm.resolveCallConfig({ provider, model, ...(effort ? { reasoningEffort: effort.id } : {}) })
2038
+ await llm.resolveCallConfig({ provider, model, ...(effort ? { reasoningEffort: effort.id } : {}), ...(selection.maxTokens !== undefined ? { maxTokens: selection.maxTokens } : {}) })
2045
2039
  }
2046
2040
  catch (cause) { return send(res, 400, envelopeError('MODEL_INVALID', cause instanceof Error ? cause.message : String(cause))) }
2047
2041
  } else if (selection.reasoningEffort) return send(res, 400, envelopeError('MODEL_INVALID', '模型不可用,无法校验思考强度'))
@@ -2049,6 +2043,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
2049
2043
  const updated = await bases.update(baseId, {
2050
2044
  name: body.name, description: body.description,
2051
2045
  extractionNote: typeof body.extractionNote === 'string' ? body.extractionNote : undefined,
2046
+ extractionTimeoutMinutes: body.extractionTimeoutMinutes,
2052
2047
  compileModel: body.compileModel && typeof body.compileModel === 'object'
2053
2048
  ? body.compileModel
2054
2049
  : undefined,
@@ -31,6 +31,7 @@ import {
31
31
  import { defaultKnowledgeRoot, failure, KnowledgeRuntime, success, type KnowledgeRuntimeOptions } from './runtime.js'
32
32
  import { defaultAgentRoot, knowledgeBaseRoot, memoryRoot, resourceRoot } from './layout.js'
33
33
  import { writeTextAtomic } from './fs-atomic.js'
34
+ import { DEFAULT_EXTRACTION_TIMEOUT_MINUTES, isExtractionTimeoutMinutes, normalizeExtractionTimeoutMinutes, type ExtractionTimeoutMinutes } from './pipeline/types.js'
34
35
 
35
36
  /** 持久化形态:配置 + 计算字段缓存分区。 */
36
37
  interface KnowledgeBaseRecord {
@@ -49,6 +50,7 @@ interface KnowledgeBaseRecord {
49
50
  extractionNote: string
50
51
  /** 编译模型路由;它是注册表配置,不属于知识正文。 */
51
52
  compileModel: CompileModelSelection
53
+ extractionTimeoutMinutes: ExtractionTimeoutMinutes
52
54
  createdAt: string
53
55
  updatedAt: string
54
56
  }
@@ -120,12 +122,13 @@ function textOr(input: unknown, fallback: string): string {
120
122
  function compileModelOf(input: unknown): CompileModelSelection {
121
123
  const value = record(input)
122
124
  const effort = typeof value.reasoningEffort === 'string' && value.reasoningEffort.trim() ? { reasoningEffort: value.reasoningEffort.trim() } : {}
123
- if (value.mode !== 'fixed') return { ...DEFAULT_COMPILE_MODEL, ...effort }
125
+ const output = typeof value.maxTokens === 'number' && Number.isSafeInteger(value.maxTokens) && value.maxTokens > 0 ? { maxTokens: value.maxTokens } : {}
126
+ if (value.mode !== 'fixed') return { ...DEFAULT_COMPILE_MODEL, ...effort, ...output }
124
127
  const provider = typeof value.provider === 'string' ? value.provider.trim() : ''
125
128
  const model = typeof value.model === 'string' ? value.model.trim() : ''
126
129
  // 旧版本没有这个字段,或留下了半截配置时,安全地回到 DSH 默认模型。
127
130
  if (!provider || !model) return { ...DEFAULT_COMPILE_MODEL }
128
- return { mode: 'fixed', provider, model, ...effort }
131
+ return { mode: 'fixed', provider, model, ...effort, ...output }
129
132
  }
130
133
 
131
134
  function normalizeExt(value: unknown, fallback: string): string {
@@ -140,6 +143,7 @@ function emptyBase(id: string): KnowledgeBase {
140
143
  id, name: id, description: '', rootDir: '', isDefault: false,
141
144
  extractionNote: '',
142
145
  compileModel: { ...DEFAULT_COMPILE_MODEL },
146
+ extractionTimeoutMinutes: DEFAULT_EXTRACTION_TIMEOUT_MINUTES,
143
147
  createdAt: '', updatedAt: '', sourceCount: 0, cardCount: 0, noteCount: 0, buildVersion: 0,
144
148
  }
145
149
  }
@@ -243,6 +247,7 @@ export class KnowledgeBases {
243
247
  // 卡片模板是系统固定契约,不按知识库维护注册表。
244
248
  // `KnowledgeRuntime` 自己提供默认模板。
245
249
  extractionNote: () => this.extractionNoteOf(base.id),
250
+ extractionTimeoutMinutes: () => this.extractionTimeoutMinutesOf(base.id),
246
251
  // 0032:记忆段跟着**这个库**走,二级用 id。`agentRoot` 没给时(只测注册表
247
252
  // 逻辑的单元测试)留空,runtime 会退回知识树里的 `04_memory`。
248
253
  ...(this.options.agentRoot ? { memoryRoot: memoryRoot(this.options.agentRoot(), base.id) } : {}),
@@ -338,6 +343,7 @@ export class KnowledgeBases {
338
343
  isDefault: false,
339
344
  extractionNote: '',
340
345
  compileModel: { ...DEFAULT_COMPILE_MODEL },
346
+ extractionTimeoutMinutes: DEFAULT_EXTRACTION_TIMEOUT_MINUTES,
341
347
  createdAt: now,
342
348
  updatedAt: now,
343
349
  }
@@ -351,14 +357,20 @@ export class KnowledgeBases {
351
357
  * 更新名称 / 描述 / 坐标提取备注。
352
358
  *
353
359
  */
354
- async update(id: string, patch: { name?: string; description?: string; extractionNote?: string; compileModel?: CompileModelSelection }): Promise<Envelope<KnowledgeBase>> {
360
+ async update(id: string, patch: { name?: string; description?: string; extractionNote?: string; compileModel?: CompileModelSelection; extractionTimeoutMinutes?: ExtractionTimeoutMinutes }): Promise<Envelope<KnowledgeBase>> {
355
361
  this.ensureLoaded()
356
362
  const base = this.records.find(item => item.id === id)
357
363
  if (!base) return failure(emptyBase(id), 'BASE_NOT_FOUND', `知识库不存在: ${id}`, true)
358
364
  // 守卫必须基于精确的实时计数,不能用可能过期的缓存。
359
365
  await this.refreshOne(base)
360
366
  let nextCompileModel = base.compileModel
367
+ if (patch.extractionTimeoutMinutes !== undefined && !isExtractionTimeoutMinutes(patch.extractionTimeoutMinutes)) {
368
+ return failure(this.view(base), 'EXTRACTION_TIMEOUT_INVALID', '文档抽取时限只能选择 30、60 或 100 分钟,最大 100 分钟。', false)
369
+ }
361
370
  if (patch.compileModel !== undefined) {
371
+ if (patch.compileModel.maxTokens !== undefined && (!Number.isSafeInteger(patch.compileModel.maxTokens) || patch.compileModel.maxTokens <= 0)) {
372
+ return failure(this.view(base), 'COMPILE_MODEL_INVALID', '单次最大输出必须是正整数 token;留空表示跟随模型默认。', false)
373
+ }
362
374
  if (patch.compileModel.mode !== 'default' && patch.compileModel.mode !== 'fixed') {
363
375
  return failure(this.view(base), 'COMPILE_MODEL_INVALID', '编译模型 mode 只能是 default 或 fixed', false, '从编译模型选择项中选择“跟随 DSH 默认模型”或一个固定模型。')
364
376
  }
@@ -377,6 +389,7 @@ export class KnowledgeBases {
377
389
  // 长度上限不是配额,是防呆:这条备注逐字进每一次抽取的提示词,几万字会把文档挤出去。
378
390
  if (patch.extractionNote !== undefined) base.extractionNote = String(patch.extractionNote).slice(0, 4000)
379
391
  base.compileModel = nextCompileModel
392
+ if (patch.extractionTimeoutMinutes !== undefined) base.extractionTimeoutMinutes = patch.extractionTimeoutMinutes
380
393
  base.updatedAt = new Date().toISOString()
381
394
  await this.refreshOne(base)
382
395
  await this.persist()
@@ -391,6 +404,10 @@ export class KnowledgeBases {
391
404
  this.ensureLoaded()
392
405
  return this.records.find(item => item.id === id)?.extractionNote ?? ''
393
406
  }
407
+ extractionTimeoutMinutesOf(id: string): ExtractionTimeoutMinutes {
408
+ this.ensureLoaded()
409
+ return normalizeExtractionTimeoutMinutes(this.records.find(item => item.id === id)?.extractionTimeoutMinutes)
410
+ }
394
411
 
395
412
  /** 当前知识库的编译模型路由。不存在的库回到默认路由,避免读路径抛错。 */
396
413
  compileModelOf(id: string): CompileModelSelection {
@@ -443,6 +460,7 @@ export class KnowledgeBases {
443
460
  isDefault: base.isDefault,
444
461
  extractionNote: base.extractionNote,
445
462
  compileModel: { ...base.compileModel },
463
+ extractionTimeoutMinutes: base.extractionTimeoutMinutes,
446
464
  createdAt: base.createdAt,
447
465
  updatedAt: base.updatedAt,
448
466
  sourceCount: stats?.sourceCount ?? 0,
@@ -462,6 +480,7 @@ export class KnowledgeBases {
462
480
  isDefault: true,
463
481
  extractionNote: '',
464
482
  compileModel: { ...DEFAULT_COMPILE_MODEL },
483
+ extractionTimeoutMinutes: DEFAULT_EXTRACTION_TIMEOUT_MINUTES,
465
484
  createdAt: now,
466
485
  updatedAt: now,
467
486
  }
@@ -524,6 +543,7 @@ export class KnowledgeBases {
524
543
  // 0068 之前存下来的注册表里没有这个键:算空串,不报错,也不影响别的字段。
525
544
  extractionNote: typeof value.extractionNote === 'string' ? value.extractionNote : '',
526
545
  compileModel: compileModelOf(value.compileModel),
546
+ extractionTimeoutMinutes: normalizeExtractionTimeoutMinutes(value.extractionTimeoutMinutes),
527
547
  createdAt: typeof value.createdAt === 'string' && value.createdAt ? value.createdAt : now,
528
548
  updatedAt: typeof value.updatedAt === 'string' && value.updatedAt ? value.updatedAt : now,
529
549
  }