dsh-data-domain-knowledge 0.1.0-alpha.3 → 0.1.0-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 (148) hide show
  1. package/README.md +126 -22
  2. package/cordis.patch.yml +25 -16
  3. package/lib/client.js +2144 -1571
  4. package/lib/doc-page.js +40 -19
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +27 -2
  7. package/lib/types/convert/host.js.map +1 -1
  8. package/lib/types/convert/ingest.d.ts +7 -4
  9. package/lib/types/convert/ingest.d.ts.map +1 -1
  10. package/lib/types/convert/ingest.js +52 -14
  11. package/lib/types/convert/ingest.js.map +1 -1
  12. package/lib/types/convert/types.d.ts +2 -2
  13. package/lib/types/convert/types.d.ts.map +1 -1
  14. package/lib/types/convert/types.js +2 -2
  15. package/lib/types/convert/types.js.map +1 -1
  16. package/lib/types/diagnosis/cards.js +2 -2
  17. package/lib/types/diagnosis/run.d.ts +7 -1
  18. package/lib/types/diagnosis/run.d.ts.map +1 -1
  19. package/lib/types/diagnosis/run.js +8 -2
  20. package/lib/types/diagnosis/run.js.map +1 -1
  21. package/lib/types/diagnosis/seed-source.js +1 -1
  22. package/lib/types/diagnosis/seed.d.ts +1 -1
  23. package/lib/types/diagnosis/seed.js +1 -1
  24. package/lib/types/diagnosis/store.d.ts +13 -31
  25. package/lib/types/diagnosis/store.d.ts.map +1 -1
  26. package/lib/types/diagnosis/store.js +33 -71
  27. package/lib/types/diagnosis/store.js.map +1 -1
  28. package/lib/types/diagnosis/types.d.ts +1 -1
  29. package/lib/types/diagnosis/types.js +1 -1
  30. package/lib/types/extract-model.d.ts +67 -27
  31. package/lib/types/extract-model.d.ts.map +1 -1
  32. package/lib/types/extract-model.js +225 -97
  33. package/lib/types/extract-model.js.map +1 -1
  34. package/lib/types/extract.d.ts +16 -0
  35. package/lib/types/extract.d.ts.map +1 -1
  36. package/lib/types/extract.js +19 -2
  37. package/lib/types/extract.js.map +1 -1
  38. package/lib/types/fs-atomic.d.ts +3 -4
  39. package/lib/types/fs-atomic.d.ts.map +1 -1
  40. package/lib/types/fs-atomic.js +3 -4
  41. package/lib/types/fs-atomic.js.map +1 -1
  42. package/lib/types/index.d.ts.map +1 -1
  43. package/lib/types/index.js +302 -89
  44. package/lib/types/index.js.map +1 -1
  45. package/lib/types/intent/types.js +1 -1
  46. package/lib/types/intent/types.js.map +1 -1
  47. package/lib/types/knowledge-bases.d.ts +13 -10
  48. package/lib/types/knowledge-bases.d.ts.map +1 -1
  49. package/lib/types/knowledge-bases.js +18 -27
  50. package/lib/types/knowledge-bases.js.map +1 -1
  51. package/lib/types/layout.d.ts +3 -3
  52. package/lib/types/layout.d.ts.map +1 -1
  53. package/lib/types/layout.js +4 -4
  54. package/lib/types/layout.js.map +1 -1
  55. package/lib/types/migrate.d.ts +1 -1
  56. package/lib/types/migrate.js +2 -2
  57. package/lib/types/migrate.js.map +1 -1
  58. package/lib/types/model-text.d.ts +99 -0
  59. package/lib/types/model-text.d.ts.map +1 -0
  60. package/lib/types/model-text.js +103 -0
  61. package/lib/types/model-text.js.map +1 -0
  62. package/lib/types/present.d.ts +239 -0
  63. package/lib/types/present.d.ts.map +1 -0
  64. package/lib/types/present.js +255 -0
  65. package/lib/types/present.js.map +1 -0
  66. package/lib/types/recall-rank.d.ts +37 -0
  67. package/lib/types/recall-rank.d.ts.map +1 -1
  68. package/lib/types/recall-rank.js +41 -1
  69. package/lib/types/recall-rank.js.map +1 -1
  70. package/lib/types/runtime.d.ts +419 -78
  71. package/lib/types/runtime.d.ts.map +1 -1
  72. package/lib/types/runtime.js +2104 -334
  73. package/lib/types/runtime.js.map +1 -1
  74. package/lib/types/settings-schema.d.ts +2 -0
  75. package/lib/types/settings-schema.d.ts.map +1 -1
  76. package/lib/types/settings-schema.js +2 -0
  77. package/lib/types/settings-schema.js.map +1 -1
  78. package/lib/types/source-images.d.ts +118 -0
  79. package/lib/types/source-images.d.ts.map +1 -0
  80. package/lib/types/source-images.js +288 -0
  81. package/lib/types/source-images.js.map +1 -0
  82. package/lib/types/types.d.ts +316 -39
  83. package/lib/types/types.d.ts.map +1 -1
  84. package/lib/types/types.js +57 -11
  85. package/lib/types/types.js.map +1 -1
  86. package/lib/types/wiki-finalize.d.ts +140 -0
  87. package/lib/types/wiki-finalize.d.ts.map +1 -0
  88. package/lib/types/wiki-finalize.js +269 -0
  89. package/lib/types/wiki-finalize.js.map +1 -0
  90. package/lib/types/wiki-types.d.ts +2 -1
  91. package/lib/types/wiki-types.d.ts.map +1 -1
  92. package/lib/types/wiki-types.js +13 -7
  93. package/lib/types/wiki-types.js.map +1 -1
  94. package/package.json +3 -3
  95. package/preset/tita-data-agent/agent.cordis.yml +25 -16
  96. package/src/client/DrawerSection.tsx +18 -0
  97. package/src/client/KnowledgeBaseTab.tsx +294 -104
  98. package/src/client/KnowledgePanel.tsx +4 -3
  99. package/src/client/KnowledgeToolRow.tsx +331 -0
  100. package/src/client/PreviewSections.tsx +115 -34
  101. package/src/client/RecallPanel.tsx +418 -127
  102. package/src/client/TypeDefinitionDrawer.tsx +82 -16
  103. package/src/client/WikiCardReview.tsx +266 -0
  104. package/src/client/WikiFlowCard.tsx +57 -95
  105. package/src/client/WikiTypeEditor.tsx +6 -20
  106. package/src/client/arch/AgentArchitectureGraph.tsx +22 -135
  107. package/src/client/architecture.css +22 -27
  108. package/src/client/architecture.tsx +12 -12
  109. package/src/client/asset-images.ts +125 -0
  110. package/src/client/attribution/AttributionTab.tsx +4 -10
  111. package/src/client/attribution/StageChrome.tsx +8 -24
  112. package/src/client/convert/ConvertPanel.tsx +30 -3
  113. package/src/client/doc-page.tsx +6 -27
  114. package/src/client/index.ts +18 -0
  115. package/src/client/problem-domains.ts +1 -1
  116. package/src/client/styles.css +80 -11
  117. package/src/convert/host.ts +27 -2
  118. package/src/convert/ingest.ts +61 -18
  119. package/src/convert/types.ts +2 -2
  120. package/src/diagnosis/cards.ts +2 -2
  121. package/src/diagnosis/run.ts +8 -2
  122. package/src/diagnosis/seed-source.ts +1 -1
  123. package/src/diagnosis/seed.ts +1 -1
  124. package/src/diagnosis/store.ts +32 -79
  125. package/src/diagnosis/types.ts +2 -2
  126. package/src/extract-model.ts +254 -99
  127. package/src/extract.ts +35 -3
  128. package/src/fs-atomic.ts +3 -4
  129. package/src/index.ts +338 -88
  130. package/src/intent/types.ts +1 -1
  131. package/src/knowledge-bases.ts +31 -32
  132. package/src/layout.ts +4 -4
  133. package/src/migrate.ts +2 -2
  134. package/src/model-text.ts +126 -0
  135. package/src/present.ts +367 -0
  136. package/src/recall-rank.ts +57 -2
  137. package/src/runtime.ts +2282 -374
  138. package/src/settings-schema.ts +2 -0
  139. package/src/source-images.ts +326 -0
  140. package/src/types.ts +354 -47
  141. package/src/wiki-finalize.ts +310 -0
  142. package/src/wiki-types.ts +13 -7
  143. package/lib/types/proposals.d.ts +0 -166
  144. package/lib/types/proposals.d.ts.map +0 -1
  145. package/lib/types/proposals.js +0 -238
  146. package/lib/types/proposals.js.map +0 -1
  147. package/src/client/ProposalReview.tsx +0 -313
  148. package/src/proposals.ts +0 -337
