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,594 @@
1
+ import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type ReactNode } from 'react'
2
+ import {
3
+ AlertCircle, Check, Copy, Download, ExternalLink, FileText, FileUp, Inbox,
4
+ Package, RefreshCw, RotateCcw, ScanEye, X,
5
+ } from '@tita-data-agent/ui/icons'
6
+ import type { LucideIcon } from '@tita-data-agent/ui/icons'
7
+ import { Badge, Button, Input, Label, SelectMenu, Switch, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
8
+ import { styles } from './styles.ts'
9
+ import '@tita-data-agent/ui/styles.css'
10
+ // The panel ships its own `tdc-*` stylesheet; without this import the class names
11
+ // resolve to nothing and the panel renders unstyled.
12
+ import './styles.css'
13
+
14
+ /**
15
+ * The conversion kernel is mounted by the knowledge plugin at `/convert`.
16
+ * Frozen contract — see spec/VERSION-MATRIX.md.
17
+ */
18
+ const BASE = '/convert'
19
+
20
+ interface TaskSummary {
21
+ id: string
22
+ state: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'
23
+ inputKind: string
24
+ title: string
25
+ sourceFormat: string
26
+ parser?: string
27
+ progress: number | null
28
+ stage: string
29
+ message?: string
30
+ error?: { code: string; message: string; suggestion?: string } | null
31
+ warnings: Array<{ code: string; message: string }>
32
+ assetCount: number
33
+ wordCount: number
34
+ contentHash?: string
35
+ externalId?: string
36
+ deliveryDir?: string
37
+ sourceTypeCandidates: string[]
38
+ }
39
+
40
+ interface AdapterCapability {
41
+ id: string
42
+ description: string
43
+ kinds: string[]
44
+ extensions: string[]
45
+ availability: { available: boolean; reason?: string; suggestion?: string }
46
+ }
47
+
48
+ interface CapabilityReport {
49
+ adapters: AdapterCapability[]
50
+ supportedExtensions: string[]
51
+ runtime: Record<string, { available: boolean; version?: string; error?: string }>
52
+ rootDir: string
53
+ outDir: string
54
+ knowledgeRoot: string
55
+ knowledgeRootSource: string
56
+ ingestEnabled: boolean
57
+ }
58
+
59
+ /** Plugin-level facts served by `/convert/api/plugin` (the kernel cannot know them). */
60
+ interface PluginInfo {
61
+ rootDir: string
62
+ outDir: string
63
+ mountPath: string
64
+ knowledgeRoot: string
65
+ knowledgeRootSource: string
66
+ knowledgeBaseUrl: string
67
+ ingestEnabled: boolean
68
+ }
69
+
70
+ interface IngestResult {
71
+ taskId: string
72
+ sourceId?: string
73
+ sourcePath?: string
74
+ knowledgeRoot: string
75
+ externalId: string
76
+ contentHash: string
77
+ sourceFormat: string
78
+ parser: string
79
+ sourceType: string
80
+ sourceTypeCandidates: string[]
81
+ assets: Array<{ rel: string; target: string; bytes: number }>
82
+ cardHints: { entityTypes: string[]; sourceRefs: string[]; title: string; summary: string; contentWithImages: string; imageRefs: string[] }
83
+ notes: string[]
84
+ knowledgeRootSource?: string
85
+ }
86
+
87
+ interface Envelope<T> { ok?: boolean; data?: T; warnings?: string[]; error?: { code?: string; message?: string; suggestion?: string } }
88
+
89
+ async function requestJson<T>(url: string, init?: RequestInit): Promise<Envelope<T>> {
90
+ const response = await fetch(url, init)
91
+ const text = await response.text()
92
+ if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
93
+ let payload: Envelope<T>
94
+ try {
95
+ payload = JSON.parse(text) as Envelope<T>
96
+ } catch {
97
+ throw new Error(`服务端返回了无效响应(HTTP ${response.status})`)
98
+ }
99
+ if (!response.ok || payload.ok === false) {
100
+ const error = new Error([payload.error?.code, payload.error?.message].filter(Boolean).join(' · ') || `请求失败(${response.status})`)
101
+ ;(error as Error & { suggestion?: string }).suggestion = payload.error?.suggestion
102
+ throw error
103
+ }
104
+ return payload
105
+ }
106
+
107
+ function describe(cause: unknown): string {
108
+ if (!(cause instanceof Error)) return String(cause)
109
+ const suggestion = (cause as Error & { suggestion?: string }).suggestion
110
+ return suggestion ? `${cause.message} — 建议:${suggestion}` : cause.message
111
+ }
112
+
113
+ const STATE_META: Record<TaskSummary['state'], { label: string; tone: 'neutral' | 'success' | 'warning' | 'blue' }> = {
114
+ queued: { label: '排队', tone: 'warning' },
115
+ running: { label: '进行中', tone: 'blue' },
116
+ succeeded: { label: '已交付', tone: 'success' },
117
+ failed: { label: '失败', tone: 'neutral' },
118
+ cancelled: { label: '已取消', tone: 'neutral' },
119
+ }
120
+
121
+ const KIND_META: Record<string, { label: string; icon: LucideIcon }> = {
122
+ file: { label: '文件', icon: FileText },
123
+ text: { label: '文本', icon: FileUp },
124
+ }
125
+
126
+ function formatBytes(bytes: number | undefined): string {
127
+ if (!bytes) return '0 B'
128
+ const units = ['B', 'KB', 'MB', 'GB']
129
+ let value = bytes
130
+ let unit = 0
131
+ while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1 }
132
+ return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`
133
+ }
134
+
135
+ export interface ConvertPanelProps {
136
+ visible: boolean
137
+ /** Settings scope handed in by the client shell; the panel reads live values from the plugin API. */
138
+ settings?: { convert?: unknown }
139
+ }
140
+
141
+ export function ConvertPanel({ visible }: ConvertPanelProps): ReactNode {
142
+ const [view, setView] = useState('tasks')
143
+ const [tasks, setTasks] = useState<TaskSummary[]>([])
144
+ const [capabilities, setCapabilities] = useState<CapabilityReport | null>(null)
145
+ const [selected, setSelected] = useState<TaskSummary | null>(null)
146
+ const [markdown, setMarkdown] = useState('')
147
+ const [ingest, setIngest] = useState<IngestResult | null>(null)
148
+ const [error, setError] = useState<string | null>(null)
149
+ const [notice, setNotice] = useState<string | null>(null)
150
+ const [busy, setBusy] = useState(false)
151
+ const [form, setForm] = useState({ kind: 'upload', path: '', text: '', title: '', sourceType: '', adapter: '' })
152
+ const [uploadFile, setUploadFile] = useState<File | null>(null)
153
+ const [ingestForm, setIngestForm] = useState({ sourceType: '', title: '', planOnly: false })
154
+
155
+ const load = useCallback(async () => {
156
+ try {
157
+ const [tasksPayload, capabilityPayload, pluginPayload] = await Promise.all([
158
+ requestJson<{ tasks: TaskSummary[] }>(`${BASE}/api/tasks`),
159
+ requestJson<CapabilityReport>(`${BASE}/api/capabilities`),
160
+ requestJson<PluginInfo>(`${BASE}/api/plugin`),
161
+ ])
162
+ setTasks(tasksPayload.data?.tasks ?? [])
163
+ setCapabilities(capabilityPayload.data ? { ...capabilityPayload.data, ...pluginPayload.data } as CapabilityReport : null)
164
+ setError(null)
165
+ } catch (cause) {
166
+ setError(describe(cause))
167
+ }
168
+ }, [])
169
+
170
+ useEffect(() => { void load() }, [load, visible])
171
+
172
+ useEffect(() => {
173
+ if (!visible) return undefined
174
+ const timer = setInterval(() => { void load() }, 4000)
175
+ return () => clearInterval(timer)
176
+ }, [load, visible])
177
+
178
+ const selectedId = selected?.id ?? ''
179
+
180
+ const openTask = useCallback(async (task: TaskSummary) => {
181
+ setSelected(task)
182
+ setIngest(null)
183
+ setMarkdown('')
184
+ setIngestForm({ sourceType: '', title: task.title ?? '', planOnly: false })
185
+ try {
186
+ const detail = await requestJson<TaskSummary>(`${BASE}/api/tasks/${encodeURIComponent(task.id)}`)
187
+ if (detail.data) setSelected(detail.data)
188
+ if (task.state === 'succeeded') {
189
+ const body = await fetch(`${BASE}/api/tasks/${encodeURIComponent(task.id)}/markdown`)
190
+ if (body.ok) setMarkdown(await body.text())
191
+ }
192
+ } catch (cause) {
193
+ setError(describe(cause))
194
+ }
195
+ }, [])
196
+
197
+ const submit = useCallback(async () => {
198
+ setBusy(true)
199
+ setError(null)
200
+ setNotice(null)
201
+ try {
202
+ const common = {
203
+ title: form.title.trim() || undefined,
204
+ source_type: form.sourceType.trim() || undefined,
205
+ adapter: form.adapter.trim() || undefined,
206
+ }
207
+ let payload: Envelope<{ id?: string }>
208
+ if (form.kind === 'upload') {
209
+ // The kernel owns multipart parsing, staging and cleanup, so the browser
210
+ // hands it the bytes directly instead of a path only the server can see.
211
+ if (!uploadFile) throw new Error('请先选择一个文件。')
212
+ const body = new FormData()
213
+ body.append('file', uploadFile, uploadFile.name)
214
+ if (common.title) body.append('title', common.title)
215
+ if (common.source_type) body.append('source_type', common.source_type)
216
+ if (common.adapter) body.append('adapter', common.adapter)
217
+ payload = await requestJson(`${BASE}/api/tasks`, { method: 'POST', body })
218
+ } else if (form.kind === 'file') {
219
+ if (!form.path.trim()) throw new Error('本机路径转换需要一个绝对路径(供 Agent 或本机脚本使用)。')
220
+ payload = await requestJson(`${BASE}/api/tasks`, {
221
+ method: 'POST',
222
+ headers: { 'content-type': 'application/json' },
223
+ body: JSON.stringify({ kind: 'file', local_path: form.path.trim(), ...common }),
224
+ })
225
+ } else {
226
+ payload = await requestJson(`${BASE}/api/tasks`, {
227
+ method: 'POST',
228
+ headers: { 'content-type': 'application/json' },
229
+ body: JSON.stringify({ kind: 'text', text: form.text, ...common }),
230
+ })
231
+ }
232
+ setNotice(`任务已受理:${payload.data?.id ?? ''}`)
233
+ setUploadFile(null)
234
+ setView('tasks')
235
+ await load()
236
+ } catch (cause) {
237
+ setError(describe(cause))
238
+ } finally {
239
+ setBusy(false)
240
+ }
241
+ }, [form, load, uploadFile])
242
+
243
+ const act = useCallback(async (taskId: string, action: 'cancel' | 'retry') => {
244
+ try {
245
+ await requestJson(`${BASE}/api/tasks/${encodeURIComponent(taskId)}/${action}`, { method: 'POST' })
246
+ await load()
247
+ } catch (cause) {
248
+ setError(describe(cause))
249
+ }
250
+ }, [load])
251
+
252
+ const runIngest = useCallback(async () => {
253
+ if (!selectedId) return
254
+ setBusy(true)
255
+ setError(null)
256
+ setNotice(null)
257
+ try {
258
+ const result = await requestJson<IngestResult>(`${BASE}/api/ingest`, {
259
+ method: 'POST',
260
+ headers: { 'content-type': 'application/json' },
261
+ body: JSON.stringify({
262
+ task_id: selectedId,
263
+ source_type: ingestForm.sourceType.trim() || undefined,
264
+ title: ingestForm.title.trim() || undefined,
265
+ plan_only: ingestForm.planOnly,
266
+ }),
267
+ })
268
+ setIngest(result.data ?? null)
269
+ setNotice(ingestForm.planOnly ? '已完成入库预演(未写入)' : `已写入知识库:${result.data?.sourceId ?? ''}`)
270
+ } catch (cause) {
271
+ setError(describe(cause))
272
+ } finally {
273
+ setBusy(false)
274
+ }
275
+ }, [ingestForm, selectedId])
276
+
277
+ const copy = useCallback((value: string) => {
278
+ void navigator.clipboard?.writeText(value).then(() => setNotice('已复制到剪贴板')).catch(() => setError('复制失败'))
279
+ }, [])
280
+
281
+ const cardPrompt = useMemo(() => {
282
+ if (!ingest) return ''
283
+ const hints = ingest.cardHints
284
+ return [
285
+ `请为刚录入的来源 ${ingest.sourceId ?? ''} 写一张 wiki 卡片,让图片进入多模态召回:`,
286
+ `- entity_type 建议:${hints.entityTypes.join(' / ')}(当前知识 schema 只接受这些)`,
287
+ `- source_refs:${hints.sourceRefs.join(', ')}`,
288
+ `- 标题:${hints.title}`,
289
+ `- 正文必须使用下面这些图片相对路径(相对 02_wiki/<entity_type>/ 解析):`,
290
+ ...hints.imageRefs.map(ref => ` - ${ref}`),
291
+ '调用 write_knowledge_card 时把图片引用原样写进 content。',
292
+ ].join('\n')
293
+ }, [ingest])
294
+
295
+ const runningCount = tasks.filter(task => task.state === 'running' || task.state === 'queued').length
296
+
297
+ return (
298
+ <div className={styles.panel}>
299
+ <div className={styles.header}>
300
+ <div className={styles.headerCopy}>
301
+ <h3 className={styles.headerTitle}>格式转换</h3>
302
+ <p className={styles.headerSubtitle}>
303
+ PDF / Office / 链接 / 视频 → 知识就绪 Markdown + 本地图片
304
+ </p>
305
+ </div>
306
+ <div className={styles.headerActions}>
307
+ <a className="tita-uiButton" href={`${BASE}/`} target="_blank" rel="noreferrer" title="打开内核控制台(Vue3)">
308
+ <ExternalLink size={14} />
309
+ </a>
310
+ <Button variant="ghost" size="sm" onClick={() => void load()} title="刷新">
311
+ <RefreshCw size={14} />
312
+ </Button>
313
+ </div>
314
+ </div>
315
+
316
+ <div className={styles.body}>
317
+ {error && <p className={cn(styles.small, styles.error)}><AlertCircle size={12} /> {error}</p>}
318
+ {notice && <p className={cn(styles.small, styles.notice)}><Check size={12} /> {notice}</p>}
319
+
320
+ <Tabs value={view} onValueChange={setView}>
321
+ <TabsList>
322
+ <TabsTrigger value="tasks">任务{runningCount > 0 ? ` (${runningCount})` : ''}</TabsTrigger>
323
+ <TabsTrigger value="new">新转换</TabsTrigger>
324
+ <TabsTrigger value="ingest">知识入库</TabsTrigger>
325
+ <TabsTrigger value="caps">能力</TabsTrigger>
326
+ </TabsList>
327
+
328
+ {/* ---------------------------------------------------------- tasks */}
329
+ <TabsContent value="tasks">
330
+ {tasks.length === 0 && <p className={styles.empty}>还没有转换任务。</p>}
331
+ <div className={styles.taskList}>
332
+ {tasks.map(task => {
333
+ const meta = STATE_META[task.state]
334
+ const kind = KIND_META[task.inputKind] ?? KIND_META.file!
335
+ const KindIcon = kind.icon
336
+ return (
337
+ <div key={task.id} className={cn(styles.task, selectedId === task.id && styles.taskActive)}>
338
+ <div className={styles.taskMain}>
339
+ <div className={styles.taskTitle}>
340
+ <KindIcon size={12} /> {task.title}
341
+ </div>
342
+ <div className={styles.taskMeta}>
343
+ {task.parser ?? task.inputKind} · {task.assetCount} 图 / {task.wordCount} 字 · {task.stage}
344
+ {task.message ? ` · ${task.message}` : ''}
345
+ </div>
346
+ {task.state === 'running' && (
347
+ <div className={styles.progress}>
348
+ <div className={styles.progressBar} style={{ width: task.progress === null ? '100%' : `${Math.round(task.progress * 100)}%` }} />
349
+ </div>
350
+ )}
351
+ {task.error && (
352
+ <div className={cn(styles.taskMeta, styles.error)}>
353
+ {task.error.code} · {task.error.message}
354
+ {task.error.suggestion ? ` — ${task.error.suggestion}` : ''}
355
+ </div>
356
+ )}
357
+ </div>
358
+ <div className={styles.taskSide}>
359
+ <Badge tone={meta.tone}>{meta.label}</Badge>
360
+ <div className={styles.taskActions}>
361
+ <Button variant="ghost" size="sm" title="详情" onClick={() => void openTask(task)}><ScanEye size={13} /></Button>
362
+ {task.state === 'running' || task.state === 'queued'
363
+ ? <Button variant="ghost" size="sm" title="取消" onClick={() => void act(task.id, 'cancel')}><X size={13} /></Button>
364
+ : <Button variant="ghost" size="sm" title="重试" onClick={() => void act(task.id, 'retry')}><RotateCcw size={13} /></Button>}
365
+ {task.state === 'succeeded' && (
366
+ <a className="tita-uiButton" href={`${BASE}/api/tasks/${encodeURIComponent(task.id)}/files/index.md`} target="_blank" rel="noreferrer" title="打开 index.md">
367
+ <Download size={13} />
368
+ </a>
369
+ )}
370
+ </div>
371
+ </div>
372
+ </div>
373
+ )
374
+ })}
375
+ </div>
376
+ </TabsContent>
377
+
378
+ {/* ------------------------------------------------------------ new */}
379
+ <TabsContent value="new">
380
+ <div className={styles.field}>
381
+ <span className={styles.fieldLabel}>输入类型</span>
382
+ <SelectMenu
383
+ value={form.kind}
384
+ onValueChange={next => setForm({ ...form, kind: next })}
385
+ aria-label="输入类型"
386
+ options={[
387
+ { value: 'upload', label: '上传文件' },
388
+ { value: 'file', label: '文档(本机路径)' },
389
+ { value: 'text', label: '内联文本' },
390
+ ]}
391
+ />
392
+ </div>
393
+
394
+ {form.kind === 'upload' && (
395
+ <label className={styles.field}>
396
+ <span className={styles.fieldLabel}>文件</span>
397
+ <input
398
+ className={styles.fileInput}
399
+ type="file"
400
+ accept={(capabilities?.supportedExtensions ?? ['.docx', '.pdf', '.pptx', '.xlsx', '.csv', '.epub', '.rtf']).join(',')}
401
+ onChange={(event: ChangeEvent<HTMLInputElement>) => setUploadFile(event.target.files?.[0] ?? null)}
402
+ />
403
+ <span className={styles.fieldHint}>
404
+ 支持 {(capabilities?.supportedExtensions ?? []).map(item => item.replace('.', '')).join(' ')};文件由内核直接接收,不需要先放到本机某个路径。
405
+ </span>
406
+ {uploadFile && <span className={styles.fieldHint}>已选择:{uploadFile.name}({formatBytes(uploadFile.size)})</span>}
407
+ </label>
408
+ )}
409
+
410
+ {form.kind === 'file' && (
411
+ <label className={styles.field}>
412
+ <span className={styles.fieldLabel}>本机绝对路径</span>
413
+ <Input value={form.path} spellCheck={false} placeholder="/Users/you/Documents/spec.docx" onChange={(event: ChangeEvent<HTMLInputElement>) => setForm({ ...form, path: event.target.value })} />
414
+ <span className={styles.fieldHint}>
415
+ 给 Agent 或本机脚本用的入口:文件必须已经在运行 DSH 的这台机器上。
416
+ </span>
417
+ </label>
418
+ )}
419
+
420
+ {form.kind === 'text' && (
421
+ <label className={styles.field}>
422
+ <span className={styles.fieldLabel}>文本</span>
423
+ <textarea className={styles.pre} rows={8} value={form.text} onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setForm({ ...form, text: event.target.value })} />
424
+ </label>
425
+ )}
426
+
427
+ <div className={styles.grid2}>
428
+ <label className={styles.field}>
429
+ <span className={styles.fieldLabel}>标题(可选)</span>
430
+ <Input value={form.title} onChange={(event: ChangeEvent<HTMLInputElement>) => setForm({ ...form, title: event.target.value })} />
431
+ </label>
432
+ <label className={styles.field}>
433
+ <span className={styles.fieldLabel}>source_type(可选)</span>
434
+ <Input value={form.sourceType} placeholder="article / document" onChange={(event: ChangeEvent<HTMLInputElement>) => setForm({ ...form, sourceType: event.target.value })} />
435
+ </label>
436
+ </div>
437
+
438
+ <div className={styles.field}>
439
+ <span className={styles.fieldLabel}>adapter(可选)</span>
440
+ <SelectMenu
441
+ value={form.adapter}
442
+ onValueChange={next => setForm({ ...form, adapter: next })}
443
+ aria-label="adapter(可选)"
444
+ options={[
445
+ { value: '', label: '自动选择' },
446
+ ...(capabilities?.adapters ?? []).map(adapter => ({
447
+ value: adapter.id,
448
+ label: `${adapter.id}${adapter.availability.available ? '' : '(不可用)'}`,
449
+ })),
450
+ ]}
451
+ />
452
+ </div>
453
+
454
+ <Button variant="default" disabled={busy} onClick={() => void submit()}>
455
+ <FileUp size={14} /> {busy ? '提交中…' : '开始转换'}
456
+ </Button>
457
+ </TabsContent>
458
+
459
+ {/* --------------------------------------------------------- ingest */}
460
+ <TabsContent value="ingest">
461
+ {!selectedId && <p className={styles.empty}>先在「任务」里选择一个已交付的任务。</p>}
462
+ {selectedId && (
463
+ <>
464
+ <dl className={styles.kv}>
465
+ <div><dt>任务</dt><dd><span className={styles.code}>{selectedId}</span></dd></div>
466
+ <div><dt>标题</dt><dd>{selected?.title}</dd></div>
467
+ <div><dt>交付目录</dt><dd><span className={styles.code}>{selected?.deliveryDir}</span></dd></div>
468
+ <div><dt>content_hash</dt><dd><span className={styles.code}>{selected?.contentHash}</span></dd></div>
469
+ <div><dt>source_type 候选</dt><dd>{(selected?.sourceTypeCandidates ?? []).join(' / ') || '—'}</dd></div>
470
+ <div><dt>知识库根目录</dt><dd><span className={styles.code}>{capabilities?.knowledgeRoot}</span> <span className={styles.muted}>({capabilities?.knowledgeRootSource})</span></dd></div>
471
+ </dl>
472
+
473
+ <div className={styles.grid2}>
474
+ <label className={styles.field}>
475
+ <span className={styles.fieldLabel}>source_type</span>
476
+ <Input value={ingestForm.sourceType} placeholder="document" onChange={(event: ChangeEvent<HTMLInputElement>) => setIngestForm({ ...ingestForm, sourceType: event.target.value })} />
477
+ <span className={styles.fieldHint}>0013 的分类注册表落地前,知识库只接受内置分类;填错会被它拒绝并返回可选值。</span>
478
+ </label>
479
+ <label className={styles.field}>
480
+ <span className={styles.fieldLabel}>标题覆盖(可选)</span>
481
+ <Input value={ingestForm.title} onChange={(event: ChangeEvent<HTMLInputElement>) => setIngestForm({ ...ingestForm, title: event.target.value })} />
482
+ </label>
483
+ </div>
484
+
485
+ <div className={cn(styles.row, styles.section)}>
486
+ <Switch checked={ingestForm.planOnly} onCheckedChange={checked => setIngestForm({ ...ingestForm, planOnly: checked })} />
487
+ <Label>仅预演(plan_only,不写入)</Label>
488
+ </div>
489
+
490
+ <Button variant="default" disabled={busy || !capabilities?.ingestEnabled} onClick={() => void runIngest()}>
491
+ <Package size={14} /> {ingestForm.planOnly ? '预演入库' : '写入知识库'}
492
+ </Button>
493
+ {capabilities && !capabilities.ingestEnabled && (
494
+ <p className={styles.fieldHint}>插件设置里关闭了「允许一键入库」。</p>
495
+ )}
496
+
497
+ {ingest && (
498
+ <div className={styles.section}>
499
+ <h4 className={styles.sectionTitle}>入库结果</h4>
500
+ <dl className={styles.kv}>
501
+ <div><dt>source_id</dt><dd><span className={styles.code}>{ingest.sourceId ?? '(预演)'}</span></dd></div>
502
+ <div><dt>来源路径</dt><dd><span className={styles.code}>{ingest.sourcePath ?? '—'}</span></dd></div>
503
+ <div><dt>图片资产</dt><dd>{ingest.assets.length} 个</dd></div>
504
+ </dl>
505
+
506
+ {ingest.assets.length > 0 && (
507
+ <div className={styles.assetGrid}>
508
+ {ingest.assets.map(asset => (
509
+ <figure key={asset.rel} className={styles.assetCard}>
510
+ <img src={`${BASE}/api/tasks/${encodeURIComponent(selectedId)}/files/${asset.rel}`} alt={asset.rel} loading="lazy" />
511
+ <figcaption>{formatBytes(asset.bytes)}</figcaption>
512
+ </figure>
513
+ ))}
514
+ </div>
515
+ )}
516
+
517
+ {ingest.notes.length > 0 && (
518
+ <ul className={cn(styles.small, styles.muted)}>
519
+ {ingest.notes.map(note => <li key={note}>{note}</li>)}
520
+ </ul>
521
+ )}
522
+
523
+ <h4 className={styles.sectionTitle}>给 Agent 的卡片提示</h4>
524
+ <pre className={styles.pre}>{cardPrompt}</pre>
525
+ <div className={cn(styles.row, styles.section)}>
526
+ <Button variant="ghost" size="sm" onClick={() => copy(cardPrompt)}><Copy size={13} /> 复制</Button>
527
+ <Button variant="ghost" size="sm" onClick={() => copy(ingest.cardHints.contentWithImages)}><Copy size={13} /> 复制卡片正文</Button>
528
+ </div>
529
+ <p className={styles.fieldHint}>
530
+ 图片必须在 wiki 卡片里被引用才会进入多模态向量(编译段每个单元只取第一张图),所以这一步不能省。
531
+ </p>
532
+ </div>
533
+ )}
534
+ </>
535
+ )}
536
+ </TabsContent>
537
+
538
+ {/* ----------------------------------------------------------- caps */}
539
+ <TabsContent value="caps">
540
+ {!capabilities && <p className={styles.empty}>能力信息不可用。</p>}
541
+ {capabilities && (
542
+ <>
543
+ <ul className={styles.capList}>
544
+ {capabilities.adapters.map(adapter => (
545
+ <li key={adapter.id} className={styles.capItem}>
546
+ <span className={adapter.availability.available ? styles.dotOn : styles.dotOff} />
547
+ <div>
548
+ <strong>{adapter.id}</strong>
549
+ <div className={styles.taskMeta}>{adapter.description}</div>
550
+ {!adapter.availability.available && adapter.availability.reason && (
551
+ <div className={cn(styles.taskMeta, styles.error)}>{adapter.availability.reason}</div>
552
+ )}
553
+ {adapter.availability.suggestion && <div className={styles.taskMeta}>{adapter.availability.suggestion}</div>}
554
+ </div>
555
+ </li>
556
+ ))}
557
+ </ul>
558
+
559
+ <h4 className={styles.sectionTitle}>支持的输入格式</h4>
560
+ <p className={styles.taskMeta}>{(capabilities.supportedExtensions ?? []).join(' ')}</p>
561
+
562
+ <h4 className={styles.sectionTitle}>引擎</h4>
563
+ <ul className={styles.capList}>
564
+ {Object.entries(capabilities.runtime).map(([name, probe]) => (
565
+ <li key={name} className={styles.capItem}>
566
+ <span className={probe.available ? styles.dotOn : styles.dotOff} />
567
+ <div>
568
+ <strong>{name}</strong>
569
+ <div className={styles.taskMeta}>{probe.available ? probe.version : probe.error}</div>
570
+ </div>
571
+ </li>
572
+ ))}
573
+ </ul>
574
+
575
+ <h4 className={styles.sectionTitle}>路径</h4>
576
+ <dl className={styles.kv}>
577
+ <div><dt>交付根目录</dt><dd><span className={styles.code}>{capabilities.outDir}</span></dd></div>
578
+ <div><dt>知识库根目录</dt><dd><span className={styles.code}>{capabilities.knowledgeRoot}</span></dd></div>
579
+ </dl>
580
+ </>
581
+ )}
582
+ </TabsContent>
583
+ </Tabs>
584
+
585
+ {selected && markdown && (
586
+ <div className={styles.section}>
587
+ <h4 className={styles.sectionTitle}><Inbox size={12} /> 交付预览 · {selected.title}</h4>
588
+ <pre className={styles.pre}>{markdown.slice(0, 6000)}</pre>
589
+ </div>
590
+ )}
591
+ </div>
592
+ </div>
593
+ )
594
+ }