dsh-data-domain-knowledge 0.1.0-alpha.1

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/cordis.patch.yml +410 -0
  4. package/eval/README.md +76 -0
  5. package/eval/evaluate-retrieval.mjs +121 -0
  6. package/eval/queries.example.jsonl +2 -0
  7. package/lib/client.js +111415 -0
  8. package/lib/types/config-schema.d.ts +35 -0
  9. package/lib/types/config-schema.d.ts.map +1 -0
  10. package/lib/types/config-schema.js +34 -0
  11. package/lib/types/config-schema.js.map +1 -0
  12. package/lib/types/convert/core-loader.d.ts +32 -0
  13. package/lib/types/convert/core-loader.d.ts.map +1 -0
  14. package/lib/types/convert/core-loader.js +60 -0
  15. package/lib/types/convert/core-loader.js.map +1 -0
  16. package/lib/types/convert/host.d.ts +88 -0
  17. package/lib/types/convert/host.d.ts.map +1 -0
  18. package/lib/types/convert/host.js +479 -0
  19. package/lib/types/convert/host.js.map +1 -0
  20. package/lib/types/convert/ingest.d.ts +125 -0
  21. package/lib/types/convert/ingest.d.ts.map +1 -0
  22. package/lib/types/convert/ingest.js +228 -0
  23. package/lib/types/convert/ingest.js.map +1 -0
  24. package/lib/types/convert/settings-schema.d.ts +18 -0
  25. package/lib/types/convert/settings-schema.d.ts.map +1 -0
  26. package/lib/types/convert/settings-schema.js +26 -0
  27. package/lib/types/convert/settings-schema.js.map +1 -0
  28. package/lib/types/convert/types.d.ts +46 -0
  29. package/lib/types/convert/types.d.ts.map +1 -0
  30. package/lib/types/convert/types.js +41 -0
  31. package/lib/types/convert/types.js.map +1 -0
  32. package/lib/types/diagnosis/cards.d.ts +71 -0
  33. package/lib/types/diagnosis/cards.d.ts.map +1 -0
  34. package/lib/types/diagnosis/cards.js +482 -0
  35. package/lib/types/diagnosis/cards.js.map +1 -0
  36. package/lib/types/diagnosis/problem-classes.d.ts +51 -0
  37. package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
  38. package/lib/types/diagnosis/problem-classes.js +66 -0
  39. package/lib/types/diagnosis/problem-classes.js.map +1 -0
  40. package/lib/types/diagnosis/schema.d.ts +65 -0
  41. package/lib/types/diagnosis/schema.d.ts.map +1 -0
  42. package/lib/types/diagnosis/schema.js +208 -0
  43. package/lib/types/diagnosis/schema.js.map +1 -0
  44. package/lib/types/diagnosis/seed-source.d.ts +20 -0
  45. package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
  46. package/lib/types/diagnosis/seed-source.js +96 -0
  47. package/lib/types/diagnosis/seed-source.js.map +1 -0
  48. package/lib/types/diagnosis/seed.d.ts +50 -0
  49. package/lib/types/diagnosis/seed.d.ts.map +1 -0
  50. package/lib/types/diagnosis/seed.js +242 -0
  51. package/lib/types/diagnosis/seed.js.map +1 -0
  52. package/lib/types/diagnosis/store.d.ts +81 -0
  53. package/lib/types/diagnosis/store.d.ts.map +1 -0
  54. package/lib/types/diagnosis/store.js +409 -0
  55. package/lib/types/diagnosis/store.js.map +1 -0
  56. package/lib/types/diagnosis/types.d.ts +124 -0
  57. package/lib/types/diagnosis/types.d.ts.map +1 -0
  58. package/lib/types/diagnosis/types.js +13 -0
  59. package/lib/types/diagnosis/types.js.map +1 -0
  60. package/lib/types/directory-picker.d.ts +94 -0
  61. package/lib/types/directory-picker.d.ts.map +1 -0
  62. package/lib/types/directory-picker.js +110 -0
  63. package/lib/types/directory-picker.js.map +1 -0
  64. package/lib/types/extract-model.d.ts +76 -0
  65. package/lib/types/extract-model.d.ts.map +1 -0
  66. package/lib/types/extract-model.js +193 -0
  67. package/lib/types/extract-model.js.map +1 -0
  68. package/lib/types/extract.d.ts +121 -0
  69. package/lib/types/extract.d.ts.map +1 -0
  70. package/lib/types/extract.js +549 -0
  71. package/lib/types/extract.js.map +1 -0
  72. package/lib/types/fts.d.ts +96 -0
  73. package/lib/types/fts.d.ts.map +1 -0
  74. package/lib/types/fts.js +184 -0
  75. package/lib/types/fts.js.map +1 -0
  76. package/lib/types/index.d.ts +39 -0
  77. package/lib/types/index.d.ts.map +1 -0
  78. package/lib/types/index.js +1112 -0
  79. package/lib/types/index.js.map +1 -0
  80. package/lib/types/intent/engine.d.ts +108 -0
  81. package/lib/types/intent/engine.d.ts.map +1 -0
  82. package/lib/types/intent/engine.js +501 -0
  83. package/lib/types/intent/engine.js.map +1 -0
  84. package/lib/types/intent/host.d.ts +88 -0
  85. package/lib/types/intent/host.d.ts.map +1 -0
  86. package/lib/types/intent/host.js +389 -0
  87. package/lib/types/intent/host.js.map +1 -0
  88. package/lib/types/intent/preset-tools.d.ts +37 -0
  89. package/lib/types/intent/preset-tools.d.ts.map +1 -0
  90. package/lib/types/intent/preset-tools.js +45 -0
  91. package/lib/types/intent/preset-tools.js.map +1 -0
  92. package/lib/types/intent/settings-schema.d.ts +59 -0
  93. package/lib/types/intent/settings-schema.d.ts.map +1 -0
  94. package/lib/types/intent/settings-schema.js +57 -0
  95. package/lib/types/intent/settings-schema.js.map +1 -0
  96. package/lib/types/intent/store.d.ts +75 -0
  97. package/lib/types/intent/store.d.ts.map +1 -0
  98. package/lib/types/intent/store.js +139 -0
  99. package/lib/types/intent/store.js.map +1 -0
  100. package/lib/types/intent/tools.d.ts +63 -0
  101. package/lib/types/intent/tools.d.ts.map +1 -0
  102. package/lib/types/intent/tools.js +108 -0
  103. package/lib/types/intent/tools.js.map +1 -0
  104. package/lib/types/intent/types.d.ts +189 -0
  105. package/lib/types/intent/types.d.ts.map +1 -0
  106. package/lib/types/intent/types.js +62 -0
  107. package/lib/types/intent/types.js.map +1 -0
  108. package/lib/types/knowledge-bases.d.ts +266 -0
  109. package/lib/types/knowledge-bases.d.ts.map +1 -0
  110. package/lib/types/knowledge-bases.js +1107 -0
  111. package/lib/types/knowledge-bases.js.map +1 -0
  112. package/lib/types/layout.d.ts +262 -0
  113. package/lib/types/layout.d.ts.map +1 -0
  114. package/lib/types/layout.js +312 -0
  115. package/lib/types/layout.js.map +1 -0
  116. package/lib/types/migrate.d.ts +110 -0
  117. package/lib/types/migrate.d.ts.map +1 -0
  118. package/lib/types/migrate.js +280 -0
  119. package/lib/types/migrate.js.map +1 -0
  120. package/lib/types/recall-rank.d.ts +158 -0
  121. package/lib/types/recall-rank.d.ts.map +1 -0
  122. package/lib/types/recall-rank.js +261 -0
  123. package/lib/types/recall-rank.js.map +1 -0
  124. package/lib/types/runtime.d.ts +568 -0
  125. package/lib/types/runtime.d.ts.map +1 -0
  126. package/lib/types/runtime.js +3045 -0
  127. package/lib/types/runtime.js.map +1 -0
  128. package/lib/types/settings-schema.d.ts +117 -0
  129. package/lib/types/settings-schema.d.ts.map +1 -0
  130. package/lib/types/settings-schema.js +92 -0
  131. package/lib/types/settings-schema.js.map +1 -0
  132. package/lib/types/settings-volatile.d.ts +102 -0
  133. package/lib/types/settings-volatile.d.ts.map +1 -0
  134. package/lib/types/settings-volatile.js +114 -0
  135. package/lib/types/settings-volatile.js.map +1 -0
  136. package/lib/types/types.d.ts +799 -0
  137. package/lib/types/types.d.ts.map +1 -0
  138. package/lib/types/types.js +387 -0
  139. package/lib/types/types.js.map +1 -0
  140. package/lib/types/wiki-types.d.ts +68 -0
  141. package/lib/types/wiki-types.d.ts.map +1 -0
  142. package/lib/types/wiki-types.js +322 -0
  143. package/lib/types/wiki-types.js.map +1 -0
  144. package/package.json +101 -0
  145. package/preset/tita-data-agent/agent.cordis.yml +376 -0
  146. package/preset/tita-data-agent/preset.yml +3 -0
  147. package/python/requirements.txt +8 -0
  148. package/scripts/doctor.mjs +70 -0
  149. package/scripts/e2e-pipeline.mjs +355 -0
  150. package/scripts/install-agent-preset.mjs +92 -0
  151. package/scripts/setup-embedding.sh +34 -0
  152. package/scripts/sync-agent-preset.mjs +188 -0
  153. package/src/client/AgentPresetCard.tsx +247 -0
  154. package/src/client/DirectoryPicker.tsx +357 -0
  155. package/src/client/InfoHint.tsx +65 -0
  156. package/src/client/IntentLogicTab.tsx +185 -0
  157. package/src/client/IntentTab.tsx +715 -0
  158. package/src/client/KnowledgeBaseTab.tsx +1454 -0
  159. package/src/client/KnowledgeGraphView.tsx +306 -0
  160. package/src/client/KnowledgePanel.tsx +381 -0
  161. package/src/client/KnowledgeStepRail.tsx +60 -0
  162. package/src/client/NodeSettings.tsx +392 -0
  163. package/src/client/PreviewSections.tsx +116 -0
  164. package/src/client/RecallPanel.tsx +399 -0
  165. package/src/client/TypeDefinitionDrawer.tsx +215 -0
  166. package/src/client/WikiFlowCard.tsx +226 -0
  167. package/src/client/WikiTypeEditor.tsx +332 -0
  168. package/src/client/agent-preset.ts +121 -0
  169. package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
  170. package/src/client/arch/NodeLogicPanel.tsx +133 -0
  171. package/src/client/arch/ProblemDetailPanel.tsx +88 -0
  172. package/src/client/arch/model.ts +691 -0
  173. package/src/client/architecture.css +191 -0
  174. package/src/client/architecture.tsx +530 -0
  175. package/src/client/attribution/AttributionGraph.tsx +104 -0
  176. package/src/client/attribution/AttributionTab.tsx +332 -0
  177. package/src/client/attribution/CaseView.tsx +102 -0
  178. package/src/client/attribution/GraphStage.tsx +87 -0
  179. package/src/client/attribution/RunView.tsx +184 -0
  180. package/src/client/attribution/StageChrome.tsx +403 -0
  181. package/src/client/attribution/ViewSwitch.tsx +107 -0
  182. package/src/client/attribution/graph-options.ts +381 -0
  183. package/src/client/attribution/model.ts +462 -0
  184. package/src/client/config-scope.ts +119 -0
  185. package/src/client/convert/ConvertPanel.tsx +594 -0
  186. package/src/client/convert/styles.css +138 -0
  187. package/src/client/convert/styles.ts +49 -0
  188. package/src/client/format.ts +39 -0
  189. package/src/client/graph/layout.ts +181 -0
  190. package/src/client/graph.css +113 -0
  191. package/src/client/index.ts +98 -0
  192. package/src/client/problem-domains.ts +114 -0
  193. package/src/client/styles.css +940 -0
  194. package/src/client/styles.ts +11 -0
  195. package/src/client/useFitScale.ts +68 -0
  196. package/src/config-schema.ts +45 -0
  197. package/src/convert/core-loader.ts +68 -0
  198. package/src/convert/host.ts +579 -0
  199. package/src/convert/ingest.ts +367 -0
  200. package/src/convert/settings-schema.ts +27 -0
  201. package/src/convert/types.ts +65 -0
  202. package/src/diagnosis/cards.ts +498 -0
  203. package/src/diagnosis/problem-classes.ts +117 -0
  204. package/src/diagnosis/schema.ts +260 -0
  205. package/src/diagnosis/seed-source.ts +107 -0
  206. package/src/diagnosis/seed.ts +291 -0
  207. package/src/diagnosis/store.ts +432 -0
  208. package/src/diagnosis/types.ts +152 -0
  209. package/src/directory-picker.ts +164 -0
  210. package/src/extract-model.ts +227 -0
  211. package/src/extract.ts +632 -0
  212. package/src/fts.ts +203 -0
  213. package/src/index.ts +1091 -0
  214. package/src/intent/README.md +63 -0
  215. package/src/intent/engine.ts +527 -0
  216. package/src/intent/host.ts +462 -0
  217. package/src/intent/preset-tools.ts +48 -0
  218. package/src/intent/settings-schema.ts +63 -0
  219. package/src/intent/store.ts +143 -0
  220. package/src/intent/tools.ts +139 -0
  221. package/src/intent/types.ts +245 -0
  222. package/src/knowledge-bases.ts +1160 -0
  223. package/src/layout.ts +350 -0
  224. package/src/migrate.ts +262 -0
  225. package/src/python/embedding_worker.py +471 -0
  226. package/src/recall-rank.ts +299 -0
  227. package/src/runtime.ts +3069 -0
  228. package/src/settings-schema.ts +94 -0
  229. package/src/settings-volatile.ts +122 -0
  230. package/src/types.ts +1140 -0
  231. package/src/wiki-types.ts +357 -0