package/src/index.ts CHANGED
@@ -10,9 +10,18 @@ import type { LineageContext } from 'dsh-data-contracts/refs'
10
10
  import {
11
11
  DEFAULT_KNOWLEDGE_CONFIG, type Envelope, type KnowledgeBase, type KnowledgeConfig,
12
12
  type KnowledgeStatus, type RecallMode, type SchemaType, type SourceType, type SourceTypeDef, type SourceTypeDraft,
13
- type WikiSchemaMode, type WikiTypeDef,
13
+ type WikiTypeDef,
14
14
  } from './types.js'
15
- import { defaultKnowledgeRoot, KnowledgeRuntime, success, failure, type KnowledgeRuntimeOptions, type WikiSchemaProvider } from './runtime.js'
15
+ import {
16
+ defaultKnowledgeRoot,
17
+ KnowledgeRuntime,
18
+ MAX_IMPORT_SOURCES,
19
+ success,
20
+ failure,
21
+ type KnowledgeRuntimeOptions,
22
+ type WikiSchemaProvider,
23
+ type ModelSeam,
24
+ } from './runtime.js'
16
25
  import { unwrapGroup } from './settings-volatile.js'
17
26
  import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './document-page.js'
18
27
  import type { ModelCall } from './extract-model.js'
@@ -75,6 +84,14 @@ export { CONVERT_SETTINGS_SCHEMA, INTENT_SETTINGS_SCHEMA }
75
84
  * 客户端按**路径**寻址(`{op:'set', path:['intent','categories']}`)。
76
85
  */
77
86
  import { Config, type KnowledgePluginConfig } from './config-schema.js'
87
+ import { buildModelRequest, collectModelText } from './model-text.js'
88
+ import {
89
+ addSourceCallView, addSourceMetaOf, addSourceResultMarkdown,
90
+ importCallView, importMetaOf, importResultMarkdown,
91
+ recallCallView, recallMetaOf, recallResultMarkdown,
92
+ rebuildCallView, rebuildMetaOf, rebuildResultMarkdown,
93
+ writeCardsCallView, writeCardsMetaOf, writeCardsResultMarkdown,
94
+ } from './present.js'
78
95
  export { Config, type KnowledgePluginConfig }
79
96
 
80
97
  /**
@@ -91,10 +108,64 @@ export function currentKnowledgeBases(): KnowledgeBases | undefined { return act
91
108
  type Args = Record<string, any>
92
109
  type WebServerLike = { register(route: { kind: 'exact' | 'prefix'; path: string; handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> }): () => unknown }
93
110
 
94
- function output() { return { schema: { type: 'json' } as const, render: (_args: unknown, value: unknown) => [{ type: 'text' as const, text: JSON.stringify(value) }] } }
95
- function tool(name: string, description: string, parameters: Record<string, unknown>, execute: (args: Args, exec: ToolRunContext) => Promise<unknown>) {
96
- return defineTool({ name, description, parameters: parameters as any, output: output(), execute: execute as any } as any)
111
+ /**
112
+ * 工具在**对话流**里的呈现(0045)。
113
+ *
114
+ * `meta` 产出的东西会随 `tool/result` 落进会话日志,Web 端由
115
+ * `src/client/KnowledgeToolRow.tsx` 读它渲染专用卡片(值必须是 JSON 可序列化的 ——
116
+ * 会话日志会整份校验,塞个函数或 Date 会在写日志时炸)。
117
+ *
118
+ * `call` / `result` 是与工具名无关的渲染意图(标题 / 分类 / 一段 Markdown),
119
+ * 给终端界面和「没有专用卡片的界面」兜底。三个都是**纯函数**:界面在实时流和会话回放
120
+ * 两条路径上都会调它们。
121
+ *
122
+ * 全部省略时行为与 0045 之前完全一致(一条工具名 + 一坨 JSON),所以只给真正需要
123
+ * 人看懂的工具加。
124
+ */
125
+ interface ToolPresentation {
126
+ meta?: (args: Args, value: unknown) => unknown
127
+ call?: (args: Args) => unknown
128
+ result?: (args: Args, result: { content: unknown[]; isError: boolean; meta?: unknown }) => unknown
129
+ }
130
+
131
+ function output(presentation?: ToolPresentation) {
132
+ return {
133
+ schema: { type: 'json' } as const,
134
+ render: (_args: unknown, value: unknown) => [{ type: 'text' as const, text: JSON.stringify(value) }],
135
+ ...(presentation?.meta ? { presentationMeta: presentation.meta } : {}),
136
+ }
137
+ }
138
+ function tool(name: string, description: string, parameters: Record<string, unknown>, execute: (args: Args, exec: ToolRunContext) => Promise<unknown>, presentation?: ToolPresentation) {
139
+ return defineTool({
140
+ name, description, parameters: parameters as any, output: output(presentation), execute: execute as any,
141
+ ...(presentation?.call ? { presentCall: presentation.call } : {}),
142
+ ...(presentation?.result ? { presentResult: presentation.result } : {}),
143
+ } as any)
144
+ }
145
+
146
+ /**
147
+ * 信封 → `data`。
148
+ *
149
+ * 工具的 `execute` 返回的是一整个 `Envelope`(`{ok, status, data, warnings, error}`),
150
+ * 而呈现层要的是里面的 `data`。失败时 `data` 仍可能是空壳(比如
151
+ * `IMPORT_ALL_FAILED` 带着逐条原因),所以这里不抛,只取。
152
+ */
153
+ function dataOf(value: unknown): Record<string, any> {
154
+ if (value && typeof value === 'object' && 'data' in (value as Record<string, unknown>)) {
155
+ const data = (value as { data?: unknown }).data
156
+ if (data && typeof data === 'object') return data as Record<string, any>
157
+ }
158
+ return {}
159
+ }
160
+
161
+ /** 一条 `presentResult` 的通用形状:用现成的 `meta` 换成一段人话,换不到就交回原内容。 */
162
+ function resultViewOf(markdownOf: (meta: any) => string) {
163
+ return (_args: Args, result: { content: unknown[]; isError: boolean; meta?: unknown }) => {
164
+ if (result.isError || !result.meta) return undefined
165
+ return { card: 'generic' as const, content: [{ type: 'text' as const, text: markdownOf(result.meta) }] }
166
+ }
97
167
  }
