dsh-data-domain-knowledge 0.1.0-alpha.13 → 0.1.0-alpha.15
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 +89 -56
- package/cordis.patch.yml +36 -129
- package/eval/benchmark-extraction-format.mjs +20 -0
- package/lib/client.js +3262 -4142
- package/lib/doc-page.js +1 -1
- package/lib/pages.js +1147 -364
- package/lib/types/axis-registry.d.ts +8 -1
- package/lib/types/axis-registry.d.ts.map +1 -1
- package/lib/types/axis-registry.js +62 -7
- package/lib/types/axis-registry.js.map +1 -1
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +3 -0
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/convert/ingest.d.ts +9 -0
- package/lib/types/convert/ingest.d.ts.map +1 -1
- package/lib/types/convert/ingest.js +68 -9
- package/lib/types/convert/ingest.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +7 -3
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +13 -8
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +4 -13
- package/lib/types/diagnosis/run.d.ts.map +1 -1
- package/lib/types/diagnosis/run.js +26 -16
- package/lib/types/diagnosis/run.js.map +1 -1
- package/lib/types/diagnosis/schema.js +2 -2
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +1 -1
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +5 -4
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +4 -4
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +6 -8
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +18 -17
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +94 -66
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +10 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/environment/dsh-python.d.ts +77 -0
- package/lib/types/environment/dsh-python.d.ts.map +1 -0
- package/lib/types/environment/dsh-python.js +139 -0
- package/lib/types/environment/dsh-python.js.map +1 -0
- package/lib/types/environment/install.d.ts +9 -1
- package/lib/types/environment/install.d.ts.map +1 -1
- package/lib/types/environment/install.js +149 -24
- package/lib/types/environment/install.js.map +1 -1
- package/lib/types/environment/model-catalog.d.ts +34 -2
- package/lib/types/environment/model-catalog.d.ts.map +1 -1
- package/lib/types/environment/model-catalog.js +40 -2
- package/lib/types/environment/model-catalog.js.map +1 -1
- package/lib/types/environment/service.d.ts +13 -3
- package/lib/types/environment/service.d.ts.map +1 -1
- package/lib/types/environment/service.js +127 -23
- package/lib/types/environment/service.js.map +1 -1
- package/lib/types/environment/types.d.ts +9 -0
- package/lib/types/environment/types.d.ts.map +1 -1
- package/lib/types/events.d.ts +2 -0
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js +13 -0
- package/lib/types/events.js.map +1 -1
- package/lib/types/extract-contract.d.ts +1 -1
- package/lib/types/extract-contract.d.ts.map +1 -1
- package/lib/types/extract-contract.js +34 -31
- package/lib/types/extract-contract.js.map +1 -1
- package/lib/types/extract-model.d.ts +2 -0
- package/lib/types/extract-model.d.ts.map +1 -1
- package/lib/types/extract-model.js.map +1 -1
- package/lib/types/extract-table-contract.d.ts +3 -0
- package/lib/types/extract-table-contract.d.ts.map +1 -0
- package/lib/types/extract-table-contract.js +30 -0
- package/lib/types/extract-table-contract.js.map +1 -0
- package/lib/types/extract-table.d.ts +8 -0
- package/lib/types/extract-table.d.ts.map +1 -0
- package/lib/types/extract-table.js +151 -0
- package/lib/types/extract-table.js.map +1 -0
- package/lib/types/extraction-runs.d.ts +30 -6
- package/lib/types/extraction-runs.d.ts.map +1 -1
- package/lib/types/extraction-runs.js +69 -8
- package/lib/types/extraction-runs.js.map +1 -1
- package/lib/types/fs-atomic.d.ts +0 -7
- package/lib/types/fs-atomic.d.ts.map +1 -1
- package/lib/types/fs-atomic.js +17 -2
- package/lib/types/fs-atomic.js.map +1 -1
- package/lib/types/images.d.ts +1 -1
- package/lib/types/images.d.ts.map +1 -1
- package/lib/types/images.js +2 -2
- package/lib/types/images.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +232 -50
- package/lib/types/index.js.map +1 -1
- package/lib/types/intent/scope.d.ts.map +1 -1
- package/lib/types/intent/scope.js +1 -0
- package/lib/types/intent/scope.js.map +1 -1
- package/lib/types/intent/tools.d.ts +19 -0
- package/lib/types/intent/tools.d.ts.map +1 -1
- package/lib/types/intent/tools.js +37 -1
- package/lib/types/intent/tools.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +5 -16
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/knowledge.d.ts +3 -0
- package/lib/types/knowledge.d.ts.map +1 -1
- package/lib/types/knowledge.js +21 -5
- package/lib/types/knowledge.js.map +1 -1
- package/lib/types/materialize-snapshot.d.ts +49 -0
- package/lib/types/materialize-snapshot.d.ts.map +1 -0
- package/lib/types/materialize-snapshot.js +2 -0
- package/lib/types/materialize-snapshot.js.map +1 -0
- package/lib/types/model-text.d.ts +1 -1
- package/lib/types/model-text.d.ts.map +1 -1
- package/lib/types/model-text.js +5 -4
- package/lib/types/model-text.js.map +1 -1
- package/lib/types/oplog.d.ts.map +1 -1
- package/lib/types/oplog.js +7 -0
- package/lib/types/oplog.js.map +1 -1
- package/lib/types/pipeline/engine.d.ts +60 -0
- package/lib/types/pipeline/engine.d.ts.map +1 -0
- package/lib/types/pipeline/engine.js +338 -0
- package/lib/types/pipeline/engine.js.map +1 -0
- package/lib/types/pipeline/store.d.ts +73 -0
- package/lib/types/pipeline/store.d.ts.map +1 -0
- package/lib/types/pipeline/store.js +143 -0
- package/lib/types/pipeline/store.js.map +1 -0
- package/lib/types/pipeline/types.d.ts +101 -0
- package/lib/types/pipeline/types.d.ts.map +1 -0
- package/lib/types/pipeline/types.js +5 -0
- package/lib/types/pipeline/types.js.map +1 -0
- package/lib/types/present.d.ts +2 -0
- package/lib/types/present.d.ts.map +1 -1
- package/lib/types/present.js +9 -4
- package/lib/types/present.js.map +1 -1
- package/lib/types/retention.d.ts +39 -0
- package/lib/types/retention.d.ts.map +1 -0
- package/lib/types/retention.js +42 -0
- package/lib/types/retention.js.map +1 -0
- package/lib/types/runtime.d.ts +115 -5
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +862 -132
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/source-evidence.d.ts +9 -1
- package/lib/types/source-evidence.d.ts.map +1 -1
- package/lib/types/source-evidence.js +13 -5
- package/lib/types/source-evidence.js.map +1 -1
- package/lib/types/store/axis-references.js +3 -3
- package/lib/types/store/axis-references.js.map +1 -1
- package/lib/types/store/derived.d.ts +1 -1
- package/lib/types/store/derived.d.ts.map +1 -1
- package/lib/types/store/derived.js +41 -7
- package/lib/types/store/derived.js.map +1 -1
- package/lib/types/store/knowledge-db.d.ts +18 -0
- package/lib/types/store/knowledge-db.d.ts.map +1 -1
- package/lib/types/store/knowledge-db.js +111 -6
- package/lib/types/store/knowledge-db.js.map +1 -1
- package/lib/types/store/migrate.js +1 -1
- package/lib/types/store/migrate.js.map +1 -1
- package/lib/types/trajectory-types.d.ts +172 -0
- package/lib/types/trajectory-types.d.ts.map +1 -0
- package/lib/types/trajectory-types.js +36 -0
- package/lib/types/trajectory-types.js.map +1 -0
- package/lib/types/types.d.ts +3 -4
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-finalize.d.ts +16 -0
- package/lib/types/wiki-finalize.d.ts.map +1 -1
- package/lib/types/wiki-finalize.js +26 -0
- package/lib/types/wiki-finalize.js.map +1 -1
- package/lib/types/wiki-materialize.d.ts.map +1 -1
- package/lib/types/wiki-materialize.js +3 -15
- package/lib/types/wiki-materialize.js.map +1 -1
- package/lib/types/wiki-trial-types.d.ts +3 -0
- package/lib/types/wiki-trial-types.d.ts.map +1 -1
- package/lib/types/wiki-trial.d.ts +2 -0
- package/lib/types/wiki-trial.d.ts.map +1 -1
- package/lib/types/wiki-trial.js +31 -7
- package/lib/types/wiki-trial.js.map +1 -1
- package/package.json +6 -3
- package/preset/tita-data-agent/agent.cordis.yml +36 -129
- package/src/axis-registry.ts +57 -9
- package/src/client/AttributionEventGuide.tsx +10 -9
- package/src/client/CompileDialogs.tsx +10 -2
- package/src/client/EnvironmentSetup.tsx +76 -16
- package/src/client/ExpansionGraph.tsx +13 -7
- package/src/client/ExtractionTasksPage.tsx +543 -156
- package/src/client/ImportFilesDialog.tsx +2 -13
- package/src/client/IntentTab.tsx +3 -1
- package/src/client/KnowledgeBaseTab.tsx +139 -122
- package/src/client/KnowledgeLayerSection.tsx +19 -2
- package/src/client/KnowledgePanel.tsx +17 -3
- package/src/client/ListPager.tsx +15 -5
- package/src/client/MaterializeTrajectoryView.tsx +124 -0
- package/src/client/NodeSettings.tsx +90 -25
- package/src/client/NormalizeTrajectoryView.tsx +136 -0
- package/src/client/PipelineNotice.tsx +26 -0
- package/src/client/RecallPanel.tsx +55 -25
- package/src/client/TabGuide.tsx +4 -4
- package/src/client/TrialReportView.tsx +61 -16
- package/src/client/TypeDefinitionDrawer.tsx +33 -9
- package/src/client/WikiCardPreview.tsx +43 -10
- package/src/client/WikiFlowPanel.tsx +4 -4
- package/src/client/architecture.tsx +1 -1
- package/src/client/attribution/AttributionTab.tsx +37 -36
- package/src/client/attribution/CaseView.tsx +3 -1
- package/src/client/attribution/EventView.tsx +14 -4
- package/src/client/attribution/RunView.tsx +2 -2
- package/src/client/attribution/ViewSwitch.tsx +20 -5
- package/src/client/attribution/graph-options.ts +38 -31
- package/src/client/attribution/model.ts +16 -9
- package/src/client/graph/palette.ts +165 -0
- package/src/client/pages.css +224 -28
- package/src/client/pages.tsx +33 -11
- package/src/client/styles.css +61 -6
- package/src/client/wiki-trial.ts +30 -1
- package/src/convert/host.ts +3 -0
- package/src/convert/ingest.ts +73 -9
- package/src/diagnosis/cards.ts +13 -8
- package/src/diagnosis/run.ts +27 -17
- package/src/diagnosis/schema.ts +2 -2
- package/src/diagnosis/seed-source.ts +5 -4
- package/src/diagnosis/seed.ts +6 -8
- package/src/diagnosis/store.ts +99 -69
- package/src/diagnosis/types.ts +10 -16
- package/src/environment/dsh-python.ts +156 -0
- package/src/environment/install.ts +134 -19
- package/src/environment/model-catalog.ts +45 -3
- package/src/environment/service.ts +111 -23
- package/src/environment/types.ts +6 -0
- package/src/events.ts +12 -0
- package/src/extract-contract.ts +6 -3
- package/src/extract-model.ts +2 -0
- package/src/extract-table-contract.ts +29 -0
- package/src/extract-table.ts +126 -0
- package/src/extraction-runs.ts +70 -11
- package/src/fs-atomic.ts +19 -2
- package/src/images.ts +2 -2
- package/src/index.ts +207 -72
- package/src/intent/scope.ts +1 -0
- package/src/intent/tools.ts +38 -1
- package/src/knowledge-bases.ts +4 -12
- package/src/knowledge.ts +24 -4
- package/src/materialize-snapshot.ts +46 -0
- package/src/model-text.ts +5 -3
- package/src/oplog.ts +7 -0
- package/src/pipeline/engine.ts +312 -0
- package/src/pipeline/store.ts +147 -0
- package/src/pipeline/types.ts +92 -0
- package/src/present.ts +10 -4
- package/src/retention.ts +76 -0
- package/src/runtime.ts +853 -131
- package/src/source-evidence.ts +12 -5
- package/src/store/axis-references.ts +3 -3
- package/src/store/derived.ts +40 -7
- package/src/store/knowledge-db.ts +111 -5
- package/src/store/migrate.ts +1 -1
- package/src/trajectory-types.ts +142 -0
- package/src/types.ts +3 -4
- package/src/wiki-finalize.ts +28 -0
- package/src/wiki-materialize.ts +3 -14
- package/src/wiki-trial-types.ts +3 -0
- package/src/wiki-trial.ts +26 -7
- package/src/client/convert/ConvertPanel.tsx +0 -583
- package/src/client/convert/styles.css +0 -138
- package/src/client/convert/styles.ts +0 -49
|
@@ -223,7 +223,12 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
|
|
|
223
223
|
* 存储位置要 `agentRoot`、构建设置要 `resources` 的解析值 —— 都在同一份响应里。
|
|
224
224
|
*/
|
|
225
225
|
const knowledgeSnapshot = useScope(settings?.knowledge)
|
|
226
|
-
|
|
226
|
+
/**
|
|
227
|
+
* `error` / `reload` 必须**真的被用掉**(U4):只取 `layout` 的话,一次失败的
|
|
228
|
+
* `GET /knowledge/layout` 会让「数据位置」那一块永远停在「正在读取数据目录…」——
|
|
229
|
+
* 既不报错,也没有重试的路。两者都交给渲染路径字段的那一块。
|
|
230
|
+
*/
|
|
231
|
+
const { layout, error: layoutError, reload: reloadLayout } = useAgentLayout(knowledgeSnapshot.revision ?? 0)
|
|
227
232
|
const environment = useKnowledgeEnvironment(visible, knowledgeSnapshot.revision ?? 0)
|
|
228
233
|
|
|
229
234
|
/**
|
|
@@ -344,9 +349,16 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
|
|
|
344
349
|
<div className={cn(styles.content, (activeTab === 'attribution' || activeTab === 'overview') && styles.contentStage)}>
|
|
345
350
|
|
|
346
351
|
{/* 一级 tab 已经说明「现在是哪一页」,这里不再重复一遍标题与说明。
|
|
347
|
-
只有真的出问题时才占一行:读取失败 / 读取中。
|
|
352
|
+
只有真的出问题时才占一行:读取失败 / 读取中。
|
|
353
|
+
失败时**必须给一条重试的路**(A5):这一栏的取数只由 `[visible, refreshStats]`
|
|
354
|
+
触发,而 `visible` 在内部各视图之间一直是 true —— 一次瞬时失败会让
|
|
355
|
+
「数据读取失败」和空计数一直挂在那里,除非重挂载整个面板。 */}
|
|
348
356
|
{(statsError || statsLoading) && <p className={styles.viewStatus} role="status">
|
|
349
|
-
{statsError
|
|
357
|
+
{statsError
|
|
358
|
+
? <><AlertCircle size={13} />数据读取失败:{statsError}
|
|
359
|
+
<Button variant="ghost" size="sm" disabled={statsLoading} onClick={() => void refreshStats()}>重新读取</Button>
|
|
360
|
+
</>
|
|
361
|
+
: '正在读取知识库统计…'}
|
|
350
362
|
</p>}
|
|
351
363
|
|
|
352
364
|
{notice && <Card className={cn(styles.notice, notice.tone === 'error' ? styles.noticeError : styles.noticeSuccess)} role="status">
|
|
@@ -469,6 +481,8 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
|
|
|
469
481
|
showAdvanced={showAdvancedSettings}
|
|
470
482
|
agentRoot={layout?.agentRoot}
|
|
471
483
|
session={remote?.session}
|
|
484
|
+
layoutError={layoutError}
|
|
485
|
+
onRetryLayout={reloadLayout}
|
|
472
486
|
{...(node === 'build' && layout?.resources ? { resolved: layout.resources } : {})}
|
|
473
487
|
/>)}
|
|
474
488
|
</div>
|
package/src/client/ListPager.tsx
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 三条纪律:
|
|
9
9
|
*
|
|
10
|
-
* 1.
|
|
11
|
-
*
|
|
10
|
+
* 1. **不重新取数**(卡片列表成立)。两份列表的数据本来整份在手(`visibleDocuments` /
|
|
11
|
+
* `visibleCards`),分页只切渲染。**文件列表是例外**:它走服务端分页(`offset`),
|
|
12
|
+
* 因为「取 500 份再本地切页」会让超过 500 份的库从第 501 份起不可达。
|
|
12
13
|
* 2. **页码在筛选变化时回到第 1 页**。停在第 5 页而结果只剩 1 页时,用户看到的是一片空白,
|
|
13
14
|
* 而空白与「没有结果」在界面上长得一样。`usePage` 的第二个参数(筛选 key)管这件事。
|
|
14
15
|
* 3. **只有一页时不渲染**。一页装得下的列表底下挂一行「第 1 / 1 页」是纯噪音。
|
|
@@ -71,11 +72,20 @@ export interface ListPagerProps {
|
|
|
71
72
|
export function ListPager({ page, pageCount, total, onChange }: ListPagerProps): ReactNode {
|
|
72
73
|
// 一页装得下就不显示:分页器只在「真的有下一页」时才有意义。
|
|
73
74
|
if (total <= PAGE_SIZE) return null
|
|
75
|
+
/**
|
|
76
|
+
* 夹紧页码 —— **不能直接信 `page`**。
|
|
77
|
+
*
|
|
78
|
+
* 卡片抽屉那份列表是服务端分页的,总数可能在抽屉开着的时候变小;那时不夹紧就会在屏幕上
|
|
79
|
+
* 出现「第 3 / 2 页」这种自相矛盾的读数,而且两个按钮都还是可点的。标签与按钮边界
|
|
80
|
+
* 必须用同一个夹紧后的数。
|
|
81
|
+
*/
|
|
82
|
+
const safePageCount = Math.max(1, Math.trunc(pageCount) || 1)
|
|
83
|
+
const current = Math.min(Math.max(1, Math.trunc(page) || 1), safePageCount)
|
|
74
84
|
return <nav className={styles.pager} aria-label="分页">
|
|
75
85
|
<span className={styles.pagerCount}>
|
|
76
|
-
共 {total} 条 · 第 <b>{
|
|
86
|
+
共 {total} 条 · 第 <b>{current}</b> / {safePageCount} 页
|
|
77
87
|
</span>
|
|
78
|
-
<Button variant="outline" size="sm" disabled={
|
|
79
|
-
<Button variant="outline" size="sm" disabled={
|
|
88
|
+
<Button variant="outline" size="sm" disabled={current <= 1} onClick={() => onChange(current - 1)}>上一页</Button>
|
|
89
|
+
<Button variant="outline" size="sm" disabled={current >= safePageCount} onClick={() => onChange(current + 1)}>下一页</Button>
|
|
80
90
|
</nav>
|
|
81
91
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 物化轨迹的渲染(2026-10-18)。
|
|
3
|
+
*
|
|
4
|
+
* 物化这一段的产物就是卡片本身,所以「卡里有什么」在 WikiCard 那一栏已经查得到。
|
|
5
|
+
* 这一页回答的是**另外**三个问题:
|
|
6
|
+
*
|
|
7
|
+
* 1. **这一次对这张卡做了什么**(新建 / 更新 / 停用),以及是按哪个分组指纹做的。
|
|
8
|
+
* 2. **正文是怎么排出来的**(模型组织,还是机械拼装;每节几条)。
|
|
9
|
+
* 3. **哪些知识进了这张卡、哪几张卡被链进来、为什么**。
|
|
10
|
+
*
|
|
11
|
+
* 第 3 条是改提示词之后最想看的东西:「这次正文里链到的那张卡,和上次是不是同一张」——
|
|
12
|
+
* 这在前一个版本里没有任何地方能回答。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { type ReactNode } from 'react'
|
|
16
|
+
import { TriangleAlert } from '@tita-agent/ui/icons'
|
|
17
|
+
import { styles } from './styles.ts'
|
|
18
|
+
import type { MaterializeTrajectoryReport } from '../trajectory-types.js'
|
|
19
|
+
|
|
20
|
+
const ROW_CAP = 200
|
|
21
|
+
|
|
22
|
+
const ACTION_LABEL: Record<MaterializeTrajectoryReport['action'], string> = {
|
|
23
|
+
created: '新建卡片',
|
|
24
|
+
updated: '更新卡片',
|
|
25
|
+
withdrawn: '停用卡片',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const REASON_LABEL: Record<string, string> = {
|
|
29
|
+
coordinate: '同一坐标',
|
|
30
|
+
relation: '关系边',
|
|
31
|
+
source: '共同来源',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function MaterializeTrajectory({ report }: { report: MaterializeTrajectoryReport }): ReactNode {
|
|
35
|
+
const card = report.card
|
|
36
|
+
const rows = report.rows ?? []
|
|
37
|
+
const related = report.related ?? []
|
|
38
|
+
return <div className={styles.trialReport}>
|
|
39
|
+
<section className={styles.trialOverview} aria-labelledby="materialize-overview-title">
|
|
40
|
+
<div className={styles.trialSectionHead}>
|
|
41
|
+
<h2 id="materialize-overview-title">结果概览</h2>
|
|
42
|
+
<span>{ACTION_LABEL[report.action] ?? report.action}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<dl className={styles.trialOverviewStats}>
|
|
45
|
+
<div><dt>知识</dt><dd>{card?.knowledge ?? 0}<small>进这张卡</small></dd></div>
|
|
46
|
+
<div><dt>Event</dt><dd>{card?.events ?? 0}<small>同坐标引用</small></dd></div>
|
|
47
|
+
<div><dt>图</dt><dd>{card?.images ?? 0}<small>跟着原文走</small></dd></div>
|
|
48
|
+
<div><dt>小节</dt><dd>{card?.sections?.length ?? 0}<small>正文结构</small></dd></div>
|
|
49
|
+
<div><dt>正文字数</dt><dd>{(card?.characters ?? 0).toLocaleString('zh-CN')}<small>字符</small></dd></div>
|
|
50
|
+
</dl>
|
|
51
|
+
<p className={styles.trialOverviewTitle}>
|
|
52
|
+
<strong>{card?.title || report.group?.title || '(没有标题)'}</strong>
|
|
53
|
+
{/* 坐标在库里是**三个 id**,不是名字 —— 所以标出这是 id,别让它读成卡名。 */}
|
|
54
|
+
<span>坐标 id:{report.group?.coordinate || '无'} · 状态 {card?.status ?? '未知'}</span>
|
|
55
|
+
</p>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
{card?.id === undefined && <p className={styles.trialError} role="alert">
|
|
59
|
+
<TriangleAlert size={12} />这条轨迹没有卡片身份,本次没有写入任何卡片。
|
|
60
|
+
</p>}
|
|
61
|
+
|
|
62
|
+
{report.warnings.length > 0 && <section className={styles.trialSection} aria-labelledby="materialize-warnings-title">
|
|
63
|
+
<div className={styles.trialSectionHead}>
|
|
64
|
+
<h2 id="materialize-warnings-title">警告</h2>
|
|
65
|
+
<span>{report.warnings.length} 条</span>
|
|
66
|
+
</div>
|
|
67
|
+
<ul className={styles.trialCandidates}>{report.warnings.map((warning, index) => <li key={index}>
|
|
68
|
+
{/* 不加装饰图标:这一段就叫「警告」,一条黄三角再标一次是同一句话说两遍。
|
|
69
|
+
而且 `trialCandidates > li` 是上下两行的栅格 —— 图标单占一行会让正文掉到第三行。 */}
|
|
70
|
+
<span className={styles.trialCandName}>{warning}</span>
|
|
71
|
+
</li>)}</ul>
|
|
72
|
+
</section>}
|
|
73
|
+
|
|
74
|
+
<section className={styles.trialSection} aria-labelledby="materialize-sections-title">
|
|
75
|
+
<div className={styles.trialSectionHead}>
|
|
76
|
+
<h2 id="materialize-sections-title">正文结构</h2>
|
|
77
|
+
<span>{report.organize?.used ? `模型组织${report.organize.origin === 'fallback' ? '(本轮退回确定性布局)' : ''}` : '机械拼装(本库未开卡片组织)'}</span>
|
|
78
|
+
</div>
|
|
79
|
+
{card?.sections && card.sections.length > 0
|
|
80
|
+
? <ul className={styles.trialItems}>{card.sections.map(section => <li key={section.label} className={styles.trialItem}>
|
|
81
|
+
<span className={styles.trialItemRef}>{section.label}</span>
|
|
82
|
+
<span className={styles.trialItemArrow} aria-hidden="true">·</span>
|
|
83
|
+
<span className={styles.trialItemValue}>{section.count} 条</span>
|
|
84
|
+
</li>)}</ul>
|
|
85
|
+
: <p className={styles.trialEmpty}>
|
|
86
|
+
{report.action === 'withdrawn' ? '停用不改正文,所以没有分节。' : '这张卡没有分节(正文里的知识全部平铺)。'}
|
|
87
|
+
</p>}
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<section className={styles.trialSection} aria-labelledby="materialize-knowledge-title">
|
|
91
|
+
<div className={styles.trialSectionHead}>
|
|
92
|
+
<h2 id="materialize-knowledge-title">进卡的知识</h2>
|
|
93
|
+
<span>{rows.length} 条</span>
|
|
94
|
+
</div>
|
|
95
|
+
{rows.length === 0
|
|
96
|
+
? <p className={styles.trialEmpty}>{report.action === 'withdrawn' ? '停用不涉及知识。' : '这一组没有知识。'}</p>
|
|
97
|
+
: <ul className={styles.trialOutputList}>{rows.slice(0, ROW_CAP).map(row => <li key={row.id} className={styles.trialOutputItem}>
|
|
98
|
+
<div className={styles.trialCandHead}>
|
|
99
|
+
<span className={styles.trialCandName}>{row.text}</span>
|
|
100
|
+
<span className={styles.trialCandMeta}>{row.id}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div className={styles.trialOutputMeta}>{row.sources} 条出处</div>
|
|
103
|
+
</li>)}</ul>}
|
|
104
|
+
{rows.length > ROW_CAP && <p className={styles.trialMore}>另有 {rows.length - ROW_CAP} 条未展开。</p>}
|
|
105
|
+
</section>
|
|
106
|
+
|
|
107
|
+
<section className={styles.trialSection} aria-labelledby="materialize-related-title">
|
|
108
|
+
<div className={styles.trialSectionHead}>
|
|
109
|
+
<h2 id="materialize-related-title">相关卡片与来源</h2>
|
|
110
|
+
<span>正文里的链接目标必须是库里真实存在的卡</span>
|
|
111
|
+
</div>
|
|
112
|
+
{related.length > 0 && <ul className={styles.trialCandidates}>{related.map((item, index) => <li key={`${item.title}-${index}`}>
|
|
113
|
+
<strong>{item.title}</strong><span>{REASON_LABEL[item.reason] ?? item.reason}</span>
|
|
114
|
+
</li>)}</ul>}
|
|
115
|
+
<dl className={styles.trialRows}>
|
|
116
|
+
<div className={styles.trialRow}><dt>来源</dt><dd>{report.sources.length > 0 ? report.sources.join('、') : '—'}</dd></div>
|
|
117
|
+
<div className={styles.trialRow}><dt>分组键</dt><dd>{report.group?.key || '—'}</dd></div>
|
|
118
|
+
<div className={styles.trialRow}><dt>分组指纹</dt><dd>{report.group?.fingerprint || '—'}</dd></div>
|
|
119
|
+
{report.organize?.layoutHash && <div className={styles.trialRow}><dt>布局指纹</dt><dd>{report.organize.layoutHash}</dd></div>}
|
|
120
|
+
<div className={styles.trialRow}><dt>模型</dt><dd>{report.model?.model ? `${report.model.provider ? `${report.model.provider} / ` : ''}${report.model.model}${report.model.reasoningEffort ? ` · ${report.model.reasoningEffort}` : ''}` : '未记录'}</dd></div>
|
|
121
|
+
</dl>
|
|
122
|
+
</section>
|
|
123
|
+
</div>
|
|
124
|
+
}
|
|
@@ -87,10 +87,10 @@ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
|
|
|
87
87
|
// 0025:`compile` 设置组已撤。卡片编译模板是系统固定契约,不是用户设置项。
|
|
88
88
|
build: {
|
|
89
89
|
scope: 'knowledge',
|
|
90
|
-
title: '
|
|
90
|
+
title: '向量运行设置',
|
|
91
91
|
fields: [
|
|
92
92
|
{ key: 'embeddingProvider', label: '向量来源', kind: 'select', fallback: 'wemm-python', options: [{ value: 'wemm-python', label: '本地模型' }, { value: 'ollama', label: 'Ollama' }, { value: 'none', label: '关闭向量' }] },
|
|
93
|
-
{ key: 'embeddingModel', label: '
|
|
93
|
+
{ key: 'embeddingModel', label: 'Ollama 模型名称', kind: 'text', fallback: '', hint: '填写 Ollama 中已安装的模型名称。', visibleWhen: { key: 'embeddingProvider', value: 'ollama' } },
|
|
94
94
|
{ key: 'embeddingBaseUrl', label: 'Ollama 地址', kind: 'text', fallback: 'http://127.0.0.1:11434', visibleWhen: { key: 'embeddingProvider', value: 'ollama' } },
|
|
95
95
|
{ key: 'embeddingDevice', label: '运行设备', kind: 'select', fallback: 'auto', advanced: true, options: [{ value: 'auto', label: '自动' }, { value: 'mps', label: 'Apple MPS' }, { value: 'cpu', label: 'CPU' }] },
|
|
96
96
|
{ key: 'embeddingDimension', label: '向量维度', kind: 'number', fallback: 512, min: 1, step: 1, advanced: true, hint: '必须与所选模型输出维度一致;修改后需要重建向量索引。' },
|
|
@@ -107,7 +107,6 @@ export const NODE_SETTINGS: Record<SettingsNode, NodeSpec> = {
|
|
|
107
107
|
{ key: 'keywordThreshold', label: '关键词阈值', kind: 'number', fallback: 0.3, min: 0, max: 1, step: 0.01, advanced: true, hint: '0–1 的归一化 BM25 分数;适合过滤弱命中,不能单独判断整批结果是否相关。' },
|
|
108
108
|
{ key: 'vectorThreshold', label: '向量阈值', kind: 'number', fallback: 0.5, min: -1, max: 1, step: 0.01, advanced: true, hint: '原始余弦分数。默认 0.5 是当前本地模型与语料的起点,换模型后应按召回诊断校准。' },
|
|
109
109
|
{ key: 'rerankThreshold', label: '重排最低分', kind: 'number', fallback: 0.2, min: -10, max: 10, step: 0.1, advanced: true },
|
|
110
|
-
{ key: 'rerankModel', label: '模型路径或名称', kind: 'text', fallback: '', advanced: true },
|
|
111
110
|
{ key: 'rerankTimeoutMs', label: '重排超时(毫秒)', kind: 'number', fallback: 60000, min: 1000, max: 600000, step: 1000, advanced: true },
|
|
112
111
|
{ key: 'contextMaxChars', label: '上下文字符上限', kind: 'number', fallback: 20000, min: 0, max: 1000000, step: 1000, hint: '0 表示不限', advanced: true },
|
|
113
112
|
],
|
|
@@ -165,9 +164,36 @@ function readField(spec: FieldSpec, value: Record<string, unknown> | undefined):
|
|
|
165
164
|
return String(raw)
|
|
166
165
|
}
|
|
167
166
|
|
|
167
|
+
/**
|
|
168
|
+
* 一个数字字段的草稿能不能落盘。返回 `null` = 能,否则是人话的原因。
|
|
169
|
+
*
|
|
170
|
+
* 这里挡的是两个具体的坏值,它们都不是「用户想存这个数」,而是**那一瞬间的输入状态**:
|
|
171
|
+
*
|
|
172
|
+
* - `Number('') === 0`:清空「向量维度」会把 0 写进设置,而 0 是被 `min` 挡掉的非法值;
|
|
173
|
+
* - `Number('abc') === NaN`:`JSON.stringify(NaN)` 是 `null`,存下去是一份坏设置。
|
|
174
|
+
*
|
|
175
|
+
* 所以非法文本**留在草稿里**(人看到自己敲的东西、右面有一句「值无效」),只是不写。
|
|
176
|
+
* 这比「敲一半就被改写」温和:中间态是人打字时的一个正常阶段,不是错误。
|
|
177
|
+
*/
|
|
178
|
+
function fieldErrorOf(spec: FieldSpec, value: string | boolean): string | undefined {
|
|
179
|
+
if (spec.kind !== 'number') return undefined
|
|
180
|
+
const text = String(value).trim()
|
|
181
|
+
if (text === '') return '值无效:请填写一个数字'
|
|
182
|
+
const parsed = Number(text)
|
|
183
|
+
if (!Number.isFinite(parsed)) return '值无效:请填写一个数字'
|
|
184
|
+
if (spec.min !== undefined && parsed < spec.min) return `值无效:不能小于 ${spec.min}`
|
|
185
|
+
if (spec.max !== undefined && parsed > spec.max) return `值无效:不能大于 ${spec.max}`
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 数字字段只在**合法**时才写。非法时返回 `undefined`,上层跳过这一次写入。 */
|
|
168
190
|
function writeField(spec: FieldSpec, draft: string | boolean): unknown {
|
|
169
191
|
if (spec.kind === 'toggle') return Boolean(draft)
|
|
170
|
-
if (spec.kind === 'number')
|
|
192
|
+
if (spec.kind === 'number') {
|
|
193
|
+
const error = fieldErrorOf(spec, draft)
|
|
194
|
+
if (error !== undefined) return undefined
|
|
195
|
+
return Number(String(draft).trim())
|
|
196
|
+
}
|
|
171
197
|
if (spec.list) return String(draft).split(',').map(item => item.trim()).filter(Boolean)
|
|
172
198
|
return String(draft)
|
|
173
199
|
}
|
|
@@ -178,6 +204,7 @@ function needsDebounce(spec: FieldSpec): boolean {
|
|
|
178
204
|
}
|
|
179
205
|
|
|
180
206
|
function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean; resolved?: string }) {
|
|
207
|
+
const invalid = fieldErrorOf(spec, value)
|
|
181
208
|
return <Field label={spec.label} hint={spec.hint}>
|
|
182
209
|
<Input
|
|
183
210
|
type={spec.kind === 'number' ? 'number' : 'text'}
|
|
@@ -189,6 +216,8 @@ function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldS
|
|
|
189
216
|
{...(resolved ? { placeholder: resolved } : {})}
|
|
190
217
|
onChange={event => onChange(event.target.value)}
|
|
191
218
|
/>
|
|
219
|
+
{/* 非法草稿**留在输入框里**,只在旁边说一句「值无效、不会写」—— 见 `fieldErrorOf`。 */}
|
|
220
|
+
{invalid && <span className="tita-settingInvalid" role="status">{invalid}</span>}
|
|
192
221
|
</Field>
|
|
193
222
|
}
|
|
194
223
|
|
|
@@ -231,11 +260,15 @@ function ToggleField({ spec, value, onChange, disabled }: { spec: FieldSpec; val
|
|
|
231
260
|
/**
|
|
232
261
|
* 固定的数据目录:只读展示实际路径,打开动作交给 DSH 的 session 能力。
|
|
233
262
|
*/
|
|
234
|
-
function StorageLocationField({ label, path, session, onError }: {
|
|
263
|
+
function StorageLocationField({ label, path, session, onError, loadError, onRetry }: {
|
|
235
264
|
label: string
|
|
236
265
|
path?: string
|
|
237
266
|
session?: AgentPresetRemote['session']
|
|
238
267
|
onError: (message: string | null) => void
|
|
268
|
+
/** `/knowledge/layout` 的读取失败。它发生在这个字段上,所以报在这个字段旁边。 */
|
|
269
|
+
loadError?: string | null
|
|
270
|
+
/** 重读一次布局。不给人这条路的话,一次瞬时失败会永久停在一句占位文字上。 */
|
|
271
|
+
onRetry?: () => void
|
|
239
272
|
}): ReactNode {
|
|
240
273
|
const [opening, setOpening] = useState(false)
|
|
241
274
|
|
|
@@ -259,19 +292,26 @@ function StorageLocationField({ label, path, session, onError }: {
|
|
|
259
292
|
|
|
260
293
|
return <Field as="div" label={label}>
|
|
261
294
|
<div className="tita-pickerRow">
|
|
262
|
-
|
|
295
|
+
{/* 读不到布局时占位文字要跟着变:原来的「正在读取数据目录…」在失败之后
|
|
296
|
+
是一句**永远为真**的话,而它掩盖了「已经不读了」这个事实。 */}
|
|
297
|
+
<Input readOnly value={path ?? ''} placeholder={loadError ? '未能读取数据目录' : '正在读取数据目录…'} aria-label={label} />
|
|
263
298
|
<Button variant="outline" onClick={() => void open()} disabled={!path || opening}>
|
|
264
299
|
{opening ? <LoaderCircle size={14} className="tita-spin" /> : <FolderOpen size={14} />}
|
|
265
300
|
{opening ? '正在打开…' : '打开目录'}
|
|
266
301
|
</Button>
|
|
267
302
|
</div>
|
|
303
|
+
{loadError && <p className="tita-settingsRetry" role="alert">
|
|
304
|
+
<AlertCircle size={13} />
|
|
305
|
+
<span>读取数据目录失败:{loadError}</span>
|
|
306
|
+
{onRetry && <Button variant="ghost" size="sm" onClick={onRetry}>重新读取</Button>}
|
|
307
|
+
</p>}
|
|
268
308
|
</Field>
|
|
269
309
|
}
|
|
270
310
|
|
|
271
311
|
/** 写入状态。`saved` 只短暂出现,`error` 留到下一次改动。 */
|
|
272
312
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error'
|
|
273
313
|
|
|
274
|
-
export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolved = {}, highlight = false, showAdvanced = false }: {
|
|
314
|
+
export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolved = {}, highlight = false, showAdvanced = false, layoutError = null, onRetryLayout }: {
|
|
275
315
|
node: SettingsNode
|
|
276
316
|
knowledge?: SettingsScopeLike
|
|
277
317
|
/** `/knowledge/layout` 返回的固定 Agent 根目录。 */
|
|
@@ -284,6 +324,10 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
|
|
|
284
324
|
highlight?: boolean
|
|
285
325
|
/** Show the model and retrieval tuning fields. */
|
|
286
326
|
showAdvanced?: boolean
|
|
327
|
+
/** `/knowledge/layout` 读取失败时的原话;只有「数据位置」那一块用得到。 */
|
|
328
|
+
layoutError?: string | null
|
|
329
|
+
/** 重读一次布局。 */
|
|
330
|
+
onRetryLayout?: () => void
|
|
287
331
|
}): ReactNode {
|
|
288
332
|
const spec = NODE_SETTINGS[node]
|
|
289
333
|
const scope = knowledge
|
|
@@ -291,11 +335,18 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
|
|
|
291
335
|
const [draft, setDraft] = useState<Record<string, string | boolean>>({})
|
|
292
336
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
|
293
337
|
const [error, setError] = useState<string | null>(null)
|
|
294
|
-
/**
|
|
295
|
-
|
|
338
|
+
/**
|
|
339
|
+
* 待落地的防抖写入,按字段键索引。
|
|
340
|
+
*
|
|
341
|
+
* 除了计时器,它还记着**那次要写的值**:卸载时那个 `setTimeout` 不会再回来,
|
|
342
|
+
* 而草稿 state 在那一刻不一定已经渲染过(`setDraft` 是异步的)—— 从 timer 里读
|
|
343
|
+
* 值才是「用户最后敲进去的东西」的唯一可靠来源。
|
|
344
|
+
*/
|
|
345
|
+
const pending = useRef(new Map<string, { timer: ReturnType<typeof setTimeout>; field: FieldSpec; value: string | boolean }>())
|
|
296
346
|
/** 正在进行的写入数。用它挡住「写完 → revision 变 → 重置草稿」把输入抹掉。 */
|
|
297
347
|
const inflight = useRef(0)
|
|
298
348
|
const savedTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
349
|
+
const disabled = !state.writable || state.status !== 'ready'
|
|
299
350
|
|
|
300
351
|
useEffect(() => {
|
|
301
352
|
// 有写入在飞的时候不重置草稿:那一刻 `state.value` 还是旧值,重置会把用户刚
|
|
@@ -304,21 +355,16 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
|
|
|
304
355
|
setDraft(Object.fromEntries(spec.fields.map(field => [field.key, readField(field, state.value)])))
|
|
305
356
|
}, [node, state.revision, state.value])
|
|
306
357
|
|
|
307
|
-
// 卸载时把还没落地的防抖写入丢掉并清计时器:一个已经离开的面板不该再写设置。
|
|
308
|
-
useEffect(() => () => {
|
|
309
|
-
for (const timer of timers.current.values()) clearTimeout(timer)
|
|
310
|
-
timers.current.clear()
|
|
311
|
-
if (savedTimer.current) clearTimeout(savedTimer.current)
|
|
312
|
-
}, [])
|
|
313
|
-
|
|
314
|
-
const disabled = !state.writable || state.status !== 'ready'
|
|
315
|
-
|
|
316
358
|
const persist = useCallback(async (field: FieldSpec, raw: string | boolean): Promise<void> => {
|
|
317
359
|
if (!scope || disabled) return
|
|
360
|
+
const value = writeField(field, raw)
|
|
361
|
+
// 非法草稿跳过这一次写入(见 `fieldErrorOf`)。它不是失败,也不该报错 ——
|
|
362
|
+
// 只是「这一笔现在还不能写」,草稿留在输入框里等人把它改对。
|
|
363
|
+
if (value === undefined) return
|
|
318
364
|
inflight.current += 1
|
|
319
365
|
setSaveState('saving'); setError(null)
|
|
320
366
|
try {
|
|
321
|
-
await scope.set(field.key,
|
|
367
|
+
await scope.set(field.key, value)
|
|
322
368
|
setSaveState('saved')
|
|
323
369
|
if (savedTimer.current) clearTimeout(savedTimer.current)
|
|
324
370
|
// 「已保存」只是一个确认,不是一条状态:两秒后收回,免得它常驻成一个
|
|
@@ -332,18 +378,35 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
|
|
|
332
378
|
}
|
|
333
379
|
}, [scope, disabled])
|
|
334
380
|
|
|
381
|
+
// 卸载时把还没落地的防抖写入**冲出去**,而不是丢掉:用户敲完立刻按 Esc 是常规操作,
|
|
382
|
+
// 而丢掉的表现是「输入框里的值过一会儿自己变回去了」,中间连一个保存指示都没有。
|
|
383
|
+
// 冲出去就是替那一个 `setTimeout` 把事做完。卸载后 `persist` 里的 setState 会被
|
|
384
|
+
// React 忽略(它已经不在树上了),写入本身照旧发出去。
|
|
385
|
+
useEffect(() => () => {
|
|
386
|
+
for (const { timer, field, value } of pending.current.values()) {
|
|
387
|
+
clearTimeout(timer)
|
|
388
|
+
void persist(field, value)
|
|
389
|
+
}
|
|
390
|
+
pending.current.clear()
|
|
391
|
+
if (savedTimer.current) clearTimeout(savedTimer.current)
|
|
392
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
393
|
+
|
|
335
394
|
const update = (field: FieldSpec, value: string | boolean): void => {
|
|
336
395
|
setDraft(current => ({ ...current, [field.key]: value }))
|
|
337
396
|
if (!needsDebounce(field)) {
|
|
338
397
|
void persist(field, value)
|
|
339
398
|
return
|
|
340
399
|
}
|
|
341
|
-
const existing =
|
|
342
|
-
if (existing) clearTimeout(existing)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
400
|
+
const existing = pending.current.get(field.key)
|
|
401
|
+
if (existing) clearTimeout(existing.timer)
|
|
402
|
+
pending.current.set(field.key, {
|
|
403
|
+
field,
|
|
404
|
+
value,
|
|
405
|
+
timer: setTimeout(() => {
|
|
406
|
+
pending.current.delete(field.key)
|
|
407
|
+
void persist(field, value)
|
|
408
|
+
}, 600),
|
|
409
|
+
})
|
|
347
410
|
}
|
|
348
411
|
|
|
349
412
|
const meta = state.status === 'loading'
|
|
@@ -399,6 +462,8 @@ export function NodeSettingsPanel({ node, knowledge, agentRoot, session, resolve
|
|
|
399
462
|
path={agentRoot}
|
|
400
463
|
session={session}
|
|
401
464
|
onError={setError}
|
|
465
|
+
loadError={layoutError}
|
|
466
|
+
{...(onRetryLayout ? { onRetry: onRetryLayout } : {})}
|
|
402
467
|
/>
|
|
403
468
|
: <TextField
|
|
404
469
|
key={field.key}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 归一轨迹的渲染(2026-10-18)。
|
|
3
|
+
*
|
|
4
|
+
* 归一这一段原来只留下两个东西:任务里一段会滚走的流式输出,和最终写进事实表的那些行。
|
|
5
|
+
* 中间那一步 ——「模型给的这些名字里,哪些命中了已有实体、哪些新建了候选、哪些因为不唯一
|
|
6
|
+
* 没猜」—— 跑完就查不到了。而它恰好是坐标错了之后唯一能复盘的东西:卡名错了,根在这里。
|
|
7
|
+
*
|
|
8
|
+
* 所以这一页只回答三个问题,按这个顺序:
|
|
9
|
+
*
|
|
10
|
+
* 1. **这次归一动了多少**(提及数、命中/新建/歧义、写进多少条事实)。
|
|
11
|
+
* 2. **每个名字落到了哪里**(名字 → 实体 id,以及凭什么)。
|
|
12
|
+
* 3. **写进去的事实长什么样**(正文、坐标、几条出处、哪几条还悬着)。
|
|
13
|
+
*
|
|
14
|
+
* 样式复用抽取报告那一套(`.tita-trial*`):三份轨迹是同一件事的三种粒度,
|
|
15
|
+
* 长得不一样会让人以为是三个不同的功能。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { type ReactNode } from 'react'
|
|
19
|
+
import { TriangleAlert } from '@tita-agent/ui/icons'
|
|
20
|
+
import { cn } from '@tita-agent/ui/components'
|
|
21
|
+
import { styles } from './styles.ts'
|
|
22
|
+
import type { NormalizeTrajectoryReport } from '../trajectory-types.js'
|
|
23
|
+
|
|
24
|
+
/** 名字最多摊这么多行。再多就该看总体结论,不该在页面上滚。 */
|
|
25
|
+
const AXIS_CAP = 200
|
|
26
|
+
const FACT_CAP = 200
|
|
27
|
+
|
|
28
|
+
const OUTCOME_LABEL: Record<string, string> = {
|
|
29
|
+
matched: '命中已有',
|
|
30
|
+
new: '新建候选',
|
|
31
|
+
ambiguous: '不唯一,没猜',
|
|
32
|
+
'no-registry': '只做文档内归一',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function NormalizeTrajectory({ report }: { report: NormalizeTrajectoryReport }): ReactNode {
|
|
36
|
+
const axes = report.axes ?? []
|
|
37
|
+
const rows = report.rows ?? []
|
|
38
|
+
const counts = report.counts ?? { matched: 0, created: 0, ambiguous: 0, mechanical: 0 }
|
|
39
|
+
const modelOk = report.model?.ok !== false
|
|
40
|
+
return <div className={styles.trialReport}>
|
|
41
|
+
<section className={styles.trialOverview} aria-labelledby="normalize-overview-title">
|
|
42
|
+
<div className={styles.trialSectionHead}>
|
|
43
|
+
<h2 id="normalize-overview-title">结果概览</h2>
|
|
44
|
+
<span>{report.knowledge?.unresolved ? `${report.knowledge.unresolved} 条坐标未决` : modelOk ? '已完成' : '需检查'}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<dl className={styles.trialOverviewStats}>
|
|
47
|
+
<div><dt>坐标提及</dt><dd>{report.mentions ?? axes.length}</dd></div>
|
|
48
|
+
<div><dt>命中 / 新建</dt><dd>{counts.matched} / {counts.created}<small>已有实体 / 候选</small></dd></div>
|
|
49
|
+
<div><dt>歧义未决</dt><dd>{counts.ambiguous}<small>不猜,留给人</small></dd></div>
|
|
50
|
+
<div><dt>写入 Knowledge</dt><dd>{report.knowledge?.saved ?? 0}<small>另并入 {report.knowledge?.merged ?? 0} 条</small></dd></div>
|
|
51
|
+
<div><dt>写入 Event</dt><dd>{report.events?.saved ?? 0}<small>另并入 {report.events?.merged ?? 0} 个</small></dd></div>
|
|
52
|
+
</dl>
|
|
53
|
+
<p className={styles.trialOverviewTitle}>
|
|
54
|
+
<strong>{report.source?.title || '(来源已删除)'}</strong>
|
|
55
|
+
<span>v{report.source?.version ?? '?'} · 依据抽取记录 {report.extractionId || '—'}</span>
|
|
56
|
+
</p>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
{!modelOk && <p className={styles.trialError} role="alert">
|
|
60
|
+
<TriangleAlert size={12} />{report.model?.why ?? '归一没有跑通'} · 这一份文档的事实没有写完。
|
|
61
|
+
</p>}
|
|
62
|
+
|
|
63
|
+
{report.warnings.length > 0 && <section className={styles.trialSection} aria-labelledby="normalize-warnings-title">
|
|
64
|
+
<div className={styles.trialSectionHead}>
|
|
65
|
+
<h2 id="normalize-warnings-title">警告</h2>
|
|
66
|
+
<span>{report.warnings.length} 条</span>
|
|
67
|
+
</div>
|
|
68
|
+
<ul className={styles.trialCandidates}>{report.warnings.map((warning, index) => <li key={index}>
|
|
69
|
+
{/* 不加装饰图标:这一段就叫「警告」,一条黄三角再标一次是同一句话说两遍。
|
|
70
|
+
而且 `trialCandidates > li` 是上下两行的栅格 —— 图标单占一行会让正文掉到第三行。 */}
|
|
71
|
+
<span className={styles.trialCandName}>{warning}</span>
|
|
72
|
+
</li>)}</ul>
|
|
73
|
+
</section>}
|
|
74
|
+
|
|
75
|
+
<section className={styles.trialSection} aria-labelledby="normalize-axes-title">
|
|
76
|
+
<div className={styles.trialSectionHead}>
|
|
77
|
+
<h2 id="normalize-axes-title">坐标解析</h2>
|
|
78
|
+
<span>{axes.length} 个名字{counts.mechanical > 0 ? ` · ${counts.mechanical} 个只做了机械归一` : ''}</span>
|
|
79
|
+
</div>
|
|
80
|
+
{axes.length === 0
|
|
81
|
+
? <p className={styles.trialEmpty}>这次归一没有解析任何坐标名。</p>
|
|
82
|
+
: <ul className={styles.trialItems}>{axes.slice(0, AXIS_CAP).map((axis, index) => <li
|
|
83
|
+
key={`${axis.kind}-${axis.name}-${index}`}
|
|
84
|
+
className={axis.outcome === 'matched' ? styles.trialItem : `${styles.trialItem} ${styles.trialItemWarn}`}
|
|
85
|
+
>
|
|
86
|
+
<span className={styles.trialItemRef}>{axis.name}</span>
|
|
87
|
+
<span className={styles.trialItemArrow} aria-hidden="true">→</span>
|
|
88
|
+
<span className={styles.trialItemValue}>
|
|
89
|
+
{(OUTCOME_LABEL[axis.outcome] ?? axis.outcome)}{axis.id ? ` ${axis.id}` : ''}
|
|
90
|
+
</span>
|
|
91
|
+
<span className={styles.trialItemNote}>
|
|
92
|
+
{KIND_LABEL[axis.kind] ?? axis.kind}{axis.how ? ` · ${axis.how}` : ''}{axis.parentMention ? ` · 作用域:${axis.parentMention}` : ''}
|
|
93
|
+
</span>
|
|
94
|
+
</li>)}</ul>}
|
|
95
|
+
{axes.length > AXIS_CAP && <p className={styles.trialItemsMore}>另有 {axes.length - AXIS_CAP} 个名字未展开。</p>}
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<section className={styles.trialSection} aria-labelledby="normalize-facts-title">
|
|
99
|
+
<div className={styles.trialSectionHead}>
|
|
100
|
+
<h2 id="normalize-facts-title">写入的事实</h2>
|
|
101
|
+
<span>{rows.length} 条{report.knowledge?.unresolved ? ` · ${report.knowledge.unresolved} 条坐标未决` : ''}</span>
|
|
102
|
+
</div>
|
|
103
|
+
{rows.length === 0
|
|
104
|
+
? <p className={styles.trialEmpty}>这次归一没有写入 Knowledge。</p>
|
|
105
|
+
: <ul className={styles.trialOutputList}>{rows.slice(0, FACT_CAP).map((row, index) => <li key={index} className={styles.trialOutputItem}>
|
|
106
|
+
<div className={styles.trialCandHead}>
|
|
107
|
+
<span className={cn(styles.trialCandName, row.unresolved && styles.trialCandBad)}>{row.text}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div className={styles.trialOutputMeta}>
|
|
110
|
+
{row.coordinate} · {row.sources} 条出处
|
|
111
|
+
{row.unresolved && <strong>坐标未决</strong>}
|
|
112
|
+
</div>
|
|
113
|
+
</li>)}</ul>}
|
|
114
|
+
{rows.length > FACT_CAP && <p className={styles.trialMore}>另有 {rows.length - FACT_CAP} 条未展开。</p>}
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<section className={styles.trialSection} aria-labelledby="normalize-fingerprint-title">
|
|
118
|
+
<div className={styles.trialSectionHead}>
|
|
119
|
+
<h2 id="normalize-fingerprint-title">续跑信息</h2>
|
|
120
|
+
<span>指纹没变,下一次归一会直接复用这一份</span>
|
|
121
|
+
</div>
|
|
122
|
+
<dl className={styles.trialRows}>
|
|
123
|
+
<div className={styles.trialRow}><dt>归一指纹</dt><dd>{report.fingerprint || '—'}</dd></div>
|
|
124
|
+
<div className={styles.trialRow}>
|
|
125
|
+
<dt>已下发坐标</dt>
|
|
126
|
+
<dd>{report.axesContext === undefined
|
|
127
|
+
? '这次没有下发(没接注册表,或库里还没有实体)'
|
|
128
|
+
: `${report.axesContext.injected} / ${report.axesContext.totals} 条${report.axesContext.truncated > 0 ? ` · 未下发 ${report.axesContext.truncated} 条` : ''}${report.axesContext.selection === 'relevant' ? ' · 当前文档相关候选' : report.axesContext.selection === 'full' ? ' · 全集' : ''}`}</dd>
|
|
129
|
+
</div>
|
|
130
|
+
<div className={styles.trialRow}><dt>模型</dt><dd>{report.model?.model ? `${report.model.provider ? `${report.model.provider} / ` : ''}${report.model.model}${report.model.reasoningEffort ? ` · ${report.model.reasoningEffort}` : ''}` : '未记录'}</dd></div>
|
|
131
|
+
</dl>
|
|
132
|
+
</section>
|
|
133
|
+
</div>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const KIND_LABEL: Record<string, string> = { object: '对象', action: '动作', scene: '场景' }
|
|
@@ -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
|
+
}
|