dsh-data-domain-knowledge 0.1.0-alpha.2 → 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 (180) hide show
  1. package/README.md +147 -19
  2. package/cordis.patch.yml +25 -10
  3. package/lib/client.js +6400 -3326
  4. package/lib/doc-page.js +7761 -0
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +63 -11
  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.d.ts +8 -7
  17. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  18. package/lib/types/diagnosis/cards.js +119 -49
  19. package/lib/types/diagnosis/cards.js.map +1 -1
  20. package/lib/types/diagnosis/run.d.ts +57 -0
  21. package/lib/types/diagnosis/run.d.ts.map +1 -0
  22. package/lib/types/diagnosis/run.js +245 -0
  23. package/lib/types/diagnosis/run.js.map +1 -0
  24. package/lib/types/diagnosis/schema.d.ts +25 -2
  25. package/lib/types/diagnosis/schema.d.ts.map +1 -1
  26. package/lib/types/diagnosis/schema.js +64 -25
  27. package/lib/types/diagnosis/schema.js.map +1 -1
  28. package/lib/types/diagnosis/seed-source.d.ts +17 -2
  29. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  30. package/lib/types/diagnosis/seed-source.js +19 -2
  31. package/lib/types/diagnosis/seed-source.js.map +1 -1
  32. package/lib/types/diagnosis/seed.d.ts +32 -42
  33. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  34. package/lib/types/diagnosis/seed.js +187 -51
  35. package/lib/types/diagnosis/seed.js.map +1 -1
  36. package/lib/types/diagnosis/store.d.ts +95 -25
  37. package/lib/types/diagnosis/store.d.ts.map +1 -1
  38. package/lib/types/diagnosis/store.js +297 -166
  39. package/lib/types/diagnosis/store.js.map +1 -1
  40. package/lib/types/diagnosis/types.d.ts +168 -16
  41. package/lib/types/diagnosis/types.d.ts.map +1 -1
  42. package/lib/types/diagnosis/types.js +1 -1
  43. package/lib/types/diagnosis/types.js.map +1 -1
  44. package/lib/types/document-page.d.ts +39 -0
  45. package/lib/types/document-page.d.ts.map +1 -0
  46. package/lib/types/document-page.js +43 -0
  47. package/lib/types/document-page.js.map +1 -0
  48. package/lib/types/extract-model.d.ts +67 -27
  49. package/lib/types/extract-model.d.ts.map +1 -1
  50. package/lib/types/extract-model.js +225 -97
  51. package/lib/types/extract-model.js.map +1 -1
  52. package/lib/types/extract.d.ts +16 -0
  53. package/lib/types/extract.d.ts.map +1 -1
  54. package/lib/types/extract.js +19 -2
  55. package/lib/types/extract.js.map +1 -1
  56. package/lib/types/fs-atomic.d.ts +11 -0
  57. package/lib/types/fs-atomic.d.ts.map +1 -0
  58. package/lib/types/fs-atomic.js +41 -0
  59. package/lib/types/fs-atomic.js.map +1 -0
  60. package/lib/types/index.d.ts.map +1 -1
  61. package/lib/types/index.js +466 -63
  62. package/lib/types/index.js.map +1 -1
  63. package/lib/types/intent/types.js +1 -1
  64. package/lib/types/intent/types.js.map +1 -1
  65. package/lib/types/knowledge-bases.d.ts +23 -12
  66. package/lib/types/knowledge-bases.d.ts.map +1 -1
  67. package/lib/types/knowledge-bases.js +37 -44
  68. package/lib/types/knowledge-bases.js.map +1 -1
  69. package/lib/types/layout.d.ts +3 -3
  70. package/lib/types/layout.d.ts.map +1 -1
  71. package/lib/types/layout.js +4 -4
  72. package/lib/types/layout.js.map +1 -1
  73. package/lib/types/migrate.d.ts +1 -1
  74. package/lib/types/migrate.js +2 -2
  75. package/lib/types/migrate.js.map +1 -1
  76. package/lib/types/model-text.d.ts +99 -0
  77. package/lib/types/model-text.d.ts.map +1 -0
  78. package/lib/types/model-text.js +103 -0
  79. package/lib/types/model-text.js.map +1 -0
  80. package/lib/types/present.d.ts +239 -0
  81. package/lib/types/present.d.ts.map +1 -0
  82. package/lib/types/present.js +255 -0
  83. package/lib/types/present.js.map +1 -0
  84. package/lib/types/recall-rank.d.ts +37 -0
  85. package/lib/types/recall-rank.d.ts.map +1 -1
  86. package/lib/types/recall-rank.js +41 -1
  87. package/lib/types/recall-rank.js.map +1 -1
  88. package/lib/types/runtime.d.ts +564 -31
  89. package/lib/types/runtime.d.ts.map +1 -1
  90. package/lib/types/runtime.js +2645 -292
  91. package/lib/types/runtime.js.map +1 -1
  92. package/lib/types/settings-schema.d.ts +4 -2
  93. package/lib/types/settings-schema.d.ts.map +1 -1
  94. package/lib/types/settings-schema.js +2 -0
  95. package/lib/types/settings-schema.js.map +1 -1
  96. package/lib/types/source-images.d.ts +118 -0
  97. package/lib/types/source-images.d.ts.map +1 -0
  98. package/lib/types/source-images.js +288 -0
  99. package/lib/types/source-images.js.map +1 -0
  100. package/lib/types/types.d.ts +356 -28
  101. package/lib/types/types.d.ts.map +1 -1
  102. package/lib/types/types.js +112 -18
  103. package/lib/types/types.js.map +1 -1
  104. package/lib/types/wiki-finalize.d.ts +140 -0
  105. package/lib/types/wiki-finalize.d.ts.map +1 -0
  106. package/lib/types/wiki-finalize.js +269 -0
  107. package/lib/types/wiki-finalize.js.map +1 -0
  108. package/lib/types/wiki-types.d.ts +10 -2
  109. package/lib/types/wiki-types.d.ts.map +1 -1
  110. package/lib/types/wiki-types.js +27 -30
  111. package/lib/types/wiki-types.js.map +1 -1
  112. package/package.json +4 -3
  113. package/preset/tita-data-agent/agent.cordis.yml +25 -10
  114. package/src/client/DirectoryPicker.tsx +12 -12
  115. package/src/client/DrawerSection.tsx +18 -0
  116. package/src/client/InfoHint.tsx +21 -8
  117. package/src/client/IntentTab.tsx +20 -26
  118. package/src/client/KnowledgeBaseTab.tsx +458 -158
  119. package/src/client/KnowledgePanel.tsx +44 -10
  120. package/src/client/KnowledgeStepRail.tsx +3 -0
  121. package/src/client/KnowledgeToolRow.tsx +331 -0
  122. package/src/client/NodeSettings.tsx +20 -22
  123. package/src/client/PreviewSections.tsx +178 -43
  124. package/src/client/RecallPanel.tsx +418 -127
  125. package/src/client/TypeDefinitionDrawer.tsx +154 -54
  126. package/src/client/WikiCardReview.tsx +266 -0
  127. package/src/client/WikiFlowCard.tsx +57 -95
  128. package/src/client/WikiTypeEditor.tsx +197 -95
  129. package/src/client/arch/AgentArchitectureGraph.tsx +30 -14
  130. package/src/client/arch/NodeLogicPanel.tsx +6 -3
  131. package/src/client/arch/model.ts +164 -26
  132. package/src/client/architecture.css +25 -10
  133. package/src/client/architecture.tsx +66 -41
  134. package/src/client/asset-images.ts +125 -0
  135. package/src/client/attribution/AttributionGraph.tsx +481 -34
  136. package/src/client/attribution/AttributionTab.tsx +501 -106
  137. package/src/client/attribution/CaseView.tsx +9 -14
  138. package/src/client/attribution/GraphStage.tsx +25 -18
  139. package/src/client/attribution/RunView.tsx +27 -9
  140. package/src/client/attribution/StageChrome.tsx +280 -140
  141. package/src/client/attribution/graph-options.ts +611 -47
  142. package/src/client/attribution/model.ts +559 -30
  143. package/src/client/convert/ConvertPanel.tsx +36 -4
  144. package/src/client/doc-page.css +134 -0
  145. package/src/client/doc-page.tsx +242 -0
  146. package/src/client/index.ts +18 -0
  147. package/src/client/panel-state.ts +105 -0
  148. package/src/client/problem-domains.ts +1 -1
  149. package/src/client/request.ts +36 -0
  150. package/src/client/styles.css +397 -54
  151. package/src/client/useFitScale.ts +15 -2
  152. package/src/client/wiki-type-groups.ts +86 -0
  153. package/src/convert/host.ts +68 -9
  154. package/src/convert/ingest.ts +61 -18
  155. package/src/convert/types.ts +2 -2
  156. package/src/diagnosis/cards.ts +120 -50
  157. package/src/diagnosis/run.ts +251 -0
  158. package/src/diagnosis/schema.ts +67 -26
  159. package/src/diagnosis/seed-source.ts +29 -4
  160. package/src/diagnosis/seed.ts +187 -85
  161. package/src/diagnosis/store.ts +310 -173
  162. package/src/diagnosis/types.ts +181 -16
  163. package/src/document-page.ts +51 -0
  164. package/src/extract-model.ts +254 -99
  165. package/src/extract.ts +35 -3
  166. package/src/fs-atomic.ts +41 -0
  167. package/src/index.ts +499 -62
  168. package/src/intent/types.ts +1 -1
  169. package/src/knowledge-bases.ts +50 -49
  170. package/src/layout.ts +4 -4
  171. package/src/migrate.ts +2 -2
  172. package/src/model-text.ts +126 -0
  173. package/src/present.ts +367 -0
  174. package/src/recall-rank.ts +57 -2
  175. package/src/runtime.ts +2802 -293
  176. package/src/settings-schema.ts +2 -0
  177. package/src/source-images.ts +326 -0
  178. package/src/types.ts +446 -45
  179. package/src/wiki-finalize.ts +310 -0
  180. package/src/wiki-types.ts +28 -32