168
+
98
169
  function envelopeError(code: string, message: string, suggestion?: string) { return failure({}, code, message, false, suggestion) }
99
170
 
100
171
  /**
@@ -370,8 +441,22 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
370
441
  * 取不到 `llm` 或默认模型时返回 undefined,链路**退回纯规则抽取**:模型是增强,
371
442
  * 不是前提。
372
443
  */
373
- const modelCallFor = (): ModelCall | undefined => {
374
- const llm = ctx.get('llm') as { stream(options: Record<string, unknown>): AsyncIterable<{ type?: string; text?: string }> } | undefined
444
+ const modelSeamFor = (): ModelSeam => {
445
+ /**
446
+ * **只能用 `ctx.get(name)`,不能用属性访问。**
447
+ *
448
+ * cordis 的 `ReflectService.get` 注释写得很清楚:「Read a service from the store
449
+ * without the inject requirement」;而 `ctx.<name>` 走的是 Proxy 的 get trap,
450
+ * 名字不在本插件的 `inject` 里时**直接抛** `cannot get property "..." without inject`。
451
+ * 本插件只 inject 了 tools / webServer / systemPrompt,所以属性访问不是「可能取不到」,
452
+ * 而是「一取就炸」—— 在 `rebuild` 里炸掉一整次编译。
453
+ *
454
+ * 本机探针(cordis 4.0.4)实测:`ctx.get` 能看见后来才注册的服务,属性访问抛异常。
455
+ */
456
+ const llm = ctx.get('llm') as
457
+ { stream(options: Record<string, unknown>): AsyncIterable<{ type?: string; text?: string }> } | undefined
458
+ if (!llm) return { why: '当前上下文里取不到 `llm` 服务(模型调用入口)' }
459
+
375
460
  /**
376
461
  * `agentDefaultModel` 是一个**服务实例**(`dsh-agent-default-model` 的
377
462
  * `AgentDefaultModelConfig extends Service`),默认模型要从它的
@@ -381,32 +466,55 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
381
466
  * 所以那个类自己也是用 `this.config.provider.get()` 读的。第一版就是按普通属性读的,
382
467
  * 结果是永远拿到 undefined —— 模型接缝静默失效。
383
468
  */
384
- const service = ctx.get('agentDefaultModel') as { currentSelection?: () => { provider?: string; model?: string; reasoningEffort?: string } } | undefined
385
- const route = service?.currentSelection?.()
386
- if (!llm || !route?.provider || !route?.model) return undefined
387
- const { provider, model, reasoningEffort } = route
388
- return async ({ system, prompt }) => {
389
- let text = ''
390
- const request = { provider, model, ...(reasoningEffort ? { reasoningEffort } : {}), system, messages: [{ role: 'user', content: prompt }] }
391
- for await (const chunk of llm.stream(request)) {
392
- if (chunk?.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text
393
- }
394
- return text
469
+ const service = ctx.get('agentDefaultModel') as
470
+ { currentSelection?: () => { provider?: string; model?: string; reasoningEffort?: string } } | undefined
471
+ if (!service) return { why: '当前上下文里取不到 `agentDefaultModel` 服务 —— 默认模型那一行可能没挂上' }
472
+ const route = service.currentSelection?.()
473
+ if (!route?.provider || !route?.model) {
474
+ return { why: `默认模型没有选好(provider=${route?.provider ?? '空'} / model=${route?.model ?? '空'})` }
395
475
  }
396
- }
397
- const runtimeOptions = (): KnowledgeRuntimeOptions => {
398
- const modelCall = modelCallFor()
476
+ const { provider, model, reasoningEffort } = route
399
477
  return {
400
- ...(defaultBaseSourceTypes ? { sourceTypes: defaultBaseSourceTypes } : {}),
401
- ...(defaultBaseWikiSchema ? { wikiSchema: defaultBaseWikiSchema } : {}),
402
- // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
403
- resourceRoot: resourceRoot(config.agentRoot),
404
- // 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
405
- memoryRoot: memoryRoot(config.agentRoot, DEFAULT_KB_ID),
406
- // #3b:模型接缝(实体抽取与归并)。没有默认模型时不提供,链路自动退回规则抽取。
407
- ...(modelCall ? { modelCall } : {}),
478
+ call: async ({ system, prompt, sessionId }) => {
479
+ /**
480
+ * 请求形状与收流都交给 `model-text.ts`(0044 实机验证第三~五批)。
481
+ *
482
+ * 三处都踩过坑,前两处还都伪装成同一句告警「模型输出不是 JSON 对象」:
483
+ * 请求里 `content` 写成字符串(平台在适配器里调 `.some()`,于是
484
+ * `content.some is not a function`),只挑 `text-delta` 收流(平台把失败表达成
485
+ * 终结性的 finish chunk,不是抛异常),以及**没带 `sessionId`**
486
+ * (OpenCode 系路由回 400 `MissingSessionID`)。
487
+ */
488
+ const request = buildModelRequest({
489
+ provider, model, reasoningEffort, system, prompt,
490
+ // 运行时按知识库给;接缝被单独调用时(测试)退回一个稳定值,
491
+ // 而不是留空 —— 留空会在 OpenCode 路由上 400。
492
+ sessionId: sessionId ?? 'knowledge:default',
493
+ })
494
+ return collectModelText(llm.stream(request) as AsyncIterable<never>)
495
+ },
408
496
  }
409
497
  }
498
+ const runtimeOptions = (): KnowledgeRuntimeOptions => ({
499
+ ...(defaultBaseSourceTypes ? { sourceTypes: defaultBaseSourceTypes } : {}),
500
+ ...(defaultBaseWikiSchema ? { wikiSchema: defaultBaseWikiSchema } : {}),
501
+ // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
502
+ resourceRoot: resourceRoot(config.agentRoot),
503
+ // 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
504
+ memoryRoot: memoryRoot(config.agentRoot, DEFAULT_KB_ID),
505
+ /**
506
+ * #3b:模型接缝(实体抽取与归并)。
507
+ *
508
+ * **给的是函数,不是值**(0044)。profile 的组合顺序是「package.json 的 bundles →
509
+ * `cordis.patch.yml`」,而 `agent-default-model` 在 patch 层 —— 插件 apply 的时刻问
510
+ * `ctx.get('agentDefaultModel')` 只会得到 undefined,而定死之后就再也不会变:
511
+ * 实测的直接后果是编译一路走规则路径(正文按 `##` 小节切),而界面上没有任何提示。
512
+ *
513
+ * 传函数之后,每次真正要用模型时才问一遍 `ctx`;解析不到时它**必须说出为什么**,
514
+ * 那句话会原样出现在 `rebuild` 的 warnings 里。
515
+ */
516
+ modelSeam: modelSeamFor,
517
+ })
410
518
  /** 默认知识库的根:`<agentRoot>/domain_knowledge/default`。 */
411
519
  const defaultBaseRoot = (): string => knowledgeBaseRoot(config.agentRoot, 'default')
412
520
  let runtime = new KnowledgeRuntime(defaultBaseRoot(), config, runtimeOptions())
@@ -423,10 +531,14 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
423
531
  registryFile: () => resourceRegistry(config.agentRoot),
424
532
  defaultRuntime: () => runtime,
425
533
  config: () => config,
534
+ // 0044:非默认库的 runtime 也要拿到模型接缝。只传 `config` 不够 —— 默认库走上面
535
+ // 那个 `new KnowledgeRuntime(... runtimeOptions())`,别的库走注册表这条支路,
536
+ // 两边必须都带上,否则「新建一个库 → 导入文档」静默变成规则抽取。
537
+ runtimeOptions: () => ({ modelSeam: modelSeamFor }),
426
538
  })
427
539
  runtime.baseId = bases.defaultId
428
540
  defaultBaseSourceTypes = () => bases.sourceTypesOf(bases.defaultId)
429
- defaultBaseWikiSchema = () => ({ types: bases.wikiTypesOf(bases.defaultId), mode: bases.wikiSchemaModeOf(bases.defaultId) })
541
+ defaultBaseWikiSchema = () => ({ types: bases.wikiTypesOf(bases.defaultId) })
430
542
  activeKnowledgeBases = bases
431
543
 
432
544
  const kbIdOf = (value: unknown): string => {
@@ -631,12 +743,24 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
631
743
  // 0.1.7:意图的设置不再走命名空间,随本 entry 的 config 一起下来。
632
744
  intentHost = applyIntent(ctx, { intentRoot: () => intentRoot(config.agentRoot), config: intentGroup })
633
745
 
634
- const writeTools = new Set(['add_source', 'write_knowledge_card', 'set_knowledge_status', 'rebuild_knowledge', 'record_feedback'])
746
+ /**
747
+ * 需要本轮用户审批的写工具。
748
+ *
749
+ * 0044 换过一批名字:`write_knowledge_card`(只接受已确认提议)退役了,因为正文现在
750
+ * 只读、卡片由编译直接写入;`list_card_proposals` 也退役了,因为待办队列不存在了。
751
+ * 新增的两个是**入库之后**的动作:加备注、review 抬分。
752
+ */
753
+ const writeTools = new Set(['add_source', 'import_sources', 'propose_wiki_cards', 'annotate_wiki_card', 'review_wiki_card', 'set_knowledge_status', 'rebuild_knowledge', 'record_feedback'])
635
754
  ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
636
755
  if (writeTools.has(exec.name)) return Promise.resolve({ kind: 'ask', reason: '该操作会写入 Tita Knowledge Store,需要本轮用户审批。' })
637
756
  return next()
638
757
  })
