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.
Files changed (172) hide show
  1. package/README.md +84 -52
  2. package/cordis.patch.yml +21 -17
  3. package/eval/benchmark-extraction-format.mjs +20 -0
  4. package/lib/client.js +2846 -3872
  5. package/lib/doc-page.js +1 -1
  6. package/lib/pages.js +1299 -1660
  7. package/lib/types/axis-registry.d.ts +6 -0
  8. package/lib/types/axis-registry.d.ts.map +1 -1
  9. package/lib/types/axis-registry.js +45 -0
  10. package/lib/types/axis-registry.js.map +1 -1
  11. package/lib/types/diagnosis/cards.d.ts +7 -3
  12. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  13. package/lib/types/diagnosis/cards.js +13 -8
  14. package/lib/types/diagnosis/cards.js.map +1 -1
  15. package/lib/types/diagnosis/run.d.ts +4 -13
  16. package/lib/types/diagnosis/run.d.ts.map +1 -1
  17. package/lib/types/diagnosis/run.js +26 -16
  18. package/lib/types/diagnosis/run.js.map +1 -1
  19. package/lib/types/diagnosis/schema.js +2 -2
  20. package/lib/types/diagnosis/schema.js.map +1 -1
  21. package/lib/types/diagnosis/seed-source.d.ts +1 -1
  22. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  23. package/lib/types/diagnosis/seed-source.js +5 -4
  24. package/lib/types/diagnosis/seed-source.js.map +1 -1
  25. package/lib/types/diagnosis/seed.d.ts +4 -4
  26. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  27. package/lib/types/diagnosis/seed.js +6 -8
  28. package/lib/types/diagnosis/seed.js.map +1 -1
  29. package/lib/types/diagnosis/store.d.ts +8 -16
  30. package/lib/types/diagnosis/store.d.ts.map +1 -1
  31. package/lib/types/diagnosis/store.js +41 -66
  32. package/lib/types/diagnosis/store.js.map +1 -1
  33. package/lib/types/diagnosis/types.d.ts +10 -16
  34. package/lib/types/diagnosis/types.d.ts.map +1 -1
  35. package/lib/types/diagnosis/types.js.map +1 -1
  36. package/lib/types/environment/service.d.ts +4 -3
  37. package/lib/types/environment/service.d.ts.map +1 -1
  38. package/lib/types/environment/service.js +91 -23
  39. package/lib/types/environment/service.js.map +1 -1
  40. package/lib/types/environment/types.d.ts +9 -0
  41. package/lib/types/environment/types.d.ts.map +1 -1
  42. package/lib/types/events.d.ts +2 -0
  43. package/lib/types/events.d.ts.map +1 -1
  44. package/lib/types/events.js +13 -0
  45. package/lib/types/events.js.map +1 -1
  46. package/lib/types/extract-contract.d.ts +1 -1
  47. package/lib/types/extract-contract.d.ts.map +1 -1
  48. package/lib/types/extract-contract.js +34 -31
  49. package/lib/types/extract-contract.js.map +1 -1
  50. package/lib/types/extract-model.d.ts +2 -0
  51. package/lib/types/extract-model.d.ts.map +1 -1
  52. package/lib/types/extract-model.js.map +1 -1
  53. package/lib/types/extract-table-contract.d.ts +3 -0
  54. package/lib/types/extract-table-contract.d.ts.map +1 -0
  55. package/lib/types/extract-table-contract.js +30 -0
  56. package/lib/types/extract-table-contract.js.map +1 -0
  57. package/lib/types/extract-table.d.ts +8 -0
  58. package/lib/types/extract-table.d.ts.map +1 -0
  59. package/lib/types/extract-table.js +151 -0
  60. package/lib/types/extract-table.js.map +1 -0
  61. package/lib/types/extraction-runs.d.ts +3 -0
  62. package/lib/types/extraction-runs.d.ts.map +1 -1
  63. package/lib/types/extraction-runs.js +6 -0
  64. package/lib/types/extraction-runs.js.map +1 -1
  65. package/lib/types/index.d.ts.map +1 -1
  66. package/lib/types/index.js +196 -44
  67. package/lib/types/index.js.map +1 -1
  68. package/lib/types/intent/scope.d.ts.map +1 -1
  69. package/lib/types/intent/scope.js +1 -0
  70. package/lib/types/intent/scope.js.map +1 -1
  71. package/lib/types/knowledge-bases.d.ts.map +1 -1
  72. package/lib/types/knowledge-bases.js +3 -2
  73. package/lib/types/knowledge-bases.js.map +1 -1
  74. package/lib/types/knowledge.d.ts +3 -0
  75. package/lib/types/knowledge.d.ts.map +1 -1
  76. package/lib/types/knowledge.js +12 -3
  77. package/lib/types/knowledge.js.map +1 -1
  78. package/lib/types/model-text.d.ts +1 -1
  79. package/lib/types/model-text.d.ts.map +1 -1
  80. package/lib/types/model-text.js +5 -4
  81. package/lib/types/model-text.js.map +1 -1
  82. package/lib/types/pipeline/engine.d.ts +57 -0
  83. package/lib/types/pipeline/engine.d.ts.map +1 -0
  84. package/lib/types/pipeline/engine.js +268 -0
  85. package/lib/types/pipeline/engine.js.map +1 -0
  86. package/lib/types/pipeline/store.d.ts +56 -0
  87. package/lib/types/pipeline/store.d.ts.map +1 -0
  88. package/lib/types/pipeline/store.js +115 -0
  89. package/lib/types/pipeline/store.js.map +1 -0
  90. package/lib/types/pipeline/types.d.ts +82 -0
  91. package/lib/types/pipeline/types.d.ts.map +1 -0
  92. package/lib/types/pipeline/types.js +5 -0
  93. package/lib/types/pipeline/types.js.map +1 -0
  94. package/lib/types/present.d.ts +2 -0
  95. package/lib/types/present.d.ts.map +1 -1
  96. package/lib/types/present.js +9 -4
  97. package/lib/types/present.js.map +1 -1
  98. package/lib/types/runtime.d.ts +20 -1
  99. package/lib/types/runtime.d.ts.map +1 -1
  100. package/lib/types/runtime.js +389 -112
  101. package/lib/types/runtime.js.map +1 -1
  102. package/lib/types/source-evidence.d.ts.map +1 -1
  103. package/lib/types/source-evidence.js +2 -0
  104. package/lib/types/source-evidence.js.map +1 -1
  105. package/lib/types/store/axis-references.js +3 -3
  106. package/lib/types/store/axis-references.js.map +1 -1
  107. package/lib/types/store/migrate.js +1 -1
  108. package/lib/types/store/migrate.js.map +1 -1
  109. package/lib/types/types.d.ts +3 -4
  110. package/lib/types/types.d.ts.map +1 -1
  111. package/lib/types/types.js.map +1 -1
  112. package/lib/types/wiki-materialize.d.ts.map +1 -1
  113. package/lib/types/wiki-materialize.js +3 -15
  114. package/lib/types/wiki-materialize.js.map +1 -1
  115. package/lib/types/wiki-trial-types.d.ts +3 -0
  116. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  117. package/lib/types/wiki-trial.d.ts +2 -0
  118. package/lib/types/wiki-trial.d.ts.map +1 -1
  119. package/lib/types/wiki-trial.js +31 -7
  120. package/lib/types/wiki-trial.js.map +1 -1
  121. package/package.json +6 -3
  122. package/preset/tita-data-agent/agent.cordis.yml +21 -17
  123. package/src/axis-registry.ts +38 -0
  124. package/src/client/AttributionEventGuide.tsx +10 -9
  125. package/src/client/EnvironmentSetup.tsx +35 -15
  126. package/src/client/ExtractionTasksPage.tsx +129 -176
  127. package/src/client/KnowledgeBaseTab.tsx +31 -76
  128. package/src/client/NodeSettings.tsx +2 -3
  129. package/src/client/PipelineNotice.tsx +26 -0
  130. package/src/client/TabGuide.tsx +4 -4
  131. package/src/client/TypeDefinitionDrawer.tsx +23 -8
  132. package/src/client/WikiCardPreview.tsx +43 -10
  133. package/src/client/WikiFlowPanel.tsx +4 -4
  134. package/src/client/architecture.tsx +1 -1
  135. package/src/client/attribution/AttributionTab.tsx +6 -6
  136. package/src/client/attribution/CaseView.tsx +3 -1
  137. package/src/client/attribution/EventView.tsx +14 -4
  138. package/src/client/attribution/RunView.tsx +2 -2
  139. package/src/client/attribution/graph-options.ts +2 -2
  140. package/src/client/pages.tsx +2 -2
  141. package/src/diagnosis/cards.ts +13 -8
  142. package/src/diagnosis/run.ts +27 -17
  143. package/src/diagnosis/schema.ts +2 -2
  144. package/src/diagnosis/seed-source.ts +5 -4
  145. package/src/diagnosis/seed.ts +6 -8
  146. package/src/diagnosis/store.ts +47 -68
  147. package/src/diagnosis/types.ts +10 -16
  148. package/src/environment/service.ts +79 -23
  149. package/src/environment/types.ts +6 -0
  150. package/src/events.ts +12 -0
  151. package/src/extract-contract.ts +6 -3
  152. package/src/extract-model.ts +2 -0
  153. package/src/extract-table-contract.ts +29 -0
  154. package/src/extract-table.ts +126 -0
  155. package/src/extraction-runs.ts +6 -0
  156. package/src/index.ts +156 -45
  157. package/src/intent/scope.ts +1 -0
  158. package/src/knowledge-bases.ts +3 -2
  159. package/src/knowledge.ts +13 -3
  160. package/src/model-text.ts +5 -3
  161. package/src/pipeline/engine.ts +238 -0
  162. package/src/pipeline/store.ts +119 -0
  163. package/src/pipeline/types.ts +74 -0
  164. package/src/present.ts +10 -4
  165. package/src/runtime.ts +350 -110
  166. package/src/source-evidence.ts +1 -0
  167. package/src/store/axis-references.ts +3 -3
  168. package/src/store/migrate.ts +1 -1
  169. package/src/types.ts +3 -4
  170. package/src/wiki-materialize.ts +3 -14
  171. package/src/wiki-trial-types.ts +3 -0
  172. package/src/wiki-trial.ts +26 -7