@@ -1,15 +1,16 @@
1
1
  import { createReadStream, existsSync } from 'node:fs';
2
- import { stat } from 'node:fs/promises';
3
- import { extname, isAbsolute, join, resolve, sep } from 'node:path';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { dirname, extname, isAbsolute, join, resolve, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { defineTool } from '@deepseek-ai/dsh-tools';
5
6
  import { DEFAULT_KNOWLEDGE_CONFIG, } from './types.js';
6
- import { KnowledgeRuntime, success, failure } from './runtime.js';
7
+ import { KnowledgeRuntime, MAX_IMPORT_SOURCES, success, failure, } from './runtime.js';
7
8
  import { unwrapGroup } from './settings-volatile.js';
9
+ import { DOC_PAGE_CHARS, DOC_PAGE_CHARS_MAX, documentContentPage } from './document-page.js';
8
10
  import { agentLayout, defaultAgentRoot, ensureAgentLayout, intentRoot, knowledgeBaseRoot, legacyDefaultKnowledgeRoot, memoryRoot, presetSourceDir, resourceRegistry, resourceRoot, } from './layout.js';
9
11
  import { migrateLegacyKnowledgeTree, migrateLegacyMemory, migrateLegacyResources } from './migrate.js';
10
12
  import { legacyResourceHome } from './types.js';
11
13
  import { PickerError, createDirectory, listDirectory, pickDirectory, pickerKind, pickerOf, } from './directory-picker.js';
12
- import { DiagnosisStore } from './diagnosis/store.js';
13
14
  import { KNOWLEDGE_SETTINGS_SCHEMA, SETTINGS_NAMESPACE } from './settings-schema.js';
14
15
  import { DEFAULT_KB_ID, KnowledgeBases } from './knowledge-bases.js';
15
16
  import { applyConvert, CONVERT_SETTINGS_NAMESPACE, CONVERT_SETTINGS_SCHEMA, MOUNT_PATH as CONVERT_MOUNT_PATH, } from './convert/host.js';
@@ -46,6 +47,8 @@ export { CONVERT_SETTINGS_SCHEMA, INTENT_SETTINGS_SCHEMA };
46
47
  * 客户端按**路径**寻址(`{op:'set', path:['intent','categories']}`)。
47
48
  */
48
49
  import { Config } from './config-schema.js';
50
+ import { buildModelRequest, collectModelText } from './model-text.js';
51
+ import { addSourceCallView, addSourceMetaOf, addSourceResultMarkdown, importCallView, importMetaOf, importResultMarkdown, recallCallView, recallMetaOf, recallResultMarkdown, rebuildCallView, rebuildMetaOf, rebuildResultMarkdown, writeCardsCallView, writeCardsMetaOf, writeCardsResultMarkdown, } from './present.js';
49
52
  export { Config };
50
53
  /**
51
54
  * The live multi-knowledge-base registry, set by `apply()`.
@@ -57,9 +60,42 @@ export { Config };
57
60
  */
58
61
  let activeKnowledgeBases;
59
62
  export function currentKnowledgeBases() { return activeKnowledgeBases; }
60
- function output() { return { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] }; }
61
- function tool(name, description, parameters, execute) {
62
- return defineTool({ name, description, parameters: parameters, output: output(), execute: execute });
63
+ function output(presentation) {
64
+ return {
65
+ schema: { type: 'json' },
66
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
67
+ ...(presentation?.meta ? { presentationMeta: presentation.meta } : {}),
68
+ };
69
+ }
70
+ function tool(name, description, parameters, execute, presentation) {
71
+ return defineTool({
72
+ name, description, parameters: parameters, output: output(presentation), execute: execute,
73
+ ...(presentation?.call ? { presentCall: presentation.call } : {}),
74
+ ...(presentation?.result ? { presentResult: presentation.result } : {}),
75
+ });
76
+ }
77
+ /**
78
+ * 信封 → `data`。
79
+ *
80
+ * 工具的 `execute` 返回的是一整个 `Envelope`(`{ok, status, data, warnings, error}`),
81
+ * 而呈现层要的是里面的 `data`。失败时 `data` 仍可能是空壳(比如
82
+ * `IMPORT_ALL_FAILED` 带着逐条原因),所以这里不抛,只取。
83
+ */
84
+ function dataOf(value) {
85
+ if (value && typeof value === 'object' && 'data' in value) {
86
+ const data = value.data;
87
+ if (data && typeof data === 'object')
88
+ return data;
89
+ }
90
+ return {};
91
+ }
92
+ /** 一条 `presentResult` 的通用形状:用现成的 `meta` 换成一段人话,换不到就交回原内容。 */
93
+ function resultViewOf(markdownOf) {
94
+ return (_args, result) => {
95
+ if (result.isError || !result.meta)
96
+ return undefined;
97
+ return { card: 'generic', content: [{ type: 'text', text: markdownOf(result.meta) }] };
98
+ };
63
99
  }
64
100
  function envelopeError(code, message, suggestion) { return failure({}, code, message, false, suggestion); }
65
101
  /**
@@ -154,6 +190,36 @@ function clampInt(raw, fallback, min, max) {
154
190
  return fallback;
155
191
  return Math.min(max, Math.max(min, value));
156
192
  }
193
+ function escapeHtml(value) {
194
+ return value.replace(/[&<>'"]/g, character => ({
195
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;',
196
+ })[character] ?? character);
197
+ }
198
+ /**
199
+ * 独立整页的 HTML 外壳。
200
+ *
201
+ * 与 Notebook 的整页同一个形状,而且**只做三件事**:标题、挂载点、把自己那个 bundle
202
+ * 拉起来。正文不在外壳里 —— 页面自己去 `/knowledge/documents/:docId/content` 一页一页地取,
203
+ * 所以这份 HTML 的大小与文档大小无关。
204
+ */
205
+ function standaloneDocumentHtml(title) {
206
+ return `<!doctype html>
207
+ <html lang="zh-CN">
208
+ <head>
209
+ <meta charset="utf-8">
210
+ <meta name="viewport" content="width=device-width, initial-scale=1">
211
+ <title>${escapeHtml(title)} · 原文</title>
212
+ </head>
213
+ <body>
214
+ <div id="tita-document-root"></div>
215
+ <script src="/knowledge/doc-page.js"></script>
216
+ </body>
217
+ </html>`;
218
+ }
219
+ /** 独立整页那个 bundle 的位置:`lib/types/index.js` → `lib/doc-page.js`。 */
220
+ function documentPageBundle() {
221
+ return resolve(dirname(fileURLToPath(import.meta.url)), '../doc-page.js');
222
+ }
157
223
  /** 删除守卫的 HTTP 状态:这三类冲突必须是 409,前端据此提示用户。 */
158
224
  const CONFLICT_CODES = new Set([
159
225
  'FILE_TYPE_IN_USE', 'BASE_IS_DEFAULT', 'BASE_IN_USE',
@@ -308,8 +374,21 @@ export function apply(ctx, pluginConfig = {}) {
308
374
  * 取不到 `llm` 或默认模型时返回 undefined,链路**退回纯规则抽取**:模型是增强,
309
375
  * 不是前提。
310
376
  */
311
- const modelCallFor = () => {
377
+ const modelSeamFor = () => {
378
+ /**
379
+ * **只能用 `ctx.get(name)`,不能用属性访问。**
380
+ *
381
+ * cordis 的 `ReflectService.get` 注释写得很清楚:「Read a service from the store
382
+ * without the inject requirement」;而 `ctx.<name>` 走的是 Proxy 的 get trap,
383
+ * 名字不在本插件的 `inject` 里时**直接抛** `cannot get property "..." without inject`。
384
+ * 本插件只 inject 了 tools / webServer / systemPrompt,所以属性访问不是「可能取不到」,
385
+ * 而是「一取就炸」—— 在 `rebuild` 里炸掉一整次编译。
386
+ *
387
+ * 本机探针(cordis 4.0.4)实测:`ctx.get` 能看见后来才注册的服务,属性访问抛异常。
388
+ */
312
389
  const llm = ctx.get('llm');
390
+ if (!llm)
391
+ return { why: '当前上下文里取不到 `llm` 服务(模型调用入口)' };
313
392
  /**
314
393
  * `agentDefaultModel` 是一个**服务实例**(`dsh-agent-default-model` 的
315
394
  * `AgentDefaultModelConfig extends Service`),默认模型要从它的
@@ -320,33 +399,54 @@ export function apply(ctx, pluginConfig = {}) {
320
399
  * 结果是永远拿到 undefined —— 模型接缝静默失效。
321
400
  */
322
401
  const service = ctx.get('agentDefaultModel');
323
- const route = service?.currentSelection?.();
324
- if (!llm || !route?.provider || !route?.model)
325
- return undefined;
402
+ if (!service)
403
+ return { why: '当前上下文里取不到 `agentDefaultModel` 服务 —— 默认模型那一行可能没挂上' };
404
+ const route = service.currentSelection?.();
405
+ if (!route?.provider || !route?.model) {
406
+ return { why: `默认模型没有选好(provider=${route?.provider ?? '空'} / model=${route?.model ?? '空'})` };
407
+ }
326
408
  const { provider, model, reasoningEffort } = route;
327
- return async ({ system, prompt }) => {
328
- let text = '';
329
- const request = { provider, model, ...(reasoningEffort ? { reasoningEffort } : {}), system, messages: [{ role: 'user', content: prompt }] };
330
- for await (const chunk of llm.stream(request)) {
331
- if (chunk?.type === 'text-delta' && typeof chunk.text === 'string')
332
- text += chunk.text;
333
- }
334
- return text;
335
- };
336
- };
337
- const runtimeOptions = () => {
338
- const modelCall = modelCallFor();
339
409
  return {
340
- ...(defaultBaseSourceTypes ? { sourceTypes: defaultBaseSourceTypes } : {}),
341
- ...(defaultBaseWikiSchema ? { wikiSchema: defaultBaseWikiSchema } : {}),
342
- // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
343
- resourceRoot: resourceRoot(config.agentRoot),
344
- // 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
345
- memoryRoot: memoryRoot(config.agentRoot, DEFAULT_KB_ID),
346
- // #3b:模型接缝(实体抽取与归并)。没有默认模型时不提供,链路自动退回规则抽取。
347
- ...(modelCall ? { modelCall } : {}),
410
+ call: async ({ system, prompt, sessionId }) => {
411
+ /**
412
+ * 请求形状与收流都交给 `model-text.ts`(0044 实机验证第三~五批)。
413
+ *
414
+ * 三处都踩过坑,前两处还都伪装成同一句告警「模型输出不是 JSON 对象」:
415
+ * 请求里 `content` 写成字符串(平台在适配器里调 `.some()`,于是
416
+ * `content.some is not a function`),只挑 `text-delta` 收流(平台把失败表达成
417
+ * 终结性的 finish chunk,不是抛异常),以及**没带 `sessionId`**
418
+ * (OpenCode 系路由回 400 `MissingSessionID`)。
419
+ */
420
+ const request = buildModelRequest({
421
+ provider, model, reasoningEffort, system, prompt,
422
+ // 运行时按知识库给;接缝被单独调用时(测试)退回一个稳定值,
423
+ // 而不是留空 —— 留空会在 OpenCode 路由上 400。
424
+ sessionId: sessionId ?? 'knowledge:default',
425
+ });
426
+ return collectModelText(llm.stream(request));
427
+ },
348
428
  };
349
429
  };
430
+ const runtimeOptions = () => ({
431
+ ...(defaultBaseSourceTypes ? { sourceTypes: defaultBaseSourceTypes } : {}),
432
+ ...(defaultBaseWikiSchema ? { wikiSchema: defaultBaseWikiSchema } : {}),
433
+ // 0029:家当目录。配置里三个路径为空时,运行时从这里解析模型与解释器。
434
+ resourceRoot: resourceRoot(config.agentRoot),
435
+ // 0032:记忆段。默认库的记忆落在 `<agentRoot>/memory/default`,与知识树平级。
436
+ memoryRoot: memoryRoot(config.agentRoot, DEFAULT_KB_ID),
437
+ /**
438
+ * #3b:模型接缝(实体抽取与归并)。
439
+ *
440
+ * **给的是函数,不是值**(0044)。profile 的组合顺序是「package.json 的 bundles →
441
+ * `cordis.patch.yml`」,而 `agent-default-model` 在 patch 层 —— 插件 apply 的时刻问
442
+ * `ctx.get('agentDefaultModel')` 只会得到 undefined,而定死之后就再也不会变:
443
+ * 实测的直接后果是编译一路走规则路径(正文按 `##` 小节切),而界面上没有任何提示。
444
+ *
445
+ * 传函数之后,每次真正要用模型时才问一遍 `ctx`;解析不到时它**必须说出为什么**,
446
+ * 那句话会原样出现在 `rebuild` 的 warnings 里。
447
+ */
448
+ modelSeam: modelSeamFor,
449
+ });
350
450
  /** 默认知识库的根:`<agentRoot>/domain_knowledge/default`。 */
351
451
  const defaultBaseRoot = () => knowledgeBaseRoot(config.agentRoot, 'default');
352
452
  let runtime = new KnowledgeRuntime(defaultBaseRoot(), config, runtimeOptions());
@@ -362,10 +462,14 @@ export function apply(ctx, pluginConfig = {}) {
362
462
  registryFile: () => resourceRegistry(config.agentRoot),
363
463
  defaultRuntime: () => runtime,
364
464
  config: () => config,
465
+ // 0044:非默认库的 runtime 也要拿到模型接缝。只传 `config` 不够 —— 默认库走上面
466
+ // 那个 `new KnowledgeRuntime(... runtimeOptions())`,别的库走注册表这条支路,
467
+ // 两边必须都带上,否则「新建一个库 → 导入文档」静默变成规则抽取。
468
+ runtimeOptions: () => ({ modelSeam: modelSeamFor }),
365
469
  });
366
470
  runtime.baseId = bases.defaultId;
367
471
  defaultBaseSourceTypes = () => bases.sourceTypesOf(bases.defaultId);
368
- defaultBaseWikiSchema = () => ({ types: bases.wikiTypesOf(bases.defaultId), mode: bases.wikiSchemaModeOf(bases.defaultId) });
472
+ defaultBaseWikiSchema = () => ({ types: bases.wikiTypesOf(bases.defaultId) });
369
473
  activeKnowledgeBases = bases;
370
474
  const kbIdOf = (value) => {
371
475
  if (value === undefined || value === null)
@@ -567,13 +671,25 @@ export function apply(ctx, pluginConfig = {}) {
567
671
  // 0032/0033:意图的配置与留痕落进 `<agentRoot>/intent`(与知识、记忆平级)。
568
672
  // 0.1.7:意图的设置不再走命名空间,随本 entry 的 config 一起下来。
569
673
  intentHost = applyIntent(ctx, { intentRoot: () => intentRoot(config.agentRoot), config: intentGroup });
570
- const writeTools = new Set(['add_source', 'write_knowledge_card', 'set_knowledge_status', 'rebuild_knowledge', 'record_feedback']);
674
+ /**
675
+ * 需要本轮用户审批的写工具。
676
+ *
677
+ * 0044 换过一批名字:`write_knowledge_card`(只接受已确认提议)退役了,因为正文现在
678
+ * 只读、卡片由编译直接写入;`list_card_proposals` 也退役了,因为待办队列不存在了。
679
+ * 新增的两个是**入库之后**的动作:加备注、review 抬分。
680
+ */
681
+ const writeTools = new Set(['add_source', 'import_sources', 'propose_wiki_cards', 'annotate_wiki_card', 'review_wiki_card', 'set_knowledge_status', 'rebuild_knowledge', 'record_feedback']);
571
682
  ctx.on('tools/pre-execute', (exec, next) => {
572
683
  if (writeTools.has(exec.name))
573
684
  return Promise.resolve({ kind: 'ask', reason: '该操作会写入 Tita Knowledge Store,需要本轮用户审批。' });
574
685
  return next();
575
686
  });
576
- 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, lineageOf(args, exec), { contextTopN: args.top_n, rerank: args.rerank }))));
687
+ 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, lineageOf(args, exec), { contextTopN: args.top_n, rerank: args.rerank, ...(typeof args.context_max_chars === 'number' ? { contextMaxChars: args.context_max_chars } : {}) })), {
688
+ call: recallCallView,
689
+ // 查询词从参数来,命中从结果来 —— 两者拼成一张能看懂的账本。
690
+ meta: (args, value) => recallMetaOf({ ...dataOf(value), query: args?.query }),
691
+ result: resultViewOf(recallResultMarkdown),
692
+ }));
577
693
  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())));