639
- ctx.tools.register(tool('recall_knowledge', 'Recall compiled knowledge: multi-route recall over wikiNotes (top_k, default from settings), resolve each hit to its wiki and 1-hop graph, BERT-rerank, then return the top_n context pack.', { query: { type: 'string', required: true }, scope: { type: 'string' }, limit: { type: 'integer' }, top_k: { type: 'integer' }, top_n: { type: 'integer' }, rerank: { type: 'boolean' }, source_types: { type: 'array' }, include_problems: { type: 'boolean' }, mode: { type: 'string', enum: ['fts', 'vector', 'graph', '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.source_types ?? [], args.include_problems ?? false, args.mode as RecallMode | undefined, lineageOf(args, exec), { contextTopN: args.top_n, rerank: args.rerank }))))
758
+ ctx.tools.register(tool('recall_knowledge', 'Recall compiled knowledge: multi-route recall over wikiNotes (top_k, default from settings), BERT-rerank the candidate pool, then assemble the context as **wiki cards** — notes are deduplicated by their card (a card keeps all its hit sections), 1-hop graph neighbours are added after that dedupe, and the whole pack is bounded by context_max_chars (default 20000). Anything over the budget goes to a temp file whose absolute path is in budget.overflow_path; read that file with your own file tools when you need more.', { query: { type: 'string', required: true }, scope: { type: 'string' }, 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 就是它的绝对路径 —— 需要更多信息时直接读那个文件。' }, source_types: { type: 'array' }, include_problems: { type: 'boolean' }, mode: { type: 'string', enum: ['fts', 'vector', 'graph', '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.source_types ?? [], 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 } : {}) })), {
759
+ call: recallCallView,
760
+ // 查询词从参数来,命中从结果来 —— 两者拼成一张能看懂的账本。
761
+ meta: (args, value) => recallMetaOf({ ...dataOf(value), query: args?.query }),
762
+ result: resultViewOf(recallResultMarkdown),
763
+ }))
640
764
  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())))
641
765
  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)))))
642
766
  ctx.tools.register(tool('query_source_truth', 'Read SourceRaw (原始可追溯文档) with access audit. Returns the top max_chars characters (default and hard max 40000) shared across the matched sources. Each source carries its absolute file `path`, `returnedChars` and `totalChars`, and the envelope carries `truncated`. When truncated is true (or returnedChars < totalChars) the content is INCOMPLETE — read that `path` directly to get the rest rather than answering from a partial document.', { query: { type: 'string', required: true }, source_types: { type: 'array' }, limit: { type: 'integer' }, max_chars: { type: 'integer' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.sourceTruth(args.query, args.source_types ?? [], args.limit ?? 20, args.max_chars ?? 40000, lineageOf(args, exec)))))
@@ -669,42 +793,116 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
669
793
  })),
670
794
  })))
