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,367 @@
1
+ /**
2
+ * The knowledge-ingestion seam.
3
+ *
4
+ * Rules this file obeys, and why:
5
+ *
6
+ * 1. **One write entry, in-process.** Since 0018 the convert host lives inside
7
+ * the knowledge plugin, so ingestion calls `KnowledgeRuntime.addSource()`
8
+ * directly instead of `POST /knowledge/sources`. That removes a JSON body
9
+ * cap (the old transport 400'd above ~1.2 MB) and removes port discovery,
10
+ * startup ordering and error-code re-translation from the failure surface.
11
+ * The structural `IngestTarget` interface is deliberately the only knowledge
12
+ * API this module may touch — conversion code must not reach into the store.
13
+ * 2. **The kernel never writes the knowledge store.** It only produces
14
+ * `convert-out/<taskId>/`. This module is the translation step.
15
+ * 3. **`source_type` stays the knowledge side's decision.** We propose
16
+ * candidates; if the store rejects the value we surface its error verbatim
17
+ * instead of silently defaulting.
18
+ * 4. **Images must land inside the knowledge root**, because retrieval only
19
+ * reads local relative paths — and only the *first* image of a unit reaches
20
+ * the multimodal embedder, so we also emit card hints that tell the Agent how
21
+ * to reference them.
22
+ */
23
+
24
+ import { cp, mkdir, stat } from 'node:fs/promises'
25
+ import { basename, join, resolve, sep } from 'node:path'
26
+ import type { ConvertService, IngestPayload } from 'dsh-convert-core'
27
+ import type { LineageContext } from 'dsh-data-contracts/refs'
28
+ import type { SourceType, SourceTypeDef } from '../types.js'
29
+ import {
30
+ failure,
31
+ KNOWLEDGE_ASSET_DIR,
32
+ KNOWLEDGE_SOURCE_DIR,
33
+ KNOWLEDGE_WIKI_DIR,
34
+ success,
35
+ type Envelope,
36
+ } from './types.js'
37
+
38
+ /**
39
+ * The only slice of the knowledge store this seam is allowed to use. Declared
40
+ * structurally so this module never imports `runtime.ts`.
41
+ */
42
+ export interface IngestTarget {
43
+ addSource(input: {
44
+ sourceType: SourceType
45
+ title?: string
46
+ content: string
47
+ externalId?: string
48
+ metadata?: Record<string, unknown>
49
+ sourceId?: string
50
+ lineage?: LineageContext
51
+ }): Promise<{
52
+ ok: boolean
53
+ data: { sourceId: string; path: string }
54
+ error?: { code?: string; message?: string; suggestion?: string } | null
55
+ }>
56
+ /**
57
+ * 该知识库的来源分类注册表(0013,**只读**)。
58
+ *
59
+ * 只用来把「候选分类」告诉调用方,转换模块不参与分类判定、也不改注册表,
60
+ * 所以它仍然是 knowledge 侧的能力,不算越界写。可选:老的测试替身可以不给。
61
+ */
62
+ sourceTypes?(): SourceTypeDef[]
63
+ }
64
+
65
+ export interface IngestOptions {
66
+ service: ConvertService
67
+ taskId: string
68
+ /** The live knowledge runtime; the write goes through `addSource` only. */
69
+ runtime: IngestTarget
70
+ /** Absolute knowledge root the runtime currently writes to. */
71
+ knowledgeRoot: string
72
+ /** Business classification. Omitted means "let the store decide/default". */
73
+ sourceType?: string
74
+ title?: string
75
+ metadata?: Record<string, unknown>
76
+ lineage?: Record<string, unknown>
77
+ /** Dry run: build the plan without copying assets or writing the source. */
78
+ planOnly?: boolean
79
+ }
80
+
81
+ export interface PlacedAsset {
82
+ rel: string
83
+ /** Absolute path inside the knowledge root. */
84
+ target: string
85
+ bytes: number
86
+ }
87
+
88
+ export interface IngestOutcome {
89
+ taskId: string
90
+ sourceId?: string
91
+ sourcePath?: string
92
+ knowledgeRoot: string
93
+ externalId: string
94
+ contentHash: string
95
+ sourceFormat: string
96
+ parser: string
97
+ sourceType: string
98
+ sourceTypeSupplied: boolean
99
+ sourceTypeCandidates: string[]
100
+ assets: PlacedAsset[]
101
+ /** Path to the source file relative to the knowledge root, e.g. `01_source_truth/x.md`. */
102
+ relativeSourcePath?: string
103
+ cardHints: CardHints
104
+ notes: string[]
105
+ }
106
+
107
+ export interface CardHints {
108
+ /** `entity_type` values accepted by the knowledge schema. */
109
+ entityTypes: string[]
110
+ sourceRefs: string[]
111
+ title: string
112
+ summary: string
113
+ /**
114
+ * Card body with image references already re-based to `02_wiki/<entityType>/`,
115
+ * so the first image actually reaches the multimodal embedder.
116
+ */
117
+ contentWithImages: string
118
+ /** The image references as they must appear inside a card body. */
119
+ imageRefs: string[]
120
+ }
121
+
122
+ const ENTITY_TYPES = ['concept', 'metric', 'table', 'event', 'playbook', 'case']
123
+
124
+ /** Refuse any path that would escape the knowledge root. */
125
+ export function safeJoin(root: string, ...parts: string[]): string | undefined {
126
+ const base = resolve(root)
127
+ const target = resolve(base, ...parts)
128
+ if (target !== base && !target.startsWith(base + sep)) return undefined
129
+ return target
130
+ }
131
+
132
+ function rewriteImageRefs(markdown: string, mapping: Map<string, string>): string {
133
+ let result = markdown
134
+ for (const [from, to] of mapping) {
135
+ result = result.split(`(${from})`).join(`(${to})`)
136
+ }
137
+ return result
138
+ }
139
+
140
+ function cardImageRef(hash: string, rel: string, entityType: string): string {
141
+ // A card lives at 02_wiki/<entityType>/<slug>.md; the asset lives at
142
+ // 01_source_truth/_assets/<hash>/<file>.
143
+ const depth = 2
144
+ const up = Array.from({ length: depth }, () => '..').join('/')
145
+ return `${up}/${KNOWLEDGE_SOURCE_DIR}/${KNOWLEDGE_ASSET_DIR}/${hash}/${basename(rel)}`
146
+ }
147
+
148
+ export function buildCardHints(input: {
149
+ payload: IngestPayload
150
+ sourceId: string
151
+ assetBucket: string
152
+ assetMapping: Map<string, string>
153
+ entityType?: string
154
+ }): CardHints {
155
+ const entityType = input.entityType && ENTITY_TYPES.includes(input.entityType) ? input.entityType : 'concept'
156
+ const content = rewriteImageRefs(
157
+ input.payload.content,
158
+ new Map([...input.assetMapping].map(([from, to]) => [from, cardImageRef(input.assetBucket, to, entityType)])),
159
+ )
160
+ const imageRefs = input.payload.assets.map(asset => cardImageRef(input.assetBucket, asset.rel, entityType))
161
+ const summary = input.payload.content
162
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
163
+ .replace(/[#>*`_\-[\]()]/g, ' ')
164
+ .replace(/\s+/g, ' ')
165
+ .trim()
166
+ .slice(0, 160)
167
+ return {
168
+ entityTypes: ENTITY_TYPES,
169
+ sourceRefs: [input.sourceId],
170
+ title: input.payload.title,
171
+ summary,
172
+ contentWithImages: content,
173
+ imageRefs,
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Turn a store rejection into an actionable suggestion without inventing one.
179
+ *
180
+ * 0013:分类候选来自**当前知识库的注册表**(含用户自定义分类),不再写死三个内置值。
181
+ */
182
+ function suggestionFor(code: string, candidates: string[]): string | undefined {
183
+ if (code === 'SOURCE_TYPE_UNKNOWN') {
184
+ return `当前知识库的分类注册表里没有这个分类。可用分类:${candidates.join(' / ') || '(无)'};要新增分类请到「知识库设置 → 来源类型」,并写清怎么判断它是这一类。`
185
+ }
186
+ if (code === 'SOURCE_TYPE_DISABLED') {
187
+ return `该分类已停用。可用分类:${candidates.join(' / ') || '(无)'};要恢复请到「知识库设置 → 来源类型」重新启用。`
188
+ }
189
+ // 这里曾经有一条 `CONTENT_TOO_LARGE` 的建议(「调大 maxSourceBytes」)。0029 撤掉
190
+ // 那个上限之后,`addSource` 再也不会产出这个 code,留着它就成了一条指向不存在开关
191
+ // 的指路牌 —— 环境里已经没有任何东西会触发它。
192
+ if (code === 'SOURCE_TRUTH_DISABLED') {
193
+ return '在知识插件设置里打开「允许写入 SourceTruth」(allowSourceTruth)。'
194
+ }
195
+ return undefined
196
+ }
197
+
198
+ export async function ingestDelivery(options: IngestOptions): Promise<Envelope<IngestOutcome>> {
199
+ const { service, taskId } = options
200
+ let payload: IngestPayload
201
+ try {
202
+ payload = await service.ingestPlan(taskId)
203
+ } catch (cause) {
204
+ const error = cause as Error & { code?: string; suggestion?: string }
205
+ return failure(
206
+ emptyOutcome(taskId, options.knowledgeRoot, options.sourceType),
207
+ error.code ?? 'TASK_NOT_READY',
208
+ error.message,
209
+ false,
210
+ error.suggestion,
211
+ )
212
+ }
213
+
214
+ const bucket = payload.contentHash.slice(0, 12)
215
+ const assetRoot = safeJoin(options.knowledgeRoot, KNOWLEDGE_SOURCE_DIR, KNOWLEDGE_ASSET_DIR, bucket)
216
+ if (!assetRoot) {
217
+ return failure(emptyOutcome(taskId, options.knowledgeRoot, options.sourceType), 'PATH_OUTSIDE_KNOWLEDGE_ROOT', '资产目标路径越出知识库根目录。')
218
+ }
219
+
220
+ const assetMapping = new Map<string, string>()
221
+ const placed: PlacedAsset[] = []
222
+ const notes: string[] = []
223
+ const warnings: string[] = []
224
+
225
+ if (options.planOnly !== true && payload.assets.length > 0) {
226
+ await mkdir(assetRoot, { recursive: true })
227
+ for (const asset of payload.assets) {
228
+ const target = safeJoin(assetRoot, basename(asset.rel))
229
+ if (!target) {
230
+ warnings.push(`跳过越界资产:${asset.rel}`)
231
+ continue
232
+ }
233
+ try {
234
+ await cp(asset.path, target, { force: true })
235
+ const info = await stat(target)
236
+ placed.push({ rel: asset.rel, target, bytes: info.size })
237
+ assetMapping.set(asset.rel, asset.rel)
238
+ } catch (cause) {
239
+ warnings.push(`资产复制失败(${asset.rel}):${cause instanceof Error ? cause.message : String(cause)}`)
240
+ }
241
+ }
242
+ if (placed.length !== payload.assets.length) {
243
+ notes.push(`${payload.assets.length - placed.length} 个图片资产未复制,正文中的对应引用会指向不存在的文件。`)
244
+ }
245
+ } else if (options.planOnly === true) {
246
+ for (const asset of payload.assets) assetMapping.set(asset.rel, asset.rel)
247
+ notes.push('planOnly=true:未复制资产、未写入 SourceRaw。')
248
+ }
249
+
250
+ const sourceContent = rewriteImageRefs(
251
+ payload.content,
252
+ new Map([...assetMapping].map(([from, to]) => [from, `${KNOWLEDGE_ASSET_DIR}/${bucket}/${basename(to)}`])),
253
+ )
254
+
255
+ const metadata: Record<string, unknown> = {
256
+ ...(options.metadata ?? {}),
257
+ source_format: payload.sourceFormat,
258
+ parser: payload.parser,
259
+ content_hash: payload.contentHash,
260
+ convert_task_id: taskId,
261
+ convert_contract_version: 1,
262
+ ...(payload.sourceUrl ? { source_url: payload.sourceUrl } : {}),
263
+ ...(payload.sourcePath ? { source_path: payload.sourcePath } : {}),
264
+ assets: placed.map(asset => `${KNOWLEDGE_ASSET_DIR}/${bucket}/${basename(asset.rel)}`),
265
+ }
266
+
267
+ const sourceType = options.sourceType?.trim() || 'document'
268
+ // 分类候选取自当前知识库的注册表(0013):内置分类 + 用户自定义,只列启用的。
269
+ const sourceTypeCandidates = (options.runtime.sourceTypes?.() ?? [])
270
+ .filter((def: SourceTypeDef) => def.enabled)
271
+ .map((def: SourceTypeDef) => def.id)
272
+ payload.sourceTypeCandidates = sourceTypeCandidates
273
+ const title = options.title?.trim() || payload.title
274
+ const externalId = payload.externalId
275
+
276
+ if (!options.planOnly) {
277
+ const result = await options.runtime.addSource({
278
+ sourceType: sourceType as SourceType,
279
+ title,
280
+ content: sourceContent,
281
+ externalId,
282
+ metadata,
283
+ ...(options.lineage ? { lineage: options.lineage as LineageContext } : {}),
284
+ })
285
+
286
+ if (!result.ok) {
287
+ const code = result.error?.code ?? 'KNOWLEDGE_WRITE_FAILED'
288
+ return failure(
289
+ emptyOutcome(taskId, options.knowledgeRoot, options.sourceType),
290
+ code,
291
+ result.error?.message ?? '知识库拒绝了该来源',
292
+ false,
293
+ result.error?.suggestion ?? suggestionFor(code, sourceTypeCandidates),
294
+ )
295
+ }
296
+
297
+ const sourceId = result.data.sourceId
298
+ const sourcePath = result.data.path
299
+ const cardHints = buildCardHints({ payload, sourceId, assetBucket: bucket, assetMapping })
300
+ notes.push('SourceRaw 已写入;图片要进入多模态召回,还需要一张引用它们的 wiki 卡片(见 cardHints)。')
301
+
302
+ return success<IngestOutcome>(
303
+ {
304
+ taskId,
305
+ sourceId,
306
+ ...(sourcePath ? { sourcePath } : {}),
307
+ knowledgeRoot: options.knowledgeRoot,
308
+ externalId,
309
+ contentHash: payload.contentHash,
310
+ sourceFormat: payload.sourceFormat,
311
+ parser: payload.parser,
312
+ sourceType,
313
+ sourceTypeSupplied: Boolean(options.sourceType?.trim()),
314
+ sourceTypeCandidates: payload.sourceTypeCandidates,
315
+ assets: placed,
316
+ ...(sourcePath ? { relativeSourcePath: relativeTo(options.knowledgeRoot, sourcePath) } : {}),
317
+ cardHints,
318
+ notes,
319
+ },
320
+ warnings,
321
+ )
322
+ }
323
+
324
+ // Dry run: report what would happen, including the card hints.
325
+ const preview = buildCardHints({ payload, sourceId: '<add_source 返回的 sourceId>', assetBucket: bucket, assetMapping })
326
+ return success<IngestOutcome>(
327
+ {
328
+ taskId,
329
+ knowledgeRoot: options.knowledgeRoot,
330
+ externalId,
331
+ contentHash: payload.contentHash,
332
+ sourceFormat: payload.sourceFormat,
333
+ parser: payload.parser,
334
+ sourceType,
335
+ sourceTypeSupplied: Boolean(options.sourceType?.trim()),
336
+ sourceTypeCandidates: payload.sourceTypeCandidates,
337
+ assets: payload.assets.map(asset => ({ rel: asset.rel, target: join(assetRoot, basename(asset.rel)), bytes: asset.bytes })),
338
+ cardHints: preview,
339
+ notes: [...notes, `将写入 ${KNOWLEDGE_SOURCE_DIR},资产放入 ${KNOWLEDGE_ASSET_DIR}/${bucket}/`],
340
+ },
341
+ )
342
+ }
343
+
344
+ function relativeTo(root: string, path: string): string {
345
+ const base = resolve(root)
346
+ const target = resolve(path)
347
+ return target.startsWith(base + sep) ? target.slice(base.length + 1) : target
348
+ }
349
+
350
+ function emptyOutcome(taskId: string, knowledgeRoot: string, sourceType: string | undefined): IngestOutcome {
351
+ return {
352
+ taskId,
353
+ knowledgeRoot,
354
+ externalId: '',
355
+ contentHash: '',
356
+ sourceFormat: '',
357
+ parser: '',
358
+ sourceType: sourceType?.trim() || 'document',
359
+ sourceTypeSupplied: Boolean(sourceType?.trim()),
360
+ sourceTypeCandidates: [],
361
+ assets: [],
362
+ cardHints: { entityTypes: ENTITY_TYPES, sourceRefs: [], title: '', summary: '', contentWithImages: '', imageRefs: [] },
363
+ notes: [],
364
+ }
365
+ }
366
+
367
+ export { ENTITY_TYPES, KNOWLEDGE_WIKI_DIR }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 转换设置的 schema(纯数据,单独一个模块)。
3
+ *
4
+ * 与 `src/intent/settings-schema.ts` 同一个理由:0.1.7 之后它要被 `Config` 组合、
5
+ * 并被 `tests/settings-schema.test.mjs` 独立解析一遍,而 `host.ts` 会 `import`
6
+ * 工具层 —— 那条依赖在 `node --test` 里拉不起来(`dsh-tools` 的
7
+ * `@deepseek-ai/dsh-sandbox` 是未安装的 peer)。
8
+ *
9
+ * 0029 起这里**没有路径字段**:交付物与工作区都从知识插件的 `agentRoot` 推出来
10
+ * (`<agentRoot>/output/<日期>/`、`<agentRoot>/tmp/`)。一个 Agent 只有一个根路径,
11
+ * 所以这里再放一个 `rootDir` 就等于把「东西存在哪」这个答案劈成两半。
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import z from '@deepseek-ai/schemastery'
17
+ import { volatileFields } from '../settings-volatile.js'
18
+ import { DEFAULT_CONVERT_CONFIG, type ConvertPluginConfig } from './types.js'
19
+
20
+ /** 显式标注是为了 TS2742(见 `intent/settings-schema.ts` 上的同一条注释)。 */
21
+ export const CONVERT_SETTINGS_SCHEMA: Schemastery<ConvertPluginConfig> = z.object(volatileFields({
22
+ maxConcurrent: z.number().step(1).min(1).max(8).default(DEFAULT_CONVERT_CONFIG.maxConcurrent),
23
+ maxInputBytes: z.number().step(1).min(1024).default(DEFAULT_CONVERT_CONFIG.maxInputBytes),
24
+ retentionDays: z.number().step(1).max(365).default(DEFAULT_CONVERT_CONFIG.retentionDays),
25
+ toolWaitMs: z.number().step(1).min(1000).max(900000).default(DEFAULT_CONVERT_CONFIG.toolWaitMs),
26
+ ingestEnabled: z.boolean().default(DEFAULT_CONVERT_CONFIG.ingestEnabled),
27
+ }))
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Plugin-level types, defaults and the envelope shape for the **convert seam**.
3
+ *
4
+ * These live inside `data-knowledge` since 0018: conversion is a sub-capability
5
+ * of the knowledge plugin, not a separately mounted plugin. The kernel itself
6
+ * (`dsh-convert-core`) stays an independent, optional package.
7
+ */
8
+
9
+ import { homedir } from 'node:os'
10
+ import { join } from 'node:path'
11
+
12
+ export const CONVERT_SETTINGS_NAMESPACE = 'tita-data-agent-convert'
13
+
14
+ /** Knowledge store layout the convert seam cooperates with (issue 0013). */
15
+ export const KNOWLEDGE_SOURCE_DIR = '01_source_truth'
16
+ export const KNOWLEDGE_WIKI_DIR = '02_wiki'
17
+ /** Sub-directory inside `01_source_truth` that holds converted images. */
18
+ export const KNOWLEDGE_ASSET_DIR = '_assets'
19
+
20
+ export interface ConvertPluginConfig {
21
+ maxConcurrent: number
22
+ maxInputBytes: number
23
+ /** Deliveries older than this are pruned on start; 0 disables. */
24
+ retentionDays: number
25
+ /** How long a tool call waits for the conversion before returning `running`. */
26
+ toolWaitMs: number
27
+ /** Allow `convert_ingest` to write into the knowledge store. */
28
+ ingestEnabled: boolean
29
+ }
30
+
31
+ /**
32
+ * @deprecated 0029:转换不再有自己的根目录。交付物落
33
+ * `<agentRoot>/output/<日期>/`、工作区落 `<agentRoot>/tmp/<日期>/`,
34
+ * 由 `layout.ts` 统一算出来。这个函数只留给**独立运行的内核**
35
+ * (`bin/tita-convert.mjs`)与它的单测,插件侧不再读它。
36
+ */
37
+ export function defaultConvertRoot(): string {
38
+ if (process.env.DSH_CONVERT_ROOT) return process.env.DSH_CONVERT_ROOT
39
+ return join(homedir(), '.tita-data-agent', 'convert')
40
+ }
41
+
42
+ export const DEFAULT_CONVERT_CONFIG: ConvertPluginConfig = {
43
+ maxConcurrent: 2,
44
+ maxInputBytes: 256 * 1024 * 1024,
45
+ retentionDays: 14,
46
+ toolWaitMs: 120_000,
47
+ ingestEnabled: true,
48
+ }
49
+
50
+ export interface Envelope<T> {
51
+ ok: boolean
52
+ data: T
53
+ warnings?: string[]
54
+ error?: { code: string; message: string; suggestion?: string; retryable?: boolean }
55
+ }
56
+
57
+ export function success<T>(data: T, warnings?: string[]): Envelope<T> {
58
+ return { ok: true, data, ...(warnings && warnings.length > 0 ? { warnings } : {}) }
59
+ }
60
+
61
+ export function failure<T>(data: T, code: string, message: string, retryable = false, suggestion?: string): Envelope<T> {
62
+ return { ok: false, data, error: { code, message, retryable, ...(suggestion ? { suggestion } : {}) } }
63
+ }
64
+
65
+ export const WRITE_TOOLS = ['convert_source', 'convert_ingest'] as const