578
694
  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)))));
579
695
  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)))));
@@ -606,11 +722,112 @@ export function apply(ctx, pluginConfig = {}) {
606
722
  })),
607
723
  })));
608
724
  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()))));
609
- ctx.tools.register(tool('list_wiki_types', 'List the Wiki card types (the per-knowledge-base card schema) with their required sections, required fields, expected source types and template card. Call this before 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())));
610
- 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) }))));
611
- ctx.tools.register(tool('write_knowledge_card', 'Write a Wiki knowledge card with mandatory source_refs. New cards default to unreviewed. DSH requests one user approval before execution.', { entity_type: { type: 'string', required: true, description: 'Wiki 卡片类型 id:先按 list_wiki_types 的类型定义与「例如」判断,命中就直接用它;必填小节与必填字段见该类型的 schema' }, entity_id: { type: 'string', required: true }, title: { type: 'string', required: true }, content: { type: 'string', required: true }, source_refs: { type: 'array', required: true }, status: { type: 'string', enum: ['unreviewed', 'draft', 'reviewed', 'active', 'deprecated', 'problem'] }, summary: { type: 'string' }, metadata: { type: 'json' }, 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.writeCard({ entityType: args.entity_type, entityId: args.entity_id, title: args.title, content: args.content, sourceRefs: args.source_refs, status: args.status, summary: args.summary, metadata: args.metadata, lineage: lineageOf(args, exec) }))));
725
+ 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())));
726
+ 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) })), {
727
+ call: addSourceCallView,
728
+ meta: (args, value) => addSourceMetaOf(args, dataOf(value)),
729
+ result: resultViewOf(addSourceResultMarkdown),
730
+ }));
731
+ /**
732
+ * `import_sources` —— **批量导入 + 一次编译**(0045)。
733
+ *
734
+ * 在这之前,「让 Agent 批量编译入库」意味着 `add_source` × N 再 `rebuild_knowledge`:
735
+ * 十份文档就是十次审批弹窗、十次往返,而人真正要同意的是「把这十份导进去」这一件事。
736
+ * 这个工具把那件事做成一次调用。
737
+ *
738
+ * 它**不替代** `add_source`:单份文档仍然用那个(参数更少、审批理由更具体)。
739
+ * 两者的差别只在「一次多少份」,落盘路径、分类闸门、编译链路完全共用 ——
740
+ * 这个工具的 `execute` 就是循环调 `importSources`,而后者逐条调 `addSource`。
741
+ *
742
+ * `compile` 默认开:批量导入的意图几乎总是「进来就能用」。关掉它的场景是「先全部
743
+ * 落盘、稍后统一编译」——那时卡片还没生成,工具返回里会明说 `compile: null`。
744
+ */
745
+ 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.', {
746
+ sources: { type: 'array', required: true, description: `来源数组,每项 { source_type, title?, content, external_id?, metadata?, allow_duplicate_title? };一次最多 ${MAX_IMPORT_SOURCES} 份,超了请分批。标题与已有来源重复的那一份会被拒(SOURCE_TITLE_DUPLICATED),逐条报出来而不是整批失败` },
747
+ compile: { type: 'boolean', description: '默认 true:写入后立刻编译成 wiki 卡片。false 表示只落盘' },
748
+ kb: { type: 'string' },
749
+ session_key: { type: 'string' },
750
+ task_id: { type: 'string' },
751
+ subtask_id: { type: 'string' },
752
+ run_id: { type: 'string' },
753
+ turn_id: { type: 'string' },
754
+ trace_id: { type: 'string' },
755
+ domain: { type: 'string' },
756
+ }, async (args, exec) => withRuntime(args.kb, target => target.importSources({
757
+ // 逐项**逐字段**点名:批量的输入是模型生成的,多带一个字段就多一分静默错位的可能。
758
+ sources: (Array.isArray(args.sources) ? args.sources : []).map(item => ({
759
+ sourceType: item?.source_type,
760
+ ...(item?.title ? { title: item.title } : {}),
761
+ content: item?.content,
762
+ ...(item?.external_id ? { externalId: item.external_id } : {}),
763
+ ...(item?.metadata ? { metadata: item.metadata } : {}),
764
+ ...(item?.allow_duplicate_title === true ? { allowDuplicateTitle: true } : {}),
765
+ })),
766
+ ...(args.compile === false ? { compile: false } : {}),
767
+ lineage: lineageOf(args, exec),
768
+ })), {
769
+ call: importCallView,
770
+ meta: (_args, value) => importMetaOf(dataOf(value)),
771
+ result: resultViewOf(importResultMarkdown),
772
+ }));
773
+ /**
774
+ * `propose_wiki_cards` —— 把一批卡片**直接写进 `02_wiki`**(工具名不变,语义在 0044 改了)。
775
+ *
776
+ * 名字保持不变是因为它是冻结契约;但描述必须说清它现在真的会写。0040 v1.5 的员工是
777
+ * 「草案停在待办队列里等人确认」,0044 撤掉了那道门:**入库在前、review 在后**。
778
+ * 判据见 `runtime.writeCards` 的注释 —— 确认门把知识挡在了门外,而 review 的价值
779
+ * 不需要靠排队来实现。
780
+ *
781
+ * 正常动线是:`add_source` 落来源 → `rebuild_knowledge` 让编译按 schema 抽实体入库。
782
+ * 这个工具服务于另一条路:Agent 手里已经有结构化的卡片草案(例如从一次排查里读出来的),
783
+ * 直接写进来,不必再走一遍抽取。
784
+ */
785
+ 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({
786
+ sourceId: args.source_id,
787
+ 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 })),
788
+ lineage: lineageOf(args, exec),
789
+ })), {
790
+ call: writeCardsCallView,
791
+ meta: (args, value) => writeCardsMetaOf(dataOf(value), String(args?.source_id ?? '')),
792
+ result: resultViewOf(writeCardsResultMarkdown),
793
+ }));
794
+ /**
795
+ * `annotate_wiki_card` —— 给卡片加**人工备注**(0044)。
796
+ *
797
+ * 这是「wiki 不可修改」的对偶:正文改不了,但判断改得了。`kind: 'anomaly'` 是
798
+ * 「这里有异常点」的显式标记(数字与原始文档对不上、两处口径互相矛盾),它会把卡片的
799
+ * 有效评分压到 `ANOMALY_SCORE` —— 异常卡片照样召回得到,只是排在后面,
800
+ * 而不是被藏起来(藏起来就没人会去修它)。
801
+ */
802
+ 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({
803
+ entityId: args.entity_id,
804
+ ...(args.entity_type ? { entityType: args.entity_type } : {}),
805
+ ...(args.kind ? { kind: args.kind } : {}),
806
+ ...(args.note ? { note: args.note } : {}),
807
+ ...(args.author ? { author: args.author } : {}),
808
+ ...(args.source_ref ? { sourceRef: args.source_ref } : {}),
809
+ ...(args.remove_id ? { removeId: args.remove_id } : {}),
810
+ lineage: lineageOf(args, exec),
811
+ }))));
812
+ /**
813
+ * `review_wiki_card` —— review 一张卡片,抬高它的召回评分(0044)。
814
+ *
815
+ * 它**不是准入闸门**:卡片在 review 之前就已经入库、可召回了。两个动作都可逆 ——
816
+ * review 是一个可以被推翻的判断,不是一次性的凭证。
817
+ */
818
+ 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({
819
+ entityId: args.entity_id,
820
+ ...(args.entity_type ? { entityType: args.entity_type } : {}),
821
+ action: args.action === 'unreview' ? 'unreview' : 'review',
822
+ ...(args.reviewer ? { reviewer: args.reviewer } : {}),
823
+ lineage: lineageOf(args, exec),
824
+ }))));
612
825
  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, args.problem_note, lineageOf(args, exec)))));
