dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +31 -7
- package/cordis.patch.yml +11 -5
- package/lib/client.js +5557 -3056
- package/lib/doc-page.js +7740 -0
- package/lib/types/convert/host.d.ts.map +1 -1
- package/lib/types/convert/host.js +36 -9
- package/lib/types/convert/host.js.map +1 -1
- package/lib/types/diagnosis/cards.d.ts +8 -7
- package/lib/types/diagnosis/cards.d.ts.map +1 -1
- package/lib/types/diagnosis/cards.js +119 -49
- package/lib/types/diagnosis/cards.js.map +1 -1
- package/lib/types/diagnosis/run.d.ts +51 -0
- package/lib/types/diagnosis/run.d.ts.map +1 -0
- package/lib/types/diagnosis/run.js +239 -0
- package/lib/types/diagnosis/run.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +25 -2
- package/lib/types/diagnosis/schema.d.ts.map +1 -1
- package/lib/types/diagnosis/schema.js +64 -25
- package/lib/types/diagnosis/schema.js.map +1 -1
- package/lib/types/diagnosis/seed-source.d.ts +17 -2
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
- package/lib/types/diagnosis/seed-source.js +18 -1
- package/lib/types/diagnosis/seed-source.js.map +1 -1
- package/lib/types/diagnosis/seed.d.ts +32 -42
- package/lib/types/diagnosis/seed.d.ts.map +1 -1
- package/lib/types/diagnosis/seed.js +187 -51
- package/lib/types/diagnosis/seed.js.map +1 -1
- package/lib/types/diagnosis/store.d.ts +114 -26
- package/lib/types/diagnosis/store.d.ts.map +1 -1
- package/lib/types/diagnosis/store.js +336 -167
- package/lib/types/diagnosis/store.js.map +1 -1
- package/lib/types/diagnosis/types.d.ts +168 -16
- package/lib/types/diagnosis/types.d.ts.map +1 -1
- package/lib/types/diagnosis/types.js +1 -1
- package/lib/types/diagnosis/types.js.map +1 -1
- package/lib/types/document-page.d.ts +39 -0
- package/lib/types/document-page.d.ts.map +1 -0
- package/lib/types/document-page.js +43 -0
- package/lib/types/document-page.js.map +1 -0
- package/lib/types/fs-atomic.d.ts +12 -0
- package/lib/types/fs-atomic.d.ts.map +1 -0
- package/lib/types/fs-atomic.js +42 -0
- package/lib/types/fs-atomic.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +214 -24
- package/lib/types/index.js.map +1 -1
- package/lib/types/knowledge-bases.d.ts +10 -2
- package/lib/types/knowledge-bases.d.ts.map +1 -1
- package/lib/types/knowledge-bases.js +20 -18
- package/lib/types/knowledge-bases.js.map +1 -1
- package/lib/types/proposals.d.ts +166 -0
- package/lib/types/proposals.d.ts.map +1 -0
- package/lib/types/proposals.js +238 -0
- package/lib/types/proposals.js.map +1 -0
- package/lib/types/runtime.d.ts +199 -7
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +728 -145
- package/lib/types/runtime.js.map +1 -1
- package/lib/types/settings-schema.d.ts +2 -2
- package/lib/types/types.d.ts +61 -10
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js +56 -8
- package/lib/types/types.js.map +1 -1
- package/lib/types/wiki-types.d.ts +8 -1
- package/lib/types/wiki-types.d.ts.map +1 -1
- package/lib/types/wiki-types.js +16 -25
- package/lib/types/wiki-types.js.map +1 -1
- package/package.json +4 -3
- package/preset/tita-data-agent/agent.cordis.yml +11 -5
- package/src/client/DirectoryPicker.tsx +12 -12
- package/src/client/InfoHint.tsx +21 -8
- package/src/client/IntentTab.tsx +20 -26
- package/src/client/KnowledgeBaseTab.tsx +190 -80
- package/src/client/KnowledgePanel.tsx +42 -9
- package/src/client/KnowledgeStepRail.tsx +3 -0
- package/src/client/NodeSettings.tsx +20 -22
- package/src/client/PreviewSections.tsx +87 -33
- package/src/client/ProposalReview.tsx +313 -0
- package/src/client/TypeDefinitionDrawer.tsx +72 -38
- package/src/client/WikiTypeEditor.tsx +193 -77
- package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
- package/src/client/arch/NodeLogicPanel.tsx +6 -3
- package/src/client/arch/model.ts +164 -26
- package/src/client/architecture.css +29 -9
- package/src/client/architecture.tsx +54 -29
- package/src/client/attribution/AttributionGraph.tsx +481 -34
- package/src/client/attribution/AttributionTab.tsx +507 -106
- package/src/client/attribution/CaseView.tsx +9 -14
- package/src/client/attribution/GraphStage.tsx +25 -18
- package/src/client/attribution/RunView.tsx +27 -9
- package/src/client/attribution/StageChrome.tsx +296 -140
- package/src/client/attribution/graph-options.ts +611 -47
- package/src/client/attribution/model.ts +559 -30
- package/src/client/convert/ConvertPanel.tsx +6 -1
- package/src/client/doc-page.css +134 -0
- package/src/client/doc-page.tsx +263 -0
- package/src/client/panel-state.ts +105 -0
- package/src/client/request.ts +36 -0
- package/src/client/styles.css +317 -43
- package/src/client/useFitScale.ts +15 -2
- package/src/client/wiki-type-groups.ts +86 -0
- package/src/convert/host.ts +41 -7
- package/src/diagnosis/cards.ts +120 -50
- package/src/diagnosis/run.ts +245 -0
- package/src/diagnosis/schema.ts +67 -26
- package/src/diagnosis/seed-source.ts +28 -3
- package/src/diagnosis/seed.ts +187 -85
- package/src/diagnosis/store.ts +358 -174
- package/src/diagnosis/types.ts +181 -16
- package/src/document-page.ts +51 -0
- package/src/fs-atomic.ts +42 -0
- package/src/index.ts +209 -22
- package/src/knowledge-bases.ts +20 -18
- package/src/proposals.ts +337 -0
- package/src/runtime.ts +736 -135
- package/src/types.ts +113 -19
- package/src/wiki-types.ts +17 -27
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'
|
|
2
|
-
import { Button, Input, SelectMenu, Switch, cn } from '@tita-data-agent/ui/components'
|
|
2
|
+
import { Button, Field, Input, SelectMenu, Switch, cn } from '@tita-data-agent/ui/components'
|
|
3
3
|
import { AlertCircle, Check, LoaderCircle, SlidersHorizontal } from '@tita-data-agent/ui/icons'
|
|
4
4
|
import { DirectoryPickerField } from './DirectoryPicker.tsx'
|
|
5
5
|
import { InfoHint } from './InfoHint.tsx'
|
|
@@ -203,9 +203,13 @@ function needsDebounce(spec: FieldSpec): boolean {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean; resolved?: string }) {
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
/* 两条说明各自成行:`Field` 的说明位是一个 span,两条挤在一行会读成一句。 */
|
|
207
|
+
const hint = <>
|
|
208
|
+
{spec.hint ? <span className="block">{spec.hint}</span> : null}
|
|
208
209
|
{/* 留空 = 自动。占位符显示**解析后**的真实路径 —— 否则「自动」等于没说。 */}
|
|
210
|
+
{resolved ? <span className="block">留空 = 自动:<code>{resolved}</code></span> : null}
|
|
211
|
+
</>
|
|
212
|
+
return <Field label={spec.label} hint={hint}>
|
|
209
213
|
<Input
|
|
210
214
|
type={spec.kind === 'number' ? 'number' : 'text'}
|
|
211
215
|
value={value}
|
|
@@ -213,23 +217,20 @@ function TextField({ spec, value, onChange, disabled, resolved }: { spec: FieldS
|
|
|
213
217
|
{...(resolved ? { placeholder: resolved } : {})}
|
|
214
218
|
onChange={event => onChange(event.target.value)}
|
|
215
219
|
/>
|
|
216
|
-
|
|
217
|
-
底部那行「字段名:说明;字段名:说明」是把标签和说明各念一遍。 */}
|
|
218
|
-
{spec.hint && <small className="tita-settingHint">{spec.hint}</small>}
|
|
219
|
-
{resolved && <small className="tita-settingHint">留空 = 自动:<code>{resolved}</code></small>}
|
|
220
|
-
</label>
|
|
220
|
+
</Field>
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* A dropdown field.
|
|
225
|
-
*
|
|
226
|
-
* `<label>`
|
|
227
|
-
*
|
|
228
|
-
*
|
|
224
|
+
* A dropdown field.
|
|
225
|
+
*
|
|
226
|
+
* `as="div"`, not the `<label>` the text and toggle fields use: the control is a
|
|
227
|
+
* `<button>` now (Radix's trigger), and a `<label>` around a button makes the
|
|
228
|
+
* visible field name an ambiguous second name — clicking the caption would also
|
|
229
|
+
* open the menu. The name travels on the trigger's `aria-label` instead, and the
|
|
230
|
+
* visible caption stays the thing a person reads.
|
|
229
231
|
*/
|
|
230
232
|
function SelectField({ spec, value, onChange, disabled }: { spec: FieldSpec; value: string; onChange: (value: string) => void; disabled: boolean }) {
|
|
231
|
-
return <
|
|
232
|
-
<span>{spec.label}</span>
|
|
233
|
+
return <Field as="div" label={spec.label} hint={spec.hint}>
|
|
233
234
|
<SelectMenu
|
|
234
235
|
value={value}
|
|
235
236
|
disabled={disabled}
|
|
@@ -237,25 +238,22 @@ function SelectField({ spec, value, onChange, disabled }: { spec: FieldSpec; val
|
|
|
237
238
|
options={spec.options ?? []}
|
|
238
239
|
aria-label={spec.label}
|
|
239
240
|
/>
|
|
240
|
-
|
|
241
|
-
</div>
|
|
241
|
+
</Field>
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* 开关字段。
|
|
246
246
|
*
|
|
247
|
-
*
|
|
247
|
+
* 它和文本 / 下拉 / 目录选择**共用同一套结构**:标签在上、控件在中、说明在下。
|
|
248
248
|
*
|
|
249
249
|
* 原来它是「带边框和底色的整行」,另外三种是无边框的堆叠 —— 四种字段两种长相,而且
|
|
250
250
|
* 标签用 `<strong>`、说明塞在标签里,对齐方式也对不上。更实际的问题是那个边框:
|
|
251
251
|
* 小节本身已经是一张卡片,字段再包一层就是**卡片套卡片**。
|
|
252
252
|
*/
|
|
253
253
|
function ToggleField({ spec, value, onChange, disabled }: { spec: FieldSpec; value: boolean; onChange: (value: boolean) => void; disabled: boolean }) {
|
|
254
|
-
return <label
|
|
255
|
-
<span>{spec.label}</span>
|
|
254
|
+
return <Field label={spec.label} hint={spec.hint}>
|
|
256
255
|
<Switch checked={value} disabled={disabled} onCheckedChange={onChange} />
|
|
257
|
-
|
|
258
|
-
</label>
|
|
256
|
+
</Field>
|
|
259
257
|
}
|
|
260
258
|
|
|
261
259
|
/** 写入状态。`saved` 只短暂出现,`error` 留到下一次改动。 */
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 预览抽屉的公共分块(0035 起):**元信息 → 内容 → 版本**。
|
|
3
3
|
*
|
|
4
4
|
* SourceRaw 的原文预览与 wiki 卡片的预览必须是**同一种形态**。两处各写一遍结构,
|
|
5
5
|
* 就会出现「一个把元信息放最上面、一个放最下面」「一个能给文本预览、另一个不能」
|
|
6
6
|
* 这类漂移 —— 而它们回答的是同一个问题:这份东西是什么、写了什么、改过几次。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 这一层只定义**顺序与分块**(小节头 + 计数 + 内容),字段仍由调用方给:原文有
|
|
9
9
|
* `content_hash`,卡片有 `entity_id`,本来就不一样,硬凑成同一张表只会两处都别扭。
|
|
10
10
|
* 时间也由调用方格式化(见 `format.ts`)—— 元信息用标准格式,列表行用紧凑格式。
|
|
11
|
+
*
|
|
12
|
+
* 0042 重排了三件事,理由都是「三种层级要一眼分得开」:
|
|
13
|
+
*
|
|
14
|
+
* 1. **小节头换了字号**(`drawerSubHead` 13px/700,上加分隔线)。此前小节头是
|
|
15
|
+
* 12px/700、正文 12px/400 —— 同字号只差一个字重,于是「内容」两个字和它下面的
|
|
16
|
+
* 正文在视觉上是同一段文字的第 0 行与第 1 行。
|
|
17
|
+
* 2. **视图切换离开小节头那一行。** 它被压到 20px 高站在 12px 的小节标题旁边,与
|
|
18
|
+
* 小节名、字符数、(曾经的)出口按钮四种东西共享一行。现在它有自己的「读法工具栏」。
|
|
19
|
+
* 3. **正文块锁高度、自己滚。** 此前整条抽屉体是一根长滚动条:正文有多长,「版本」
|
|
20
|
+
* 就被推到多远,而版本是要动手的地方(回滚)。现在正文块有上限、块内滚动,
|
|
21
|
+
* 下面的分块位置不再随正文长度漂移。
|
|
11
22
|
*/
|
|
12
23
|
|
|
13
24
|
import { useState, type ReactNode } from 'react'
|
|
14
|
-
import { FileCode2, FileText, History, Image, ListTree } from '@tita-data-agent/ui/icons'
|
|
15
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tita-data-agent/ui/components'
|
|
25
|
+
import { ChevronDown, ChevronUp, FileCode2, FileText, History, Image, ListTree } from '@tita-data-agent/ui/icons'
|
|
26
|
+
import { Button, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
|
|
16
27
|
import { VersionManager, type VersionManagerProps } from '@tita-data-agent/ui/versions'
|
|
17
28
|
// 平台提供的 Markdown 原子(DSH 的模块表里有它,本插件的 client bundle 也已把它列为
|
|
18
29
|
// external)。原文与卡片正文本身就是 Markdown,用 `<pre>` 显示等于把标题、表格、链接
|
|
@@ -35,8 +46,17 @@ const MARKDOWN_LABELS: MarkdownLabels = {
|
|
|
35
46
|
footnotes: '脚注',
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 正文在抽屉里最多渲染这么多字符。**与服务端 `SOURCE_PREVIEW_CHARS` 同值。**
|
|
51
|
+
*
|
|
52
|
+
* 服务端在传输层就把原文封顶了,前端再取同一个数,是为了让「界面上写出来的那个数」
|
|
53
|
+
* 与「服务端封顶的那个数」不可能是两个:0042 之前客户端另有一个 20000,
|
|
54
|
+
* 于是同一份材料有两个上限,而界面上一个都没写。
|
|
55
|
+
*
|
|
56
|
+
* wiki 卡片没有独立整页出口,所以卡片正文超过它时给**本地展开**(见 `PreviewContent`
|
|
57
|
+
* 的「展开全部」),不是只截断不给出路。
|
|
58
|
+
*/
|
|
59
|
+
const PREVIEW_CHARS = 4000
|
|
40
60
|
|
|
41
61
|
export interface PreviewRow {
|
|
42
62
|
label: string
|
|
@@ -45,13 +65,21 @@ export interface PreviewRow {
|
|
|
45
65
|
code?: boolean
|
|
46
66
|
}
|
|
47
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 抽屉里的小节头 —— 三种层级里的中间那层(标题 15px / 小节头 13px / 正文 12px)。
|
|
70
|
+
* 分隔线在 `.tita-drawerSection` 上(小节与上一块之间),所以这里只管内容。
|
|
71
|
+
*/
|
|
72
|
+
function SectionHead({ icon, title, children }: { icon: ReactNode; title: string; children?: ReactNode }): ReactNode {
|
|
73
|
+
return <div className={styles.drawerSubHead}>
|
|
74
|
+
<h4>{icon}{title}</h4>
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
/** 元信息:一张两列的事实表。标签列宽固定,所以标签要短且彼此对齐。 */
|
|
49
80
|
export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
|
|
50
|
-
// 每一块都包一层 `drawerSection`:标题与它自己的内容是一块,块与块之间的间距由
|
|
51
|
-
// 外层 `drawerStack` 给。不包的话「元信息」标题和它下面那张表是两个同级节点,
|
|
52
|
-
// 外层 gap 会把标题和自己的表也拉开。
|
|
53
81
|
return <section className={styles.drawerSection}>
|
|
54
|
-
<
|
|
82
|
+
<SectionHead icon={<ListTree size={13} />} title="元信息" />
|
|
55
83
|
<dl className={styles.kbKv}>
|
|
56
84
|
{rows.map(row => <div key={row.label}>
|
|
57
85
|
<dt>{row.label}</dt>
|
|
@@ -64,33 +92,55 @@ export function PreviewMeta({ rows }: { rows: PreviewRow[] }): ReactNode {
|
|
|
64
92
|
export interface PreviewAsset { rel: string; url: string; bytes: number }
|
|
65
93
|
|
|
66
94
|
/**
|
|
67
|
-
*
|
|
95
|
+
* 内容:小节头 + 读法工具栏 + 正文(锁高内滚)。
|
|
68
96
|
*
|
|
69
97
|
* 两个视图是**同一份内容**的两种读法:Markdown 是阅读态(默认),文本是源码态,
|
|
70
98
|
* 用于逐字核对 —— 与版本 diff 是同一种读法。切换不重新取数,也不改内容。
|
|
71
99
|
* 图片素材排在这一块的末尾:它也是内容,只是渲染器没能就地展开的那部分。
|
|
100
|
+
*
|
|
101
|
+
* `totalChars` 说全文有多少字符(服务端按 `SOURCE_PREVIEW_CHARS` 封顶,
|
|
102
|
+
* `content.length` 已经不是全文长度了)。读全文的出口在**抽屉头**上,不在这一块里:
|
|
103
|
+
* 它是「这份东西还能去哪看」,位置要固定,不该随正文是否被截断而出现或消失。
|
|
72
104
|
*/
|
|
73
|
-
export function PreviewContent({ content, assets = []
|
|
105
|
+
export function PreviewContent({ content, assets = [], totalChars }: {
|
|
106
|
+
content: string
|
|
107
|
+
assets?: PreviewAsset[]
|
|
108
|
+
/** 全文总字符数。省略时按 `content.length` 算(它本来就是全文)。 */
|
|
109
|
+
totalChars?: number
|
|
110
|
+
}): ReactNode {
|
|
74
111
|
const [view, setView] = useState<'md' | 'text'>('md')
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
<div className={styles.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
112
|
+
/** 卡片正文超过上限时的本地展开:卡片没有整页出口,只能在这里给出路。 */
|
|
113
|
+
const [expanded, setExpanded] = useState(false)
|
|
114
|
+
const shown = expanded ? content : content.slice(0, PREVIEW_CHARS)
|
|
115
|
+
const total = totalChars ?? content.length
|
|
116
|
+
const clipped = shown.length < total
|
|
117
|
+
const canExpand = content.length > PREVIEW_CHARS
|
|
118
|
+
return <section className={styles.drawerSection}>
|
|
119
|
+
<SectionHead icon={<FileText size={13} />} title="内容">
|
|
120
|
+
<span className={styles.drawerHeadFact}>{total} 字符{clipped ? ` · 这里显示前 ${shown.length}` : ''}</span>
|
|
121
|
+
</SectionHead>
|
|
122
|
+
{/* 读法工具栏:这个分块的**两种读法**是它自己的事,不再挤进小节标题那一行。 */}
|
|
123
|
+
<Tabs value={view} onValueChange={next => setView(next as 'md' | 'text')}>
|
|
124
|
+
<div className={styles.drawerToolbar}>
|
|
125
|
+
<TabsList className={cn(styles.tabsList, styles.kbViewTabs)}>
|
|
126
|
+
<TabsTrigger value="md">Markdown</TabsTrigger>
|
|
127
|
+
<TabsTrigger value="text"><FileCode2 size={11} />文本</TabsTrigger>
|
|
128
|
+
</TabsList>
|
|
129
|
+
{canExpand && <Button
|
|
130
|
+
variant="ghost" size="sm" className={styles.drawerMore}
|
|
131
|
+
onClick={() => setExpanded(value => !value)}
|
|
132
|
+
>{expanded ? <ChevronUp size={11} /> : <ChevronDown size={11} />}
|
|
133
|
+
{expanded ? '收起' : `展开全部(还有 ${content.length - PREVIEW_CHARS} 字符)`}</Button>}
|
|
134
|
+
</div>
|
|
135
|
+
<TabsContent value="md">
|
|
136
|
+
<div className={styles.drawerBody}><div className={styles.kbMarkdown}><MarkdownText text={shown} labels={MARKDOWN_LABELS} /></div></div>
|
|
137
|
+
</TabsContent>
|
|
138
|
+
<TabsContent value="text">
|
|
139
|
+
<div className={styles.drawerBody}><pre className={styles.kbPre}>{shown}</pre></div>
|
|
140
|
+
</TabsContent>
|
|
141
|
+
</Tabs>
|
|
92
142
|
{assets.length > 0 && <>
|
|
93
|
-
<
|
|
143
|
+
<SectionHead icon={<Image size={13} />} title="图片素材"><span className={styles.drawerHeadFact}>{assets.length} 张</span></SectionHead>
|
|
94
144
|
<div className={styles.kbAssets}>
|
|
95
145
|
{assets.map(asset => <figure key={asset.rel} className={styles.kbAsset}>
|
|
96
146
|
<img src={asset.url} alt={asset.rel} loading="lazy" />
|
|
@@ -98,18 +148,22 @@ export function PreviewContent({ content, assets = [] }: { content: string; asse
|
|
|
98
148
|
</figure>)}
|
|
99
149
|
</div>
|
|
100
150
|
</>}
|
|
101
|
-
</
|
|
151
|
+
</section>
|
|
102
152
|
}
|
|
103
153
|
|
|
104
154
|
/**
|
|
105
|
-
*
|
|
155
|
+
* 版本:小节头 + 计数 + 一句「什么时候会留快照」+ 时间线。
|
|
106
156
|
*
|
|
107
157
|
* `hint` 不是可有可无的装饰:两种产物的快照触发条件不同(原文是内容变化,卡片是
|
|
108
158
|
* 正文变化 —— 只改状态不造版本),不写清楚,用户会以为「我改过它,为什么没有版本」。
|
|
159
|
+
*
|
|
160
|
+
* 0042 没有把它降级成抽屉头的一枚按钮,理由有两条:它是当前**唯一**的回滚入口
|
|
161
|
+
* (`setRestoreTarget` 只在它这里被调用),而且正文块锁高之后它已经在一屏之内 ——
|
|
162
|
+
* 降级要新造一层状态,收益只是省一块高度。
|
|
109
163
|
*/
|
|
110
164
|
export function PreviewVersions({ hint, ...manager }: { hint?: string } & VersionManagerProps): ReactNode {
|
|
111
165
|
return <section className={styles.drawerSection}>
|
|
112
|
-
<
|
|
166
|
+
<SectionHead icon={<History size={13} />} title="版本"><span className={styles.drawerHeadFact}>{manager.entries?.length ?? 0} 个</span></SectionHead>
|
|
113
167
|
{hint && <p className={styles.kbHint}>{hint}</p>}
|
|
114
168
|
<VersionManager {...manager} />
|
|
115
169
|
</section>
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「待确认的卡片提议」—— 0021 §6.1 那道门的**人这一侧**。
|
|
3
|
+
*
|
|
4
|
+
* ## 它为什么长在这里
|
|
5
|
+
*
|
|
6
|
+
* review 界面**不在归因图谱上**,而在「知识库 → Wiki」这一屏:它与 Pass 0 的候选草案、
|
|
7
|
+
* lint 待补清单是同一处,不新造第二套 review 界面(PRD §7.1 的明确要求)。
|
|
8
|
+
* 智能归因那边只挂一个「N 条待确认」角标,点它跳过来。
|
|
9
|
+
*
|
|
10
|
+
* ## 这一屏要回答的三个问题
|
|
11
|
+
*
|
|
12
|
+
* 1. **这是什么、从哪来** —— 触发它的那条来源(`source_ref`)与触发方式(编译抽取 /
|
|
13
|
+
* Agent 主动提议 / 一次排查结束)。没有这一行,人只能凭卡片标题猜它该不该存在。
|
|
14
|
+
* 2. **它合不合格** —— 提议阶段跑出来的三层校验问题(缺小节 / 缺字段 / 图断了 /
|
|
15
|
+
* 这条知识没用)逐条列出来。**校验是确认的前置**:不看清就通过,等于把校验器绕过去。
|
|
16
|
+
* 3. **通过之后会变成什么** —— 卡片正文的原文(只读呈现,不是输入框:它是待审内容,
|
|
17
|
+
* 不是可编辑表单)。
|
|
18
|
+
*
|
|
19
|
+
* ## 为什么没有「全部通过」
|
|
20
|
+
*
|
|
21
|
+
* 有的话它一定会被当成默认动作,而这道门的全部价值就在于**逐条看过**。
|
|
22
|
+
* 批量通过等于把「人确认」退化成一个仪式。
|
|
23
|
+
*
|
|
24
|
+
* ## 三个动作的边界
|
|
25
|
+
*
|
|
26
|
+
* | 动作 | 做什么 | 什么时候用 |
|
|
27
|
+
* | --- | --- | --- |
|
|
28
|
+
* | **通过并写入** | 确认 + 立刻落卡 | 默认。这一步之后卡片就进编译、进图、能被召回 |
|
|
29
|
+
* | **仅确认** | 只确认,不落卡 | 想让 Agent 用 `write_knowledge_card` 落卡时(那条路要求一条已确认且尚未落卡的提议) |
|
|
30
|
+
* | **拒绝** | 留原因、不落卡 | 这条不算知识。**原因必须写** —— 它同时是「下次不要再提同一条」的判据 |
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
|
34
|
+
import { AlertCircle, Check, RefreshCw, ShieldCheck, Sparkles, Trash2 } from '@tita-data-agent/ui/icons'
|
|
35
|
+
import { Badge, Button, Input, cn } from '@tita-data-agent/ui/components'
|
|
36
|
+
import { requestJson } from './request.ts'
|
|
37
|
+
|
|
38
|
+
/** 服务端返回的一条提议(`/knowledge/card-proposals`)。字段与 `proposals.ts` 同形。 */
|
|
39
|
+
export interface CardProposalView {
|
|
40
|
+
id: string
|
|
41
|
+
sourceRef: string
|
|
42
|
+
trigger: 'extraction' | 'agent' | 'diagnosis-run'
|
|
43
|
+
action: 'create' | 'update' | 'merge'
|
|
44
|
+
entityType: string
|
|
45
|
+
entityId: string
|
|
46
|
+
title: string
|
|
47
|
+
summary?: string
|
|
48
|
+
content: string
|
|
49
|
+
sourceRefs: string[]
|
|
50
|
+
status: 'pending' | 'confirmed' | 'rejected'
|
|
51
|
+
issues: Array<{ level: string; field?: string; message: string }>
|
|
52
|
+
createdAt: string
|
|
53
|
+
updatedAt: string
|
|
54
|
+
confirmedBy?: string
|
|
55
|
+
writtenAt?: string
|
|
56
|
+
reviewedBy?: string
|
|
57
|
+
reason?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TRIGGER_LABEL: Record<CardProposalView['trigger'], string> = {
|
|
61
|
+
extraction: '编译时自动抽取',
|
|
62
|
+
agent: 'Agent 主动提议',
|
|
63
|
+
'diagnosis-run': '一次排查结束',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ACTION_LABEL: Record<CardProposalView['action'], string> = {
|
|
67
|
+
create: '新建卡片',
|
|
68
|
+
update: '更新已有卡片',
|
|
69
|
+
merge: '并入已有卡片',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 校验问题的三层标签。
|
|
74
|
+
*
|
|
75
|
+
* 三层分开报不是洁癖(0021 §6.5):**格式错说明卡片没填完,引用错说明图断了,
|
|
76
|
+
* 语义错说明这条知识本身没用**。三者对应的动作完全不同(补字段 / 修边 / 重写),
|
|
77
|
+
* 混成同一个「校验失败」人就学不会怎么写卡片。
|
|
78
|
+
*/
|
|
79
|
+
const LEVEL_LABEL: Record<string, { label: string; tone: 'warning' | 'violet' | 'neutral' }> = {
|
|
80
|
+
format: { label: '格式', tone: 'warning' },
|
|
81
|
+
reference: { label: '引用', tone: 'violet' },
|
|
82
|
+
semantic: { label: '语义', tone: 'neutral' },
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ProposalReview({ kbId, onApplied, onOpenWikiTypes }: {
|
|
86
|
+
kbId: string
|
|
87
|
+
/** 落卡之后通知父级刷新卡片列表与统计 —— 卡片数变了。 */
|
|
88
|
+
onApplied?: () => void
|
|
89
|
+
/** 「去类型定义」的入口。提议里的类型问题要在那一屏改。 */
|
|
90
|
+
onOpenWikiTypes?: () => void
|
|
91
|
+
}): ReactNode {
|
|
92
|
+
const [pending, setPending] = useState<CardProposalView[]>([])
|
|
93
|
+
const [rejected, setRejected] = useState<CardProposalView[]>([])
|
|
94
|
+
const [phase, setPhase] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
95
|
+
const [busy, setBusy] = useState<string | null>(null)
|
|
96
|
+
const [notice, setNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
|
|
97
|
+
/** 拒绝原因:按提议 id 存,因为每一条要写的原因不一样。 */
|
|
98
|
+
const [reasons, setReasons] = useState<Record<string, string>>({})
|
|
99
|
+
/** 展开的正文:默认收起,否则一条提议就吃掉整屏。 */
|
|
100
|
+
const [open, setOpen] = useState<ReadonlySet<string>>(new Set())
|
|
101
|
+
|
|
102
|
+
const load = useCallback(async (): Promise<void> => {
|
|
103
|
+
setPhase('loading')
|
|
104
|
+
try {
|
|
105
|
+
const [waiting, refused] = await Promise.all([
|
|
106
|
+
requestJson<{ proposals: CardProposalView[] }>(`/knowledge/card-proposals?kb=${encodeURIComponent(kbId)}&status=pending`),
|
|
107
|
+
requestJson<{ proposals: CardProposalView[] }>(`/knowledge/card-proposals?kb=${encodeURIComponent(kbId)}&status=rejected`),
|
|
108
|
+
])
|
|
109
|
+
setPending(waiting.data?.proposals ?? [])
|
|
110
|
+
setRejected(refused.data?.proposals ?? [])
|
|
111
|
+
setPhase('ready')
|
|
112
|
+
} catch {
|
|
113
|
+
setPhase('error')
|
|
114
|
+
}
|
|
115
|
+
}, [kbId])
|
|
116
|
+
|
|
117
|
+
useEffect(() => { void load() }, [load])
|
|
118
|
+
|
|
119
|
+
const review = async (item: CardProposalView, action: 'confirm-and-write' | 'confirm' | 'reject'): Promise<void> => {
|
|
120
|
+
setBusy(item.id)
|
|
121
|
+
setNotice(null)
|
|
122
|
+
try {
|
|
123
|
+
await requestJson('/knowledge/card-proposals/review', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'content-type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
kb: kbId,
|
|
128
|
+
proposal_id: item.id,
|
|
129
|
+
action,
|
|
130
|
+
...(action === 'reject' ? { reason: reasons[item.id] ?? '' } : {}),
|
|
131
|
+
}),
|
|
132
|
+
})
|
|
133
|
+
setNotice({
|
|
134
|
+
tone: 'success',
|
|
135
|
+
text: action === 'reject'
|
|
136
|
+
? `已拒绝「${item.title}」,原因留档 —— 内容不变就不会再提同一条`
|
|
137
|
+
: action === 'confirm'
|
|
138
|
+
? `已确认「${item.title}」。它还没落卡:Agent 可以用 write_knowledge_card 落它`
|
|
139
|
+
: `「${item.title}」已落卡,下一次编译就会进 wikiNotes 与图`,
|
|
140
|
+
})
|
|
141
|
+
if (action !== 'confirm') onApplied?.()
|
|
142
|
+
await load()
|
|
143
|
+
} catch (error) {
|
|
144
|
+
setNotice({ tone: 'error', text: error instanceof Error ? error.message : String(error) })
|
|
145
|
+
} finally {
|
|
146
|
+
setBusy(null)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toggle = (id: string): void => {
|
|
151
|
+
const next = new Set(open)
|
|
152
|
+
if (next.has(id)) next.delete(id); else next.add(id)
|
|
153
|
+
setOpen(next)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 一条提议都没有时**整块不出现**。
|
|
158
|
+
*
|
|
159
|
+
* 它不是空态该占的位置:这一屏的主体是 wiki 卡片列表,提议是插在它前面的待办。
|
|
160
|
+
* 没有待办时留一块「暂无待办」的框,等于每次打开都先读一句废话。
|
|
161
|
+
*/
|
|
162
|
+
if (phase === 'ready' && pending.length === 0 && rejected.length === 0) return null
|
|
163
|
+
|
|
164
|
+
return <section
|
|
165
|
+
aria-labelledby="tita-proposal-title"
|
|
166
|
+
className="flex flex-col gap-1.5 rounded-xl border border-tita-line bg-tita-surface-muted p-2.5"
|
|
167
|
+
>
|
|
168
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
169
|
+
<h3 id="tita-proposal-title" className="flex items-center gap-1.5 text-[12px] font-semibold text-tita-ink">
|
|
170
|
+
<ShieldCheck size={13} />待确认的卡片提议
|
|
171
|
+
</h3>
|
|
172
|
+
<span className="text-[10.5px] text-tita-ink-muted">
|
|
173
|
+
{phase === 'loading' ? '正在读取…' : phase === 'error' ? '读取失败' : `${pending.length} 条待确认`}
|
|
174
|
+
</span>
|
|
175
|
+
<span className="ml-auto flex items-center gap-1">
|
|
176
|
+
<Button
|
|
177
|
+
variant="ghost" size="icon"
|
|
178
|
+
className="h-[26px] w-[26px] min-w-[26px]"
|
|
179
|
+
title="重新读取待办队列" aria-label="重新读取待办队列"
|
|
180
|
+
onClick={() => void load()}
|
|
181
|
+
><RefreshCw size={13} /></Button>
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* 这一屏的规矩要写在明面上:**未确认的提议不落 `02_wiki/`**。它不参与编译、
|
|
186
|
+
不参与召回、不在图上出现 —— 因为它连一张卡片都还不是。 */}
|
|
187
|
+
<p className="text-[10.5px] leading-relaxed text-tita-ink-muted">
|
|
188
|
+
Agent 与编译只会**提议**,不会写卡片。未确认的提议不落 <b>02_wiki/</b>,
|
|
189
|
+
因此不进编译、不参与召回、不在归因图上出现。通过之后才落卡。
|
|
190
|
+
</p>
|
|
191
|
+
|
|
192
|
+
{notice && <p
|
|
193
|
+
role="status"
|
|
194
|
+
className={cn('flex items-start gap-1.5 text-[10.5px] leading-relaxed', notice.tone === 'error' ? 'text-tita-danger' : 'text-tita-ink-muted')}
|
|
195
|
+
>
|
|
196
|
+
<AlertCircle size={12} className="mt-0.5 shrink-0" />
|
|
197
|
+
<span className="min-w-0 flex-1">{notice.text}</span>
|
|
198
|
+
</p>}
|
|
199
|
+
|
|
200
|
+
{phase === 'error' && <p className="text-[10.5px] leading-relaxed text-tita-danger">
|
|
201
|
+
读不到这个知识库的待办队列。宿主刚起来、或知识库根目录暂时读不到时会这样。
|
|
202
|
+
</p>}
|
|
203
|
+
|
|
204
|
+
{pending.length > 0 && <ul className="flex flex-col gap-1.5">
|
|
205
|
+
{pending.map(item => <li
|
|
206
|
+
key={item.id}
|
|
207
|
+
className="flex flex-col gap-1.5 rounded-lg border border-tita-line bg-tita-surface p-2"
|
|
208
|
+
>
|
|
209
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
210
|
+
<span className="min-w-0 flex-1 truncate text-[11.5px] font-semibold text-tita-ink" title={item.title}>{item.title}</span>
|
|
211
|
+
<code className="shrink-0 font-mono text-[9.5px] text-tita-ink-faint">{item.entityId}</code>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
215
|
+
<Badge tone="neutral">{item.entityType}</Badge>
|
|
216
|
+
<Badge tone="neutral">{ACTION_LABEL[item.action]}</Badge>
|
|
217
|
+
<Badge tone="violet">{TRIGGER_LABEL[item.trigger]}</Badge>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* 出处:没有它,人只能凭卡片标题猜这条提议该不该存在。 */}
|
|
221
|
+
<p className="truncate text-[10px] text-tita-ink-faint" title={item.sourceRefs.join(' · ')}>
|
|
222
|
+
出处 {item.sourceRefs.join(' · ') || item.sourceRef}
|
|
223
|
+
</p>
|
|
224
|
+
|
|
225
|
+
{/* 三层校验的问题清单。**校验是确认的前置**:不看清就通过,等于把校验器绕过去。 */}
|
|
226
|
+
{item.issues.length > 0 && <ul className="flex flex-col gap-0.5">
|
|
227
|
+
{item.issues.map((issue, index) => {
|
|
228
|
+
const level = LEVEL_LABEL[issue.level] ?? { label: issue.level, tone: 'neutral' as const }
|
|
229
|
+
return <li key={index} className="flex items-start gap-1.5 text-[10px] leading-relaxed text-tita-ink-muted">
|
|
230
|
+
<Badge tone={level.tone}>{level.label}</Badge>
|
|
231
|
+
<span className="min-w-0 flex-1">{issue.field ? `${issue.field}:` : ''}{issue.message}</span>
|
|
232
|
+
</li>
|
|
233
|
+
})}
|
|
234
|
+
</ul>}
|
|
235
|
+
|
|
236
|
+
<div className="flex items-center gap-1">
|
|
237
|
+
<Button
|
|
238
|
+
variant="ghost" size="sm"
|
|
239
|
+
className="h-[26px] min-h-[26px] px-2 text-[10.5px]"
|
|
240
|
+
aria-expanded={open.has(item.id)}
|
|
241
|
+
onClick={() => toggle(item.id)}
|
|
242
|
+
>{open.has(item.id) ? '收起正文' : '看正文'}</Button>
|
|
243
|
+
{item.issues.length > 0 && onOpenWikiTypes && <Button
|
|
244
|
+
variant="ghost" size="sm"
|
|
245
|
+
className="h-[26px] min-h-[26px] px-2 text-[10.5px]"
|
|
246
|
+
onClick={onOpenWikiTypes}
|
|
247
|
+
>去改类型定义</Button>}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* 正文用**只读呈现**(等宽块),不是 `<textarea>`:它是待审内容,不是可编辑表单。
|
|
251
|
+
用输入框展示等于告诉人「这里能改」,试两次才发现点不动。 */}
|
|
252
|
+
{open.has(item.id) && <pre className="max-h-[220px] overflow-auto overscroll-contain whitespace-pre-wrap break-words rounded-lg border border-tita-line bg-tita-canvas p-2 font-mono text-[10px] leading-relaxed text-tita-ink-muted">{item.content}</pre>}
|
|
253
|
+
|
|
254
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
255
|
+
<Button
|
|
256
|
+
variant="default" size="sm"
|
|
257
|
+
className="h-[26px] min-h-[26px] px-2 text-[10.5px]"
|
|
258
|
+
disabled={busy !== null}
|
|
259
|
+
title="确认并立刻落卡:这一步之后它进编译、进图、能被召回"
|
|
260
|
+
onClick={() => void review(item, 'confirm-and-write')}
|
|
261
|
+
><Check size={12} />通过并写入</Button>
|
|
262
|
+
<Button
|
|
263
|
+
variant="outline" size="sm"
|
|
264
|
+
className="h-[26px] min-h-[26px] px-2 text-[10.5px]"
|
|
265
|
+
disabled={busy !== null}
|
|
266
|
+
title="只确认、不落卡:交给 Agent 用 write_knowledge_card 落它"
|
|
267
|
+
onClick={() => void review(item, 'confirm')}
|
|
268
|
+
><ShieldCheck size={12} />仅确认</Button>
|
|
269
|
+
<span className="ml-auto flex min-w-0 flex-1 items-center gap-1">
|
|
270
|
+
<Input
|
|
271
|
+
value={reasons[item.id] ?? ''}
|
|
272
|
+
placeholder="拒绝原因(必写:它决定下次还会不会再提同一条)"
|
|
273
|
+
className="h-[26px] min-h-[26px] text-[10.5px]"
|
|
274
|
+
onChange={event => setReasons({ ...reasons, [item.id]: event.target.value })}
|
|
275
|
+
/>
|
|
276
|
+
<Button
|
|
277
|
+
variant="ghost" size="sm"
|
|
278
|
+
className="h-[26px] min-h-[26px] shrink-0 px-2 text-[10.5px]"
|
|
279
|
+
disabled={busy !== null}
|
|
280
|
+
onClick={() => void review(item, 'reject')}
|
|
281
|
+
><Trash2 size={12} />拒绝</Button>
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
</li>)}
|
|
285
|
+
</ul>}
|
|
286
|
+
|
|
287
|
+
{rejected.length > 0 && <details className="rounded-lg border border-tita-line bg-tita-surface px-2 py-1.5">
|
|
288
|
+
<summary className="cursor-pointer text-[10.5px] text-tita-ink-muted">
|
|
289
|
+
已拒绝 {rejected.length} 条 —— 原因留档,内容不变就不会再提同一条
|
|
290
|
+
</summary>
|
|
291
|
+
<ul className="mt-1.5 flex flex-col gap-1">
|
|
292
|
+
{rejected.map(item => <li key={item.id} className="flex flex-col gap-0.5 text-[10px] leading-relaxed text-tita-ink-muted">
|
|
293
|
+
<span className="flex items-center gap-1.5">
|
|
294
|
+
<code className="font-mono text-[9.5px] text-tita-ink-faint">{item.entityId}</code>
|
|
295
|
+
<span className="min-w-0 flex-1 truncate">{item.title}</span>
|
|
296
|
+
{item.reviewedBy && <span className="shrink-0 text-tita-ink-faint">{item.reviewedBy}</span>}
|
|
297
|
+
</span>
|
|
298
|
+
<span className="text-tita-danger">{item.reason}</span>
|
|
299
|
+
</li>)}
|
|
300
|
+
</ul>
|
|
301
|
+
</details>}
|
|
302
|
+
|
|
303
|
+
{pending.length === 0 && rejected.length > 0 && phase === 'ready' && <p className="flex items-center gap-1.5 text-[10.5px] text-tita-ink-muted">
|
|
304
|
+
<Sparkles size={12} />待办清空了。
|
|
305
|
+
</p>}
|
|
306
|
+
</section>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** 待确认条数的角标:智能归因那一栏用它,点了跳到这里。 */
|
|
310
|
+
export function ProposalBadge({ count }: { count: number }): ReactNode {
|
|
311
|
+
if (count <= 0) return null
|
|
312
|
+
return <Badge tone="warning">{count} 条待确认</Badge>
|
|
313
|
+
}
|