671
795
  ctx.tools.register(tool('list_source_types', 'List the SourceRaw business classifications (the per-knowledge-base source_type registry) together with the built-in read-only classifier prompt. Call this before add_source whenever the classification is not obvious; the registry is editable per knowledge base, so custom ids such as test_unit may exist.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => Promise.resolve(target.sourceTypeCatalog()))))
672
- ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields, template card, and the number of card proposals awaiting human review. Call this before propose_wiki_cards / write_knowledge_card: entity_type must be one of these ids, and the required sections/fields are what makes the card usable.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => target.wikiTypeCatalog())))
673
- ctx.tools.register(tool('add_source', 'Add or update a SourceRaw document (原始可追溯文档). source_type is a per-knowledge-base business classification: call list_source_types first when unsure, and never guess silently. DSH requests one user approval before execution.', { source_type: { type: 'string', required: true, description: '来源业务分类 id(按业务场景,不是文件格式)。先调 list_source_types 拿注册表;拿不准就返回候选问用户,不要默认 document。' }, title: { type: 'string' }, content: { type: 'string', required: true }, external_id: { type: 'string' }, metadata: { type: 'json' }, source_id: { 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.addSource({ sourceType: args.source_type, title: args.title, content: args.content, externalId: args.external_id, metadata: args.metadata, sourceId: args.source_id, lineage: lineageOf(args, exec) }))))
674
- /**
675
- * `write_knowledge_card` —— **它只接受一条已确认、且尚未落卡的提议**(0021 §6.1)。
796
+ ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields, template card, and the current review progress (how many cards are reviewed / unreviewed / flagged as anomalies). Call this before propose_wiki_cards: entity_type must be one of these ids, and the required sections/fields are what makes the card usable.', { kb: { type: 'string', description: '目标知识库 id;省略则用默认库' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, target => target.wikiTypeCatalog())))
797
+ ctx.tools.register(tool('add_source', 'Add or update a SourceRaw document (原始可追溯文档). source_type is a per-knowledge-base business classification: call list_source_types first when unsure, and never guess silently. DSH requests one user approval before execution.', { source_type: { type: 'string', required: true, description: '来源业务分类 id(按业务场景,不是文件格式)。先调 list_source_types 拿注册表;拿不准就返回候选问用户,不要默认 document。' }, 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 才允许并存(落成 <标题>-<内容哈希前6位>.md,并回一条 warning)。' }, 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({ sourceType: args.source_type, 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) })), {
798
+ call: addSourceCallView,
799
+ meta: (args, value) => addSourceMetaOf(args, dataOf(value)),
800
+ result: resultViewOf(addSourceResultMarkdown),
801
+ }))
802
+
803
+ /**
804
+ * `import_sources` —— **批量导入 + 一次编译**(0045)。
805
+ *
806
+ * 在这之前,「让 Agent 批量编译入库」意味着 `add_source` × N 再 `rebuild_knowledge`:
807
+ * 十份文档就是十次审批弹窗、十次往返,而人真正要同意的是「把这十份导进去」这一件事。
808
+ * 这个工具把那件事做成一次调用。
676
809
  *
677
- * 工具名与参数形状不变(冻结契约),但语义收紧了:没有 `proposal_id` 就没有写入。
678
- * 这不是流程洁癖 —— 类型层是**收敛**的那一层,它错一条,所有现象的排序一起错;
679
- * 而 Agent 手里只要还留着一条「未经确认也能写」的路,上面那条规则就只是文档上的一句话。
810
+ * 它**不替代** `add_source`:单份文档仍然用那个(参数更少、审批理由更具体)。
811
+ * 两者的差别只在「一次多少份」,落盘路径、分类闸门、编译链路完全共用 ——
812
+ * 这个工具的 `execute` 就是循环调 `importSources`,而后者逐条调 `addSource`。
680
813
  *
681
- * 正常动线是「Agent 提提议 → 人在面板确认 → 面板直接落卡」。这个工具服务于另一条
682
- * 更慢的路:人在面板上**只确认**(不落卡),落卡交给 Agent 这一轮来做。
814
+ * `compile` 默认开:批量导入的意图几乎总是「进来就能用」。关掉它的场景是「先全部
815
+ * 落盘、稍后统一编译」——那时卡片还没生成,工具返回里会明说 `compile: null`。
683
816
  */
684
- ctx.tools.register(tool('write_knowledge_card', 'Write a Wiki knowledge card for a proposal that a HUMAN has already confirmed (status=confirmed, not yet written). Cards can no longer be written without a confirmed proposal_id: call propose_wiki_cards first, then ask the user to review it in the knowledge panel. DSH requests one user approval before execution.', { proposal_id: { type: 'string', required: true, description: '已确认、尚未落卡的提议 id。它来自 propose_wiki_cards 的返回,确认动作只能由人在面板上做' }, 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.applyProposal(args.proposal_id, lineageOf(args, exec)))))
817
+ 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. source_type is a per-knowledge-base business classification: call list_source_types first when unsure. DSH requests one user approval before execution.', {
818
+ sources: { type: 'array', required: true, description: `来源数组,每项 { source_type, title?, content, external_id?, metadata?, allow_duplicate_title? };一次最多 ${MAX_IMPORT_SOURCES} 份,超了请分批。标题与已有来源重复的那一份会被拒(SOURCE_TITLE_DUPLICATED),逐条报出来而不是整批失败` },
819
+ compile: { type: 'boolean', description: '默认 true:写入后立刻编译成 wiki 卡片。false 表示只落盘' },
820
+ kb: { type: 'string' },
821
+ session_key: { type: 'string' },
822
+ task_id: { type: 'string' },
823
+ subtask_id: { type: 'string' },
824
+ run_id: { type: 'string' },
825
+ turn_id: { type: 'string' },
826
+ trace_id: { type: 'string' },
827
+ domain: { type: 'string' },
828
+ }, async (args, exec) => withRuntime(args.kb, target => target.importSources({
829
+ // 逐项**逐字段**点名:批量的输入是模型生成的,多带一个字段就多一分静默错位的可能。
830
+ sources: (Array.isArray(args.sources) ? args.sources : []).map(item => ({
831
+ sourceType: item?.source_type,
832
+ ...(item?.title ? { title: item.title } : {}),
833
+ content: item?.content,
834
+ ...(item?.external_id ? { externalId: item.external_id } : {}),
835
+ ...(item?.metadata ? { metadata: item.metadata } : {}),
836
+ ...(item?.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}),
837
+ })),
838
+ ...(args.compile === false ? { compile: false } : {}),
839
+ lineage: lineageOf(args, exec),
840
+ })), {
841
+ call: importCallView,
842
+ meta: (_args, value) => importMetaOf(dataOf(value)),
843
+ result: resultViewOf(importResultMarkdown),
844
+ }))
685
845
 
686
846
  /**
687
- * `propose_wiki_cards` —— 草案工具(0024 §4)。
847
+ * `propose_wiki_cards` —— 把一批卡片**直接写进 `02_wiki`**(工具名不变,语义在 0044 改了)。
848
+ *
849
+ * 名字保持不变是因为它是冻结契约;但描述必须说清它现在真的会写。0040 v1.5 的员工是
850
+ * 「草案停在待办队列里等人确认」,0044 撤掉了那道门:**入库在前、review 在后**。
851
+ * 判据见 `runtime.writeCards` 的注释 —— 确认门把知识挡在了门外,而 review 的价值
852
+ * 不需要靠排队来实现。
688
853
  *
689
- * 入参是一条 `source_id`(提议挂在触发它的那条 SourceRaw 上)加一批草案。
690
- * **它不落库**:草案停在待办队列里,等人确认。
854
+ * 正常动线是:`add_source` 落来源 → `rebuild_knowledge` 让编译按 schema 抽实体入库。
855
+ * 这个工具服务于另一条路:Agent 手里已经有结构化的卡片草案(例如从一次排查里读出来的),
856
+ * 直接写进来,不必再走一遍抽取。
691
857
  */