613
- 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)))));
826
+ 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))), {
827
+ call: rebuildCallView,
828
+ meta: (args, value) => rebuildMetaOf(args?.only ?? 'all', dataOf(value)),
829
+ result: resultViewOf(rebuildResultMarkdown),
830
+ }));
614
831
  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) }))));
615
832
  ctx.inject(['webServer'], webCtx => {
616
833
  const web = webCtx;
@@ -632,20 +849,27 @@ export function apply(ctx, pluginConfig = {}) {
632
849
  /**
633
850
  * 智能归因图谱的存储句柄,按 rootDir 缓存。
634
851
  *
635
- * 每个知识库有自己的 `05_diagnosis/`,和它的 wiki / 编译产物同源;缓存键用
636
- * rootDir 而不是 baseId,因为独立 runtime(未挂到知识库列表上的)也要能读写。
852
+ * 归因知识住在各库自己的 `01_source_raw/agent_diagnosis_case/` 与
853
+ * `02_wiki/diagnosis/` 里(`05_diagnosis/` 已于 v1.1 撤销),和它的 wiki 同源;
854
+ * 缓存键用 rootDir 而不是 baseId,因为独立 runtime(未挂到知识库列表上的)也要能读写。
637
855
  */
638
- const diagnosisStores = new Map();
639
- const storeOf = (target) => {
640
- const cached = diagnosisStores.get(target.rootDir);
641
- if (cached)
642
- return cached;
643
- // 卡片要经运行时的 writeCard 落盘(它同时校验 schema 与「出处必须真实存在」),
644
- // 所以 store 拿的是运行时本身,不只是 rootDir。
645
- const created = new DiagnosisStore(target);
646
- diagnosisStores.set(target.rootDir, created);
647
- return created;
648
- };
856
+ const storeOf = (target) => target.diagnosisStore();
857
+ /**
858
+ * `/diagnosis/graph` 与 `/diagnosis/sample` 的同一个响应体。
859
+ *
860
+ * 两条路由必须长得一样:界面拿到的要么是「这个库当前的知识」,要么是「刚载入示例之后
861
+ * 的知识」,它不该为第二种再写一条解析分支。
862
+ *
863
+ * 字段各有理由,不是把 snapshot 摊平:
864
+ * - `issues` 三层校验的结果要能直接显示,而不是只写进日志;
865
+ * - `source` / `migrated` 让界面说清「读的是卡片还是旧 jsonl」「迁移有没有生效」;
866
+ * - `canLoadSample` 是空态的开关 —— 这个库还什么都没有时才为真。
867
+ */
868
+ const diagnosisPayload = (snapshot) => ({
869
+ graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
870
+ seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source,
871
+ canLoadSample: snapshot.canLoadSample, problems: snapshot.problems,
872
+ });
649
873
  const routes = [
650
874
  { kind: 'exact', path: '/knowledge/stats', handler: async (req, res) => {
651
875
  const resolved = resolveBase(query(req).searchParams.get('kb'));
@@ -663,7 +887,14 @@ export function apply(ctx, pluginConfig = {}) {
663
887
  const topN = url.searchParams.get('top_n');
664
888
  const rerankParam = url.searchParams.get('rerank');
665
889
  // 响应里始终带 routes + backend,前端据此显示「关键词 / 向量 / 知识图谱」三路状态。
666
- return send(res, 200, await resolved.runtime.recall(url.searchParams.get('query') ?? '', topK === null ? undefined : Number(topK), sourceTypes, url.searchParams.get('include_problems') === 'true', (url.searchParams.get('mode') ?? undefined), lineageFromUrl(url), { ...(topN === null ? {} : { contextTopN: Number(topN) }), ...(rerankParam === null ? {} : { rerank: rerankParam !== 'false' && rerankParam !== '0' }) }));
890
+ return send(res, 200, await resolved.runtime.recall(url.searchParams.get('query') ?? '', topK === null ? undefined : Number(topK), sourceTypes, url.searchParams.get('include_problems') === 'true', (url.searchParams.get('mode') ?? undefined), lineageFromUrl(url), {
891
+ ...(topN === null ? {} : { contextTopN: Number(topN) }),
892
+ ...(rerankParam === null ? {} : { rerank: rerankParam !== 'false' && rerankParam !== '0' }),
893
+ ...(url.searchParams.get('context_max_chars') === null ? {} : { contextMaxChars: Number(url.searchParams.get('context_max_chars')) }),
894
+ // 面板账本(0050):只在 HTTP 上给,工具调用不带 —— 几十行候选分数进
895
+ // 模型上下文是纯噪声,而面板要的就是「哪条被丢了、各自多少分」。
896
+ ...(url.searchParams.get('detail') === 'true' ? { detail: true } : {}),
897
+ }));
667
898
  } },
668
899
  { kind: 'exact', path: '/knowledge/embedding/health', handler: async (req, res) => {
669
900
  const resolved = resolveBase(query(req).searchParams.get('kb'));
@@ -698,6 +929,37 @@ export function apply(ctx, pluginConfig = {}) {
698
929
  return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)));
699
930
  }
700
931
  } },
932
+ /**
933
+ * 批量导入 + 一次编译(0045)。
934
+ *
935
+ * 与 `import_sources` 工具**同一条实现**(`runtime.importSources`):
936
+ * 「面板能做的 Agent 也能做、Agent 能做的面板也能做」是这个插件一贯的规矩,
937
+ * 差异只允许出现在审批与呈现上,不允许出现在行为上。
938
+ */
939
+ { kind: 'exact', path: '/knowledge/import', handler: async (req, res) => {
940
+ try {
941
+ if (req.method !== 'POST')
942
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'));
943
+ const body = await readBody(req, 4_000_000);
944
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'));
945
+ if ('failure' in resolved)
946
+ return sendEnvelope(res, resolved.failure);
947
+ return send(res, 200, await resolved.runtime.importSources({
948
+ sources: (Array.isArray(body.sources) ? body.sources : []).map((item) => ({
949
+ sourceType: item?.source_type,
950
+ ...(item?.title ? { title: String(item.title) } : {}),
951
+ content: item?.content,
952
+ ...(item?.external_id ? { externalId: String(item.external_id) } : {}),
953
+ ...(item?.metadata && typeof item.metadata === 'object' ? { metadata: item.metadata } : {}),
954
+ })),
955
+ ...(body.compile === false ? { compile: false } : {}),
956
+ lineage: lineageOf(body),
957
+ }));
958
+ }
959
+ catch (cause) {
960
+ return send(res, 400, envelopeError('REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)));
961
+ }
962
+ } },
701
963
  { kind: 'exact', path: '/knowledge/sources', handler: async (req, res) => {
702
964
  try {
703
965
  if (req.method === 'POST') {
@@ -705,7 +967,7 @@ export function apply(ctx, pluginConfig = {}) {
705
967
  const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'));
706
968
  if ('failure' in resolved)
707
969
  return sendEnvelope(res, resolved.failure);
708
- 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) }));
970
+ 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) }));
709
971
  }
