dsh-data-domain-knowledge 0.1.1-alpha.2 → 0.1.1-alpha.4
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 +33 -16
- package/lib/client.js +1042 -2097
- package/lib/contract-refs.d.ts +0 -1
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +2489 -1060
- package/lib/types/agent-behavior-rules.d.ts.map +1 -1
- package/lib/types/agent-behavior-rules.js +3 -2
- package/lib/types/agent-behavior-rules.js.map +1 -1
- package/lib/types/axis-contract.js +1 -1
- package/lib/types/axis-contract.js.map +1 -1
- package/lib/types/card-organize.d.ts +2 -1
- package/lib/types/card-organize.d.ts.map +1 -1
- package/lib/types/card-organize.js +5 -3
- package/lib/types/card-organize.js.map +1 -1
- package/lib/types/config-schema.d.ts +1 -1
- package/lib/types/config-schema.js +1 -1
- package/lib/types/convert/host.d.ts +2 -3
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +11 -2
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +14 -20
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +8 -5
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +2 -2
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +10 -2
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/extract-model.d.ts +3 -0
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/images.js +1 -1
- package/lib/types/index.d.ts +0 -4
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +99 -91
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +3 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +22 -2
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/knowledge-contract.d.ts +6 -0
- package/lib/types/knowledge-contract.d.ts.map +1 -1
- package/lib/types/knowledge-contract.js +6 -2
- package/lib/types/knowledge-contract.js.map +1 -1
- package/lib/types/layout.d.ts +9 -26
- package/lib/types/layout.d.ts.map +1 -1
- package/lib/types/layout.js +10 -28
- package/lib/types/layout.js.map +1 -1
- package/lib/types/materialize-snapshot.d.ts +5 -0
- package/lib/types/materialize-snapshot.d.ts.map +1 -1
- package/lib/types/migrate.d.ts +3 -1
- package/lib/types/migrate.d.ts.map +1 -1
- package/lib/types/migrate.js +3 -1
- package/lib/types/migrate.js.map +1 -1
- package/lib/types/model-text.d.ts +7 -0
- package/lib/types/model-text.d.ts.map +1 -1
- package/lib/types/model-text.js +31 -9
- package/lib/types/model-text.js.map +1 -1
- package/lib/types/organize-contract.d.ts.map +1 -1
- package/lib/types/organize-contract.js +2 -1
- package/lib/types/organize-contract.js.map +1 -1
- package/lib/types/pipeline/engine.d.ts +28 -4
- package/lib/types/pipeline/engine.d.ts.map +1 -1
- package/lib/types/pipeline/engine.js +68 -15
- package/lib/types/pipeline/engine.js.map +1 -1
- package/lib/types/pipeline/progress.d.ts +12 -0
- package/lib/types/pipeline/progress.d.ts.map +1 -0
- package/lib/types/pipeline/progress.js +37 -0
- package/lib/types/pipeline/progress.js.map +1 -0
- package/lib/types/pipeline/resumable-extract.d.ts +45 -0
- package/lib/types/pipeline/resumable-extract.d.ts.map +1 -0
- package/lib/types/pipeline/resumable-extract.js +262 -0
- package/lib/types/pipeline/resumable-extract.js.map +1 -0
- package/lib/types/pipeline/store.d.ts +8 -2
- package/lib/types/pipeline/store.d.ts.map +1 -1
- package/lib/types/pipeline/store.js +27 -5
- package/lib/types/pipeline/store.js.map +1 -1
- package/lib/types/pipeline/types.d.ts +56 -0
- package/lib/types/pipeline/types.d.ts.map +1 -1
- package/lib/types/pipeline/types.js +18 -1
- package/lib/types/pipeline/types.js.map +1 -1
- package/lib/types/present.d.ts +1 -1
- package/lib/types/present.js +1 -1
- package/lib/types/preset-ids.d.ts +0 -1
- package/lib/types/preset-ids.d.ts.map +1 -1
- package/lib/types/preset-ids.js +0 -3
- package/lib/types/preset-ids.js.map +1 -1
- package/lib/types/query-analysis.d.ts +0 -5
- package/lib/types/query-analysis.d.ts.map +1 -1
- package/lib/types/query-analysis.js +0 -16
- package/lib/types/query-analysis.js.map +1 -1
- package/lib/types/recall-delivery.d.ts +36 -0
- package/lib/types/recall-delivery.d.ts.map +1 -0
- package/lib/types/recall-delivery.js +16 -0
- package/lib/types/recall-delivery.js.map +1 -0
- package/lib/types/retrieval/scope.d.ts +17 -0
- package/lib/types/retrieval/scope.d.ts.map +1 -0
- package/lib/types/retrieval/scope.js +11 -0
- package/lib/types/retrieval/scope.js.map +1 -0
- package/lib/types/runtime.d.ts +75 -14
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +454 -198
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-volatile.d.ts +0 -7
- package/lib/types/settings-volatile.d.ts.map +1 -1
- package/lib/types/settings-volatile.js +0 -7
- package/lib/types/settings-volatile.js.map +1 -1
- package/lib/types/source-identity.d.ts +34 -0
- package/lib/types/source-identity.d.ts.map +1 -0
- package/lib/types/source-identity.js +85 -0
- package/lib/types/source-identity.js.map +1 -0
- package/lib/types/source-read.d.ts +18 -0
- package/lib/types/source-read.d.ts.map +1 -0
- package/lib/types/source-read.js +49 -0
- package/lib/types/source-read.js.map +1 -0
- package/lib/types/store/axis-references.d.ts.map +1 -1
- package/lib/types/store/axis-references.js +5 -3
- package/lib/types/store/axis-references.js.map +1 -1
- package/lib/types/store/knowledge-db.d.ts +5 -29
- package/lib/types/store/knowledge-db.d.ts.map +1 -1
- package/lib/types/store/knowledge-db.js +57 -2
- package/lib/types/store/knowledge-db.js.map +1 -1
- package/lib/types/store/source-identity.d.ts +8 -0
- package/lib/types/store/source-identity.d.ts.map +1 -0
- package/lib/types/store/source-identity.js +56 -0
- package/lib/types/store/source-identity.js.map +1 -0
- package/lib/types/types.d.ts +46 -5
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +6 -3
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-availability.d.ts +11 -0
- package/lib/types/wiki-availability.d.ts.map +1 -0
- package/lib/types/wiki-availability.js +39 -0
- package/lib/types/wiki-availability.js.map +1 -0
- package/lib/types/wiki-finalize.d.ts +4 -2
- package/lib/types/wiki-finalize.d.ts.map +1 -1
- package/lib/types/wiki-finalize.js +9 -27
- package/lib/types/wiki-finalize.js.map +1 -1
- package/lib/types/wiki-links.d.ts +35 -0
- package/lib/types/wiki-links.d.ts.map +1 -0
- package/lib/types/wiki-links.js +129 -0
- package/lib/types/wiki-links.js.map +1 -0
- 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 +3 -3
- package/lib/types/wiki-trial.d.ts.map +1 -1
- package/lib/types/wiki-trial.js +14 -3
- package/lib/types/wiki-trial.js.map +1 -1
- package/package.json +16 -16
- package/scripts/doctor.mjs +0 -7
- package/scripts/setup-embedding.sh +0 -0
- package/src/agent-behavior-rules.ts +3 -2
- package/src/axis-contract.ts +1 -1
- package/src/card-organize.ts +5 -3
- package/src/client/AttributionEventGuide.tsx +3 -3
- package/src/client/AxisMatrix.tsx +86 -19
- package/src/client/AxisRegistrySection.tsx +1 -1
- package/src/client/CompileSettingsView.tsx +473 -0
- package/src/client/DirectoryPicker.tsx +0 -2
- package/src/client/ExtractionTasksPage.tsx +557 -423
- package/src/client/GuideArchitectureCanvas.tsx +5 -12
- package/src/client/ImportFilesDialog.tsx +1 -0
- package/src/client/InfoHint.tsx +1 -1
- package/src/client/KnowledgeBaseTab.tsx +141 -148
- package/src/client/KnowledgePanel.tsx +16 -9
- package/src/client/OperationLogTab.tsx +1 -1
- package/src/client/PipelineRunView.tsx +173 -0
- package/src/client/RecallPanel.tsx +5 -16
- package/src/client/TabGuide.tsx +3 -7
- package/src/client/WikiFlowPanel.tsx +10 -11
- package/src/client/arch/NodeLogicPanel.tsx +3 -3
- package/src/client/arch/model.ts +5 -113
- package/src/client/architecture.css +1 -1
- package/src/client/architecture.tsx +13 -27
- package/src/client/attribution/AttributionTab.tsx +141 -10
- package/src/client/pages.css +305 -180
- package/src/client/pages.tsx +7 -12
- package/src/client/styles.css +15 -96
- package/src/config-schema.ts +1 -1
- package/src/convert/host.ts +13 -5
- package/src/convert/ingest.ts +20 -7
- package/src/diagnosis/store.ts +7 -2
- package/src/diagnosis/types.ts +1 -1
- package/src/extract-model.ts +3 -0
- package/src/images.ts +1 -1
- package/src/index.ts +92 -97
- package/src/knowledge-bases.ts +23 -3
- package/src/knowledge-contract.ts +7 -2
- package/src/layout.ts +10 -32
- package/src/materialize-snapshot.ts +1 -0
- package/src/migrate.ts +3 -1
- package/src/model-text.ts +33 -11
- package/src/organize-contract.ts +2 -1
- package/src/pipeline/engine.ts +85 -22
- package/src/pipeline/progress.ts +40 -0
- package/src/pipeline/resumable-extract.ts +233 -0
- package/src/pipeline/store.ts +28 -6
- package/src/pipeline/types.ts +47 -2
- package/src/present.ts +1 -1
- package/src/preset-ids.ts +0 -4
- package/src/query-analysis.ts +0 -20
- package/src/recall-delivery.ts +19 -0
- package/src/retrieval/scope.ts +10 -0
- package/src/runtime.ts +412 -206
- package/src/settings-volatile.ts +0 -7
- package/src/source-identity.ts +85 -0
- package/src/source-read.ts +47 -0
- package/src/store/axis-references.ts +5 -3
- package/src/store/knowledge-db.ts +54 -2
- package/src/store/source-identity.ts +42 -0
- package/src/types.ts +18 -5
- package/src/wiki-availability.ts +37 -0
- package/src/wiki-finalize.ts +9 -25
- package/src/wiki-links.ts +120 -0
- package/src/wiki-trial-types.ts +3 -2
- package/src/wiki-trial.ts +17 -6
- package/lib/types/intent/engine.d.ts +0 -122
- package/lib/types/intent/engine.d.ts.map +0 -1
- package/lib/types/intent/engine.js +0 -488
- package/lib/types/intent/engine.js.map +0 -1
- package/lib/types/intent/host.d.ts +0 -68
- package/lib/types/intent/host.d.ts.map +0 -1
- package/lib/types/intent/host.js +0 -249
- package/lib/types/intent/host.js.map +0 -1
- package/lib/types/intent/scope.d.ts +0 -25
- package/lib/types/intent/scope.d.ts.map +0 -1
- package/lib/types/intent/scope.js +0 -116
- package/lib/types/intent/scope.js.map +0 -1
- package/lib/types/intent/store.d.ts +0 -33
- package/lib/types/intent/store.d.ts.map +0 -1
- package/lib/types/intent/store.js +0 -239
- package/lib/types/intent/store.js.map +0 -1
- package/lib/types/intent/tools.d.ts +0 -62
- package/lib/types/intent/tools.d.ts.map +0 -1
- package/lib/types/intent/tools.js +0 -130
- package/lib/types/intent/tools.js.map +0 -1
- package/lib/types/intent/types.d.ts +0 -170
- package/lib/types/intent/types.d.ts.map +0 -1
- package/lib/types/intent/types.js +0 -56
- package/lib/types/intent/types.js.map +0 -1
- package/src/client/IntentLogic.tsx +0 -192
- package/src/client/IntentTab.tsx +0 -343
- package/src/client/TypeDefinitionDrawer.tsx +0 -195
- package/src/intent/README.md +0 -42
- package/src/intent/engine.ts +0 -515
- package/src/intent/host.ts +0 -315
- package/src/intent/scope.ts +0 -125
- package/src/intent/store.ts +0 -218
- package/src/intent/tools.ts +0 -154
- package/src/intent/types.ts +0 -229
|
@@ -1,103 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 知识处理 —— 一整页。它回答四个问题,按阅读顺序排:
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 1. **现在什么状态**:顶部一句实时结论(在跑哪一段、几分之几;或者「没有在跑」)。
|
|
5
|
+
* 2. **五段各走到哪**:一条阶段轨。每格只放三样 —— 段名、最近一次的结论、还有多少待办。
|
|
6
|
+
* 3. **这一段有哪些文档、抽出了什么**:主栏是文档列表(或分组 / 索引),每行是
|
|
7
|
+
* 「它是什么 + 抽到了多少 + 跑过几次」。
|
|
8
|
+
* 4. **这一份的轨迹**:侧栏。轨迹是过程记录,不是主结论;放在旁边,点主栏一行就换成那一份的。
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
* 2. **四段各差多少**:一条步骤轨。每格只放三样东西 —— 段名、最近一次的结论、还有多少待办。
|
|
8
|
-
* 3. **这一段的细节**:输入范围、结果摘要、抽取报告、原始输出;右边一栏是「处理范围」与「运行记录」。
|
|
10
|
+
* ## 2026-10-28 为什么重排
|
|
9
11
|
*
|
|
10
|
-
*
|
|
12
|
+
* 上一版把「输入范围 / 结果摘要 / 抽取报告 / 原始日志」全塞进主栏,而**真正要处理的对象**
|
|
13
|
+
* (文档)缩在右栏一个 260px 的小列表里。于是这一页看起来像「任务详情」,不像「处理台」:
|
|
14
|
+
* 人找不到「哪些文档还没抽」,也看不出「这份文档抽出了多少内容」。
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
* 段头再说一次,结果摘要的第一行又是「状态:部分失败」,下面再写一遍「完成 0/1 · 失败 1」。
|
|
14
|
-
* 而真正要紧的「还剩多少待办」缩成一行 10.5px 的灰字挤在页签上方,模型原始日志却占着一屏。
|
|
16
|
+
* 这一版把主次换回来:
|
|
15
17
|
*
|
|
16
|
-
*
|
|
18
|
+
* - **主栏是对象**:文档 / 分组 / 索引,按当前阶段给。每行第二、三行就是抽取产出与轨迹次数。
|
|
19
|
+
* - **侧栏是过程**:轨迹、运行记录两个页签。轨迹按选中那一份文档过滤,不选就是这一段的最近记录。
|
|
20
|
+
* - **编译设置**也住这一页(视图切换),模型选择改成对话框里的可选列表:它要容纳
|
|
21
|
+
* provider、模型说明、思考强度,一行一个扁平下拉读不出来。
|
|
22
|
+
* - **解析内核**的入口在这一页:它把文件变成 Markdown,是这条流水线的上游。
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - **原始输出默认收起。** 它是排查用的底料,不是结论;任务在跑时自动展开。
|
|
21
|
-
* - **左右两栏各自回答一类问题**:左栏「这一段跑了什么」,右栏「我要跑哪些」+「以前跑过什么」。
|
|
22
|
-
*
|
|
23
|
-
* 分页在服务端(`limit` / `offset`):每跑一次都会往里加,整份在手迟早会变成整份传一遍。
|
|
24
|
-
* 页码状态走 `ListPager`,与知识库其余列表同一个 `PAGE_SIZE`。
|
|
24
|
+
* 分页仍在服务端(`limit` / `offset`):每跑一次都会往任务表里加,整份在手迟早会变成
|
|
25
|
+
* 整份传一遍。页码状态走 `ListPager`,与知识库其余列表同一个 `PAGE_SIZE`。
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
|
-
import { Fragment, useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
28
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
28
29
|
import { Badge, Button, Tabs, TabsContent, TabsList, TabsTrigger } from '@tita-agent/ui/components'
|
|
29
|
-
import { ArrowRight, Check, ChevronDown, ExternalLink, Pause, Play, RefreshCw, X } from '@tita-agent/ui/icons'
|
|
30
|
+
import { AlertCircle, ArrowRight, Check, ChevronDown, ExternalLink, FileUp, History, Pause, Play, RefreshCw, X } from '@tita-agent/ui/icons'
|
|
30
31
|
import { openAppPath } from '@tita-agent/ui/overlays'
|
|
31
|
-
import { PIPELINE_ACTIVE_STATES, PIPELINE_STAGE_LABEL, type
|
|
32
|
+
import { PIPELINE_ACTIVE_STATES, PIPELINE_STAGE_LABEL, type PipelineStage, type PipelineStatus, type PipelineTask } from '../pipeline/types.js'
|
|
32
33
|
import { formatDateTime } from './format.ts'
|
|
33
34
|
import { ListPager, PAGE_SIZE } from './ListPager.tsx'
|
|
34
35
|
import { requestJson } from './request.ts'
|
|
35
36
|
import { requestExtractionRuns, trajectoryStageOf, type ExtractionRunSummary } from './wiki-trial.ts'
|
|
36
|
-
import {
|
|
37
|
+
import type { TrajectoryStage } from '../trajectory-types.js'
|
|
38
|
+
import { CompileSettingsView } from './CompileSettingsView.tsx'
|
|
39
|
+
import { PIPELINE_STATE_LABEL as STATES, observeTaskStage, orderActiveTasks } from '../pipeline/progress.js'
|
|
40
|
+
import { PIPELINE_STAGE_ORDER } from '../pipeline/types.js'
|
|
41
|
+
import { RawTaskOutput, RunActivity, RunSummary, useTaskFeed } from './PipelineRunView.tsx'
|
|
37
42
|
|
|
38
|
-
const STATES: Record<PipelineState, string> = { queued: '排队中', running: '进行中', pausing: '暂停中', paused: '已暂停', cancelling: '停止中', succeeded: '已完成', partial: '部分完成', failed: '失败', cancelled: '已停止', interrupted: '已中断' }
|
|
39
|
-
const SOURCE_STATES = { pending: '待抽取', extracted: '待归一', normalized: '已归一', outdated: '来源已更新' }
|
|
40
43
|
const GROUP_STATES = { new: '待新建', changed: '待更新', current: '已物化', withdrawn: '待停用', blocked: '先归一来源' }
|
|
41
|
-
|
|
44
|
+
const DOC_STATES: Record<PipelineStatus['sources'][number]['state'], { label: string; tone: 'neutral' | 'success' | 'warning' }> = {
|
|
45
|
+
pending: { label: '待抽取', tone: 'neutral' },
|
|
46
|
+
extracted: { label: '待归一', tone: 'neutral' },
|
|
47
|
+
normalized: { label: '已归一', tone: 'success' },
|
|
48
|
+
outdated: { label: '来源已更新', tone: 'warning' },
|
|
49
|
+
}
|
|
50
|
+
interface History { tasks: PipelineTask[]; total: number; activeTasks?: PipelineTask[] }
|
|
51
|
+
interface DocumentMeta { docId: string; title: string; sourceFormat: string; ext: string; bytes: number; updatedAt: string; versionCount: number }
|
|
52
|
+
interface DocumentPage { documents?: DocumentMeta[]; total?: number; extractionRuns?: Record<string, number> }
|
|
53
|
+
interface DocumentRow extends DocumentMeta {
|
|
54
|
+
id: string
|
|
55
|
+
version: number
|
|
56
|
+
state: PipelineStatus['sources'][number]['state']
|
|
57
|
+
runs: ExtractionRunSummary[]
|
|
58
|
+
trailCount: number
|
|
59
|
+
}
|
|
42
60
|
const toggle = (ids: string[], id: string): string[] => ids.includes(id) ? ids.filter(value => value !== id) : [...ids, id]
|
|
43
61
|
type StageId = Exclude<PipelineStage, 'all'>
|
|
44
62
|
interface StageObservation { label: string; tone: 'neutral' | 'success' | 'warning' | 'blue'; detail: string; task?: PipelineTask }
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
/**
|
|
65
|
+
* 阶段顺序就是流水线的方向。**`derive` 在 `materialize` 之前**:Procedure 是知识,
|
|
66
|
+
* 卡片是按坐标把它们组织起来的产物;先有方法才谈得上组织。
|
|
67
|
+
*/
|
|
68
|
+
const STAGE_ORDER = PIPELINE_STAGE_ORDER
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
if (stage !== 'build' && result.reused === result.total) return { label: '全部复用', tone: 'success', detail: `复用 ${result.reused}/${result.total} 项已有结果。`, task }
|
|
56
|
-
const detail = result.summary ?? (stage === 'build'
|
|
57
|
-
? `检索索引已更新。`
|
|
58
|
-
: `处理 ${result.done}/${result.total}${result.reused ? ` · 复用 ${result.reused}` : ''}。`)
|
|
59
|
-
return { label: '已完成', tone: 'success', detail, task }
|
|
70
|
+
const STAGE_DETAIL: Record<StageId, string> = {
|
|
71
|
+
extract: '从来源原文抽取 Knowledge、Event 与 Case,保留引文。',
|
|
72
|
+
normalize: '合并同指实体,把对象 / 动作 / 场景统一成坐标。',
|
|
73
|
+
derive: '只从有过程证据的 Case 提炼 Procedure;没有证据就不生成方法。',
|
|
74
|
+
materialize: '按完整坐标把知识与事件组织成 WikiCard。',
|
|
75
|
+
build: '从已物化卡片构建全文、向量与图谱索引,然后统一发布。',
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
? '正在构建并发布检索索引。'
|
|
69
|
-
: task.total > 0
|
|
70
|
-
? `处理 ${task.done}/${task.total} · 复用 ${task.reused}${task.failed ? ` · 失败 ${task.failed}` : ''}`
|
|
71
|
-
: '正在读取本阶段的待处理项。',
|
|
72
|
-
task,
|
|
73
|
-
})
|
|
74
|
-
const running = tasks.find(task => active(task) && task.currentStage === stage)
|
|
75
|
-
if (running) return progress(running)
|
|
76
|
-
const queued = tasks.find(task => active(task) && task.stage === stage)
|
|
77
|
-
if (queued) return { label: STATES[queued.state], tone: 'blue', detail: queued.summary, task: queued }
|
|
78
|
-
|
|
79
|
-
const activeAll = tasks.find(task => active(task) && task.stage === 'all')
|
|
80
|
-
if (activeAll) {
|
|
81
|
-
const result = activeAll.results?.[stage]
|
|
82
|
-
if (result) return completedStageObservation(stage, activeAll, result)
|
|
83
|
-
if (!activeAll.currentStage) return stage === 'extract'
|
|
84
|
-
? { label: STATES[activeAll.state], tone: 'blue', detail: '全链路排队后将从来源抽取开始。', task: activeAll }
|
|
85
|
-
: { label: '等待执行', tone: 'neutral', detail: '全链路到达此阶段后会自动运行。', task: activeAll }
|
|
86
|
-
const currentIndex = STAGE_ORDER.indexOf(activeAll.currentStage)
|
|
87
|
-
const stageIndex = STAGE_ORDER.indexOf(stage)
|
|
88
|
-
if (stageIndex > currentIndex) return { label: '等待执行', tone: 'neutral', detail: '全链路到达此阶段后会自动运行。', task: activeAll }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const completed = tasks.find(task => task.results?.[stage])
|
|
92
|
-
const result = completed?.results?.[stage]
|
|
93
|
-
if (completed && result) return completedStageObservation(stage, completed, result)
|
|
78
|
+
/** 侧栏叫「轨迹」而不是「报告」:它是过程记录,不是结论。 */
|
|
79
|
+
const TRAJECTORY_TITLE: Record<TrajectoryStage, string> = {
|
|
80
|
+
extract: '抽取轨迹',
|
|
81
|
+
normalize: '归一轨迹',
|
|
82
|
+
materialize: '物化轨迹',
|
|
83
|
+
}
|
|
94
84
|
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
function bytesText(bytes: number): string {
|
|
86
|
+
if (bytes < 1024) return `${bytes} B`
|
|
87
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`
|
|
88
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
89
|
+
}
|
|
100
90
|
|
|
91
|
+
function stageObservation(stage: StageId, task?: PipelineTask, status?: PipelineStatus): StageObservation {
|
|
92
|
+
if (task) return { ...observeTaskStage(task, stage), task }
|
|
101
93
|
if (!status) return { label: '读取中', tone: 'neutral', detail: '正在读取当前处理状态。' }
|
|
102
94
|
if (stage === 'extract') {
|
|
103
95
|
const count = status.sources.length
|
|
@@ -127,13 +119,9 @@ function stageObservation(stage: StageId, tasks: PipelineTask[], status?: Pipeli
|
|
|
127
119
|
}
|
|
128
120
|
|
|
129
121
|
/**
|
|
130
|
-
* 这一步**还差多少活** ——
|
|
122
|
+
* 这一步**还差多少活** —— 阶段轨上那一格的第二行。
|
|
131
123
|
*
|
|
132
|
-
*
|
|
133
|
-
* (已完成 / 部分失败),这一行说的是**现在还剩什么**。原来这两件事被挤在同一行
|
|
134
|
-
* 10.5px 的灰字里(「0 张待新建 · 58 张待更新 · …」),既没有主语也没有和阶段对上。
|
|
135
|
-
*
|
|
136
|
-
* 数字全部来自 `status`,算不出来就说算不出来,不补 0。
|
|
124
|
+
* 仅在没有查看任务时使用;不与任务状态混在同一格。
|
|
137
125
|
*/
|
|
138
126
|
function stagePending(stage: StageId, status?: PipelineStatus): string {
|
|
139
127
|
if (!status) return '读取中'
|
|
@@ -147,152 +135,281 @@ function stagePending(stage: StageId, status?: PipelineStatus): string {
|
|
|
147
135
|
const waiting = status.sources.filter(source => source.state === 'pending' || source.state === 'outdated').length
|
|
148
136
|
return waiting ? `等待 ${waiting} 份抽取` : '无待归一'
|
|
149
137
|
}
|
|
138
|
+
// 提炼按 Case 运行,不按文档计数;这里不编一个「可读 Case」的数。
|
|
139
|
+
if (stage === 'derive') return '按 Case 运行'
|
|
150
140
|
if (stage === 'materialize') {
|
|
151
141
|
const pending = status.groups.filter(group => group.state !== 'current').length
|
|
152
142
|
return pending ? `${pending} 个分组待处理` : '无待物化'
|
|
153
143
|
}
|
|
154
|
-
return status.indexPending ? '
|
|
144
|
+
return status.indexPending ? '索引有变化' : '已发布'
|
|
155
145
|
}
|
|
156
146
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const [
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let terminal = false
|
|
167
|
-
setFailure('')
|
|
168
|
-
setEvents([])
|
|
169
|
-
const read = async (): Promise<void> => {
|
|
170
|
-
if (busy || terminal) return
|
|
171
|
-
busy = true
|
|
172
|
-
try {
|
|
173
|
-
let more = true
|
|
174
|
-
while (active && more) {
|
|
175
|
-
const result = await requestJson<{ task: PipelineTask; events: PipelineEvent[]; next: number; hasMore: boolean }>(`/knowledge/pipeline-task?${new URLSearchParams({ kb, id, after: String(cursor) })}`)
|
|
176
|
-
if (!active || !result.data) return
|
|
177
|
-
const page = result.data
|
|
178
|
-
cursor = page.next
|
|
179
|
-
more = page.hasMore
|
|
180
|
-
terminal = !more && !PIPELINE_ACTIVE_STATES.includes(page.task.state)
|
|
181
|
-
callback.current(page.task)
|
|
182
|
-
setEvents(current => [...current, ...page.events].slice(-600))
|
|
183
|
-
setFailure('')
|
|
184
|
-
}
|
|
185
|
-
} catch (cause) { if (active) setFailure(cause instanceof Error ? cause.message : String(cause)) }
|
|
186
|
-
finally { busy = false }
|
|
187
|
-
}
|
|
188
|
-
void read()
|
|
189
|
-
const timer = window.setInterval(() => { void read() }, 1200)
|
|
190
|
-
return () => { active = false; window.clearInterval(timer) }
|
|
191
|
-
}, [kb, id])
|
|
192
|
-
const stageEvents = events.filter(event => !stageFilter || event.stage === stageFilter || (!event.stage && taskStage === stageFilter))
|
|
193
|
-
const hasUnscopedLegacyEvents = Boolean(stageFilter && taskStage === 'all' && events.some(event => !event.stage))
|
|
194
|
-
const output = stageEvents.filter(event => event.kind !== 'report').map(event => event.kind === 'output' ? event.text : `\n${event.text}\n`).join('').slice(-200000)
|
|
195
|
-
const reports = events.filter(event => event.kind === 'report' && (!stageFilter || event.stage === stageFilter || (!event.stage && (taskStage === stageFilter || stageFilter === 'extract' && taskStage === 'all'))))
|
|
196
|
-
return <div className="extractTasksOutputBody">
|
|
197
|
-
<p>{stageFilter ? hasUnscopedLegacyEvents ? '旧版全链路日志没有阶段标记;完整输出仍可在运行记录中查看。' : '这里只显示当前阶段的进度、模型输出和警告。' : '模型正式输出持续返回;刷新后可以补读。预览保留最近内容,完整返回保存在报告中。'}</p>
|
|
198
|
-
{failure && <p className="extractTasksError" role="alert">{failure}</p>}
|
|
199
|
-
<pre className="extractTasksPre" tabIndex={0} aria-label={stageFilter ? `${PIPELINE_STAGE_LABEL[stageFilter]}运行输出` : '实时输出'}>{output || (hasUnscopedLegacyEvents ? '这条历史记录没有可按阶段筛选的日志。' : '等待本阶段返回内容…')}</pre>
|
|
200
|
-
{/*
|
|
201
|
-
* 应用内路径不能用裸 `<a href>` 开(见 `KnowledgeBaseTab` 顶栏那条注释):桌面壳里
|
|
202
|
-
* `/knowledge/pages/...` 会被解析成 `dsh-app://` 地址,而壳的 `setWindowOpenHandler`
|
|
203
|
-
* 只放行 http/https —— 表现是「点了没有任何反应,也不报错」。走 `openAppPath()`
|
|
204
|
-
* 才是同一枚按钮在两种外壳下各自成立的那件事。
|
|
205
|
-
*/}
|
|
206
|
-
<div className="extractTasksReportsInline">{reports.map(event => <Button
|
|
207
|
-
key={event.seq}
|
|
208
|
-
variant="ghost"
|
|
209
|
-
size="sm"
|
|
210
|
-
className="extractTasksLink"
|
|
211
|
-
onClick={() => { void openAppPath(`/knowledge/pages/run?${new URLSearchParams({ kb, run: event.text })}`).then(opened => { if (!opened) onOpenFailed('打不开抽取报告:当前外壳下拿不到它的地址。') }) }}
|
|
212
|
-
>查看抽取报告 <ExternalLink size={11} className="inline" /></Button>)}</div>
|
|
213
|
-
</div>
|
|
147
|
+
/** Raw output is optional detail; observation stays mounted in the page. */
|
|
148
|
+
function StageOutput({ children }: { children: ReactNode }): ReactNode {
|
|
149
|
+
const [open, setOpen] = useState(false)
|
|
150
|
+
return <section className="ktOutput">
|
|
151
|
+
<Button variant="ghost" size="sm" className="ktOutputToggle" aria-expanded={open} onClick={() => setOpen(current => !current)}>
|
|
152
|
+
<ChevronDown size={13} />原始模型输出<span>{open ? '收起' : '按需展开'}</span>
|
|
153
|
+
</Button>
|
|
154
|
+
{open && children}
|
|
155
|
+
</section>
|
|
214
156
|
}
|
|
215
157
|
|
|
216
158
|
/**
|
|
217
|
-
*
|
|
159
|
+
* 主栏的文档列表。
|
|
218
160
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* 每段最多露 8 条:这一页的主线是「现在什么状态」,历史该去整页翻(点开就是整页)。
|
|
161
|
+
* 一行三件事,从上到下:**它是谁**(标题)、**它是什么**(格式 / 体积 / 版本 / 更新时间)、
|
|
162
|
+
* **抽出了多少**(最近一次抽取的规模描述)。左侧方框是**下一次运行的范围**,行本身是
|
|
163
|
+
* **查看这一份的轨迹** —— 两件事各有各的命中区,不互相代替。
|
|
224
164
|
*/
|
|
225
|
-
|
|
165
|
+
function itemObservation(task: PipelineTask | undefined, stage: StageId, id: string, kind: 'source' | 'group'): StageObservation | undefined {
|
|
166
|
+
if (!task) return undefined
|
|
167
|
+
if (task.stage !== 'all' && task.stage !== stage) return { label: '本次未运行', tone: 'neutral', detail: '' }
|
|
168
|
+
const scope = kind === 'source' ? task.selection.sourceIds : task.selection.groupKeys
|
|
169
|
+
if (scope && !scope.includes(id)) return { label: '不在本次范围', tone: 'neutral', detail: '' }
|
|
170
|
+
const item = task.items?.[stage]?.[id]
|
|
171
|
+
if (item) {
|
|
172
|
+
if (item.state === 'processing' && !PIPELINE_ACTIVE_STATES.includes(task.state)) return { label: STATES[task.state], tone: 'warning', detail: '此项尚未完成。' }
|
|
173
|
+
const labels = { processing: '正在处理', succeeded: '本次完成', reused: '本次复用', failed: '本次失败' }
|
|
174
|
+
return { label: item.state === 'processing' && task.state === 'paused' ? '已暂停' : labels[item.state], tone: item.state === 'failed' ? 'warning' : item.state === 'processing' ? 'blue' : 'success', detail: item.summary ?? '' }
|
|
175
|
+
}
|
|
176
|
+
const stageState = observeTaskStage(task, stage)
|
|
177
|
+
if (task.results?.[stage]) return { label: task.items?.[stage] ? '本次未处理' : '逐项状态未记录', tone: 'neutral', detail: '' }
|
|
178
|
+
return { label: PIPELINE_ACTIVE_STATES.includes(task.state) ? '等待处理' : stageState.label === '未执行' ? '未执行' : '尚未完成', tone: 'neutral', detail: '' }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function DocumentList({ rows, selected, onSelect, selectedIds, onToggle, stage, task, currentSourceId }: {
|
|
182
|
+
rows: DocumentRow[]
|
|
183
|
+
selected: string
|
|
184
|
+
onSelect: (id: string) => void
|
|
185
|
+
selectedIds: string[]
|
|
186
|
+
onToggle: (id: string) => void
|
|
187
|
+
stage: StageId
|
|
188
|
+
task?: PipelineTask
|
|
189
|
+
currentSourceId?: string
|
|
190
|
+
}): ReactNode {
|
|
191
|
+
if (rows.length === 0) return <p className="ktEmpty">这个库还没有来源。先在来源栏导入文档,再回来抽取。</p>
|
|
192
|
+
return <ul className="ktDocs" aria-label="文档列表">
|
|
193
|
+
{rows.map(row => {
|
|
194
|
+
const state = DOC_STATES[row.state]
|
|
195
|
+
const current = task && !task.items && task.currentStage === stage && currentSourceId === row.id && PIPELINE_ACTIVE_STATES.includes(task.state)
|
|
196
|
+
? { label: task.state === 'running' ? '正在处理' : STATES[task.state], tone: 'blue' as const, detail: '' }
|
|
197
|
+
: itemObservation(task, stage, row.id, 'source')
|
|
198
|
+
const picked = selectedIds.includes(row.id)
|
|
199
|
+
const inspecting = selected === row.id
|
|
200
|
+
const latest = row.runs
|
|
201
|
+
.filter(run => run.stage === stage)
|
|
202
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0]
|
|
203
|
+
return <li key={row.id} className="ktDoc" data-object-id={row.id} data-selected={inspecting || undefined}>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className="ktDocPick"
|
|
207
|
+
aria-pressed={picked}
|
|
208
|
+
aria-label={picked ? `把 ${row.title} 移出下次运行范围` : `把 ${row.title} 加入下次运行范围`}
|
|
209
|
+
onClick={() => onToggle(row.id)}
|
|
210
|
+
><span className="ktDocPickBox" aria-hidden="true">{picked && <Check size={11} />}</span></button>
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
className="ktDocMain"
|
|
214
|
+
aria-current={inspecting ? 'true' : undefined}
|
|
215
|
+
onClick={() => onSelect(row.id)}
|
|
216
|
+
>
|
|
217
|
+
<span className="ktDocTitle">{row.title}</span>
|
|
218
|
+
<span className="ktDocMeta">
|
|
219
|
+
{row.ext !== '' || row.sourceFormat !== ''
|
|
220
|
+
? (row.ext || row.sourceFormat).replace(/^\./, '').toUpperCase()
|
|
221
|
+
: '来源'}
|
|
222
|
+
{row.bytes > 0 && <><i aria-hidden="true">·</i>{bytesText(row.bytes)}</>}
|
|
223
|
+
<i aria-hidden="true">·</i>v{row.version}
|
|
224
|
+
<i aria-hidden="true">·</i>共 {row.trailCount} 条轨迹
|
|
225
|
+
<i aria-hidden="true">·</i>{latest ? '上次轨迹 ' : '来源更新 '}{formatDateTime(latest?.createdAt ?? row.updatedAt)}
|
|
226
|
+
<i aria-hidden="true">·</i>库内状态:{state.label}
|
|
227
|
+
</span>
|
|
228
|
+
<span className="ktDocFacts">
|
|
229
|
+
{latest
|
|
230
|
+
? `历史记录:${latest.summary}`
|
|
231
|
+
: stage === 'extract' ? '还没有抽取记录。' : '还没有归一记录。'}
|
|
232
|
+
</span>
|
|
233
|
+
</button>
|
|
234
|
+
<span className="ktDocSide">
|
|
235
|
+
<Badge tone={current?.tone ?? state.tone} title={current?.detail}>{current?.label ?? state.label}</Badge>
|
|
236
|
+
</span>
|
|
237
|
+
</li>
|
|
238
|
+
})}
|
|
239
|
+
</ul>
|
|
240
|
+
}
|
|
226
241
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
/** 物化阶段的对象是**分组**,不是文档:同坐标的知识与事件一起成卡。 */
|
|
243
|
+
function GroupList({ groups, selectedKeys, onToggle, task, inspectingKey }: {
|
|
244
|
+
groups: PipelineStatus['groups']
|
|
245
|
+
selectedKeys: string[]
|
|
246
|
+
onToggle: (key: string) => void
|
|
247
|
+
task?: PipelineTask
|
|
248
|
+
inspectingKey?: string
|
|
249
|
+
}): ReactNode {
|
|
250
|
+
if (groups.length === 0) return <p className="ktEmpty">还没有可物化的分组。先抽取并归一文档内容。</p>
|
|
251
|
+
return <ul className="ktDocs" aria-label="卡片分组列表">
|
|
252
|
+
{groups.map(group => {
|
|
253
|
+
const picked = selectedKeys.includes(group.key)
|
|
254
|
+
const current = task?.selection.sourceIds && !group.sourceIds.some(id => task.selection.sourceIds!.includes(id))
|
|
255
|
+
? { label: '不在本次范围', tone: 'neutral' as const }
|
|
256
|
+
: itemObservation(task, 'materialize', group.key, 'group')
|
|
257
|
+
return <li key={group.key} className="ktDoc" data-object-id={group.key} data-selected={picked || inspectingKey === group.key || undefined}>
|
|
258
|
+
<button type="button" className="ktDocPick" aria-pressed={picked} aria-label={picked ? `取消选择 ${group.title}` : `选择 ${group.title}`} onClick={() => onToggle(group.key)}>
|
|
259
|
+
<span className="ktDocPickBox" aria-hidden="true">{picked && <Check size={11} />}</span>
|
|
260
|
+
</button>
|
|
261
|
+
<div className="ktDocMain ktDocMainStatic">
|
|
262
|
+
<span className="ktDocTitle">{group.title}</span>
|
|
263
|
+
<span className="ktDocMeta">
|
|
264
|
+
{group.knowledgeCount} 条 Knowledge<i aria-hidden="true">·</i>{group.eventCount} 个 Event<i aria-hidden="true">·</i>{group.sourceIds.length} 份来源
|
|
265
|
+
<i aria-hidden="true">·</i>库内:{GROUP_STATES[group.state]}
|
|
266
|
+
</span>
|
|
267
|
+
{group.reason !== undefined && group.reason !== '' && <span className="ktDocFacts ktDocReason">{group.reason}</span>}
|
|
268
|
+
</div>
|
|
269
|
+
<span className="ktDocSide"><Badge tone={current?.tone ?? (group.state === 'current' ? 'success' : group.state === 'blocked' ? 'warning' : 'neutral')}>{current?.label ?? GROUP_STATES[group.state]}</Badge></span>
|
|
270
|
+
</li>
|
|
271
|
+
})}
|
|
272
|
+
</ul>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 侧栏的轨迹列表。按**当前阶段 + 选中文档**过滤;不选文档时列这一段的全部最近记录。 */
|
|
276
|
+
function TrajectoryPanel({ kb, stage, selectedDoc, docTitle, runs, onClearDoc, onOpenFailed }: {
|
|
277
|
+
kb: string
|
|
278
|
+
stage: StageId
|
|
279
|
+
selectedDoc: string
|
|
280
|
+
docTitle: string
|
|
281
|
+
runs: ExtractionRunSummary[]
|
|
282
|
+
onClearDoc: () => void
|
|
283
|
+
onOpenFailed: (message: string) => void
|
|
284
|
+
}): ReactNode {
|
|
285
|
+
if (stage === 'derive') {
|
|
286
|
+
return <div className="ktSideEmpty">
|
|
287
|
+
<strong>提炼 Procedure 没有独立轨迹</strong>
|
|
288
|
+
<p>它读 Case 的检查与动作,产出记在 Case 的派生结果里;运行详情在「运行记录」里看。</p>
|
|
289
|
+
</div>
|
|
290
|
+
}
|
|
291
|
+
if (stage === 'build') {
|
|
292
|
+
return <div className="ktSideEmpty">
|
|
293
|
+
<strong>构建索引不留轨迹</strong>
|
|
294
|
+
<p>它的产物是可重建的检索索引,不是一次判断;跑成什么样在「运行记录」和索引状态里看。</p>
|
|
295
|
+
</div>
|
|
296
|
+
}
|
|
297
|
+
const own = runs.filter(run => trajectoryStageOf(run) === stage && (selectedDoc === '' || run.docId === selectedDoc))
|
|
298
|
+
return <>
|
|
299
|
+
<div className="ktTrailHead">
|
|
233
300
|
<div>
|
|
234
|
-
<h3
|
|
235
|
-
<p
|
|
301
|
+
<h3>{TRAJECTORY_TITLE[stage as TrajectoryStage]}</h3>
|
|
302
|
+
<p>{selectedDoc === '' ? '这一段最近的运行记录;点开在整页读全文。' : '只看选中这一份。'}</p>
|
|
236
303
|
</div>
|
|
237
|
-
<span className="
|
|
304
|
+
<span className="ktTrailCount">{own.length} 条</span>
|
|
238
305
|
</div>
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
306
|
+
{selectedDoc !== '' && <div className="ktTrailFilter">
|
|
307
|
+
<span title={docTitle}>只看:{docTitle || selectedDoc}</span>
|
|
308
|
+
<Button variant="ghost" size="sm" className="ktLink" onClick={onClearDoc}>看全部<X size={11} /></Button>
|
|
309
|
+
</div>}
|
|
310
|
+
{own.length === 0
|
|
311
|
+
? <p className="ktSideEmptyText">{selectedDoc === '' ? '这一段还没有轨迹。运行一次就有了。' : '这份文档在这一段还没有轨迹。'}</p>
|
|
312
|
+
: <ul className="ktTrailList">{own.slice(0, 20).map(run => <li key={run.id}>
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
className="ktTrailRow"
|
|
316
|
+
onClick={() => { void openAppPath(`/knowledge/pages/run?${new URLSearchParams({ kb, run: run.id })}`).then(opened => { if (!opened) onOpenFailed('打不开轨迹:当前外壳下拿不到它的地址。') }) }}
|
|
317
|
+
>
|
|
318
|
+
<span className="ktTrailRowTitle">{run.title || '(没有标题)'}{!run.modelOk && <Badge tone="warning">模型未接通</Badge>}</span>
|
|
319
|
+
<span className="ktTrailRowSummary">{run.summary}</span>
|
|
320
|
+
<span className="ktTrailRowMeta">{formatDateTime(run.createdAt)}<ExternalLink size={12} /></span>
|
|
321
|
+
</button>
|
|
322
|
+
</li>)}</ul>}
|
|
323
|
+
</>
|
|
249
324
|
}
|
|
250
325
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
326
|
+
/** 侧栏的运行记录:暂停 / 继续 / 停止与历史输出。 */
|
|
327
|
+
function RunHistoryPanel({ tasks, orderedTasks, total, page, onPage, activeCount, selectedTask, onToggleTask, busy, onAct }: {
|
|
328
|
+
tasks: PipelineTask[]
|
|
329
|
+
orderedTasks: PipelineTask[]
|
|
330
|
+
total: number
|
|
331
|
+
page: number
|
|
332
|
+
onPage: (page: number) => void
|
|
333
|
+
activeCount: number
|
|
334
|
+
selectedTask: string
|
|
335
|
+
onToggleTask: (id: string) => void
|
|
336
|
+
busy: boolean
|
|
337
|
+
onAct: (id: string, action: string) => void
|
|
338
|
+
}): ReactNode {
|
|
339
|
+
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
|
340
|
+
return <div className="ktRuns">
|
|
341
|
+
<p className="ktRunsNote">{activeCount ? '暂停在安全点生效,停止会保留已保存的结果。' : '选择一条任务查看它自己的阶段与活动记录。'}</p>
|
|
342
|
+
{tasks.length === 0 && <p className="ktEmpty">还没有阶段任务。选好范围后运行一段就有了。</p>}
|
|
343
|
+
{orderedTasks.map(task => <article key={task.id} className="ktRunRow">
|
|
344
|
+
<div className="ktRunTitle">
|
|
345
|
+
<strong>{PIPELINE_STAGE_LABEL[task.stage]}</strong>
|
|
346
|
+
<Badge tone={task.state === 'succeeded' ? 'success' : ['failed', 'partial', 'interrupted'].includes(task.state) ? 'warning' : PIPELINE_ACTIVE_STATES.includes(task.state) ? 'blue' : 'neutral'}>{STATES[task.state]}</Badge>
|
|
347
|
+
</div>
|
|
348
|
+
<p className="ktRunSummary">{task.summary}</p>
|
|
349
|
+
<p className="ktRunTime">{formatDateTime(task.queuedAt)}{task.model.model ? ` · ${task.model.provider ?? ''} / ${task.model.model}` : ''}{task.model.reasoningEffort ? ` · ${task.model.reasoningEffort}` : ''}</p>
|
|
350
|
+
<div className="ktRunControls">
|
|
351
|
+
<Button size="sm" variant="ghost" className="ktRunToggle" aria-pressed={selectedTask === task.id} onClick={() => onToggleTask(task.id)}>{selectedTask === task.id ? '正在查看此任务' : '查看此任务'}</Button>
|
|
352
|
+
{PIPELINE_ACTIVE_STATES.includes(task.state) ? <>
|
|
353
|
+
<Button size="sm" variant="outline" disabled={busy || task.state === 'cancelling' || task.state === 'pausing'} onClick={() => onAct(task.id, task.state === 'paused' ? 'resume' : 'pause')}>{task.state === 'paused' ? <Play size={12} /> : <Pause size={12} />}{task.state === 'paused' ? '继续' : '暂停'}</Button>
|
|
354
|
+
<Button size="sm" variant="ghost" disabled={busy || task.state === 'cancelling'} onClick={() => onAct(task.id, 'cancel')}><X size={12} />停止</Button>
|
|
355
|
+
</> : task.state === 'succeeded'
|
|
356
|
+
// 跑完的任务没有「未完成部分」。再跑一次是主栏那个阶段动作的事,
|
|
357
|
+
// 不在这里放一枚读起来像「上次没跑完」的按钮。
|
|
358
|
+
? null
|
|
359
|
+
: <Button size="sm" variant="outline" disabled={busy} onClick={() => onAct(task.id, 'retry')}>继续未完成部分</Button>}
|
|
360
|
+
</div>
|
|
361
|
+
</article>)}
|
|
362
|
+
<ListPager page={page} pageCount={pageCount} total={total} onChange={onPage} />
|
|
363
|
+
</div>
|
|
267
364
|
}
|
|
268
365
|
|
|
269
366
|
export function ExtractionTasksPage({ kb }: { kb: string }): ReactNode {
|
|
270
367
|
const params = new URLSearchParams(typeof window === 'undefined' ? '' : window.location.search)
|
|
368
|
+
const [view, setView] = useState<'run' | 'settings'>(params.get('view') === 'settings' ? 'settings' : 'run')
|
|
369
|
+
const [stage, setStage] = useState<StageId>('extract')
|
|
271
370
|
const [status, setStatus] = useState<PipelineStatus>()
|
|
371
|
+
const [documents, setDocuments] = useState<DocumentMeta[]>([])
|
|
372
|
+
/** 服务端给的「每份文档有几条轨迹」;比按当前这 100 条轨迹数更准。 */
|
|
373
|
+
const [runCounts, setRunCounts] = useState<Record<string, number>>({})
|
|
272
374
|
const [history, setHistory] = useState<History>()
|
|
273
375
|
const [sourceIds, setSourceIds] = useState<string[]>(() => params.get('source') ? [params.get('source')!] : [])
|
|
274
376
|
const [groupKeys, setGroupKeys] = useState<string[]>([])
|
|
377
|
+
const [selectedDoc, setSelectedDoc] = useState('')
|
|
378
|
+
/** 侧栏的页签:点主栏一行要能自动带出「轨迹」,否则人点了文档却看不到它的轨迹。 */
|
|
379
|
+
const [sideTab, setSideTab] = useState('activity')
|
|
380
|
+
/** 刷新要连编译设置一起重读(它是另一个视图自己的数据),所以用 key 让它重挂一次。 */
|
|
381
|
+
const [settingsKey, setSettingsKey] = useState(0)
|
|
275
382
|
const [selectedTask, setSelectedTask] = useState(() => params.get('task') ?? '')
|
|
276
|
-
const
|
|
383
|
+
const selectedTaskRef = useRef(selectedTask)
|
|
384
|
+
selectedTaskRef.current = selectedTask
|
|
385
|
+
const [pinnedTask, setPinnedTask] = useState(Boolean(params.get('task')))
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
const url = new URL(window.location.href)
|
|
388
|
+
if (pinnedTask && selectedTask) url.searchParams.set('task', selectedTask)
|
|
389
|
+
else url.searchParams.delete('task')
|
|
390
|
+
window.history.replaceState(null, '', url)
|
|
391
|
+
}, [pinnedTask, selectedTask])
|
|
392
|
+
const [followRun, setFollowRun] = useState(true)
|
|
393
|
+
const [activeTasks, setActiveTasks] = useState<PipelineTask[]>([])
|
|
394
|
+
const [notice, setNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
|
|
277
395
|
const [busy, setBusy] = useState(false)
|
|
278
396
|
/** 1-based:与 `ListPager` 同一个口径。 */
|
|
279
397
|
const [page, setPage] = useState(1)
|
|
280
398
|
const refreshSequence = useRef(0)
|
|
281
|
-
/**
|
|
282
|
-
* 四段步轨的阶段结论读这里,而不是当前这一页任务。
|
|
283
|
-
*
|
|
284
|
-
* 任务列表是服务端分页的(一页 `PAGE_SIZE` 条):某个阶段的最新结论一旦翻出第一页,
|
|
285
|
-
* 直接搜当前页会让那一段悄悄退回「待运行」—— 屏幕上看起来像从来没跑过。
|
|
286
|
-
* 所以按 id 累积见过的状态,只留最近 200 条(这只是阶段结论的缓存,完整历史在服务端)。
|
|
287
|
-
*/
|
|
399
|
+
/** Cache viewed executions independently of the paginated history. */
|
|
288
400
|
const [stageTasks, setStageTasks] = useState<PipelineTask[]>([])
|
|
289
|
-
/**
|
|
401
|
+
/** 三段轨迹的列表(不含报告本体)。与任务分页**无关**。 */
|
|
290
402
|
const [trajectories, setTrajectories] = useState<ExtractionRunSummary[]>([])
|
|
291
403
|
const seenTasks = useRef(new Map<string, PipelineTask>())
|
|
292
404
|
const rememberTasks = useCallback((rows: PipelineTask[]): void => {
|
|
293
|
-
for (const task of rows)
|
|
405
|
+
for (const task of rows) {
|
|
406
|
+
const previous = seenTasks.current.get(task.id)
|
|
407
|
+
const stamp = (value: PipelineTask): string => value.finishedAt ?? value.activity?.at ?? value.startedAt ?? value.queuedAt
|
|
408
|
+
if (!previous || stamp(task) >= stamp(previous)) seenTasks.current.set(task.id, task)
|
|
409
|
+
}
|
|
294
410
|
if (seenTasks.current.size > 200) {
|
|
295
|
-
const
|
|
411
|
+
const priority = (task: PipelineTask): number => Number(task.id === selectedTaskRef.current) * 2 + Number(PIPELINE_ACTIVE_STATES.includes(task.state))
|
|
412
|
+
const ordered = [...seenTasks.current.values()].sort((left, right) => priority(right) - priority(left) || right.queuedAt.localeCompare(left.queuedAt)).slice(0, 200)
|
|
296
413
|
seenTasks.current = new Map(ordered.map(task => [task.id, task]))
|
|
297
414
|
}
|
|
298
415
|
setStageTasks([...seenTasks.current.values()].sort((left, right) => right.queuedAt.localeCompare(left.queuedAt)))
|
|
@@ -301,278 +418,295 @@ export function ExtractionTasksPage({ kb }: { kb: string }): ReactNode {
|
|
|
301
418
|
if (!kb) return
|
|
302
419
|
const sequence = ++refreshSequence.current
|
|
303
420
|
try {
|
|
304
|
-
const [nextStatus, nextHistory, nextTrajectories] = await Promise.all([
|
|
421
|
+
const [nextStatus, nextHistory, nextTrajectories, nextDocuments] = await Promise.all([
|
|
305
422
|
requestJson<PipelineStatus>(`/knowledge/pipeline?kb=${encodeURIComponent(kb)}`),
|
|
306
423
|
requestJson<History>(`/knowledge/pipeline-tasks?${new URLSearchParams({ kb, limit: String(PAGE_SIZE), offset: String((page - 1) * PAGE_SIZE) })}`),
|
|
307
424
|
// 轨迹库不可用时返回 `available: false` 而不是报错:这一段没有轨迹,不该让整页红一下。
|
|
308
|
-
requestExtractionRuns({ kb, limit:
|
|
425
|
+
requestExtractionRuns({ kb, limit: 100 }).catch(() => null),
|
|
426
|
+
requestJson<DocumentPage>(`/knowledge/documents?${new URLSearchParams({ kb, limit: '500' })}`).catch(() => null),
|
|
309
427
|
])
|
|
310
428
|
if (sequence !== refreshSequence.current) return
|
|
311
429
|
if (nextStatus.data) setStatus(nextStatus.data)
|
|
312
|
-
if (nextHistory.data) {
|
|
430
|
+
if (nextHistory.data) {
|
|
431
|
+
setHistory(nextHistory.data)
|
|
432
|
+
const active = nextHistory.data.activeTasks ?? nextHistory.data.tasks.filter(task => PIPELINE_ACTIVE_STATES.includes(task.state))
|
|
433
|
+
setActiveTasks(active)
|
|
434
|
+
rememberTasks([...active, ...nextHistory.data.tasks])
|
|
435
|
+
}
|
|
313
436
|
if (nextTrajectories) setTrajectories(nextTrajectories.runs)
|
|
314
|
-
|
|
315
|
-
|
|
437
|
+
if (nextDocuments?.data?.documents) {
|
|
438
|
+
setDocuments(nextDocuments.data.documents)
|
|
439
|
+
setRunCounts(nextDocuments.data.extractionRuns ?? {})
|
|
440
|
+
}
|
|
441
|
+
} catch (cause) {
|
|
442
|
+
if (sequence === refreshSequence.current) setNotice({ tone: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
443
|
+
}
|
|
316
444
|
}, [kb, page, rememberTasks])
|
|
317
|
-
const activeCount =
|
|
445
|
+
const activeCount = activeTasks.length
|
|
318
446
|
useEffect(() => {
|
|
319
447
|
void refresh()
|
|
320
448
|
const focus = (): void => { void refresh() }
|
|
321
449
|
window.addEventListener('focus', focus)
|
|
322
|
-
const timer =
|
|
323
|
-
return () => {
|
|
450
|
+
const timer = window.setInterval(focus, activeCount ? 2500 : 5000)
|
|
451
|
+
return () => { window.clearInterval(timer); window.removeEventListener('focus', focus) }
|
|
324
452
|
}, [refresh, activeCount])
|
|
325
|
-
|
|
326
|
-
|
|
453
|
+
|
|
454
|
+
const start = async (next: PipelineStage, force = false): Promise<void> => {
|
|
455
|
+
setBusy(true)
|
|
456
|
+
setNotice(null)
|
|
327
457
|
try {
|
|
328
|
-
const sourceScope =
|
|
329
|
-
const groupScope =
|
|
330
|
-
const result = await requestJson<{ task: PipelineTask }>('/knowledge/pipeline', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kb, stage, ...(sourceScope && sourceIds.length ? { sourceIds } : {}), ...(groupScope && groupKeys.length ? { groupKeys } : {}), force }) })
|
|
331
|
-
if (result.data?.task) {
|
|
458
|
+
const sourceScope = next === 'all' || next === 'extract' || next === 'normalize' || next === 'derive'
|
|
459
|
+
const groupScope = next === 'all' || next === 'materialize'
|
|
460
|
+
const result = await requestJson<{ task: PipelineTask }>('/knowledge/pipeline', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kb, stage: next, ...(sourceScope && sourceIds.length ? { sourceIds } : {}), ...(groupScope && groupKeys.length ? { groupKeys } : {}), force }) })
|
|
461
|
+
if (result.data?.task) {
|
|
462
|
+
rememberTasks([result.data.task]); setSelectedTask(result.data.task.id); setPinnedTask(true); setFollowRun(true)
|
|
463
|
+
setSideTab('activity'); setPage(1); setStage(next === 'all' ? 'extract' : next)
|
|
464
|
+
}
|
|
332
465
|
await refresh()
|
|
333
|
-
} catch (cause) {
|
|
334
|
-
|
|
466
|
+
} catch (cause) {
|
|
467
|
+
setNotice({ tone: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
468
|
+
} finally { setBusy(false) }
|
|
335
469
|
}
|
|
336
470
|
const act = async (id: string, action: string): Promise<void> => {
|
|
337
471
|
setBusy(true)
|
|
338
472
|
try {
|
|
339
473
|
const result = await requestJson<{ task: PipelineTask }>('/knowledge/pipeline-task', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kb, id, action }) })
|
|
340
|
-
if (result.data?.task) setSelectedTask(result.data.task.id)
|
|
474
|
+
if (result.data?.task) { rememberTasks([result.data.task]); setSelectedTask(result.data.task.id); setPinnedTask(true); setFollowRun(true); setSideTab('activity') }
|
|
341
475
|
await refresh()
|
|
342
|
-
} catch (cause) {
|
|
343
|
-
|
|
476
|
+
} catch (cause) {
|
|
477
|
+
setNotice({ tone: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
478
|
+
} finally { setBusy(false) }
|
|
344
479
|
}
|
|
345
480
|
const replaceTask = useCallback((task: PipelineTask) => {
|
|
346
481
|
setHistory(current => current ? { ...current, tasks: current.tasks.map(row => row.id === task.id ? task : row) } : current)
|
|
482
|
+
setActiveTasks(current => PIPELINE_ACTIVE_STATES.includes(task.state)
|
|
483
|
+
? [...current.filter(row => row.id !== task.id), task]
|
|
484
|
+
: current.filter(row => row.id !== task.id))
|
|
347
485
|
rememberTasks([task])
|
|
348
486
|
}, [rememberTasks])
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
487
|
+
|
|
488
|
+
const docIndex = useMemo(() => new Map(documents.map(doc => [doc.docId, doc])), [documents])
|
|
489
|
+
const trailCounts = useMemo(() => {
|
|
490
|
+
const counts = new Map<string, number>()
|
|
491
|
+
for (const run of trajectories) counts.set(run.docId, (counts.get(run.docId) ?? 0) + 1)
|
|
492
|
+
return counts
|
|
493
|
+
}, [trajectories])
|
|
494
|
+
/** 主栏的行:文档状态来自 pipeline,格式 / 体积来自文档列表,规模描述来自最近一条轨迹。 */
|
|
495
|
+
const documentRows = useMemo<DocumentRow[]>(() => (status?.sources ?? []).map(source => {
|
|
496
|
+
const meta = docIndex.get(source.id)
|
|
497
|
+
return {
|
|
498
|
+
id: source.id,
|
|
499
|
+
docId: source.id,
|
|
500
|
+
title: source.title,
|
|
501
|
+
version: source.version,
|
|
502
|
+
state: source.state,
|
|
503
|
+
sourceFormat: meta?.sourceFormat ?? '',
|
|
504
|
+
ext: meta?.ext ?? '',
|
|
505
|
+
bytes: meta?.bytes ?? 0,
|
|
506
|
+
updatedAt: meta?.updatedAt ?? '',
|
|
507
|
+
versionCount: meta?.versionCount ?? source.version,
|
|
508
|
+
runs: trajectories.filter(run => run.docId === source.id),
|
|
509
|
+
trailCount: runCounts[source.id] ?? trailCounts.get(source.id) ?? 0,
|
|
510
|
+
}
|
|
511
|
+
}), [status, trajectories, docIndex, trailCounts, runCounts])
|
|
512
|
+
|
|
358
513
|
const total = history?.total ?? 0
|
|
359
|
-
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
|
360
514
|
const orderedTasks = [...(history?.tasks ?? [])].sort((left, right) => {
|
|
361
515
|
const activeOrder = Number(PIPELINE_ACTIVE_STATES.includes(right.state)) - Number(PIPELINE_ACTIVE_STATES.includes(left.state))
|
|
362
516
|
return activeOrder || right.queuedAt.localeCompare(left.queuedAt)
|
|
363
517
|
})
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
const sourceRange = sourceSelection
|
|
380
|
-
? names(sourceSelection, id => status?.sources.find(source => source.id === id)?.title ?? id)
|
|
381
|
-
: `全部来源(${status?.sources.length ?? '当前'} 份)`
|
|
382
|
-
const rows: Array<[string, string]> = []
|
|
383
|
-
if (stage === 'extract' || stage === 'normalize' || stage === 'derive') {
|
|
384
|
-
rows.push(['来源范围', sourceRange])
|
|
385
|
-
rows.push(['强制处理', selection.force ? '是,忽略可复用结果' : '否,复用未变化结果'])
|
|
386
|
-
} else if (stage === 'materialize') {
|
|
387
|
-
const groupRange = groupSelection
|
|
388
|
-
? names(groupSelection, id => status?.groups.find(group => group.key === id)?.title ?? id)
|
|
389
|
-
: task?.stage === 'all' && selection.sourceIds
|
|
390
|
-
? `全部待办分组中关联所选来源的分组(${sourceSelection?.length ?? 0} 份来源)`
|
|
391
|
-
: '全部待处理分组'
|
|
392
|
-
rows.push(['分组范围', groupRange])
|
|
393
|
-
if (task?.stage === 'all' && selection.sourceIds) rows.push(['来源限制', sourceRange])
|
|
394
|
-
rows.push(['强制处理', selection.force ? '是,重新物化所选分组' : '否,只处理有变化的分组'])
|
|
395
|
-
} else {
|
|
396
|
-
rows.push(['索引范围', '整个知识库;会按当前知识内容构建'])
|
|
397
|
-
rows.push(['选择范围', '来源与分组选择不缩小索引范围'])
|
|
398
|
-
}
|
|
399
|
-
if (stage !== 'build') {
|
|
400
|
-
const model = task?.model
|
|
401
|
-
rows.push(['模型配置', model?.model ? `${model.provider ? `${model.provider} / ` : ''}${model.model}${model.reasoningEffort ? ` · ${model.reasoningEffort}` : ''}` : '运行时读取当前模型设置'])
|
|
402
|
-
}
|
|
403
|
-
return rows
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* 结果摘要**不再重复「状态」**:那枚徽标在步骤轨上,是这一段的身份,不是结果的第二行。
|
|
407
|
-
* 这里只放「跑了多少、什么时候跑的」—— 一个问题一个答案。
|
|
408
|
-
*/
|
|
409
|
-
const outputRows = (stage: StageId, observation: StageObservation): Array<[string, string]> => {
|
|
410
|
-
const task = observation.task
|
|
411
|
-
const result = task?.results?.[stage]
|
|
412
|
-
const activeStage = task && PIPELINE_ACTIVE_STATES.includes(task.state) && task.currentStage === stage
|
|
413
|
-
const liveResult = activeStage ? task : task?.stage === stage && !result ? task : undefined
|
|
414
|
-
const counts = result ?? liveResult
|
|
415
|
-
const summary = result?.summary ?? (activeStage ? task?.summary : observation.detail) ?? observation.detail
|
|
416
|
-
return [
|
|
417
|
-
['摘要', summary],
|
|
418
|
-
...(counts ? [['已处理', `${counts.done}/${counts.total} 项`] as [string, string]] : []),
|
|
419
|
-
...(counts?.reused ? [['复用', `${counts.reused} 项`] as [string, string]] : []),
|
|
420
|
-
...(counts?.failed ? [['失败', `${counts.failed} 项`] as [string, string]] : []),
|
|
421
|
-
...(task ? [['最近运行', formatDateTime(task.finishedAt ?? task.startedAt ?? task.queuedAt)] as [string, string]] : []),
|
|
422
|
-
]
|
|
423
|
-
}
|
|
424
|
-
const runningTask = orderedTasks.find(task => PIPELINE_ACTIVE_STATES.includes(task.state))
|
|
518
|
+
const runningTask = orderActiveTasks(activeTasks)[0]
|
|
519
|
+
const focusedTask = pinnedTask ? stageTasks.find(task => task.id === selectedTask) : runningTask ?? stageTasks.find(task => task.id === selectedTask) ?? orderedTasks[0]
|
|
520
|
+
const feed = useTaskFeed(kb, pinnedTask ? selectedTask || undefined : focusedTask?.id, replaceTask)
|
|
521
|
+
const currentSourceId = focusedTask?.activity?.objectId ?? feed.events.filter(event => event.sourceId).at(-1)?.sourceId
|
|
522
|
+
const currentObjectTitle = documentRows.find(row => row.id === currentSourceId)?.title
|
|
523
|
+
useEffect(() => { if (!pinnedTask && focusedTask) setSelectedTask(focusedTask.id) }, [pinnedTask, focusedTask?.id])
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (followRun && focusedTask?.currentStage) setStage(focusedTask.currentStage)
|
|
526
|
+
}, [followRun, focusedTask?.id, focusedTask?.currentStage])
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
if (!selectedDoc) return
|
|
529
|
+
const target = [...document.querySelectorAll<HTMLElement>('[data-object-id]')].find(element => element.dataset.objectId === selectedDoc)
|
|
530
|
+
target?.scrollIntoView({ block: 'nearest' })
|
|
531
|
+
}, [selectedDoc, stage])
|
|
425
532
|
const headline = runningTask
|
|
426
|
-
?
|
|
533
|
+
? `当前任务${STATES[runningTask.state]};本次进度与控制在下方。`
|
|
427
534
|
: history
|
|
428
|
-
? '
|
|
535
|
+
? '当前没有运行中的任务。选好范围后按阶段运行;未变化的结果会自动复用。'
|
|
429
536
|
: '正在读取这个库的处理状态…'
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
537
|
+
const stageObservationOf = (id: StageId): StageObservation => stageObservation(id, focusedTask, status)
|
|
538
|
+
const selectedDocTitle = documentRows.find(row => row.id === selectedDoc)?.title ?? ''
|
|
539
|
+
const chooseTask = (id: string): void => { setSelectedTask(id); setPinnedTask(true); setFollowRun(true); setSideTab('activity') }
|
|
540
|
+
const followCurrent = (): void => { setFollowRun(true); if (focusedTask?.currentStage) setStage(focusedTask.currentStage) }
|
|
541
|
+
if (!kb) return <p className="ktError" role="alert"><AlertCircle size={13} />缺少知识库,请从知识库工具栏打开知识处理。</p>
|
|
542
|
+
|
|
543
|
+
const openKernel = (): void => {
|
|
544
|
+
void openAppPath('/convert/').then(opened => {
|
|
545
|
+
if (!opened) setNotice({ tone: 'error', text: '打不开解析内核控制台:当前外壳下拿不到它的地址。' })
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const renderStageBody = (id: StageId): ReactNode => {
|
|
550
|
+
const observation = stageObservationOf(id)
|
|
551
|
+
const result = observation.task?.results?.[id]
|
|
552
|
+
const scopeText = id === 'materialize'
|
|
553
|
+
? groupKeys.length ? `已选 ${groupKeys.length} 个分组` : `全部待处理分组(${status?.groups.length ?? 0} 个)`
|
|
554
|
+
: sourceIds.length ? `已选 ${sourceIds.length} 份文档` : `全部来源(${status?.sources.length ?? 0} 份)`
|
|
555
|
+
const model = observation.task?.model
|
|
556
|
+
const modelText = model?.model
|
|
557
|
+
? `${model.provider ? `${model.provider} / ` : ''}${model.model}${model.reasoningEffort ? ` · ${model.reasoningEffort}` : ''}`
|
|
558
|
+
: '运行时读取编译设置'
|
|
559
|
+
const runLabel = activeCount ? '加入队列' : id === 'materialize' && groupKeys.length > 0
|
|
560
|
+
? `物化选中 ${groupKeys.length} 个分组`
|
|
561
|
+
: sourceIds.length > 0 && id !== 'materialize' && id !== 'build'
|
|
562
|
+
? `运行选中 ${sourceIds.length} 份`
|
|
563
|
+
: '运行此阶段'
|
|
564
|
+
return <div className="ktStage">
|
|
565
|
+
<div className="ktStageHead">
|
|
566
|
+
<div className="ktStageIntro">
|
|
567
|
+
<h2>{PIPELINE_STAGE_LABEL[id]}</h2>
|
|
568
|
+
<p>{STAGE_DETAIL[id]}</p>
|
|
569
|
+
</div>
|
|
570
|
+
<div className="ktStageActions">
|
|
571
|
+
{sourceIds.length > 0 && <Button size="sm" variant="ghost" disabled={busy} onClick={() => setSourceIds([])}>清除选择</Button>}
|
|
572
|
+
{groupKeys.length > 0 && <Button size="sm" variant="ghost" disabled={busy} onClick={() => setGroupKeys([])}>清除选择</Button>}
|
|
573
|
+
{(id === 'extract' || id === 'normalize') && sourceIds.length > 0 && <Button size="sm" variant="ghost" disabled={busy} onClick={() => { void start(id, true) }}>强制重跑</Button>}
|
|
574
|
+
<Button size="sm" variant="outline" disabled={busy || !status || ((id === 'extract' || id === 'normalize') && !status.sources.length)} onClick={() => { void start(id) }}>{runLabel}</Button>
|
|
575
|
+
</div>
|
|
436
576
|
</div>
|
|
577
|
+
<p className="ktStageMeta">
|
|
578
|
+
<span>下次运行范围:{scopeText}</span>
|
|
579
|
+
<i aria-hidden="true">·</i><span>模型:{modelText}</span>
|
|
580
|
+
{result && <><i aria-hidden="true">·</i><span>已处理 {result.done}/{result.total}{result.reused ? ` · 复用 ${result.reused}` : ''}{result.failed ? ` · 失败 ${result.failed}` : ''}</span></>}
|
|
581
|
+
{observation.task && <><i aria-hidden="true">·</i><span>查看任务 {formatDateTime(observation.task.finishedAt ?? observation.task.startedAt ?? observation.task.queuedAt)}</span></>}
|
|
582
|
+
</p>
|
|
583
|
+
|
|
584
|
+
{id === 'extract' || id === 'normalize'
|
|
585
|
+
? <DocumentList
|
|
586
|
+
rows={documentRows}
|
|
587
|
+
selected={selectedDoc}
|
|
588
|
+
onSelect={next => { setSelectedDoc(current => current === next ? '' : next); setSideTab('trail') }}
|
|
589
|
+
selectedIds={sourceIds}
|
|
590
|
+
onToggle={next => setSourceIds(current => toggle(current, next))}
|
|
591
|
+
stage={id}
|
|
592
|
+
task={focusedTask}
|
|
593
|
+
currentSourceId={currentSourceId}
|
|
594
|
+
/>
|
|
595
|
+
: null}
|
|
596
|
+
{id === 'derive' && <div className="ktDerive">
|
|
597
|
+
<div>
|
|
598
|
+
<h3>从 Case 提炼可复用的方法</h3>
|
|
599
|
+
<p>这一段的对象是 Case,不是文档:只有带检查与动作证据的 Case 才会产出 Procedure。没有证据就不生成,这是刻意的。</p>
|
|
600
|
+
</div>
|
|
601
|
+
<dl>
|
|
602
|
+
<div><dt>处理对象</dt><dd>Case(带检查与动作证据的那些),不是文档。</dd></div>
|
|
603
|
+
<div><dt>{focusedTask ? '本次结论' : '库内状态'}</dt><dd>{observation.label} · {observation.detail}</dd></div>
|
|
604
|
+
</dl>
|
|
605
|
+
</div>}
|
|
606
|
+
{id === 'materialize' && <GroupList groups={status?.groups ?? []} selectedKeys={groupKeys} onToggle={key => setGroupKeys(current => toggle(current, key))} task={focusedTask} inspectingKey={selectedDoc} />}
|
|
607
|
+
{id === 'build' && <div className="ktIndex">
|
|
608
|
+
<div className="ktIndexState">
|
|
609
|
+
{status?.indexPending ? <Badge tone="warning">索引待更新</Badge> : <Badge tone="success">索引已最新</Badge>}
|
|
610
|
+
<p>{observation.detail}</p>
|
|
611
|
+
</div>
|
|
612
|
+
<dl>
|
|
613
|
+
<div><dt>索引范围</dt><dd>整个知识库。构建会读已物化卡片,不重新抽取文档。</dd></div>
|
|
614
|
+
<div><dt>换向量模型</dt><dd>只需重新构建索引,文档不用再抽一遍。</dd></div>
|
|
615
|
+
</dl>
|
|
616
|
+
</div>}
|
|
617
|
+
|
|
618
|
+
{focusedTask && <StageOutput key={`${focusedTask.id}:${id}`}><RawTaskOutput kb={kb} task={focusedTask} events={feed.events} stage={id} onOpenFailed={message => setNotice({ tone: 'error', text: message })} /></StageOutput>}
|
|
437
619
|
</div>
|
|
438
|
-
|
|
620
|
+
}
|
|
439
621
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
</Fragment>
|
|
456
|
-
})}
|
|
457
|
-
</TabsList>
|
|
622
|
+
return <div className="ktPage">
|
|
623
|
+
<div className="ktTop">
|
|
624
|
+
<div className="ktTopLead">
|
|
625
|
+
<div className="ktViews" role="group" aria-label="知识处理视图">
|
|
626
|
+
<Button variant="ghost" aria-pressed={view === 'run'} className="ktView" onClick={() => setView('run')}>处理</Button>
|
|
627
|
+
<Button variant="ghost" aria-pressed={view === 'settings'} className="ktView" onClick={() => setView('settings')}>编译设置</Button>
|
|
628
|
+
</div>
|
|
629
|
+
<p className="ktHeadline" role="status">{view === 'run' ? headline : '编译模型、提取备注与坐标实体决定这个库怎么把文档变成知识。'}</p>
|
|
630
|
+
</div>
|
|
631
|
+
<div className="ktTopActions">
|
|
632
|
+
<Button variant="ghost" size="sm" onClick={() => { void refresh(); setSettingsKey(current => current + 1) }}><RefreshCw size={13} />刷新</Button>
|
|
633
|
+
<Button variant="outline" size="sm" title="打开解析内核控制台:文件 / 链接 → 知识就绪 Markdown" onClick={openKernel}><FileUp size={13} />解析内核</Button>
|
|
634
|
+
{view === 'run' && <Button size="sm" title="按顺序运行全部阶段:抽取 → 归一 → 提炼 → 物化 → 构建索引" disabled={busy || !status?.sources.length} onClick={() => { void start('all') }}><Play size={13} />{activeCount ? '全链路加入队列' : '全链路运行'}</Button>}
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
458
637
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
{observation.task && <StageOutput active={live}><TaskOutput kb={kb} id={observation.task.id} onTask={replaceTask} onOpenFailed={setFailure} stageFilter={stage.id} taskStage={observation.task.stage} /></StageOutput>}
|
|
486
|
-
</TabsContent>
|
|
638
|
+
{notice && <p className={notice.tone === 'error' ? 'ktError' : 'ktNotice'} role={notice.tone === 'error' ? 'alert' : 'status'}>
|
|
639
|
+
{notice.tone === 'error' ? <AlertCircle size={13} /> : <Check size={13} />}
|
|
640
|
+
<span>{notice.text}</span>
|
|
641
|
+
<Button variant="ghost" size="icon" className="ktNoticeClose" aria-label="关闭提示" onClick={() => setNotice(null)}><X size={13} /></Button>
|
|
642
|
+
</p>}
|
|
643
|
+
|
|
644
|
+
{view === 'settings'
|
|
645
|
+
? <CompileSettingsView key={settingsKey} kb={kb} onNotice={(tone, text) => setNotice({ tone, text })} />
|
|
646
|
+
: <Tabs value={stage} onValueChange={next => { setStage(next as StageId); setSelectedDoc(''); setFollowRun(false) }} className="ktWork">
|
|
647
|
+
{focusedTask && <RunSummary task={focusedTask} events={feed.events} failure={feed.failure} busy={busy} onAct={(id, action) => { void act(id, action) }} onFollow={followCurrent} onRecovery={chooseTask} following={followRun && stage === focusedTask.currentStage} queueCount={activeTasks.filter(task => task.state === 'queued' && task.id !== focusedTask.id).length} queuePosition={orderActiveTasks(activeTasks).filter(task => task.state === 'queued').findIndex(task => task.id === focusedTask.id) + 1} objectTitle={currentObjectTitle} />}
|
|
648
|
+
{runningTask && focusedTask?.id !== runningTask.id && <p className="ktNotice"><span>正在查看历史或排队任务;另有任务{STATES[runningTask.state]}。</span><Button onClick={() => { setPinnedTask(false); setFollowRun(true); setSideTab('activity') }}>查看当前运行</Button></p>}
|
|
649
|
+
{pinnedTask && selectedTask && !focusedTask && <p className="ktHint">{feed.failure ? `指定任务读取失败:${feed.failure}` : '正在读取指定任务…'}{feed.failure && <Button variant="ghost" onClick={() => setPinnedTask(false)}>查看最近任务</Button>}</p>}
|
|
650
|
+
<div className="ktRailHeading"><strong>{focusedTask ? '本次任务阶段' : '知识库待处理状态'}</strong><span>点击阶段查看详情;带「进行中」标记的阶段正在执行。</span></div>
|
|
651
|
+
<TabsList aria-label="知识处理阶段" className="ktRail">
|
|
652
|
+
{STAGE_ORDER.map((id, index) => {
|
|
653
|
+
const observation = stageObservationOf(id)
|
|
654
|
+
return <Fragment key={id}>
|
|
655
|
+
{index > 0 && <span className="ktRailArrow" aria-hidden="true"><ArrowRight size={15} /></span>}
|
|
656
|
+
<TabsTrigger value={id} className="ktRailStep" data-running={focusedTask?.currentStage === id && PIPELINE_ACTIVE_STATES.includes(focusedTask.state) || undefined} title={observation.detail}>
|
|
657
|
+
<span className="ktRailTop"><span className="ktRailIndex" aria-hidden="true">{index + 1}</span><span className="ktRailName">{PIPELINE_STAGE_LABEL[id]}</span></span>
|
|
658
|
+
<span className="ktRailFacts">
|
|
659
|
+
<Badge tone={observation.tone}>{observation.label}</Badge>
|
|
660
|
+
<span className="ktRailPending">{focusedTask ? focusedTask.results?.[id] ? `${focusedTask.results[id]!.done}/${focusedTask.results[id]!.total} 已处理` : focusedTask.currentStage === id && focusedTask.total ? `${focusedTask.done}/${focusedTask.total} 已处理` : '' : stagePending(id, status)}</span>
|
|
661
|
+
</span>
|
|
662
|
+
</TabsTrigger>
|
|
663
|
+
</Fragment>
|
|
487
664
|
})}
|
|
488
|
-
</
|
|
665
|
+
</TabsList>
|
|
489
666
|
|
|
490
|
-
<
|
|
491
|
-
<
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
<h2 id="scope-title">处理范围</h2>
|
|
495
|
-
<p>不选就是全部;点一行选中它。</p>
|
|
496
|
-
</div>
|
|
497
|
-
</div>
|
|
498
|
-
<div className="extractTasksPanelBody">
|
|
499
|
-
<Tabs defaultValue="sources">
|
|
500
|
-
<TabsList><TabsTrigger value="sources">来源</TabsTrigger><TabsTrigger value="groups">卡片分组</TabsTrigger></TabsList>
|
|
501
|
-
<TabsContent value="sources" className="extractTasksScopeTab">
|
|
502
|
-
<p className="extractTasksScopeNote">{sourceIds.length ? `已选 ${sourceIds.length} 份文档。` : '未选择时处理全部来源,未变化的结果自动复用。'}</p>
|
|
503
|
-
{!status && <p className="extractTasksEmpty">正在读取来源…</p>}
|
|
504
|
-
{status?.sources.length === 0 && <p className="extractTasksEmpty">当前没有来源。添加来源后即可开始处理。</p>}
|
|
505
|
-
<ul className="extractTasksChoiceList" aria-label="来源选择">{status?.sources.map(source => {
|
|
506
|
-
const on = sourceIds.includes(source.id)
|
|
507
|
-
return <li key={source.id}>
|
|
508
|
-
<button type="button" className="extractTasksPick" aria-pressed={on} onClick={() => setSourceIds(current => toggle(current, source.id))}>
|
|
509
|
-
<span className="extractTasksPickBox" aria-hidden="true">{on && <Check size={11} />}</span>
|
|
510
|
-
<span className="extractTasksChoiceName">{source.title}<span>v{source.version}</span></span>
|
|
511
|
-
<Badge tone="neutral">{SOURCE_STATES[source.state]}</Badge>
|
|
512
|
-
</button>
|
|
513
|
-
</li>
|
|
514
|
-
})}</ul>
|
|
515
|
-
{sourceIds.length > 0 && <div className="extractTasksScopeActions">
|
|
516
|
-
<Button size="sm" variant="outline" disabled={busy} onClick={() => { void start('extract', true) }}>强制重抽 {sourceIds.length} 份</Button>
|
|
517
|
-
<Button size="sm" variant="ghost" onClick={() => setSourceIds([])}>清除选择</Button>
|
|
518
|
-
</div>}
|
|
519
|
-
</TabsContent>
|
|
520
|
-
<TabsContent value="groups" className="extractTasksScopeTab">
|
|
521
|
-
<p className="extractTasksScopeNote">{groupKeys.length ? `已选 ${groupKeys.length} 个完整卡片分组。` : '未选择时物化全部待办;同坐标的知识与事件一起处理。'}</p>
|
|
522
|
-
{!status && <p className="extractTasksEmpty">正在读取分组…</p>}
|
|
523
|
-
{status?.groups.length === 0 && <p className="extractTasksEmpty">还没有可物化分组。先抽取并归一文档内容。</p>}
|
|
524
|
-
<ul className="extractTasksChoiceList extractTasksGroupList" aria-label="卡片分组选择">{status?.groups.map(group => {
|
|
525
|
-
const on = groupKeys.includes(group.key)
|
|
526
|
-
return <li key={group.key}>
|
|
527
|
-
<button type="button" className="extractTasksPick" aria-pressed={on} onClick={() => setGroupKeys(current => toggle(current, group.key))}>
|
|
528
|
-
<span className="extractTasksPickBox" aria-hidden="true">{on && <Check size={11} />}</span>
|
|
529
|
-
<span className="extractTasksChoiceName"><strong>{group.title}</strong><span>{group.knowledgeCount} 条 Knowledge · {group.eventCount} 个 Event · {group.sourceIds.length} 份来源</span>{group.reason && <span className="extractTasksGroupReason">{group.reason}</span>}</span>
|
|
530
|
-
<Badge tone={group.state === 'current' ? 'success' : 'neutral'}>{GROUP_STATES[group.state]}</Badge>
|
|
531
|
-
</button>
|
|
532
|
-
</li>
|
|
533
|
-
})}</ul>
|
|
534
|
-
{groupKeys.length > 0 && <div className="extractTasksScopeActions">
|
|
535
|
-
<Button size="sm" variant="outline" disabled={busy} onClick={() => { void start('materialize') }}>物化选中的 {groupKeys.length} 个分组</Button>
|
|
536
|
-
<Button size="sm" variant="ghost" onClick={() => setGroupKeys([])}>清除选择</Button>
|
|
537
|
-
</div>}
|
|
538
|
-
</TabsContent>
|
|
539
|
-
</Tabs>
|
|
540
|
-
</div>
|
|
541
|
-
</section>
|
|
667
|
+
<div className="ktBody">
|
|
668
|
+
<div className="ktMain">
|
|
669
|
+
{STAGE_ORDER.map(id => <TabsContent key={id} value={id} className="ktStageTab">{renderStageBody(id)}</TabsContent>)}
|
|
670
|
+
</div>
|
|
542
671
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
<
|
|
548
|
-
<
|
|
549
|
-
</
|
|
550
|
-
<
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
672
|
+
<aside className="ktSide" aria-label="这一段的轨迹与运行记录">
|
|
673
|
+
<Tabs value={sideTab} onValueChange={setSideTab} className="ktSideTabs">
|
|
674
|
+
<TabsList className="ktSideTabsList">
|
|
675
|
+
<TabsTrigger value="activity">本次活动</TabsTrigger>
|
|
676
|
+
<TabsTrigger value="trail"><History size={12} />历史轨迹</TabsTrigger>
|
|
677
|
+
<TabsTrigger value="history">历史任务{total ? <span className="ktSideBadge">{total}</span> : null}</TabsTrigger>
|
|
678
|
+
</TabsList>
|
|
679
|
+
<TabsContent value="activity" className="ktSidePanel">
|
|
680
|
+
{focusedTask ? <RunActivity key={focusedTask.id} task={focusedTask} events={feed.events} onInspect={(id, nextStage) => { setSelectedDoc(id); if (nextStage) setStage(nextStage); setFollowRun(false) }} /> : <p className="ktSideEmptyText">运行任务后,在这里查看保存、重试和完成记录。</p>}
|
|
681
|
+
</TabsContent>
|
|
682
|
+
<TabsContent value="trail" className="ktSidePanel">
|
|
683
|
+
<TrajectoryPanel
|
|
684
|
+
kb={kb}
|
|
685
|
+
stage={stage}
|
|
686
|
+
selectedDoc={selectedDoc}
|
|
687
|
+
docTitle={selectedDocTitle}
|
|
688
|
+
runs={trajectories}
|
|
689
|
+
onClearDoc={() => setSelectedDoc('')}
|
|
690
|
+
onOpenFailed={message => setNotice({ tone: 'error', text: message })}
|
|
691
|
+
/>
|
|
692
|
+
</TabsContent>
|
|
693
|
+
<TabsContent value="history" className="ktSidePanel">
|
|
694
|
+
<RunHistoryPanel
|
|
695
|
+
tasks={history?.tasks ?? []}
|
|
696
|
+
orderedTasks={orderedTasks}
|
|
697
|
+
total={total}
|
|
698
|
+
page={page}
|
|
699
|
+
onPage={setPage}
|
|
700
|
+
activeCount={activeCount}
|
|
701
|
+
selectedTask={focusedTask?.id ?? ''}
|
|
702
|
+
onToggleTask={chooseTask}
|
|
703
|
+
busy={busy}
|
|
704
|
+
onAct={(id, action) => { void act(id, action) }}
|
|
705
|
+
/>
|
|
706
|
+
</TabsContent>
|
|
707
|
+
</Tabs>
|
|
708
|
+
</aside>
|
|
709
|
+
</div>
|
|
710
|
+
</Tabs>}
|
|
577
711
|
</div>
|
|
578
712
|
}
|