692
- ctx.tools.register(tool('propose_wiki_cards', 'Propose Wiki card drafts attached to a SourceRaw. This does NOT write any card: drafts stay in the review queue until a HUMAN confirms them in the knowledge panel. Call list_wiki_types first to pick entity_type and see the required sections/fields.', { source_id: { type: 'string', required: true, description: '触发这批提议的那条 SourceRaw 的 source_id(先 add_source)' }, proposals: { type: 'array', required: true, description: '草案数组,每项 { action?, entity_type, entity_id, title, summary?, content, source_refs?, metadata? };content 是卡片正文(小节 + ```json diagnosis 结构化块)' }, 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 => withRuntime(args.kb, target => target.proposeCards({ sourceId: args.source_id, proposals: (Array.isArray(args.proposals) ? args.proposals : []).map(item => ({ action: item?.action, entityType: item?.entity_type, entityId: item?.entity_id, title: item?.title, summary: item?.summary, content: item?.content, sourceRefs: item?.source_refs, metadata: item?.metadata })) }))))
858
+ ctx.tools.register(tool('propose_wiki_cards', 'Write Wiki cards compiled from a SourceRaw directly into 02_wiki. The confirmation gate was retired in 0044: cards land immediately and are marked unreviewed, so a human reviews them AFTER entry (review raises the card score; the body itself is read-only). Call list_wiki_types first to pick entity_type and to see the required sections/fields. DSH requests one user approval before execution.', { source_id: { type: 'string', required: true, description: '这批卡片引用的那条 SourceRaw 的 source_id(先 add_source)' }, proposals: { type: 'array', required: true, description: '卡片数组,每项 { action?, entity_type, entity_id, title, summary?, content, source_refs?, metadata? };content 是卡片正文(按该类型的必填小节组织)' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.writeCards({
859
+ sourceId: args.source_id,
860
+ cards: (Array.isArray(args.proposals) ? args.proposals : []).map(item => ({ action: item?.action, entityType: item?.entity_type, entityId: item?.entity_id, title: item?.title, summary: item?.summary, content: item?.content, sourceRefs: item?.source_refs, metadata: item?.metadata })),
861
+ lineage: lineageOf(args, exec),
862
+ })), {
863
+ call: writeCardsCallView,
864
+ meta: (args, value) => writeCardsMetaOf(dataOf(value), String(args?.source_id ?? '')),
865
+ result: resultViewOf(writeCardsResultMarkdown),
866
+ }))
693
867
 
694
868
  /**
695
- * `list_card_proposals` —— Agent 自己看得到待办队列。
869
+ * `annotate_wiki_card` —— 给卡片加**人工备注**(0044)。
696
870
  *
697
- * 少了它,Agent 会**反复提同一条**:它看不到「我已经提过了」「这条被人拒了」。
698
- * 判重其实在存储层(内容指纹),但工具层给出清单,模型才有机会换一种写法。
871
+ * 这是「wiki 不可修改」的对偶:正文改不了,但判断改得了。`kind: 'anomaly'` 是
872
+ * 「这里有异常点」的显式标记(数字与原始文档对不上、两处口径互相矛盾),它会把卡片的
873
+ * 有效评分压到 `ANOMALY_SCORE` —— 异常卡片照样召回得到,只是排在后面,
874
+ * 而不是被藏起来(藏起来就没人会去修它)。
699
875
  */
700
- ctx.tools.register(tool('list_card_proposals', 'List Wiki card proposals awaiting human review (plus the rejected ones, so you do not re-propose something a human already turned down). Proposals never land in 02_wiki until a human confirms them.', { status: { type: 'string', enum: ['pending', 'confirmed', 'rejected', 'all'], description: '默认 pending' }, kb: { type: 'string' }, session_key: { type: 'string' } }, async args => withRuntime(args.kb, async target => {
701
- const wanted = args.status ?? 'pending'
702
- const all = await target.cardProposals().list()
703
- const items = all.filter(item => wanted === 'all' || item.status === wanted)
704
- return success({ total: items.length, proposals: items.map(item => ({ id: item.id, status: item.status, trigger: item.trigger, action: item.action, entity_type: item.entityType, entity_id: item.entityId, title: item.title, source_ref: item.sourceRef, issues: item.issues, ...(item.reason ? { reason: item.reason } : {}) })) })
705
- })))
876
+ ctx.tools.register(tool('annotate_wiki_card', 'Attach a human annotation to a Wiki card, or remove one. Wiki bodies are read-only — this is how a person records a judgement about a card ("this number disagrees with the source", "these two definitions conflict"). kind=anomaly additionally lowers the card score for recall ranking while keeping it retrievable. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, entity_type: { type: 'string' }, kind: { type: 'string', enum: ['note', 'anomaly'], description: '默认 note;anomaly 表示异常点' }, note: { type: 'string', description: '备注正文;撤销一条时省略并给 remove_id' }, author: { type: 'string' }, source_ref: { type: 'string', description: '这条备注针对哪一份来源(可选)' }, remove_id: { type: 'string', description: '要撤销的备注 id' }, kb: { type: 'string' }, session_key: { type: 'string' }, task_id: { type: 'string' }, subtask_id: { type: 'string' }, run_id: { type: 'string' }, turn_id: { type: 'string' }, trace_id: { type: 'string' }, domain: { type: 'string' } }, async (args, exec) => withRuntime(args.kb, target => target.annotateCard({
877
+ entityId: args.entity_id,
878
+ ...(args.entity_type ? { entityType: args.entity_type as SchemaType } : {}),
879
+ ...(args.kind ? { kind: args.kind as 'note' | 'anomaly' } : {}),
880
+ ...(args.note ? { note: args.note } : {}),
881
+ ...(args.author ? { author: args.author } : {}),
882
+ ...(args.source_ref ? { sourceRef: args.source_ref } : {}),
883
+ ...(args.remove_id ? { removeId: args.remove_id } : {}),
884
+ lineage: lineageOf(args, exec),
885
+ }))))
886
+
887
+ /**
888
+ * `review_wiki_card` —— review 一张卡片,抬高它的召回评分(0044)。
889
+ *
890
+ * 它**不是准入闸门**:卡片在 review 之前就已经入库、可召回了。两个动作都可逆 ——
891
+ * review 是一个可以被推翻的判断,不是一次性的凭证。
892
+ */
893
+ ctx.tools.register(tool('review_wiki_card', 'Mark a Wiki card as reviewed (raises its score, which mildly boosts it in recall ranking) or undo that. Review happens AFTER entry and is reversible — it is not a gate. DSH requests one user approval before execution.', { entity_id: { type: 'string', required: true }, entity_type: { type: 'string' }, action: { type: 'string', enum: ['review', 'unreview'], required: true }, reviewer: { 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.reviewCard({
894
+ entityId: args.entity_id,
895
+ ...(args.entity_type ? { entityType: args.entity_type as SchemaType } : {}),
896
+ action: args.action === 'unreview' ? 'unreview' : 'review',
897
+ ...(args.reviewer ? { reviewer: args.reviewer } : {}),
898
+ lineage: lineageOf(args, exec),
899
+ }))))
706
900
  ctx.tools.register(tool('set_knowledge_status', 'Change a Wiki card 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 KnowledgeStatus, args.problem_note, lineageOf(args, exec)))))
707
- 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)))))
901
+ 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))), {
902
+ call: rebuildCallView,
903
+ meta: (args, value) => rebuildMetaOf(args?.only ?? 'all', dataOf(value)),
904
+ result: resultViewOf(rebuildResultMarkdown),
905
+ }))
708
906
  ctx.tools.register(tool('record_feedback', 'Record bounded feedback for a knowledge result. DSH requests one user approval before execution.', { feedback_id: { type: 'string' }, unit_id: { type: 'string' }, rating: { type: 'integer', required: true }, comment: { 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.recordFeedback({ feedbackId: args.feedback_id, unitId: args.unit_id, rating: args.rating, comment: args.comment, sessionKey: args.session_key, lineage: lineageOf(args, exec) }))))
709
907
 
710
908
  ctx.inject(['webServer'], webCtx => {
@@ -727,8 +925,9 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
727
925
  /**
728
926
  * 智能归因图谱的存储句柄,按 rootDir 缓存。
729
927
  *
730
- * 每个知识库有自己的 `05_diagnosis/`,和它的 wiki / 编译产物同源;缓存键用
731
- * rootDir 而不是 baseId,因为独立 runtime(未挂到知识库列表上的)也要能读写。
928
+ * 归因知识住在各库自己的 `01_source_raw/agent_diagnosis_case/` 与
929
+ * `02_wiki/diagnosis/` 里(`05_diagnosis/` 已于 v1.1 撤销),和它的 wiki 同源;
930
+ * 缓存键用 rootDir 而不是 baseId,因为独立 runtime(未挂到知识库列表上的)也要能读写。
732
931
  */
733
932
  const storeOf = (target: KnowledgeRuntime): DiagnosisStore => target.diagnosisStore()
734
933
 
@@ -747,8 +946,6 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
747
946
  graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
748
947
  seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source,
749
948
  canLoadSample: snapshot.canLoadSample, problems: snapshot.problems,
750
- // 待确认的提议条数:归因这一栏只挂角标,review 在「知识库 → Wiki」的待办区。
751
- proposals: snapshot.proposals,
752
949
  })
753
950
 
754
951
  const routes = [
@@ -773,7 +970,14 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
773
970
  url.searchParams.get('include_problems') === 'true',
774
971
  (url.searchParams.get('mode') ?? undefined) as RecallMode | undefined,
775
972
  lineageFromUrl(url),
776
- { ...(topN === null ? {} : { contextTopN: Number(topN) }), ...(rerankParam === null ? {} : { rerank: rerankParam !== 'false' && rerankParam !== '0' }) },
973
+ {
974
+ ...(topN === null ? {} : { contextTopN: Number(topN) }),
975
+ ...(rerankParam === null ? {} : { rerank: rerankParam !== 'false' && rerankParam !== '0' }),
976
+ ...(url.searchParams.get('context_max_chars') === null ? {} : { contextMaxChars: Number(url.searchParams.get('context_max_chars')) }),
977
+ // 面板账本(0050):只在 HTTP 上给,工具调用不带 —— 几十行候选分数进
978
+ // 模型上下文是纯噪声,而面板要的就是「哪条被丢了、各自多少分」。
979
+ ...(url.searchParams.get('detail') === 'true' ? { detail: true } : {}),
980
+ },
777
981
  ))
