dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.14
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/README.md +84 -52
- package/cordis.patch.yml +21 -17
- package/eval/benchmark-extraction-format.mjs +20 -0
- package/lib/client.js +2846 -3872
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +1299 -1660
- package/lib/types/axis-registry.d.ts +6 -0
- package/lib/types/axis-registry.d.ts.map +1 -1
- package/lib/types/axis-registry.js +45 -0
- package/lib/types/axis-registry.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +7 -3
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +13 -8
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +4 -13
- package/lib/types/diagnosis/run.d.ts.map +1 -1
- package/lib/types/diagnosis/run.js +26 -16
- package/lib/types/diagnosis/run.js.map +1 -1
- package/lib/types/diagnosis/schema.js +2 -2
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +1 -1
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +5 -4
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +4 -4
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +6 -8
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +8 -16
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +41 -66
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +10 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/environment/service.d.ts +4 -3
- package/lib/types/environment/service.d.ts.map +1 -1
- package/lib/types/environment/service.js +91 -23
- package/lib/types/environment/service.js.map +1 -1
- package/lib/types/environment/types.d.ts +9 -0
- package/lib/types/environment/types.d.ts.map +1 -1
- package/lib/types/events.d.ts +2 -0
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js +13 -0
- package/lib/types/events.js.map +1 -1
- package/lib/types/extract-contract.d.ts +1 -1
- package/lib/types/extract-contract.d.ts.map +1 -1
- package/lib/types/extract-contract.js +34 -31
- package/lib/types/extract-contract.js.map +1 -1
- package/lib/types/extract-model.d.ts +2 -0
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract-table-contract.d.ts +3 -0
- package/lib/types/extract-table-contract.d.ts.map +1 -0
- package/lib/types/extract-table-contract.js +30 -0
- package/lib/types/extract-table-contract.js.map +1 -0
- package/lib/types/extract-table.d.ts +8 -0
- package/lib/types/extract-table.d.ts.map +1 -0
- package/lib/types/extract-table.js +151 -0
- package/lib/types/extract-table.js.map +1 -0
- package/lib/types/extraction-runs.d.ts +3 -0
- package/lib/types/extraction-runs.d.ts.map +1 -1
- package/lib/types/extraction-runs.js +6 -0
- package/lib/types/extraction-runs.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +196 -44
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/scope.d.ts.map +1 -1
- package/lib/types/intent/scope.js +1 -0
- package/lib/types/intent/scope.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +3 -2
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/knowledge.d.ts +3 -0
- package/lib/types/knowledge.d.ts.map +1 -1
- package/lib/types/knowledge.js +12 -3
- package/lib/types/knowledge.js.map +1 -1
- package/lib/types/model-text.d.ts +1 -1
- package/lib/types/model-text.d.ts.map +1 -1
- package/lib/types/model-text.js +5 -4
- package/lib/types/model-text.js.map +1 -1
- package/lib/types/pipeline/engine.d.ts +57 -0
- package/lib/types/pipeline/engine.d.ts.map +1 -0
- package/lib/types/pipeline/engine.js +268 -0
- package/lib/types/pipeline/engine.js.map +1 -0
- package/lib/types/pipeline/store.d.ts +56 -0
- package/lib/types/pipeline/store.d.ts.map +1 -0
- package/lib/types/pipeline/store.js +115 -0
- package/lib/types/pipeline/store.js.map +1 -0
- package/lib/types/pipeline/types.d.ts +82 -0
- package/lib/types/pipeline/types.d.ts.map +1 -0
- package/lib/types/pipeline/types.js +5 -0
- package/lib/types/pipeline/types.js.map +1 -0
- package/lib/types/present.d.ts +2 -0
- package/lib/types/present.d.ts.map +1 -1
- package/lib/types/present.js +9 -4
- package/lib/types/present.js.map +1 -1
- package/lib/types/runtime.d.ts +20 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +389 -112
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/source-evidence.d.ts.map +1 -1
- package/lib/types/source-evidence.js +2 -0
- package/lib/types/source-evidence.js.map +1 -1
- package/lib/types/store/axis-references.js +3 -3
- package/lib/types/store/axis-references.js.map +1 -1
- package/lib/types/store/migrate.js +1 -1
- package/lib/types/store/migrate.js.map +1 -1
- package/lib/types/types.d.ts +3 -4
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-materialize.d.ts.map +1 -1
- package/lib/types/wiki-materialize.js +3 -15
- package/lib/types/wiki-materialize.js.map +1 -1
- package/lib/types/wiki-trial-types.d.ts +3 -0
- package/lib/types/wiki-trial-types.d.ts.map +1 -1
- package/lib/types/wiki-trial.d.ts +2 -0
- package/lib/types/wiki-trial.d.ts.map +1 -1
- package/lib/types/wiki-trial.js +31 -7
- package/lib/types/wiki-trial.js.map +1 -1
- package/package.json +6 -3
- package/preset/tita-data-agent/agent.cordis.yml +21 -17
- package/src/axis-registry.ts +38 -0
- package/src/client/AttributionEventGuide.tsx +10 -9
- package/src/client/EnvironmentSetup.tsx +35 -15
- package/src/client/ExtractionTasksPage.tsx +129 -176
- package/src/client/KnowledgeBaseTab.tsx +31 -76
- package/src/client/NodeSettings.tsx +2 -3
- package/src/client/PipelineNotice.tsx +26 -0
- package/src/client/TabGuide.tsx +4 -4
- package/src/client/TypeDefinitionDrawer.tsx +23 -8
- package/src/client/WikiCardPreview.tsx +43 -10
- package/src/client/WikiFlowPanel.tsx +4 -4
- package/src/client/architecture.tsx +1 -1
- package/src/client/attribution/AttributionTab.tsx +6 -6
- package/src/client/attribution/CaseView.tsx +3 -1
- package/src/client/attribution/EventView.tsx +14 -4
- package/src/client/attribution/RunView.tsx +2 -2
- package/src/client/attribution/graph-options.ts +2 -2
- package/src/client/pages.tsx +2 -2
- package/src/diagnosis/cards.ts +13 -8
- package/src/diagnosis/run.ts +27 -17
- package/src/diagnosis/schema.ts +2 -2
- package/src/diagnosis/seed-source.ts +5 -4
- package/src/diagnosis/seed.ts +6 -8
- package/src/diagnosis/store.ts +47 -68
- package/src/diagnosis/types.ts +10 -16
- package/src/environment/service.ts +79 -23
- package/src/environment/types.ts +6 -0
- package/src/events.ts +12 -0
- package/src/extract-contract.ts +6 -3
- package/src/extract-model.ts +2 -0
- package/src/extract-table-contract.ts +29 -0
- package/src/extract-table.ts +126 -0
- package/src/extraction-runs.ts +6 -0
- package/src/index.ts +156 -45
- package/src/intent/scope.ts +1 -0
- package/src/knowledge-bases.ts +3 -2
- package/src/knowledge.ts +13 -3
- package/src/model-text.ts +5 -3
- package/src/pipeline/engine.ts +238 -0
- package/src/pipeline/store.ts +119 -0
- package/src/pipeline/types.ts +74 -0
- package/src/present.ts +10 -4
- package/src/runtime.ts +350 -110
- package/src/source-evidence.ts +1 -0
- package/src/store/axis-references.ts +3 -3
- package/src/store/migrate.ts +1 -1
- package/src/types.ts +3 -4
- package/src/wiki-materialize.ts +3 -14
- package/src/wiki-trial-types.ts +3 -0
- package/src/wiki-trial.ts +26 -7
|
@@ -1,189 +1,142 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
|
+
import { Badge, Button, Tabs, TabsContent, TabsList, TabsTrigger } from '@tita-agent/ui/components'
|
|
3
|
+
import { Check, ExternalLink, Pause, Play, RefreshCw, X } from '@tita-agent/ui/icons'
|
|
4
|
+
import { PIPELINE_ACTIVE_STATES, PIPELINE_STAGE_LABEL, type PipelineEvent, type PipelineStage, type PipelineState, type PipelineStatus, type PipelineTask } from '../pipeline/types.js'
|
|
5
5
|
import { formatDateTime } from './format.ts'
|
|
6
6
|
import { requestJson } from './request.ts'
|
|
7
|
+
import type { ExtractionRunSummary } from './wiki-trial.ts'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const STATES: Record<PipelineState, string> = { queued: '排队中', running: '进行中', pausing: '暂停中', paused: '已暂停', cancelling: '停止中', succeeded: '已完成', partial: '部分完成', failed: '失败', cancelled: '已停止', interrupted: '已中断' }
|
|
10
|
+
const SOURCE_STATES = { pending: '待抽取', extracted: '待归一', normalized: '已归一', outdated: '来源已更新' }
|
|
11
|
+
const GROUP_STATES = { new: '待新建', changed: '待更新', current: '已物化', withdrawn: '待停用', blocked: '先归一来源' }
|
|
12
|
+
interface History { tasks: PipelineTask[]; total: number; extractions: { runs: ExtractionRunSummary[]; total: number } | null }
|
|
13
|
+
const toggle = (ids: string[], id: string): string[] => ids.includes(id) ? ids.filter(value => value !== id) : [...ids, id]
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
only: string
|
|
14
|
-
state: TaskState
|
|
15
|
-
queuedAt: string
|
|
16
|
-
startedAt: string
|
|
17
|
-
finishedAt?: string
|
|
18
|
-
queued?: boolean
|
|
19
|
-
sourcesTotal: number
|
|
20
|
-
sourcesDone: number
|
|
21
|
-
steps: CompileStepView[]
|
|
22
|
-
outcome?: string
|
|
23
|
-
failed?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const STATE_LABEL: Record<TaskState, string> = {
|
|
27
|
-
queued: '排队中', running: '进行中', pausing: '暂停中', paused: '已暂停',
|
|
28
|
-
cancelling: '停止中', succeeded: '已完成', failed: '失败', cancelled: '已停止',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const STATE_TONE: Record<TaskState, 'neutral' | 'success' | 'warning' | 'blue'> = {
|
|
32
|
-
queued: 'neutral', running: 'blue', pausing: 'warning', paused: 'warning',
|
|
33
|
-
cancelling: 'warning', succeeded: 'success', failed: 'warning', cancelled: 'neutral',
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const TASK_LABEL: Record<string, string> = {
|
|
37
|
-
all: '全量重编', wiki: '增量编译', schema: 'Schema 更新', embeddings: '向量重建',
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function loadTasks(kb: string): Promise<ExtractionTask[]> {
|
|
41
|
-
const params = new URLSearchParams({ kb })
|
|
42
|
-
const envelope = await requestJson<{ tasks: ExtractionTask[] }>(`/knowledge/extraction-tasks?${params}`)
|
|
43
|
-
return envelope.data?.tasks ?? []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function updateTask(kb: string, runId: string, action: TaskAction): Promise<ExtractionTask> {
|
|
47
|
-
const envelope = await requestJson<{ task: ExtractionTask }>('/knowledge/extraction-task', {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: { 'content-type': 'application/json' },
|
|
50
|
-
body: JSON.stringify({ kb, run_id: runId, action }),
|
|
51
|
-
})
|
|
52
|
-
if (!envelope.data?.task) throw new Error('任务状态没有返回')
|
|
53
|
-
return envelope.data.task
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function stateOf(task: ExtractionTask): TaskState {
|
|
57
|
-
return task.state ?? (task.finishedAt ? (task.failed ? 'failed' : 'succeeded') : task.queued ? 'queued' : 'running')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function taskDetail(task: ExtractionTask): string {
|
|
61
|
-
const state = stateOf(task)
|
|
62
|
-
if (state === 'queued') return '等待前面的任务结束'
|
|
63
|
-
const step = task.steps.find(item => item.state === 'running')
|
|
64
|
-
const progress = task.sourcesTotal > 0 ? `文档 ${task.sourcesDone}/${task.sourcesTotal}` : ''
|
|
65
|
-
if (state === 'running' || state === 'pausing' || state === 'paused' || state === 'cancelling') {
|
|
66
|
-
return [step?.facts[0] ?? step?.id, progress].filter(Boolean).join(' · ') || '正在准备编译'
|
|
67
|
-
}
|
|
68
|
-
return task.outcome ?? (state === 'succeeded' ? '任务完成' : state === 'cancelled' ? '任务已停止' : '任务失败')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function TaskActions({ task, kb, onChange }: {
|
|
72
|
-
task: ExtractionTask
|
|
73
|
-
kb: string
|
|
74
|
-
onChange: (task: ExtractionTask) => void
|
|
75
|
-
}): ReactNode {
|
|
76
|
-
const [busy, setBusy] = useState(false)
|
|
77
|
-
const [failure, setFailure] = useState('')
|
|
78
|
-
const state = stateOf(task)
|
|
79
|
-
const active = ['queued', 'running', 'pausing', 'paused', 'cancelling'].includes(state)
|
|
80
|
-
const act = async (action: TaskAction): Promise<void> => {
|
|
81
|
-
setBusy(true)
|
|
82
|
-
setFailure('')
|
|
83
|
-
try { onChange(await updateTask(kb, task.runId, action)) }
|
|
84
|
-
catch (cause) { setFailure(cause instanceof Error ? cause.message : String(cause)) }
|
|
85
|
-
finally { setBusy(false) }
|
|
86
|
-
}
|
|
87
|
-
if (!active) return null
|
|
88
|
-
return <span className="extractTaskActions">
|
|
89
|
-
{failure && <span className="extractTaskActionError" role="alert">{failure}</span>}
|
|
90
|
-
{state !== 'cancelling' && <>
|
|
91
|
-
{state === 'paused'
|
|
92
|
-
? <Button size="sm" variant="outline" disabled={busy} onClick={() => void act('resume')}><Play size={13} />继续</Button>
|
|
93
|
-
: <Button size="sm" variant="outline" disabled={busy || state === 'pausing'} onClick={() => void act('pause')}><Pause size={13} />暂停</Button>}
|
|
94
|
-
<Button size="sm" variant="destructive" disabled={busy} onClick={() => void act('cancel')}><CircleX size={13} />停止</Button>
|
|
95
|
-
</>}
|
|
96
|
-
</span>
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function TaskRow({ task, kb, onChange }: {
|
|
100
|
-
task: ExtractionTask
|
|
101
|
-
kb: string
|
|
102
|
-
onChange: (task: ExtractionTask) => void
|
|
103
|
-
}): ReactNode {
|
|
104
|
-
const [stepsOpen, setStepsOpen] = useState(() => ['queued', 'running', 'pausing', 'paused', 'cancelling'].includes(stateOf(task)))
|
|
105
|
-
const state = stateOf(task)
|
|
106
|
-
return <article className="extractTaskRow">
|
|
107
|
-
<div className="extractTaskMain">
|
|
108
|
-
<div className="extractTaskTitle">
|
|
109
|
-
<strong>{TASK_LABEL[task.only] ?? '编译任务'}</strong>
|
|
110
|
-
<Badge tone={STATE_TONE[state]}>{STATE_LABEL[state]}</Badge>
|
|
111
|
-
</div>
|
|
112
|
-
<p>{taskDetail(task)}</p>
|
|
113
|
-
<span className="extractTaskTime"><Clock3 size={12} />{formatDateTime(task.startedAt || task.queuedAt)}</span>
|
|
114
|
-
</div>
|
|
115
|
-
<div className="extractTaskControls">
|
|
116
|
-
<Button size="sm" variant="ghost" className="extractTaskStepToggle" aria-expanded={stepsOpen}
|
|
117
|
-
onClick={() => setStepsOpen(value => !value)}>
|
|
118
|
-
{stepsOpen ? '收起步骤' : '查看步骤'}<ChevronDown size={13} />
|
|
119
|
-
</Button>
|
|
120
|
-
<TaskActions task={task} kb={kb} onChange={onChange} />
|
|
121
|
-
</div>
|
|
122
|
-
{stepsOpen && <div className="extractTaskSteps"><CompileStepList steps={task.steps} /></div>}
|
|
123
|
-
</article>
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function ExtractionTasksPage({ kb }: { kb: string }): ReactNode {
|
|
127
|
-
const [tasks, setTasks] = useState<ExtractionTask[] | null>(null)
|
|
128
|
-
const [kbName, setKbName] = useState('')
|
|
15
|
+
function TaskOutput({ kb, id, onTask }: { kb: string; id: string; onTask: (task: PipelineTask) => void }) {
|
|
16
|
+
const [events, setEvents] = useState<PipelineEvent[]>([])
|
|
129
17
|
const [failure, setFailure] = useState('')
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
18
|
+
const callback = useRef(onTask)
|
|
19
|
+
callback.current = onTask
|
|
133
20
|
useEffect(() => {
|
|
134
|
-
if (!kb) return undefined
|
|
135
21
|
let active = true
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
22
|
+
let busy = false
|
|
23
|
+
let cursor = 0
|
|
24
|
+
let terminal = false
|
|
25
|
+
setFailure('')
|
|
26
|
+
setEvents([])
|
|
27
|
+
const read = async (): Promise<void> => {
|
|
28
|
+
if (busy || terminal) return
|
|
29
|
+
busy = true
|
|
139
30
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
requestJson<{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
31
|
+
let more = true
|
|
32
|
+
while (active && more) {
|
|
33
|
+
const result = await requestJson<{ task: PipelineTask; events: PipelineEvent[]; next: number; hasMore: boolean }>(`/knowledge/pipeline-task?${new URLSearchParams({ kb, id, after: String(cursor) })}`)
|
|
34
|
+
if (!active || !result.data) return
|
|
35
|
+
const page = result.data
|
|
36
|
+
cursor = page.next
|
|
37
|
+
more = page.hasMore
|
|
38
|
+
terminal = !more && !PIPELINE_ACTIVE_STATES.includes(page.task.state)
|
|
39
|
+
callback.current(page.task)
|
|
40
|
+
setEvents(current => [...current, ...page.events].slice(-600))
|
|
41
|
+
setFailure('')
|
|
42
|
+
}
|
|
43
|
+
} catch (cause) { if (active) setFailure(cause instanceof Error ? cause.message : String(cause)) }
|
|
44
|
+
finally { busy = false }
|
|
154
45
|
}
|
|
155
|
-
void
|
|
156
|
-
const timer = window.setInterval(() => { void
|
|
46
|
+
void read()
|
|
47
|
+
const timer = window.setInterval(() => { void read() }, 1200)
|
|
157
48
|
return () => { active = false; window.clearInterval(timer) }
|
|
158
|
-
}, [kb])
|
|
49
|
+
}, [kb, id])
|
|
50
|
+
const output = events.filter(event => event.kind !== 'report').map(event => event.kind === 'output' ? event.text : `\n${event.text}\n`).join('').slice(-200000)
|
|
51
|
+
return <div className="mt-3 grid min-w-0 gap-2">
|
|
52
|
+
<p className="m-0 text-xs text-tita-ink-muted">模型正式输出持续返回;刷新后可以补读。预览保留最近内容,完整返回保存在报告中。</p>
|
|
53
|
+
{failure && <p className="m-0 text-xs text-tita-danger" role="alert">{failure}</p>}
|
|
54
|
+
<pre className="m-0 max-h-96 overflow-auto whitespace-pre-wrap break-words rounded-tita-control bg-tita-surface-muted p-3 text-xs leading-relaxed text-tita-ink" tabIndex={0} aria-label="实时输出">{output || '等待模型返回内容…'}</pre>
|
|
55
|
+
<div className="flex flex-wrap gap-3">{events.filter(event => event.kind === 'report').map(event => <a key={event.seq} className="text-xs text-tita-primary underline underline-offset-2" target="_blank" rel="noreferrer" href={`/knowledge/pages/run?${new URLSearchParams({ kb, run: event.text })}`}>查看抽取报告 <ExternalLink size={11} className="inline" /></a>)}</div>
|
|
56
|
+
</div>
|
|
57
|
+
}
|
|
159
58
|
|
|
160
|
-
|
|
161
|
-
|
|
59
|
+
export function ExtractionTasksPage({ kb }: { kb: string }): ReactNode {
|
|
60
|
+
const params = new URLSearchParams(typeof window === 'undefined' ? '' : window.location.search)
|
|
61
|
+
const [status, setStatus] = useState<PipelineStatus>()
|
|
62
|
+
const [history, setHistory] = useState<History>()
|
|
63
|
+
const [sourceIds, setSourceIds] = useState<string[]>(() => params.get('source') ? [params.get('source')!] : [])
|
|
64
|
+
const [groupKeys, setGroupKeys] = useState<string[]>([])
|
|
65
|
+
const [selectedTask, setSelectedTask] = useState(() => params.get('task') ?? '')
|
|
66
|
+
const [failure, setFailure] = useState('')
|
|
67
|
+
const [busy, setBusy] = useState(false)
|
|
68
|
+
const [page, setPage] = useState(0)
|
|
69
|
+
const refreshSequence = useRef(0)
|
|
70
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
71
|
+
if (!kb) return
|
|
72
|
+
const sequence = ++refreshSequence.current
|
|
73
|
+
try {
|
|
74
|
+
const [nextStatus, nextHistory] = await Promise.all([
|
|
75
|
+
requestJson<PipelineStatus>(`/knowledge/pipeline?kb=${encodeURIComponent(kb)}`),
|
|
76
|
+
requestJson<History>(`/knowledge/pipeline-tasks?${new URLSearchParams({ kb, offset: String(page * 20) })}`),
|
|
77
|
+
])
|
|
78
|
+
if (sequence !== refreshSequence.current) return
|
|
79
|
+
if (nextStatus.data) setStatus(nextStatus.data)
|
|
80
|
+
if (nextHistory.data) setHistory(nextHistory.data)
|
|
81
|
+
setFailure('')
|
|
82
|
+
} catch (cause) { if (sequence === refreshSequence.current) setFailure(cause instanceof Error ? cause.message : String(cause)) }
|
|
83
|
+
}, [kb, page])
|
|
84
|
+
const activeCount = history?.tasks.filter(task => PIPELINE_ACTIVE_STATES.includes(task.state)).length ?? 0
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
void refresh()
|
|
87
|
+
const focus = (): void => { void refresh() }
|
|
88
|
+
window.addEventListener('focus', focus)
|
|
89
|
+
const timer = activeCount ? window.setInterval(focus, 2500) : undefined
|
|
90
|
+
return () => { if (timer) window.clearInterval(timer); window.removeEventListener('focus', focus) }
|
|
91
|
+
}, [refresh, activeCount])
|
|
92
|
+
const start = async (stage: PipelineStage, force = false): Promise<void> => {
|
|
93
|
+
setBusy(true); setFailure('')
|
|
94
|
+
try {
|
|
95
|
+
const result = await requestJson<{ task: PipelineTask }>('/knowledge/pipeline', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kb, stage, ...(sourceIds.length ? { sourceIds } : {}), ...(groupKeys.length ? { groupKeys } : {}), force }) })
|
|
96
|
+
if (result.data?.task) { setSelectedTask(result.data.task.id); setPage(0) }
|
|
97
|
+
await refresh()
|
|
98
|
+
} catch (cause) { setFailure(cause instanceof Error ? cause.message : String(cause)) }
|
|
99
|
+
finally { setBusy(false) }
|
|
162
100
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
</
|
|
188
|
-
|
|
101
|
+
const act = async (id: string, action: string): Promise<void> => {
|
|
102
|
+
setBusy(true)
|
|
103
|
+
try {
|
|
104
|
+
const result = await requestJson<{ task: PipelineTask }>('/knowledge/pipeline-task', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kb, id, action }) })
|
|
105
|
+
if (result.data?.task) setSelectedTask(result.data.task.id)
|
|
106
|
+
await refresh()
|
|
107
|
+
} catch (cause) { setFailure(cause instanceof Error ? cause.message : String(cause)) }
|
|
108
|
+
finally { setBusy(false) }
|
|
109
|
+
}
|
|
110
|
+
const replaceTask = useCallback((task: PipelineTask) => setHistory(current => current ? { ...current, tasks: current.tasks.map(row => row.id === task.id ? task : row) } : current), [])
|
|
111
|
+
if (!kb) return <p role="alert">缺少知识库,请从知识库工具栏打开知识处理。</p>
|
|
112
|
+
const total = Math.max(history?.total ?? 0, history?.extractions?.total ?? 0)
|
|
113
|
+
return <div className="grid min-w-0 gap-7 text-tita-ink">
|
|
114
|
+
<section className="grid gap-3" aria-label="独立运行知识阶段">
|
|
115
|
+
<div className="flex flex-wrap items-center justify-between gap-3"><h2 className="m-0 text-xl font-semibold">知识处理</h2><Button variant="outline" size="sm" onClick={() => { void refresh() }}><RefreshCw size={13} />刷新状态</Button></div>
|
|
116
|
+
<p className="m-0 text-sm leading-relaxed text-tita-ink-muted">来源默认只入库。单独运行抽取、归一合并和物化,最后发布检索索引。</p>
|
|
117
|
+
<div className="flex flex-wrap gap-2">{(['extract', 'normalize', 'materialize', 'build'] as const).map(stage => <Button key={stage} variant="outline" size="sm" disabled={busy || !status || ((stage === 'extract' || stage === 'normalize') && !status.sources.length)} onClick={() => { void start(stage) }}>{PIPELINE_STAGE_LABEL[stage]}</Button>)}<Button size="sm" disabled={busy || !status?.sources.length} onClick={() => { void start('all') }}>全链路运行</Button></div>
|
|
118
|
+
<p className="m-0 text-xs leading-relaxed text-tita-ink-muted" role="status">{status ? `${status.pendingNew} 张待新建 · ${status.pendingUpdated} 张待更新 · ${status.pendingWithdrawn} 张待停用${status.blocked ? ` · ${status.blocked} 条暂不可物化` : ''}${status.indexPending ? ' · 索引待更新' : ''}` : '正在读取处理状态…'}</p>
|
|
119
|
+
{failure && <p className="m-0 text-sm text-tita-danger" role="alert">{failure}</p>}
|
|
120
|
+
</section>
|
|
121
|
+
<Tabs defaultValue="sources">
|
|
122
|
+
<TabsList><TabsTrigger value="sources">来源范围</TabsTrigger><TabsTrigger value="groups">WikiCard 分组</TabsTrigger></TabsList>
|
|
123
|
+
<TabsContent value="sources" className="mt-3">
|
|
124
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2"><p className="m-0 text-xs text-tita-ink-muted">{sourceIds.length ? `已选 ${sourceIds.length} 份文档` : '未选择文档:处理全部来源,未变化结果自动复用'}</p><Button size="sm" variant="ghost" disabled={!sourceIds.length} onClick={() => setSourceIds([])}>清除选择</Button></div>
|
|
125
|
+
<ul className="m-0 grid max-h-80 list-none overflow-auto p-0" aria-label="来源选择">{status?.sources.map(source => <li key={source.id} className="flex min-w-0 items-center gap-3 border-b border-tita-line py-2"><Button variant={sourceIds.includes(source.id) ? 'default' : 'ghost'} size="sm" aria-pressed={sourceIds.includes(source.id)} onClick={() => setSourceIds(current => toggle(current, source.id))}>{sourceIds.includes(source.id) && <Check size={13} />}选择</Button><span className="min-w-0 flex-1 break-words text-sm">{source.title}<span className="ml-2 text-xs text-tita-ink-muted">v{source.version}</span></span><Badge tone="neutral">{SOURCE_STATES[source.state]}</Badge></li>)}</ul>
|
|
126
|
+
{sourceIds.length > 0 && <Button className="mt-3" size="sm" variant="outline" disabled={busy} onClick={() => { void start('extract', true) }}>强制重抽选中的 {sourceIds.length} 份文档</Button>}
|
|
127
|
+
</TabsContent>
|
|
128
|
+
<TabsContent value="groups" className="mt-3">
|
|
129
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2"><p className="m-0 text-xs text-tita-ink-muted">{groupKeys.length ? `已选 ${groupKeys.length} 张完整卡片分组` : '未选择分组:物化全部待办,同坐标的知识和事件一起处理'}</p><Button size="sm" variant="ghost" disabled={!groupKeys.length} onClick={() => setGroupKeys([])}>清除选择</Button></div>
|
|
130
|
+
{!status?.groups.length && <p className="text-sm text-tita-ink-muted">还没有可物化分组。先抽取并归一文档内容。</p>}
|
|
131
|
+
<ul className="m-0 grid max-h-96 list-none overflow-auto p-0" aria-label="卡片分组选择">{status?.groups.map(group => <li key={group.key} className="flex min-w-0 items-center gap-3 border-b border-tita-line py-2"><Button variant={groupKeys.includes(group.key) ? 'default' : 'ghost'} size="sm" aria-pressed={groupKeys.includes(group.key)} onClick={() => setGroupKeys(current => toggle(current, group.key))}>{groupKeys.includes(group.key) && <Check size={13} />}选择</Button><div className="min-w-0 flex-1"><p className="m-0 break-words text-sm">{group.title}</p><p className="m-0 mt-1 text-xs text-tita-ink-muted">{group.knowledgeCount} 条 Knowledge · {group.eventCount} 个 Event · {group.sourceIds.length} 份来源</p>{group.reason && <p className="m-0 mt-1 text-xs text-tita-warning">{group.reason}</p>}</div><Badge tone={group.state === 'current' ? 'success' : 'neutral'}>{GROUP_STATES[group.state]}</Badge></li>)}</ul>
|
|
132
|
+
</TabsContent>
|
|
133
|
+
</Tabs>
|
|
134
|
+
<section id="history" className="min-w-0 border-t border-tita-line pt-5" aria-labelledby="pipeline-history">
|
|
135
|
+
<h2 id="pipeline-history" className="m-0 text-lg font-semibold">任务历史</h2><p className="mt-2 text-xs text-tita-ink-muted">{activeCount ? `${activeCount} 项未结束。` : '当前没有运行中的任务。'} 暂停与停止在当前文档或卡片处理完成后生效。</p>
|
|
136
|
+
{!history?.tasks.length && <p className="text-sm text-tita-ink-muted">还没有阶段任务;已有文档抽取记录在下方保留。</p>}
|
|
137
|
+
{history?.tasks.map(task => <article key={task.id} className="min-w-0 border-b border-tita-line py-4"><div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><div className="min-w-0 flex-1"><div className="flex flex-wrap items-center gap-2"><strong className="text-sm">{PIPELINE_STAGE_LABEL[task.stage]}</strong><Badge tone={task.state === 'succeeded' ? 'success' : ['failed', 'partial', 'interrupted'].includes(task.state) ? 'warning' : 'neutral'}>{STATES[task.state]}</Badge></div><p className="mb-0 mt-2 break-words text-xs leading-relaxed">{task.summary}</p><p className="mb-0 mt-1 text-xs text-tita-ink-muted">{formatDateTime(task.queuedAt)}{task.model.model ? ` · ${task.model.provider ?? ''} / ${task.model.model}` : ''}{task.model.reasoningEffort ? ` · ${task.model.reasoningEffort}` : ''}</p></div><div className="flex flex-wrap gap-1"><Button size="sm" variant="ghost" aria-expanded={selectedTask === task.id} onClick={() => setSelectedTask(current => current === task.id ? '' : task.id)}>{selectedTask === task.id ? '收起输出' : '查看输出'}</Button>{PIPELINE_ACTIVE_STATES.includes(task.state) ? <><Button size="sm" variant="outline" disabled={busy || task.state === 'cancelling' || task.state === 'pausing'} onClick={() => { void act(task.id, task.state === 'paused' ? 'resume' : 'pause') }}>{task.state === 'paused' ? <Play size={12} /> : <Pause size={12} />}{task.state === 'paused' ? '继续' : '暂停'}</Button><Button size="sm" variant="ghost" disabled={busy || task.state === 'cancelling'} onClick={() => { void act(task.id, 'cancel') }}><X size={12} />停止</Button></> : <Button size="sm" variant="outline" disabled={busy} onClick={() => { void act(task.id, 'retry') }}>重新运行</Button>}</div></div>{selectedTask === task.id && <TaskOutput kb={kb} id={task.id} onTask={replaceTask} />}</article>)}
|
|
138
|
+
{total > 20 && <div className="mt-3 flex items-center gap-3"><Button size="sm" variant="outline" disabled={page === 0} onClick={() => setPage(current => current - 1)}>上一页</Button><span className="text-xs text-tita-ink-muted">第 {page + 1} 页</span><Button size="sm" variant="outline" disabled={(page + 1) * 20 >= total} onClick={() => setPage(current => current + 1)}>下一页</Button></div>}
|
|
139
|
+
</section>
|
|
140
|
+
<section className="border-t border-tita-line pt-5"><h2 className="m-0 text-lg font-semibold">文档抽取记录</h2><p className="mt-2 text-xs text-tita-ink-muted">保留现有抽取报告。早期记录按文档展示,不补造批次。</p>{history?.extractions?.runs.map(run => <a key={run.id} className="flex min-w-0 flex-wrap items-center justify-between gap-2 border-b border-tita-line py-3 text-sm text-tita-ink underline-offset-2 hover:underline" target="_blank" rel="noreferrer" href={`/knowledge/pages/run?${new URLSearchParams({ kb, run: run.id })}`}><span className="min-w-0 break-words">{run.title} · {run.summary}</span><span className="text-xs text-tita-ink-muted">{formatDateTime(run.createdAt)} <ExternalLink size={12} className="inline" /></span></a>)}{history?.extractions?.total === 0 && <p className="text-sm text-tita-ink-muted">还没有抽取记录。</p>}</section>
|
|
141
|
+
</div>
|
|
189
142
|
}
|
|
@@ -78,6 +78,7 @@ import { ImportFilesDialog, type ImportItemView, type ImportStage } from './Impo
|
|
|
78
78
|
import { TypeDefinitionDrawer, type CompileModelSelectionView } from './TypeDefinitionDrawer.tsx'
|
|
79
79
|
import { DeleteDocumentDialog } from './DeleteDocumentDialog.tsx'
|
|
80
80
|
import { InfoHint } from './InfoHint.tsx'
|
|
81
|
+
import { PipelineNotice } from './PipelineNotice.tsx'
|
|
81
82
|
|
|
82
83
|
// ------------------------------------------------------------------ contract
|
|
83
84
|
// Mirrors the plugin HTTP contract (see plugins/dsh-data-domain-knowledge/README.md).
|
|
@@ -613,6 +614,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
613
614
|
const [eventsLoading, setEventsLoading] = useState(false)
|
|
614
615
|
const [eventsError, setEventsError] = useState('')
|
|
615
616
|
const [knowledgeContent, setKnowledgeContent] = useState<'knowledge' | 'event'>('knowledge')
|
|
617
|
+
const [focusedEventId, setFocusedEventId] = useState<string>()
|
|
616
618
|
const eventsRequestRef = useRef(0)
|
|
617
619
|
/** 当前打开的文档的轨迹列表(不含报告本体)。报告整页读(见 `openRunPage`)。 */
|
|
618
620
|
const [detailRuns, setDetailRuns] = useState<ExtractionRunSummary[]>([])
|
|
@@ -725,6 +727,11 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
725
727
|
setTab(next)
|
|
726
728
|
remember('knowledgeTab', next)
|
|
727
729
|
}, [])
|
|
730
|
+
const openEvent = useCallback((eventId: string): void => {
|
|
731
|
+
setFocusedEventId(eventId)
|
|
732
|
+
changeTab('knowledge')
|
|
733
|
+
setKnowledgeContent('event')
|
|
734
|
+
}, [changeTab])
|
|
728
735
|
/** 编译设置抽屉:本库的编译模型、坐标实体与抽取备注。 */
|
|
729
736
|
const [typeDef, setTypeDef] = useState(false)
|
|
730
737
|
const [detail, setDetail] = useState<{ document: KnowledgeDocumentDetail; versions: DocumentVersion[] } | null>(null)
|
|
@@ -1203,15 +1210,10 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1203
1210
|
}
|
|
1204
1211
|
await loadBases(base.id)
|
|
1205
1212
|
await Promise.all([loadDocuments(base.id, query), loadCards(base.id)])
|
|
1206
|
-
let report: RebuildReportView | null = null
|
|
1207
1213
|
if (done > 0) {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
say(failedCount === 0 ? 'success' : 'error',
|
|
1212
|
-
failedCount === 0
|
|
1213
|
-
? `已导入并编译 ${done} 个文件到「${base.name}」`
|
|
1214
|
-
: `导入 ${done} 个、失败 ${failedCount} 个 —— 已对成功的那些跑了增量编译`)
|
|
1214
|
+
say(failedCount === 0 ? 'success' : 'error', failedCount === 0
|
|
1215
|
+
? `已保存 ${done} 个文件到「${base.name}」,可单独运行抽取`
|
|
1216
|
+
: `已保存 ${done} 个文件,失败 ${failedCount} 个;成功文件可单独抽取`)
|
|
1215
1217
|
}
|
|
1216
1218
|
/**
|
|
1217
1219
|
* 收尾摘要。它是**这一批处理结果的唯一一处汇总** —— 对话框下半截显示它,处理日志里
|
|
@@ -1220,46 +1222,15 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1220
1222
|
const summary: ImportSummaryView = {
|
|
1221
1223
|
files: outcomes,
|
|
1222
1224
|
done, failed: failedCount,
|
|
1223
|
-
compile:
|
|
1224
|
-
version: report.version,
|
|
1225
|
-
skipped: report.skipped === true,
|
|
1226
|
-
entities: report.detail?.entities ?? null,
|
|
1227
|
-
phases: report.detail?.phases ?? null,
|
|
1228
|
-
cards: report.detail?.cards ?? null,
|
|
1229
|
-
notes: report.detail?.notes ?? null,
|
|
1230
|
-
relatedLinks: report.detail?.relatedLinks,
|
|
1231
|
-
deadLinks: report.detail?.deadLinks,
|
|
1232
|
-
build: report.graphNodes === undefined
|
|
1233
|
-
? null
|
|
1234
|
-
: {
|
|
1235
|
-
graphNodes: report.graphNodes,
|
|
1236
|
-
graphEdges: report.graphEdges ?? 0,
|
|
1237
|
-
embedding: report.embedding ?? 'skipped',
|
|
1238
|
-
embeddingItems: report.embeddingItems ?? 0,
|
|
1239
|
-
embeddingReused: report.embeddingReused ?? 0,
|
|
1240
|
-
},
|
|
1241
|
-
extractModel: (report.warnings ?? []).some(warning => warning.includes('模型抽取已开启')) ? 'unavailable' : 'ok',
|
|
1242
|
-
},
|
|
1225
|
+
compile: null,
|
|
1243
1226
|
}
|
|
1244
1227
|
setImportSummary(summary)
|
|
1245
1228
|
await settleBatchLog(batch, {
|
|
1246
1229
|
status: failedCount === 0 ? 'done' : done > 0 ? 'partial' : 'failed',
|
|
1247
1230
|
summary: done === 0
|
|
1248
1231
|
? `${failedCount} 个文件全部失败,没有东西进入知识库。`
|
|
1249
|
-
: `入库 ${done} 个${failedCount > 0 ? `、失败 ${failedCount} 个` : ''}
|
|
1250
|
-
|
|
1251
|
-
* `report === null` 说的是**这一批的编译没读到结论**,不是「没有编译」(0066)。
|
|
1252
|
-
*
|
|
1253
|
-
* 这句原来是「这一批没有触发编译。」—— 而面板从没有过判断这件事的依据:它只知道
|
|
1254
|
-
* 自己那条请求的结局。实机上那条请求在 300 秒被切断,编译在 312 秒成功,于是日志
|
|
1255
|
-
* 里留下了一句与事实相反的话。**只写自己知道的事**:
|
|
1256
|
-
*/
|
|
1257
|
-
+ (report === null
|
|
1258
|
-
? '编译已发起,但没读到它的结论(可能仍在跑);结论看那一次的处理日志。'
|
|
1259
|
-
: report.skipped === true
|
|
1260
|
-
? `编译:语料没变化,索引仍为 v${report.version}。`
|
|
1261
|
-
: `编译:新建 ${report.detail?.cards?.created.length ?? 0} 张、并入 ${report.detail?.cards?.updated.length ?? 0} 张、复用 ${report.detail?.cards?.reused.length ?? 0} 张,索引 v${report.version}。`),
|
|
1262
|
-
detail: { files: outcomes, ...(report?.detail === undefined ? {} : { compile: report.detail }) },
|
|
1232
|
+
: `入库 ${done} 个${failedCount > 0 ? `、失败 ${failedCount} 个` : ''};本次只保存来源,可在知识处理中运行抽取。`,
|
|
1233
|
+
detail: { files: outcomes },
|
|
1263
1234
|
})
|
|
1264
1235
|
setImportRunning(false)
|
|
1265
1236
|
}
|
|
@@ -1768,7 +1739,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
1768
1739
|
headers: { 'content-type': 'application/json' },
|
|
1769
1740
|
// 面板的「重新编译」也承担存量数据修复:明确开启抽取,并绕过旧指纹,
|
|
1770
1741
|
// 否则旧版本留下的空坐标会被当成未变化结果永久复用。
|
|
1771
|
-
body: JSON.stringify({ only, kb: baseId, extract:
|
|
1742
|
+
body: JSON.stringify({ only, kb: baseId, extract: false, forceExtract: false }),
|
|
1772
1743
|
})
|
|
1773
1744
|
compileStartPendingRef.current = false
|
|
1774
1745
|
runId = started.data?.runId ?? null
|
|
@@ -2305,7 +2276,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2305
2276
|
? <p className="rounded-md border border-tita-line bg-tita-surface-muted px-2.5 py-2 text-[10.5px] leading-relaxed text-tita-danger" role="alert">Event 加载失败:{eventsError}</p>
|
|
2306
2277
|
: eventsLoading && events.length === 0
|
|
2307
2278
|
? <p className="py-5 text-center text-[11px] text-tita-ink-muted" role="status">正在读取 Event…</p>
|
|
2308
|
-
: <EventView events={events} />}
|
|
2279
|
+
: <EventView events={events} focusEventId={focusedEventId} />}
|
|
2309
2280
|
</TabsContent>
|
|
2310
2281
|
</Tabs>
|
|
2311
2282
|
|
|
@@ -2361,36 +2332,12 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2361
2332
|
title={active ? `编辑 ${active.name} 的编译设置` : '先选择一个知识库'}
|
|
2362
2333
|
onClick={() => setTypeDef(true)}
|
|
2363
2334
|
><Tags size={13} />编译设置</Button>
|
|
2364
|
-
{
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
所以它带一个确认浮层。 */}
|
|
2371
|
-
<span className="kbCompileAction">
|
|
2372
|
-
<Button
|
|
2373
|
-
variant="outline"
|
|
2374
|
-
size="sm"
|
|
2375
|
-
className="kbCompileActionMain"
|
|
2376
|
-
disabled={busy !== null || !active}
|
|
2377
|
-
title={active ? `重新编译「${active.name}」的全部 SourceRaw` : '先选择一个知识库'}
|
|
2378
|
-
onClick={() => setConfirmOpen(true)}
|
|
2379
|
-
><RefreshCw size={14} />{busy === 'compile' ? '编译中…' : '重新编译'}</Button>
|
|
2380
|
-
<DropdownMenu>
|
|
2381
|
-
<DropdownMenuTrigger asChild disabled={!active}>
|
|
2382
|
-
<Button variant="outline" size="sm" className="kbCompileActionMenu" aria-label="编译任务">
|
|
2383
|
-
<ChevronDown size={13} />
|
|
2384
|
-
</Button>
|
|
2385
|
-
</DropdownMenuTrigger>
|
|
2386
|
-
<DropdownMenuContent align="end" sideOffset={5}>
|
|
2387
|
-
<DropdownMenuItem onSelect={() => {
|
|
2388
|
-
const kb = activeIdRef.current
|
|
2389
|
-
if (kb) void fullPage.open({ src: `/knowledge/pages/tasks?kb=${encodeURIComponent(kb)}`, title: '抽取任务' })
|
|
2390
|
-
}}><ListOrdered size={14} />抽取任务</DropdownMenuItem>
|
|
2391
|
-
</DropdownMenuContent>
|
|
2392
|
-
</DropdownMenu>
|
|
2393
|
-
</span>
|
|
2335
|
+
<Button variant="outline" size="sm" disabled={!active} onClick={() => {
|
|
2336
|
+
if (active) void fullPage.open({ src: `/knowledge/pages/tasks?kb=${encodeURIComponent(active.id)}`, title: '知识处理与任务历史' })
|
|
2337
|
+
}}><Layers3 size={14} />知识处理</Button>
|
|
2338
|
+
<Button variant="ghost" size="sm" disabled={!active} onClick={() => {
|
|
2339
|
+
if (active) void fullPage.open({ src: `/knowledge/pages/tasks?kb=${encodeURIComponent(active.id)}#history`, title: '任务历史' })
|
|
2340
|
+
}}><ListOrdered size={14} />任务历史</Button>
|
|
2394
2341
|
<Button
|
|
2395
2342
|
variant="outline"
|
|
2396
2343
|
size="sm"
|
|
@@ -2414,6 +2361,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2414
2361
|
|
|
2415
2362
|
{/* 分栏是**一条流水线**,所以切换器是步骤轨而不是一排等宽 tab:收录 → 抽取归一 →
|
|
2416
2363
|
物化 → 检索精排。方向本身就是信息(写入侧在左、读取侧在右)。 */}
|
|
2364
|
+
{active && <PipelineNotice kb={active.id} revision={active.updatedAt} onOpen={() => { void fullPage.open({ src: `/knowledge/pages/tasks?kb=${encodeURIComponent(active.id)}`, title: '知识处理' }) }} />}
|
|
2417
2365
|
<KnowledgeStepRail tab={tab} onChange={changeTab} facts={stepFacts} />
|
|
2418
2366
|
|
|
2419
2367
|
{/* 删除确认:**先查账再弹框**(见 `askDeleteDocument`)。 */}
|
|
@@ -2622,7 +2570,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2622
2570
|
? <p className={styles.kbHint}>
|
|
2623
2571
|
{detailKnowledgeLoading
|
|
2624
2572
|
? '正在取这份来源抽出的知识…'
|
|
2625
|
-
:
|
|
2573
|
+
: <>这份来源还没有抽出知识。请在「知识处理」中运行抽取,再运行归一合并。</>}
|
|
2626
2574
|
</p>
|
|
2627
2575
|
: <>
|
|
2628
2576
|
<ul className={styles.knowledgeList}>
|
|
@@ -2676,6 +2624,9 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2676
2624
|
<div className={styles.drawerSection}>
|
|
2677
2625
|
<div className={styles.kbSubHead}>
|
|
2678
2626
|
<h4><Sparkles size={13} />抽取轨迹</h4>
|
|
2627
|
+
<Button variant="outline" size="sm" onClick={() => {
|
|
2628
|
+
if (active) void fullPage.open({ src: `/knowledge/pages/tasks?kb=${encodeURIComponent(active.id)}&source=${encodeURIComponent(detail.document.docId)}`, title: '单文档抽取' })
|
|
2629
|
+
}}>单独抽取此文档</Button>
|
|
2679
2630
|
<span>{detailRuns.length > 0 ? `${detailRuns.length} 次` : '还没有'}</span>
|
|
2680
2631
|
</div>
|
|
2681
2632
|
{detail !== null && compileProgress !== null && compileProgress.finishedAt === undefined
|
|
@@ -2711,7 +2662,7 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2711
2662
|
</p>
|
|
2712
2663
|
: detailRuns.length === 0
|
|
2713
2664
|
? <p className={styles.kbHint}>
|
|
2714
|
-
|
|
2665
|
+
这份文档还没有抽取轨迹。保存来源后,在「知识处理」中单独运行抽取。
|
|
2715
2666
|
</p>
|
|
2716
2667
|
: <ul className={styles.trialRunList}>
|
|
2717
2668
|
{detailRuns.map(run => <li key={run.id}>
|
|
@@ -2786,6 +2737,8 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2786
2737
|
onCompare={entry => void compareCardVersion(entry)}
|
|
2787
2738
|
versionDiff={cardDiff}
|
|
2788
2739
|
onOpenRelated={link => void openCard(link)}
|
|
2740
|
+
onOpenEvent={openEvent}
|
|
2741
|
+
events={events}
|
|
2789
2742
|
sourceTitles={sourceTitles}
|
|
2790
2743
|
/>
|
|
2791
2744
|
</DrawerBody>
|
|
@@ -2817,6 +2770,8 @@ export function KnowledgeBaseTab({ visible, onChanged, onActiveBaseChange, knowl
|
|
|
2817
2770
|
onCompare={entry => void compareCardVersion(entry)}
|
|
2818
2771
|
versionDiff={cardDiff}
|
|
2819
2772
|
onOpenRelated={link => void openCard(link)}
|
|
2773
|
+
onOpenEvent={openEvent}
|
|
2774
|
+
events={events}
|
|
2820
2775
|
sourceTitles={sourceTitles}
|
|
2821
2776
|
/>
|
|
2822
2777
|
</DrawerBody>
|
|
@@ -87,10 +87,10 @@ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
|
|
|
87
87
|
// 0025:`compile` 设置组已撤。卡片编译模板是系统固定契约,不是用户设置项。
|
|
88
88
|
build: {
|
|
89
89
|
scope: 'knowledge',
|
|
90
|
-
title: '
|
|
90
|
+
title: '向量运行设置',
|
|
91
91
|
fields: [
|
|
92
92
|
{ key: 'embeddingProvider', label: '向量来源', kind: 'select', fallback: 'wemm-python', options: [{ value: 'wemm-python', label: '本地模型' }, { value: 'ollama', label: 'Ollama' }, { value: 'none', label: '关闭向量' }] },
|
|
93
|
-
{ key: 'embeddingModel', label: '
|
|
93
|
+
{ key: 'embeddingModel', label: 'Ollama 模型名称', kind: 'text', fallback: '', hint: '填写 Ollama 中已安装的模型名称。', visibleWhen: { key: 'embeddingProvider', value: 'ollama' } },
|
|
94
94
|
{ key: 'embeddingBaseUrl', label: 'Ollama 地址', kind: 'text', fallback: 'http://127.0.0.1:11434', visibleWhen: { key: 'embeddingProvider', value: 'ollama' } },
|
|
95
95
|
{ key: 'embeddingDevice', label: '运行设备', kind: 'select', fallback: 'auto', advanced: true, options: [{ value: 'auto', label: '自动' }, { value: 'mps', label: 'Apple MPS' }, { value: 'cpu', label: 'CPU' }] },
|
|
96
96
|
{ key: 'embeddingDimension', label: '向量维度', kind: 'number', fallback: 512, min: 1, step: 1, advanced: true, hint: '必须与所选模型输出维度一致;修改后需要重建向量索引。' },
|
|
@@ -107,7 +107,6 @@ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
|
|
|
107
107
|
{ key: 'keywordThreshold', label: '关键词阈值', kind: 'number', fallback: 0.3, min: 0, max: 1, step: 0.01, advanced: true, hint: '0–1 的归一化 BM25 分数;适合过滤弱命中,不能单独判断整批结果是否相关。' },
|
|
108
108
|
{ key: 'vectorThreshold', label: '向量阈值', kind: 'number', fallback: 0.5, min: -1, max: 1, step: 0.01, advanced: true, hint: '原始余弦分数。默认 0.5 是当前本地模型与语料的起点,换模型后应按召回诊断校准。' },
|
|
109
109
|
{ key: 'rerankThreshold', label: '重排最低分', kind: 'number', fallback: 0.2, min: -10, max: 10, step: 0.1, advanced: true },
|
|
110
|
-
{ key: 'rerankModel', label: '模型路径或名称', kind: 'text', fallback: '', advanced: true },
|
|
111
110
|
{ key: 'rerankTimeoutMs', label: '重排超时(毫秒)', kind: 'number', fallback: 60000, min: 1000, max: 600000, step: 1000, advanced: true },
|
|
112
111
|
{ key: 'contextMaxChars', label: '上下文字符上限', kind: 'number', fallback: 20000, min: 0, max: 1000000, step: 1000, hint: '0 表示不限', advanced: true },
|
|
113
112
|
],
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Button } from '@tita-agent/ui/components'
|
|
3
|
+
import type { PipelineStatus } from '../pipeline/types.js'
|
|
4
|
+
import { requestJson } from './request.ts'
|
|
5
|
+
|
|
6
|
+
export function PipelineNotice({ kb, revision, onOpen }: { kb: string; revision?: string; onOpen: () => void }) {
|
|
7
|
+
const [status, setStatus] = useState<PipelineStatus>()
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
let active = true
|
|
10
|
+
const refresh = (): void => { void requestJson<PipelineStatus>(`/knowledge/pipeline?kb=${encodeURIComponent(kb)}`).then(result => { if (active && result.data) setStatus(result.data) }).catch(() => undefined) }
|
|
11
|
+
refresh()
|
|
12
|
+
window.addEventListener('focus', refresh)
|
|
13
|
+
return () => { active = false; window.removeEventListener('focus', refresh) }
|
|
14
|
+
}, [kb, revision])
|
|
15
|
+
if (!status) return null
|
|
16
|
+
const pending = status.pendingNew + status.pendingUpdated + status.pendingWithdrawn
|
|
17
|
+
const parts = [
|
|
18
|
+
status.sources.filter(source => source.state === 'pending' || source.state === 'outdated').length,
|
|
19
|
+
status.sources.filter(source => source.state === 'extracted').length,
|
|
20
|
+
]
|
|
21
|
+
if (!parts.some(Boolean) && !pending && !status.indexPending) return null
|
|
22
|
+
return <div className="mx-3 flex min-w-0 items-center gap-2 border-b border-tita-line py-2 text-xs text-tita-ink-muted" role="status">
|
|
23
|
+
<span className="min-w-0 flex-1 leading-relaxed">{[parts[0] ? `${parts[0]} 份待抽取` : '', parts[1] ? `${parts[1]} 份待归一` : '', pending ? `${pending} 张 WikiCard 待物化` : '', status.indexPending ? '索引待更新' : ''].filter(Boolean).join(' · ')}</span>
|
|
24
|
+
<Button size="sm" variant="ghost" onClick={onOpen}>处理</Button>
|
|
25
|
+
</div>
|
|
26
|
+
}
|
package/src/client/TabGuide.tsx
CHANGED
|
@@ -162,9 +162,9 @@ const COMPILE_PROMPT_SECTIONS: GuideSection[] = [
|
|
|
162
162
|
{
|
|
163
163
|
title: '这一步发给模型的提示词',
|
|
164
164
|
points: [
|
|
165
|
-
'
|
|
166
|
-
'**归一坐标**(Resolve
|
|
167
|
-
'**建索引**(Index
|
|
165
|
+
'抽取 Knowledge / Event 使用 LLM;归一遇到歧义时调用模型补判;WikiCard 可选择模型组织。三个阶段独立运行。',
|
|
166
|
+
'**归一坐标**(Resolve)优先查 canonical、alias 与作用域;需要语义比较时由模型在已有候选中补判,不确定就保留待处理。',
|
|
167
|
+
'**建索引**(Index)从已保存卡片切召回单元,构建关键词、向量和图谱;向量编码使用所选向量模型,不重复文档抽取。',
|
|
168
168
|
'所以下面两段就是这条链路全部的模型输入。它们都是 **system 段**;user 段每次都不同(文档正文、这一坐标下的知识清单),列不出固定文本。',
|
|
169
169
|
],
|
|
170
170
|
},
|
|
@@ -176,7 +176,7 @@ const COMPILE_PROMPT_SECTIONS: GuideSection[] = [
|
|
|
176
176
|
'这是**稳定前缀**:同一份契约每份文档都发一遍,所以它进 system 而不是 user。',
|
|
177
177
|
'运行时在这段之后还会注入两样随库与文档变化的东西,所以不在上面那段里:本库**已有的坐标清单**(对象 / 动作 / 场景,含定义与别名),以及**本库骨架允许的小节名**。',
|
|
178
178
|
'已有坐标刻意放在 user 段 —— system 保持稳定,上游缓存才命中得了(`sessionId` 被钉成 `knowledge:<库 id>` 就是为了这件事)。',
|
|
179
|
-
'
|
|
179
|
+
'模型默认回 TOON 固定列表格,历史 JSON 仍兼容:`knowledge` 是抽出来的知识(每条只挂一个坐标),`objects` / `actions` / `scenes` 是它用到的原始说法与定义,`dropped` 是它丢掉且说得出理由的片段。',
|
|
180
180
|
],
|
|
181
181
|
wide: true,
|
|
182
182
|
},
|