710
972
  if (req.method !== 'GET')
711
973
  return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET 或 POST'));
@@ -800,14 +1062,24 @@ export function apply(ctx, pluginConfig = {}) {
800
1062
  const resolved = resolveBase(query(req).searchParams.get('kb'));
801
1063
  if ('failure' in resolved)
802
1064
  return sendEnvelope(res, resolved.failure);
803
- const snapshot = await storeOf(resolved.runtime).snapshot();
804
- // issues 一并返回:三层校验的结果要能直接显示在界面上,而不是只写进日志。
805
- // source / migrated / problems 一并返回:界面必须能说清「现在读的是卡片还是旧 jsonl」,
806
- // 以及迁移有没有生效 —— 这类状态靠猜就会变成一句含糊的「05_diagnosis」。
807
- return send(res, 200, success({
808
- graph: snapshot.graph, cases: snapshot.cases, issues: snapshot.issues,
809
- seeded: snapshot.seeded, migrated: snapshot.migrated, source: snapshot.source, problems: snapshot.problems,
810
- }));
1065
+ return send(res, 200, success(diagnosisPayload(await storeOf(resolved.runtime).snapshot())));
1066
+ } },
1067
+ /**
1068
+ * 载入**内置示例**(0021 的示例内容,原来是自动写入的)。
1069
+ *
1070
+ * 它是一次显式的**写**:用户在这个库上还没录任何排查知识时,界面上那枚
1071
+ * 「载入内置示例」按钮打的就是这里。单独一条路由而不是让读取顺手写 ——
1072
+ * 读接口写用户的 `01_source_raw/` 正是上一版的毛病。
1073
+ */
1074
+ { kind: 'exact', path: '/diagnosis/sample', handler: async (req, res) => {
1075
+ if (req.method !== 'POST')
1076
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'));
1077
+ const resolved = resolveBase(query(req).searchParams.get('kb'));
1078
+ if ('failure' in resolved)
1079
+ return sendEnvelope(res, resolved.failure);
1080
+ const store = storeOf(resolved.runtime);
1081
+ const { problems } = await store.loadSample();
1082
+ return send(res, 200, success(diagnosisPayload(await store.snapshot()), problems));
811
1083
  } },