@@ -1,189 +1,142 @@
1
- import { useEffect, useRef, useState, type ReactNode } from 'react'
2
- import { ChevronDown, CircleX, Clock3, ListOrdered, Pause, Play, RefreshCw } from '@tita-agent/ui/icons'
3
- import { Badge, Button } from '@tita-agent/ui/components'
4
- import { CompileStepList, type CompileStepView } from './CompileDialogs.tsx'
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
- type TaskState = 'queued' | 'running' | 'pausing' | 'paused' | 'cancelling' | 'succeeded' | 'failed' | 'cancelled'
9
- type TaskAction = 'pause' | 'resume' | 'cancel'
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
- interface ExtractionTask {
12
- runId: string
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 [refreshing, setRefreshing] = useState(false)
131
- const busyRef = useRef(false)
132
-
18
+ const callback = useRef(onTask)
19
+ callback.current = onTask
133
20
  useEffect(() => {
134
- if (!kb) return undefined
135
21
  let active = true
136
- const load = async (): Promise<void> => {
137
- if (busyRef.current) return
138
- busyRef.current = true
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
- const [nextTasks, basesResponse] = await Promise.all([
141
- loadTasks(kb),
142
- requestJson<{ bases?: Array<{ id: string; name: string }> }>('/knowledge/bases'),
143
- ])
144
- if (!active) return
145
- setTasks(nextTasks)
146
- setKbName(basesResponse.data?.bases?.find(base => base.id === kb)?.name ?? kb)
147
- setFailure('')
148
- } catch (cause) {
149
- if (active) setFailure(cause instanceof Error ? cause.message : String(cause))
150
- } finally {
151
- busyRef.current = false
152
- if (active) setRefreshing(false)
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 load()
156
- const timer = window.setInterval(() => { void load() }, 2000)
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
- const replaceTask = (task: ExtractionTask): void => {
161
- setTasks(current => current?.map(item => item.runId === task.runId ? task : item) ?? [task])
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
- if (!kb) return <section className="page-state"><h2>缺少知识库</h2><p>从知识库工具栏打开抽取任务页。</p></section>
165
-
166
- const activeCount = tasks?.filter(task => ['queued', 'running', 'pausing', 'paused', 'cancelling'].includes(stateOf(task))).length ?? 0
167
- return <>
168
- <div className="extractTasksHead">
169
- <div>
170
- <h2>抽取任务</h2>
171
- <p>{kbName || kb} · {activeCount > 0 ? `${activeCount} 项未结束` : '当前没有进行中的任务'}</p>
172
- </div>
173
- <Button size="sm" variant="outline" disabled={refreshing} onClick={() => {
174
- setRefreshing(true)
175
- void loadTasks(kb).then(setTasks).catch(cause => setFailure(cause instanceof Error ? cause.message : String(cause))).finally(() => setRefreshing(false))
176
- }}><RefreshCw size={13} className={refreshing ? 'extractTaskSpin' : undefined} />刷新</Button>
177
- </div>
178
-
179
- <p className="extractTasksNote">暂停会在当前文档或阶段处理完成后生效。停止不会回滚已写入的内容。</p>
180
- {failure && <p className="page-notice" role="alert">读取任务失败:{failure}</p>}
181
- {tasks === null
182
- ? <p className="page-loading" role="status">正在读取任务…</p>
183
- : tasks.length === 0
184
- ? <div className="extractTasksEmpty"><ListOrdered size={17} /><span>还没有编译任务。导入文档或重新编译后,任务会显示在这里。</span></div>
185
- : <div className="extractTaskList">
186
- {tasks.map(task => <TaskRow key={task.runId} task={task} kb={kb} onChange={replaceTask} />)}
187
- </div>}
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
- setCompileProgress(null)
1209
- report = await rebuild('wiki')
1210
- await Promise.all([loadCards(base.id), loadStats(base.id), loadKnowledge(base.id)])
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: report === null ? null : {
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: true, forceExtract: only === 'all' }),
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
- (`importSources` 的 `compile: 'wiki'`),而它本身在语料未变时是空操作。
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: '模型路径或名称', kind: 'text', fallback: '', hint: '本地模型留空时自动选择;Ollama 填模型名称。' },
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
+ }
@@ -162,9 +162,9 @@ const COMPILE_PROMPT_SECTIONS: GuideSection[] = [
162
162
  {
163
163
  title: '这一步发给模型的提示词',
164
164
  points: [
165
- '四段里只有两段调模型:**抽取 Knowledge**(Extract)与 **物化 WikiCard 里的组织**(Organize)。',
166
- '**归一坐标**(Resolve)不调模型:作用域内 canonical → alias → 全局唯一 → 新建候选,全是查注册表。',
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
- '模型回一个 JSON:`knowledge` 是抽出来的知识(每条只挂一个坐标),`objects` / `actions` / `scenes` 是它用到的原始说法与定义,`dropped` 是它丢掉且说得出理由的片段。',
179
+ '模型默认回 TOON 固定列表格,历史 JSON 仍兼容:`knowledge` 是抽出来的知识(每条只挂一个坐标),`objects` / `actions` / `scenes` 是它用到的原始说法与定义,`dropped` 是它丢掉且说得出理由的片段。',
180
180
  ],
181
181
  wide: true,
182
182
  },