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.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- 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
|
+
}
|