812
1084
  { kind: 'exact', path: '/diagnosis/cases', handler: async (req, res) => {
813
1085
  const resolved = resolveBase(query(req).searchParams.get('kb'));
@@ -815,6 +1087,70 @@ export function apply(ctx, pluginConfig = {}) {
815
1087
  return sendEnvelope(res, resolved.failure);
816
1088
  return send(res, 200, success({ cases: (await storeOf(resolved.runtime).snapshot()).cases }));
817
1089
  } },
1090
+ /**
1091
+ * 一次排查结束:落一条 `agent_diagnosis_case` 的 SourceRaw,并写入一张案例卡片。
1092
+ *
1093
+ * 0044:卡片不再只提议 —— 它直接入库并标成 `unreviewed`。响应给 `sourceId` 与
1094
+ * `entityId`,界面据此说清「记录留下了,卡片也建了,等人 review」。
1095
+ */
1096
+ { kind: 'exact', path: '/diagnosis/run', handler: async (req, res) => {
1097
+ if (req.method !== 'POST')
1098
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'));
1099
+ const body = await readBody(req, 2_000_000);
1100
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'));
1101
+ if ('failure' in resolved)
1102
+ return sendEnvelope(res, resolved.failure);
1103
+ const record = body.record;
1104
+ if (!record || typeof record.runId !== 'string' || typeof record.symptomId !== 'string') {
1105
+ return send(res, 400, envelopeError('RUN_RECORD_INVALID', 'record 必须是 { runId, symptomId, startedAt, durationMinutes, path, evidence, rootCause?, misleading? }'));
1106
+ }
1107
+ const outcome = await storeOf(resolved.runtime).recordRun(record);
1108
+ if (outcome.problems.length > 0 && outcome.sourceId === undefined) {
1109
+ return send(res, 200, failure({ sourceId: '', entityId: '' }, 'RUN_WRITE_FAILED', outcome.problems.join(';'), true));
1110
+ }
1111
+ return send(res, 200, success({ sourceId: outcome.sourceId ?? '', entityId: outcome.entityId ?? '' }, outcome.problems));
1112
+ } },
1113
+ /**
1114
+ * 入库之后的两个人工动作(0044):加备注、review 抬分。
1115
+ *
1116
+ * 它们取代了原来的 `/knowledge/card-proposals/review`。差别是**对象变了**:
1117
+ * 以前 review 的是一条还没落库的提议(通过 = 允许入库),现在 review 的是一张
1118
+ * 已经在 `02_wiki` 里、已经能召回的卡片(通过 = 抬高它的召回评分)。
1119
+ * 前者是准入,后者是事后判断 —— 后者可逆,前者不可逆。
1120
+ */
1121
+ { kind: 'exact', path: '/knowledge/card-annotate', handler: async (req, res) => {
1122
+ if (req.method !== 'POST')
1123
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'));
1124
+ const body = await readBody(req, 2_000_000);
1125
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'));
1126
+ if ('failure' in resolved)
1127
+ return sendEnvelope(res, resolved.failure);
1128
+ return sendEnvelope(res, await resolved.runtime.annotateCard({
1129
+ entityId: String(body.entity_id ?? ''),
1130
+ ...(body.entity_type ? { entityType: String(body.entity_type) } : {}),
1131
+ ...(body.kind === 'anomaly' ? { kind: 'anomaly' } : body.kind === 'note' ? { kind: 'note' } : {}),
1132
+ ...(body.note ? { note: String(body.note) } : {}),
1133
+ ...(body.author ? { author: String(body.author) } : {}),
1134
+ ...(body.source_ref ? { sourceRef: String(body.source_ref) } : {}),
1135
+ ...(body.remove_id ? { removeId: String(body.remove_id) } : {}),
1136
+ lineage: lineageOf(body),
1137
+ }));
1138
+ } },
1139
+ { kind: 'exact', path: '/knowledge/card-review', handler: async (req, res) => {
1140
+ if (req.method !== 'POST')
1141
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'));
1142
+ const body = await readBody(req, 2_000_000);
1143
+ const resolved = resolveBase(body.kb ?? query(req).searchParams.get('kb'));
1144
+ if ('failure' in resolved)
1145
+ return sendEnvelope(res, resolved.failure);
1146
+ return sendEnvelope(res, await resolved.runtime.reviewCard({
1147
+ entityId: String(body.entity_id ?? ''),
1148
+ ...(body.entity_type ? { entityType: String(body.entity_type) } : {}),
1149
+ action: body.action === 'unreview' ? 'unreview' : 'review',
1150
+ ...(body.reviewer ? { reviewer: String(body.reviewer) } : {}),
1151
+ lineage: lineageOf(body),
1152
+ }));
1153
+ } },
818
1154
  { kind: 'prefix', path: '/knowledge', handler: async (req, res) => {
819
1155
  const method = req.method ?? 'GET';
820
1156
  try {
@@ -836,6 +1172,26 @@ export function apply(ctx, pluginConfig = {}) {
836
1172
  const body = await readBody(req);
837
1173
  return sendEnvelope(res, await withRuntime(body.kb, target => target.rebuild(body.only ?? 'all', lineageOf(body))));
838
1174
  }
1175
+ /**
1176
+ * 独立整页的 bundle(`lib/doc-page.js`,构建产物)。
1177
+ *
1178
+ * 它挂在本插件的 `/knowledge` 前缀下而不是另开一条顶层路由:整页是这个插件
1179
+ * 的一部分,路径上不该假装是一个独立服务。`no-cache` 是刻意的 —— 这份文件在
1180
+ * 开发时会被反复重建,缓存住它等于让人对着旧页面调试。
1181
+ */
1182
+ if (rest.length === 1 && rest[0] === 'doc-page.js') {
1183
+ if (method !== 'GET')
1184
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'));
1185
+ try {
1186
+ const bundle = await readFile(documentPageBundle());
1187
+ res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' });
1188
+ res.end(bundle);
1189
+ }
1190
+ catch {
1191
+ return send(res, 404, envelopeError('NOT_FOUND', 'doc-page.js 不存在;先构建这个插件(pnpm build)'));
1192
+ }
1193
+ return;
1194
+ }
839
1195
  // ---------------------------------------------- /knowledge/bases
840
1196
  if (rest[0] === 'bases') {
841
1197
  if (rest.length === 1) {
@@ -863,7 +1219,7 @@ export function apply(ctx, pluginConfig = {}) {
863
1219
  if (rest.length === 2) {
864
1220
  if (method === 'PATCH') {
865
1221
  const body = await readBody(req);
866
- const updated = await bases.update(baseId, { name: body.name, description: body.description, sourceTypes: body.sourceTypes, wikiTypes: body.wikiTypes, wikiSchemaMode: body.wikiSchemaMode, resetWikiTypes: body.resetWikiTypes === true });
1222
+ const updated = await bases.update(baseId, { name: body.name, description: body.description, sourceTypes: body.sourceTypes, wikiTypes: body.wikiTypes, resetWikiTypes: body.resetWikiTypes === true });
867
1223
  if (!updated.ok)
868
1224
  return sendEnvelope(res, updated);
869
1225
  return send(res, 200, success({ base: updated.data }, updated.warnings));
@@ -983,7 +1339,19 @@ export function apply(ctx, pluginConfig = {}) {
983
1339
  const card = await resolved.runtime.readCard(entityId, entityType);
984
1340
  if (!card)
985
1341
  return sendEnvelope(res, failure({}, 'CARD_NOT_FOUND', `知识卡片不存在: ${entityId}`, true));
986
- return send(res, 200, success({ card }));
1342
+ // 正文里的图要能在面板里显示,就得给 URL(`imagePaths` 是磁盘路径,浏览器
1343
+ // 加载不了)。与 SourceRaw 详情同一种形状,两个预览抽屉共用一套解析。
1344
+ const assets = await resolved.runtime.assetsOfCard(card);
1345
+ /**
1346
+ * 相关卡片(0049):出链与入链。
1347
+ *
1348
+ * 由服务端算,而不是让客户端去解析正文里那一块 `## 相关` —— 正文里那块是
1349
+ * 给人 `grep` 的(`[[类型/标题]]` 在面板里点不开),而抽屉里这几行是**可点的
1350
+ * 入口**。两处读同一个计划(`planRelated`),所以不会出现「文件里写着 A、
1351
+ * 界面上列着 B」。
1352
+ */
1353
+ const related = await resolved.runtime.relatedOfCard(card);
1354
+ return send(res, 200, success({ card, assets, related }));
987
1355
  }
988
1356
  if (rest.length === 3 && rest[2] === 'versions') {
989
1357
  if (method !== 'GET')
@@ -1031,6 +1399,12 @@ export function apply(ctx, pluginConfig = {}) {
1031
1399
  return sendEnvelope(res, resolved.failure);
1032
1400
  if (rest.length === 2) {
1033
1401
  if (method === 'GET') {
1402
+ /**
1403
+ * 正文在这里**按 `SOURCE_PREVIEW_CHARS` 封顶**(`readDocument` 的默认值)。
1404
+ * 抽屉只渲染前 4000 字符,而过去这个接口把整份正文发过去 —— 传输、JSON
1405
+ * 解析、进 React state 全按全文的量级付费,那就是「打开弹层要等半天」。
1406
+ * 要读全文有独立整页:`.../view`,一页一页地取。
1407
+ */
1034
1408
  const document = await resolved.runtime.readDocument(docId);
1035
1409
  if (!document)
1036
1410
  return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true));
@@ -1052,6 +1426,35 @@ export function apply(ctx, pluginConfig = {}) {
1052
1426
  return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true));
1053
1427
  return send(res, 200, success({ versions }));
1054
1428
  }
1429
+ /**
1430
+ * 独立整页的**正文分页**接口:一次一段,切点对齐空行(见 `documentContentPage`)。
1431
+ *
1432
+ * 它是 `.../view` 那个页面的数据源,也是「原文全文」唯一的分发口 ——
1433
+ * 面板那条明细接口刻意不发全文。
1434
+ */
1435
+ if (rest.length === 3 && rest[2] === 'content') {
1436
+ if (method !== 'GET')
1437
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'));
1438
+ const body = await resolved.runtime.readDocumentBody(docId);
1439
+ if (body === undefined)
1440
+ return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true));
1441
+ const offset = clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER);
1442
+ const limit = clampInt(url.searchParams.get('limit'), DOC_PAGE_CHARS, 1, DOC_PAGE_CHARS_MAX);
1443
+ return send(res, 200, success(documentContentPage(body, offset, limit)));
1444
+ }
1445
+ /**
1446
+ * 独立整页的外壳。前端从抽屉里「新页面打开」跳过来,也可以直接粘贴这个地址。
1447
+ */
1448
+ if (rest.length === 3 && rest[2] === 'view') {
1449
+ if (method !== 'GET')
1450
+ return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 GET'));
1451
+ const document = await resolved.runtime.readDocument(docId);
1452
+ if (!document)
1453
+ return sendEnvelope(res, failure({}, 'DOCUMENT_NOT_FOUND', `文档不存在: ${docId}`, true));
1454
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
1455
+ res.end(standaloneDocumentHtml(document.title));
1456
+ return;
1457
+ }
1055
1458
  if (rest.length === 3 && rest[2] === 'revert') {
1056
1459
  if (method !== 'POST')
1057
1460
  return send(res, 405, envelopeError('METHOD_NOT_ALLOWED', '该 knowledge endpoint 只接受 POST'));