778
982
  } },
779
983
  { kind: 'exact' as const, path: '/knowledge/embedding/health', handler: async (req: IncomingMessage, res: ServerResponse) => {
@@ -803,13 +1007,41 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
803
1007
  return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
804
1008
  }
805
1009
  } },
1010
+ /**
1011
+ * 批量导入 + 一次编译(0045)。
1012
+ *
1013
+ * 与 `import_sources` 工具**同一条实现**(`runtime.importSources`):
1014
+ * 「面板能做的 Agent 也能做、Agent 能做的面板也能做」是这个插件一贯的规矩,
1015
+ * 差异只允许出现在审批与呈现上,不允许出现在行为上。
1016
+ */
1017
+ { kind: 'exact' as const, path: '/knowledge/import', handler: async (req: IncomingMessage, res: ServerResponse) => {
1018
+ try {
1019
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
1020
+ const body = await readBody(req, 4_000_000)
1021
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
1022
+ if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
1023
+ return send(res, 200, await resolved.runtime.importSources({
1024
+ sources: (Array.isArray(body.sources) ? body.sources : []).map((item: Record<string, unknown>) => ({
1025
+ sourceType: item?.source_type as SourceType,
1026
+ ...(item?.title ? { title: String(item.title) } : {}),
1027
+ content: item?.content as string,
1028
+ ...(item?.external_id ? { externalId: String(item.external_id) } : {}),
1029
+ ...(item?.metadata && typeof item.metadata === 'object' ? { metadata: item.metadata as Record<string, unknown> } : {}),
1030
+ })),
1031
+ ...(body.compile === false ? { compile: false } : {}),
1032
+ lineage: lineageOf(body),
1033
+ }))
1034
+ } catch (cause) {
1035
+ return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
1036
+ }
1037
+ } },
806
1038
  { kind: 'exact' as const, path: '/knowledge/sources', handler: async (req: IncomingMessage, res: ServerResponse) => {
807
1039
  try {
808
1040
  if (req.method === 'POST') {
809
1041
  const body = await readBody(req, 1_200_000)
810
1042
  const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
811
1043
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
812
- return send(res, 200, await resolved.runtime.addSource({ sourceType: body.source_type ?? 'document', title: body.title, content: body.content, externalId: body.external_id, metadata: body.metadata, sourceId: body.source_id, lineage: lineageOf(body) }))
1044
+ return send(res, 200, await resolved.runtime.addSource({ sourceType: body.source_type ?? 'document', 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) }))
813
1045
  }
814
1046
  if (req.method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET 或 POST'))
815
1047
  const url = query(req)
@@ -902,7 +1134,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
902
1134
  *
903
1135
  * 它是一次显式的**写**:用户在这个库上还没录任何排查知识时,界面上那枚
904
1136
  * 「载入内置示例」按钮打的就是这里。单独一条路由而不是让读取顺手写 ——
905
- * 读接口写用户的 `01_source_truth/` 正是上一版的毛病。
1137
+ * 读接口写用户的 `01_source_raw/` 正是上一版的毛病。
906
1138
  */
907
1139
  { kind: 'exact' as const, path: '/diagnosis/sample', handler: async (req: IncomingMessage, res: ServerResponse) => {
908
1140
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'))
@@ -918,11 +1150,10 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
918
1150
  return send(res, 200, success({ cases: (await storeOf(resolved.runtime).snapshot()).cases }))
919
1151
  } },
920
1152
  /**
921
- * 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并顺手**提议**一张案例卡片。
1153
+ * 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并写入一张案例卡片。
922
1154
  *
923
- * 两件事一起做但性质不同:日志是实例层的原始记录(Agent 的产物,可以直接落),
924
- * 案例卡片落在收敛的那一侧(只能提议,等人确认)。所以响应里分别给 `sourceId`
925
- * 与 `proposalId` —— 界面要能说清「记录已经留下了,但那张卡片还在等人 review」。
1155
+ * 0044:卡片不再只提议 —— 它直接入库并标成 `unreviewed`。响应给 `sourceId` 与
1156
+ * `entityId`,界面据此说清「记录留下了,卡片也建了,等人 review」。
926
1157
  */
927
1158
  { kind: 'exact' as const, path: '/diagnosis/run', handler: async (req: IncomingMessage, res: ServerResponse) => {
928
1159
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
@@ -935,37 +1166,44 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
935
1166
  }
936
1167
  const outcome = await storeOf(resolved.runtime).recordRun(record)
937
1168
  if (outcome.problems.length > 0 && outcome.sourceId === undefined) {
938
- return send(res, 200, failure({ sourceId: '', proposalId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
1169
+ return send(res, 200, failure({ sourceId: '', entityId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true))
939
1170
  }
940
- return send(res, 200, success({ sourceId: outcome.sourceId ?? '', proposalId: outcome.proposalId ?? '' }, outcome.problems))
1171
+ return send(res, 200, success({ sourceId: outcome.sourceId ?? '', entityId: outcome.entityId ?? '' }, outcome.problems))
941
1172
  } },
942
1173
  /**
943
- * 卡片提议的待办队列(0021 §6.1)。
1174
+ * 入库之后的两个人工动作(0044):加备注、review 抬分。
944
1175
  *
945
- * 读接口给「知识库 → Wiki」的待办区用;写接口是**人的 review 动作**,
946
- * 刻意不做成 Agent 工具 —— 确认权收在人手里,Agent 只有 `propose_wiki_cards`。
1176
+ * 它们取代了原来的 `/knowledge/card-proposals/review`。差别是**对象变了**:
1177
+ * 以前 review 的是一条还没落库的提议(通过 = 允许入库),现在 review 的是一张
1178
+ * 已经在 `02_wiki` 里、已经能召回的卡片(通过 = 抬高它的召回评分)。
1179
+ * 前者是准入,后者是事后判断 —— 后者可逆,前者不可逆。
947
1180
  */
948
- { kind: 'exact' as const, path: '/knowledge/card-proposals', handler: async (req: IncomingMessage, res: ServerResponse) => {
949
- const url = query(req)
950
- const resolved = resolveBase(url.searchParams.get('kb'))
1181
+ { kind: 'exact' as const, path: '/knowledge/card-annotate', handler: async (req: IncomingMessage, res: ServerResponse) => {
1182
+ if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
1183
+ const body = await readBody(req, 2_000_000)
1184
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
951
1185
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
952
- const wanted = url.searchParams.get('status') ?? 'pending'
953
- const all = await resolved.runtime.cardProposals().list()
954
- const items = all.filter(item => wanted === 'all' || item.status === wanted)
955
- return send(res, 200, success({ total: items.length, proposals: items }))
1186
+ return sendEnvelope(res, await resolved.runtime.annotateCard({
1187
+ entityId: String(body.entity_id ?? ''),
1188
+ ...(body.entity_type ? { entityType: String(body.entity_type) as SchemaType } : {}),
1189
+ ...(body.kind === 'anomaly' ? { kind: 'anomaly' as const } : body.kind === 'note' ? { kind: 'note' as const } : {}),
1190
+ ...(body.note ? { note: String(body.note) } : {}),
1191
+ ...(body.author ? { author: String(body.author) } : {}),
1192
+ ...(body.source_ref ? { sourceRef: String(body.source_ref) } : {}),
1193
+ ...(body.remove_id ? { removeId: String(body.remove_id) } : {}),
1194
+ lineage: lineageOf(body),
1195
+ }))
956
1196
  } },
957
- { kind: 'exact' as const, path: '/knowledge/card-proposals/review', handler: async (req: IncomingMessage, res: ServerResponse) => {
1197
+ { kind: 'exact' as const, path: '/knowledge/card-review', handler: async (req: IncomingMessage, res: ServerResponse) => {
958
1198
  if (req.method !== 'POST') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
959
1199
  const body = await readBody(req, 2_000_000)
960
1200
  const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'))
961
1201
  if ('failure' in resolved) return sendEnvelope(res, resolved.failure)
962
- const action = body.action === 'reject' ? 'reject' : body.action === 'confirm' ? 'confirm' : 'confirm-and-write'
963
- return sendEnvelope(res, await resolved.runtime.reviewProposal({
964
- proposalId: String(body.proposal_id ?? ''),
965
- action,
1202
+ return sendEnvelope(res, await resolved.runtime.reviewCard({
1203
+ entityId: String(body.entity_id ?? ''),
1204
+ ...(body.entity_type ? { entityType: String(body.entity_type) as SchemaType } : {}),
1205
+ action: body.action === 'unreview' ? 'unreview' : 'review',
966
1206
  ...(body.reviewer ? { reviewer: String(body.reviewer) } : {}),
967
- ...(body.reason ? { reason: String(body.reason) } : {}),
968
- ...(body.patch ? { patch: body.patch as { title?: string; summary?: string; content?: string } } : {}),
969
1207
  lineage: lineageOf(body),
970
1208
  }))
971
1209
  } },
@@ -1033,7 +1271,7 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1033
1271
  if (rest.length === 2) {
1034
1272
  if (method === 'PATCH') {
1035
1273
  const body = await readBody(req)
1036
- const updated = await bases.update(baseId, { name: body.name, description: body.description, sourceTypes: body.sourceTypes as SourceTypeDef[] | undefined, wikiTypes: body.wikiTypes as WikiTypeDef[] | undefined, wikiSchemaMode: body.wikiSchemaMode as WikiSchemaMode | undefined, resetWikiTypes: body.resetWikiTypes === true })
1274
+ const updated = await bases.update(baseId, { name: body.name, description: body.description, sourceTypes: body.sourceTypes as SourceTypeDef[] | undefined, wikiTypes: body.wikiTypes as WikiTypeDef[] | undefined, resetWikiTypes: body.resetWikiTypes === true })
1037
1275
  if (!updated.ok) return sendEnvelope(res, updated)
1038
1276
  return send(res, 200, success({ base: updated.data }, updated.warnings))
1039
1277
  }
@@ -1141,7 +1379,19 @@ export function apply(ctx: Context, pluginConfig: KnowledgePluginConfig = {}): v
1141
1379
  if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))
1142
1380
  const card = await resolved.runtime.readCard(entityId, entityType)
1143
1381
  if (!card) return sendEnvelope(res, failure({}, 'CARD_NOT_FOUND', `知识卡片不存在: ${entityId}`, true))
1144
- return send(res, 200, success({ card }))
1382
+ // 正文里的图要能在面板里显示,就得给 URL(`imagePaths` 是磁盘路径,浏览器
1383
+ // 加载不了)。与 SourceRaw 详情同一种形状,两个预览抽屉共用一套解析。
1384
+ const assets = await resolved.runtime.assetsOfCard(card)
1385
+ /**
1386
+ * 相关卡片(0049):出链与入链。
1387
+ *
1388
+ * 由服务端算,而不是让客户端去解析正文里那一块 `## 相关` —— 正文里那块是
1389
+ * 给人 `grep` 的(`[[类型/标题]]` 在面板里点不开),而抽屉里这几行是**可点的
1390
+ * 入口**。两处读同一个计划(`planRelated`),所以不会出现「文件里写着 A、
1391
+ * 界面上列着 B」。
1392
+ */
1393
+ const related = await resolved.runtime.relatedOfCard(card)
1394
+ return send(res, 200, success({ card, assets, related }))
1145
1395
  }
1146
1396
  if (rest.length === 3 && rest[2] === 'versions') {
1147
1397
  if (method !== 'GET') return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'))