@@ -0,0 +1,579 @@
1
+ /**
2
+ * The convert host — the DSH-facing half of multi-format conversion.
3
+ *
4
+ * Since 0018 this lives inside `plugins/dsh-data-domain-knowledge` (issue 0018):
5
+ * conversion is a sub-capability of knowledge, mounted as one plugin, with one
6
+ * sidebar entry and one settings surface for the user. The kernel
7
+ * (`dsh-convert-core`) stays an independent, optional package that
8
+ * `core-loader.ts` imports lazily.
9
+ *
10
+ * Layering this file must keep:
11
+ *
12
+ * kernel (convert-core) input → knowledge-ready Markdown + assets + console
13
+ * this host settings, agent tools, /convert route, ingestion
14
+ *
15
+ * Nothing about parsing lives here; nothing about DSH lives in the kernel. That
16
+ * is why the same console still works standalone at `tita-convert`.
17
+ */
18
+
19
+ import type { IncomingMessage, ServerResponse } from 'node:http'
20
+ import { stat } from 'node:fs/promises'
21
+ import { basename, resolve } from 'node:path'
22
+ import type { Context } from '@deepseek-ai/cordis'
23
+ import z from '@deepseek-ai/schemastery'
24
+ import { defineTool, type PreToolDecision, type ToolRunContext } from '@deepseek-ai/dsh-tools'
25
+ import type {} from '@deepseek-ai/dsh-settings'
26
+ import type { ConvertRequest, ConvertService, ConvertTask } from 'dsh-convert-core'
27
+ import { ConvertCoreMissingError, loadConvertCore } from './core-loader.js'
28
+ import { tmpRoot, workspaceOutputRoot } from '../layout.js'
29
+ import { ingestDelivery, type IngestOutcome, type IngestTarget } from './ingest.js'
30
+ import {
31
+ CONVERT_SETTINGS_NAMESPACE,
32
+ DEFAULT_CONVERT_CONFIG,
33
+ failure,
34
+ success,
35
+ WRITE_TOOLS,
36
+ type ConvertPluginConfig,
37
+ type Envelope,
38
+ } from './types.js'
39
+
40
+ export { CONVERT_SETTINGS_NAMESPACE, DEFAULT_CONVERT_CONFIG, defaultConvertRoot } from './types.js'
41
+
42
+ /** Route prefix the console and API are mounted on. Frozen contract. */
43
+ export const MOUNT_PATH = '/convert'
44
+
45
+ const PREVIEW_CHARS = 1200
46
+
47
+ /**
48
+ * 转换的设置 schema。
49
+ *
50
+ * 0029 起它**没有路径字段**:交付物与工作区都从知识插件的 `agentRoot` 推出来
51
+ * (`<agentRoot>/output/<日期>/`、`<agentRoot>/tmp/`)。一个 Agent 只有一个根路径,
52
+ * 所以这里再放一个 `rootDir` 就等于把「东西存在哪」这个答案劈成两半。
53
+ *
54
+ * 定义本身搬到了 `./settings-schema.js`(纯数据、可被 `Config` 组合并在
55
+ * `node --test` 里独立解析);这里 re-export,保持既有出口不变。
56
+ */
57
+ export { CONVERT_SETTINGS_SCHEMA } from './settings-schema.js'
58
+
59
+ /** What the convert host needs from the knowledge plugin it now lives in. */
60
+ export interface ConvertHostDeps {
61
+ /** The live knowledge runtime; the seam may only call `addSource` on it. */
62
+ runtime(): IngestTarget
63
+ /** Absolute knowledge root the runtime currently writes to. */
64
+ knowledgeRoot(): string
65
+ /**
66
+ * 当前工作项目(交付物的落点)。省略时退回进程的工作目录。
67
+ *
68
+ * 转换内核是**宿主级单例**,而「工作项目」是**每个会话各自**的 —— 内核这一层拿不到
69
+ * 会话,所以这里只能给一个宿主级的答案。单项目部署(一个 DSH 进程服务同一个项目)下
70
+ * 两者一致;多项目共用同一个宿主时,交付物会集中落到宿主启动目录下,而不是各自项目里。
71
+ * 要做到完全按会话分,需要内核支持**按请求**解析输出目录,那是内核侧的一处改造。
72
+ */
73
+ workspace?(): string
74
+ /**
75
+ * Resolve an ingest target for one knowledge base (issue 0019). Optional so the
76
+ * host still works when the registry is unavailable; when it is absent every
77
+ * conversion lands in the default knowledge base.
78
+ */
79
+ resolveTarget?(kbId?: string): { runtime: IngestTarget; knowledgeRoot: string }
80
+ /**
81
+ * Tita Agent 根路径(0029)。转换的交付物与工作区都从它推出来,所以转换
82
+ * **没有自己的路径设置** —— 一个 Agent 只有一个根路径。
83
+ */
84
+ agentRoot(): string
85
+ /**
86
+ * 已解析的 `convert` 设置组(来自插件 entry 的 config)。
87
+ *
88
+ * 0.1.7 起设置不再是命名空间,而是随 `apply(ctx, config)` 进来的 entry config,
89
+ * 所以这里由调用方把那一组传下来。省略时用 `DEFAULT_CONVERT_CONFIG`。
90
+ *
91
+ * 允许给**函数**的理由与意图那组相同(见 `intent/host.ts` 的 `IntentHostDeps.config`):
92
+ * 字段是 volatile,解包是一次快照,而 volatile 改动不会重载插件。
93
+ */
94
+ config?: Partial<ConvertPluginConfig> | (() => Partial<ConvertPluginConfig>)
95
+ }
96
+
97
+ /**
98
+ * `applyConvert` 交给组合根的那一个把手,与 `intent/host.ts` 的 `IntentHost` 同形。
99
+ *
100
+ * 转换这一组也有 volatile 字段(`maxConcurrent` / `maxInputBytes` / `retentionDays` /
101
+ * `toolWaitMs` / `ingestEnabled`),其中一个解包漏了就是沉默的错误:`maxInputBytes`
102
+ * 是引用时 `info.size > config.maxInputBytes` 恒为 false,体积上限静默消失;
103
+ * `ingestEnabled` 是引用时它恒为真,开关关不掉。所以这一组也要能就地重灌。
104
+ */
105
+ export interface ConvertHost {
106
+ /** 按**当前**的 entry config 重灌一次设置,并按新值重置转换内核。 */
107
+ reprocess(): void
108
+ }
109
+
110
+ type Args = Record<string, any>
111
+
112
+ interface WebServerLike {
113
+ port?: number
114
+ host?: string
115
+ register(route: { kind: 'exact' | 'prefix'; path: string; handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> | void }): () => unknown
116
+ }
117
+
118
+ function output() {
119
+ return { schema: { type: 'json' } as const, render: (_args: unknown, value: unknown) => [{ type: 'text' as const, text: JSON.stringify(value) }] }
120
+ }
121
+
122
+ function tool(toolName: string, description: string, parameters: Record<string, unknown>, execute: (args: Args, exec: ToolRunContext) => Promise<unknown>) {
123
+ return defineTool({ name: toolName, description, parameters: parameters as any, output: output(), execute: execute as any } as any)
124
+ }
125
+
126
+ function lineageOf(args: Args, exec?: ToolRunContext): Record<string, string> | undefined {
127
+ const lineage: Record<string, string> = {}
128
+ const sessionId = typeof args.session_key === 'string' && args.session_key.trim() ? args.session_key.trim() : exec?.agent?.session.header.id
129
+ if (sessionId) lineage.sessionId = sessionId
130
+ const fields: Array<[string, string]> = [
131
+ ['turnId', 'turn_id'], ['intentId', 'intent_id'], ['taskId', 'task_id'],
132
+ ['subtaskId', 'subtask_id'], ['parentSubtaskId', 'parent_subtask_id'], ['runId', 'run_id'], ['traceId', 'trace_id'], ['domain', 'domain'],
133
+ ]
134
+ for (const [target, source] of fields) if (typeof args[source] === 'string' && args[source].trim()) lineage[target] = args[source].trim().slice(0, 160)
135
+ return Object.keys(lineage).length > 0 ? lineage : undefined
136
+ }
137
+
138
+ async function readJsonBody(req: IncomingMessage, maxBytes = 2 * 1024 * 1024): Promise<any> {
139
+ const chunks: Buffer[] = []
140
+ let bytes = 0
141
+ for await (const chunk of req) {
142
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
143
+ bytes += buffer.length
144
+ if (bytes > maxBytes) throw new Error('REQUEST_TOO_LARGE')
145
+ chunks.push(buffer)
146
+ }
147
+ if (!chunks.length) return {}
148
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
149
+ }
150
+
151
+ function send(res: ServerResponse, status: number, body: unknown): void {
152
+ res.statusCode = status
153
+ res.setHeader('content-type', 'application/json; charset=utf-8')
154
+ res.setHeader('cache-control', 'no-store')
155
+ res.end(JSON.stringify(body))
156
+ }
157
+
158
+ /** Compact task view for tools — the full ingest payload never enters context. */
159
+ function taskView(task: ConvertTask): Record<string, unknown> {
160
+ const manifest = task.manifest
161
+ return {
162
+ taskId: task.id,
163
+ state: task.state,
164
+ inputKind: task.inputKind,
165
+ title: manifest?.title ?? task.label,
166
+ sourceFormat: manifest?.sourceFormat ?? task.sourceFormat,
167
+ parser: manifest?.parser,
168
+ progress: task.progress,
169
+ stage: task.stage,
170
+ message: task.message,
171
+ error: task.error ?? null,
172
+ warnings: task.warnings.map(warning => warning.code),
173
+ attempts: task.attempts,
174
+ deliveryDir: task.deliveryDir,
175
+ assetCount: manifest?.assets.length ?? 0,
176
+ wordCount: manifest?.wordCount ?? 0,
177
+ contentHash: manifest?.contentHash,
178
+ externalId: manifest?.externalId,
179
+ sourceTypeCandidates: manifest?.ingest.sourceTypeCandidates ?? [],
180
+ sourceTypeRequired: manifest?.ingest.sourceTypeRequired ?? true,
181
+ preview: manifest?.ingest.content.slice(0, PREVIEW_CHARS) ?? null,
182
+ }
183
+ }
184
+
185
+ async function waitForTask(service: ConvertService, taskId: string, timeoutMs: number): Promise<ConvertTask | undefined> {
186
+ const deadline = Date.now() + timeoutMs
187
+ while (Date.now() < deadline) {
188
+ const task = service.get(taskId)
189
+ if (!task) return undefined
190
+ if (task.state === 'succeeded' || task.state === 'failed' || task.state === 'cancelled') return task
191
+ await new Promise(resolve => setTimeout(resolve, 250))
192
+ }
193
+ return service.get(taskId)
194
+ }
195
+
196
+ export function applyConvert(ctx: Context, deps: ConvertHostDeps): ConvertHost {
197
+ let config: ConvertPluginConfig = { ...DEFAULT_CONVERT_CONFIG }
198
+ let service: ConvertService | undefined
199
+ let starting: Promise<ConvertService | undefined> | undefined
200
+ let kernelHandler: ((req: IncomingMessage, res: ServerResponse) => Promise<void>) | undefined
201
+ /**
202
+ * Set when the optional kernel could not be loaded. Kept as the original error
203
+ * so the tool/route layer can surface its code and install suggestion verbatim.
204
+ */
205
+ let dependencyError: unknown
206
+
207
+ async function ensureService(): Promise<ConvertService | undefined> {
208
+ if (service) return service
209
+ if (!starting) {
210
+ starting = loadConvertCore()
211
+ .then(async core => {
212
+ // 目录都从知识插件的根路径推出来:状态文件落在 `tmp/` 根(它是运行期
213
+ // 事实,不是交付物,所以不占日期桶),交付与工作区各自按日期分桶。
214
+ const root = deps.agentRoot()
215
+ const created = await core.createConvertService({
216
+ rootDir: tmpRoot(root),
217
+ outRoot: workspaceOutputRoot(deps.workspace?.() ?? process.cwd()),
218
+ workRoot: tmpRoot(root),
219
+ // 根路径契约里 output/ 与 tmp/ 的二级就是日期(`layout.ts`),所以插件
220
+ // 显式打开分桶;独立内核保持扁平布局不受影响。
221
+ bucketByDay: true,
222
+ maxConcurrent: config.maxConcurrent,
223
+ maxInputBytes: config.maxInputBytes,
224
+ retentionDays: config.retentionDays,
225
+ })
226
+ service = created
227
+ kernelHandler = core.createRequestHandler({ service: created, basePath: MOUNT_PATH })
228
+ dependencyError = undefined
229
+ return created
230
+ })
231
+ .catch(error => {
232
+ starting = undefined
233
+ dependencyError = error
234
+ return undefined
235
+ })
236
+ }
237
+ return await starting
238
+ }
239
+
240
+ async function resetService(): Promise<void> {
241
+ const current = service
242
+ service = undefined
243
+ starting = undefined
244
+ kernelHandler = undefined
245
+ if (current) await current.close().catch(() => undefined)
246
+ }
247
+
248
+ /** The kernel is optional; this is the single place its absence becomes an answer. */
249
+ function missingDependency<T>(data: T): Envelope<T> {
250
+ const error = dependencyError
251
+ const message = error instanceof Error ? error.message : '转换内核 dsh-convert-core 不可用。'
252
+ const suggestion = error instanceof ConvertCoreMissingError
253
+ ? error.suggestion
254
+ : '安装可选依赖 dsh-convert-core 后重试。'
255
+ return failure(data, 'DEPENDENCY_MISSING', message, true, suggestion)
256
+ }
257
+
258
+ function knowledgeBaseUrl(): string {
259
+ const web = ctx.get('webServer') as WebServerLike | undefined
260
+ const host = web?.host === '0.0.0.0' ? '127.0.0.1' : web?.host ?? '127.0.0.1'
261
+ return `http://${host}:${web?.port ?? 0}`
262
+ }
263
+
264
+ /**
265
+ * Since 0018 the knowledge root is not resolved from another plugin's settings
266
+ * or an environment variable: the convert host runs inside the knowledge
267
+ * plugin and writes exactly where the live runtime reads.
268
+ *
269
+ * Since 0019 a delivery can also target a **specific** knowledge base: the
270
+ * panel imports a file into whichever base the user selected.
271
+ */
272
+ function resolveKnowledgeRoot(): { root: string; source: string } {
273
+ return { root: deps.knowledgeRoot(), source: 'knowledge-runtime' }
274
+ }
275
+
276
+ /** Ingest target for one knowledge base, falling back to the default base. */
277
+ function targetFor(kbId?: unknown): { runtime: IngestTarget; knowledgeRoot: string; baseId?: string } {
278
+ const id = typeof kbId === 'string' && kbId.trim() ? kbId.trim() : undefined
279
+ const resolved = id ? deps.resolveTarget?.(id) : undefined
280
+ if (resolved) return { ...resolved, baseId: id }
281
+ return { runtime: deps.runtime(), knowledgeRoot: deps.knowledgeRoot(), ...(id ? { baseId: id } : {}) }
282
+ }
283
+
284
+ /**
285
+ * 目标知识库中「存在且启用」的来源分类 id(0013)。
286
+ *
287
+ * 用作转换内核的候选提示,**取代内核按载体格式猜出来的那张表**——那张表会给出
288
+ * video / image 这类载体名,和「source_type 描述业务场景」直接冲突。分类判定
289
+ * 仍然由 knowledge 侧负责,这里只是把真实候选告诉调用方。
290
+ */
291
+ function sourceTypeCandidateIds(runtime: IngestTarget): string[] {
292
+ return (runtime.sourceTypes?.() ?? []).filter(def => def.enabled).map(def => def.id)
293
+ }
294
+
295
+ /**
296
+ * 设置来源。0.1.7 起设置随 `applyConvert(ctx, deps)` 的 `deps.config` 进来
297
+ * (插件 entry 的 `convert` 组),`settings.installSection` 那套命名空间注册
298
+ * 已经整个删除。组合层会合上 schema 默认值,这里再铺一层
299
+ * `DEFAULT_CONVERT_CONFIG` 兜底,好让直接调用 `applyConvert` 的装配方式也成立。
300
+ *
301
+ * 「改设置会让插件整个重载」这句话对 volatile 字段**不成立** —— loader 只就地改
302
+ * 引用再发一个事件(见 `settings-volatile.ts`)。所以重灌这件事由组合根在
303
+ * `loader/volatile-update` 时调 `reprocess()` 补上,这里只负责读值。
304
+ */
305
+ const readConfig = (): Partial<ConvertPluginConfig> =>
306
+ (typeof deps.config === 'function' ? deps.config() : deps.config) ?? {}
307
+ config = { ...DEFAULT_CONVERT_CONFIG, ...readConfig() }
308
+ void resetService()
309
+
310
+ ctx.on('tools/pre-execute', (exec, next): Promise<PreToolDecision> => {
311
+ if ((WRITE_TOOLS as readonly string[]).includes(exec.name)) {
312
+ return Promise.resolve({ kind: 'ask', reason: '该操作会转换本地内容,并写入 Tita Knowledge Store,需要本轮用户审批。' })
313
+ }
314
+ return next()
315
+ })
316
+
317
+ // ---------------------------------------------------------------- tools
318
+
319
+ ctx.tools.register(tool(
320
+ 'convert_capabilities',
321
+ 'Inspect the conversion kernel: available adapters, engine version, supported formats and where deliveries are written. Read-only; call this before promising a format will work.',
322
+ { session_key: { type: 'string' } },
323
+ async () => {
324
+ const kernel = await ensureService()
325
+ if (!kernel) return missingDependency({ adapters: [], supportedExtensions: [] })
326
+ // Forced: this is an explicit question ("can you convert X?"), not the console's
327
+ // poll, and the service caches the report so that poll stays cheap. An agent that
328
+ // just installed a binary and then asked would otherwise get the stale answer.
329
+ const report = await kernel.capabilities({ force: true })
330
+ const knowledge = resolveKnowledgeRoot()
331
+ return success({
332
+ adapters: report.adapters,
333
+ runtime: report.runtime,
334
+ defaults: report.defaults,
335
+ supportedExtensions: report.extensions,
336
+ rootDir: kernel.rootDir,
337
+ outDir: kernel.outDir,
338
+ consolePath: `${MOUNT_PATH}/`,
339
+ knowledgeRoot: knowledge.root,
340
+ knowledgeRootSource: knowledge.source,
341
+ knowledgeBaseUrl: knowledgeBaseUrl(),
342
+ ingestEnabled: config.ingestEnabled,
343
+ })
344
+ },
345
+ ))
346
+
347
+ ctx.tools.register(tool(
348
+ 'convert_source',
349
+ 'Convert a document (Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / text-layer PDF) or inline text into knowledge-ready Markdown + local images. Async kernel task; returns taskId, delivery dir and ingest hints (source_type candidates, external_id, content_hash). Write operation: DSH asks the user once before execution.',
350
+ {
351
+ kind: { type: 'string', enum: ['file', 'text'], required: true },
352
+ path: { type: 'string' },
353
+ text: { type: 'string' },
354
+ title: { type: 'string' },
355
+ source_type: { type: 'string' },
356
+ adapter: { type: 'string' },
357
+ wait: { type: 'boolean' },
358
+ session_key: { type: 'string' },
359
+ task_id: { type: 'string' },
360
+ subtask_id: { type: 'string' },
361
+ parent_subtask_id: { type: 'string' },
362
+ run_id: { type: 'string' },
363
+ turn_id: { type: 'string' },
364
+ trace_id: { type: 'string' },
365
+ domain: { type: 'string' },
366
+ },
367
+ async (args: Args, exec) => {
368
+ const kernel = await ensureService()
369
+ if (!kernel) return missingDependency({ taskId: null })
370
+ const kind = args.kind as ConvertRequest['kind']
371
+ const candidates = sourceTypeCandidateIds(targetFor(args.kb).runtime)
372
+ const request: ConvertRequest = {
373
+ kind,
374
+ ...(candidates.length > 0 ? { sourceTypeCandidates: candidates } : {}),
375
+ ...(typeof args.text === 'string' ? { text: args.text } : {}),
376
+ ...(typeof args.title === 'string' && args.title.trim() ? { title: args.title.trim() } : {}),
377
+ ...(typeof args.source_type === 'string' && args.source_type.trim() ? { sourceType: args.source_type.trim(), sourceTypeSource: 'agent' as const } : {}),
378
+ ...(typeof args.adapter === 'string' && args.adapter.trim() ? { adapter: args.adapter.trim() } : {}),
379
+ ...(lineageOf(args, exec) ? { lineage: lineageOf(args, exec)! } : {}),
380
+ }
381
+
382
+ // Local files are handed to the kernel as a path: it owns staging, size
383
+ // limits and cleanup, so the host never buffers a 256 MB file.
384
+ if (kind === 'file') {
385
+ if (typeof args.path !== 'string' || !args.path.trim()) {
386
+ return failure({ taskId: null }, 'PATH_REQUIRED', 'kind=file 必须提供本地绝对路径 path。', false, '用户上传的文件请走知识面板的「转换」页;Agent 侧只能读取本机已有路径。')
387
+ }
388
+ const absolute = resolve(args.path.trim())
389
+ try {
390
+ const info = await stat(absolute)
391
+ if (!info.isFile()) return failure({ taskId: null }, 'PATH_NOT_A_FILE', `路径不是文件:${absolute}`)
392
+ if (info.size > config.maxInputBytes) {
393
+ return failure({ taskId: null }, 'INPUT_TOO_LARGE', `文件 ${info.size} 字节超过上限 ${config.maxInputBytes}。`, false, '调大转换设置 maxInputBytes,或先压缩文件。')
394
+ }
395
+ } catch (cause) {
396
+ return failure({ taskId: null }, 'PATH_UNREADABLE', `无法读取 ${absolute}:${cause instanceof Error ? cause.message : String(cause)}`)
397
+ }
398
+ request.localPath = absolute
399
+ if (!request.title) request.title = basename(absolute)
400
+ }
401
+
402
+ const submitted = await kernel.submit(request)
403
+ const shouldWait = args.wait !== false
404
+ const finished = shouldWait ? await waitForTask(kernel, submitted.id, config.toolWaitMs) : submitted
405
+ const view = taskView(finished ?? submitted)
406
+ return success({
407
+ ...view,
408
+ waited: shouldWait,
409
+ ingestHint: '调用 convert_ingest 把该任务写入知识库;它会把 Markdown 与图片一起落地,并返回 cardHints。',
410
+ })
411
+ },
412
+ ))
413
+
414
+ ctx.tools.register(tool(
415
+ 'convert_task',
416
+ 'Inspect the conversion queue: list, get, cancel, retry, or read the delivered Markdown/manifest of a task. Read-only.',
417
+ {
418
+ action: { type: 'string', enum: ['list', 'get', 'cancel', 'retry', 'markdown', 'manifest'], required: true },
419
+ task_id: { type: 'string' },
420
+ limit: { type: 'integer' },
421
+ },
422
+ async (args: Args) => {
423
+ const kernel = await ensureService()
424
+ if (!kernel) return missingDependency({ tasks: [] })
425
+ const action = String(args.action)
426
+ if (action === 'list') {
427
+ const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(100, Number(args.limit))) : 20
428
+ return success({ tasks: kernel.list().slice(0, limit).map(taskView) })
429
+ }
430
+ const taskId = typeof args.task_id === 'string' ? args.task_id.trim() : ''
431
+ if (!taskId) return failure({ taskId: null }, 'TASK_ID_REQUIRED', `${action} 需要 task_id。`)
432
+ if (action === 'cancel') return success(taskView(kernel.cancel(taskId)))
433
+ if (action === 'retry') return success(taskView(await kernel.retry(taskId)))
434
+ if (action === 'markdown') {
435
+ const file = await kernel.readDeliveryFile(taskId, 'index.md')
436
+ if (!file) return failure({ taskId }, 'ARTIFACT_NOT_FOUND', '该任务没有 index.md(未成功或已被清理)。')
437
+ return success({ taskId, markdown: file.data.toString('utf8') })
438
+ }
439
+ if (action === 'manifest') {
440
+ const file = await kernel.readDeliveryFile(taskId, 'manifest.json')
441
+ if (!file) return failure({ taskId }, 'ARTIFACT_NOT_FOUND', '该任务没有 manifest.json。')
442
+ return success({ taskId, manifest: JSON.parse(file.data.toString('utf8')) })
443
+ }
444
+ const task = kernel.get(taskId)
445
+ if (!task) return failure({ taskId }, 'TASK_NOT_FOUND', `任务不存在:${taskId}`)
446
+ return success(taskView(task))
447
+ },
448
+ ))
449
+
450
+ ctx.tools.register(tool(
451
+ 'convert_ingest',
452
+ 'Ingest a delivered conversion into the knowledge store: copies images into the knowledge root, rewrites local image references, writes the SourceRaw document in-process, and returns cardHints so a wiki card can carry the images into multimodal recall. Write operation: DSH asks the user once before execution. Set plan_only=true for a dry run.',
453
+ {
454
+ convert_task_id: { type: 'string', required: true },
455
+ kb: { type: 'string', description: '目标知识库 id;省略则写入默认知识库' },
456
+ source_type: { type: 'string' },
457
+ title: { type: 'string' },
458
+ plan_only: { type: 'boolean' },
459
+ session_key: { type: 'string' },
460
+ task_id: { type: 'string' },
461
+ subtask_id: { type: 'string' },
462
+ parent_subtask_id: { type: 'string' },
463
+ run_id: { type: 'string' },
464
+ turn_id: { type: 'string' },
465
+ trace_id: { type: 'string' },
466
+ domain: { type: 'string' },
467
+ },
468
+ async (args: Args, exec) => {
469
+ if (!config.ingestEnabled) {
470
+ return failure({ taskId: String(args.convert_task_id) }, 'INGEST_DISABLED', '当前设置禁止转换写入知识库(ingestEnabled=false)。', false, '在 Tita Data Agent → 设置 → 转换 中开启「允许一键入库」,或手动调用 add_source。')
471
+ }
472
+ const kernel = await ensureService()
473
+ if (!kernel) return missingDependency({ taskId: String(args.convert_task_id) })
474
+ const target = targetFor(args.kb)
475
+ const result = await ingestDelivery({
476
+ service: kernel,
477
+ taskId: String(args.convert_task_id),
478
+ runtime: target.runtime,
479
+ knowledgeRoot: target.knowledgeRoot,
480
+ ...(typeof args.source_type === 'string' && args.source_type.trim() ? { sourceType: args.source_type.trim() } : {}),
481
+ ...(typeof args.title === 'string' && args.title.trim() ? { title: args.title.trim() } : {}),
482
+ planOnly: args.plan_only === true,
483
+ ...(lineageOf(args, exec) ? { lineage: lineageOf(args, exec)! } : {}),
484
+ })
485
+ return {
486
+ ...result,
487
+ data: {
488
+ ...(result.data as IngestOutcome),
489
+ knowledgeRootSource: 'knowledge-runtime',
490
+ ...(target.baseId ? { knowledgeBaseId: target.baseId } : {}),
491
+ },
492
+ }
493
+ },
494
+ ))
495
+
496
+ // ---------------------------------------------------------------- routes
497
+
498
+ ctx.inject(['webServer'], webCtx => {
499
+ const web = webCtx as unknown as Context & { webServer: WebServerLike }
500
+
501
+ // Plugin-level facts the kernel cannot know about itself.
502
+ const pluginFacts = (): Record<string, unknown> => {
503
+ const knowledge = resolveKnowledgeRoot()
504
+ const root = deps.agentRoot()
505
+ return {
506
+ rootDir: service?.rootDir ?? tmpRoot(root),
507
+ outDir: service?.outRoot ?? workspaceOutputRoot(deps.workspace?.() ?? process.cwd()),
508
+ workDir: service?.workRoot ?? tmpRoot(root),
509
+ mountPath: MOUNT_PATH,
510
+ knowledgeRoot: knowledge.root,
511
+ knowledgeRootSource: knowledge.source,
512
+ knowledgeBaseUrl: knowledgeBaseUrl(),
513
+ ingestEnabled: config.ingestEnabled,
514
+ kernelLoaded: service !== undefined,
515
+ }
516
+ }
517
+
518
+ const dispatch = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
519
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1')
520
+
521
+ if (url.pathname === `${MOUNT_PATH}/api/ingest`) {
522
+ if (req.method !== 'POST') return send(res, 405, failure({}, 'METHOD_NOT_ALLOWED', '该 endpoint 只接受 POST'))
523
+ try {
524
+ const body = await readJsonBody(req)
525
+ const kernel = await ensureService()
526
+ if (!kernel) return send(res, 503, missingDependency({ taskId: String(body.task_id ?? '') }))
527
+ const target = targetFor(body.kb)
528
+ const result = await ingestDelivery({
529
+ service: kernel,
530
+ taskId: String(body.task_id ?? ''),
531
+ runtime: target.runtime,
532
+ knowledgeRoot: target.knowledgeRoot,
533
+ ...(typeof body.source_type === 'string' && body.source_type.trim() ? { sourceType: body.source_type.trim() } : {}),
534
+ ...(typeof body.title === 'string' && body.title.trim() ? { title: body.title.trim() } : {}),
535
+ planOnly: body.plan_only === true,
536
+ ...(body.lineage && typeof body.lineage === 'object' ? { lineage: body.lineage as Record<string, string> } : {}),
537
+ })
538
+ return send(res, 200, {
539
+ ...result,
540
+ data: {
541
+ ...(result.data as IngestOutcome),
542
+ knowledgeRootSource: 'knowledge-runtime',
543
+ ...(target.baseId ? { knowledgeBaseId: target.baseId } : {}),
544
+ },
545
+ })
546
+ } catch (cause) {
547
+ return send(res, 400, failure({}, 'REQUEST_INVALID', cause instanceof Error ? cause.message : String(cause)))
548
+ }
549
+ }
550
+
551
+ if (url.pathname === `${MOUNT_PATH}/api/plugin`) {
552
+ if (req.method !== 'GET') return send(res, 405, failure({}, 'METHOD_NOT_ALLOWED', '该 endpoint 只接受 GET'))
553
+ return send(res, 200, success(pluginFacts()))
554
+ }
555
+
556
+ // Everything else belongs to the kernel console/API.
557
+ if (!kernelHandler) await ensureService()
558
+ if (!kernelHandler) return send(res, 503, missingDependency({ mountPath: MOUNT_PATH }))
559
+ return await kernelHandler(req, res)
560
+ }
561
+
562
+ const dispose = web.webServer.register({ kind: 'prefix', path: MOUNT_PATH, handler: dispatch })
563
+ return () => {
564
+ void dispose()
565
+ void resetService()
566
+ }
567
+ })
568
+
569
+ return {
570
+ reprocess: () => {
571
+ const next = { ...DEFAULT_CONVERT_CONFIG, ...readConfig() }
572
+ const changed = (Object.keys(next) as Array<keyof ConvertPluginConfig>).some(key => next[key] !== config[key])
573
+ config = next
574
+ // 只有真变了才重置内核:`loader/volatile-update` 是**这个 entry**的事件,但知识那组
575
+ // 的改动也会走到这里,无谓地关掉内核会让下一次转换重新起一遍内核。
576
+ if (changed) void resetService()
577
+ },
578
+ }
579
+ }