dsh-data-domain-knowledge 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 意图 Tab —— 读本插件的 `/intent/overview`、`/intent/store`、`/intent/registry`,
|
|
3
|
+
* 并**就地编辑**预置的意图分类(issue 0023)。
|
|
4
|
+
*
|
|
5
|
+
* 意图记录本身是 per-session、内存内、有 TTL 的审计事实(issues/0012),所以记录区
|
|
6
|
+
* 只读;分类注册表则是配置,走 `settings.intent` 命名空间写回 DSH,保存后引擎立刻
|
|
7
|
+
* 按新配置生成路由提示词。
|
|
8
|
+
*
|
|
9
|
+
* 0020 把意图的宿主实现并进了本插件(`src/intent/`),但这条 HTTP 边界**必须保留**:
|
|
10
|
+
* 面板跑在浏览器半、意图引擎跑在宿主半,二者无法直接互相 import。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, type ChangeEvent, type ReactNode } from 'react'
|
|
14
|
+
import {
|
|
15
|
+
AlertCircle, BarChart3, BrainCircuit, Check, Clock3, Eye, GitBranch, Lightbulb, Pencil, Plus, RefreshCw, RotateCcw, Settings2, ShieldCheck, Trash2, X, Zap,
|
|
16
|
+
} from '@tita-data-agent/ui/icons'
|
|
17
|
+
import { Badge, Button, Card, Input, Label, SelectMenu, Switch, Tabs, TabsContent, TabsList, TabsTrigger, cn } from '@tita-data-agent/ui/components'
|
|
18
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@tita-data-agent/ui/overlays'
|
|
19
|
+
import { styles } from './styles.ts'
|
|
20
|
+
import { NodeSettingsPanel, type SettingsScopeLike, type SettingsSnapshot } from './NodeSettings.tsx'
|
|
21
|
+
import { IntentLogicTab } from './IntentLogicTab.tsx'
|
|
22
|
+
|
|
23
|
+
interface IntentRecord {
|
|
24
|
+
id: string
|
|
25
|
+
act: string
|
|
26
|
+
category?: string
|
|
27
|
+
predicate?: string
|
|
28
|
+
confidence?: number
|
|
29
|
+
lowConfidence?: boolean
|
|
30
|
+
relation: string
|
|
31
|
+
source: 'model' | 'fallback'
|
|
32
|
+
userInput: string
|
|
33
|
+
createdAt: string
|
|
34
|
+
updatedAt: string
|
|
35
|
+
lineage: Record<string, string>
|
|
36
|
+
meta: Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SessionSummary {
|
|
40
|
+
sessionKey: string
|
|
41
|
+
recordCount: number
|
|
42
|
+
lastAt: string
|
|
43
|
+
lastAct?: string
|
|
44
|
+
lastCategory?: string
|
|
45
|
+
lastRelation?: string
|
|
46
|
+
lastConfidence?: number
|
|
47
|
+
activeIntentId?: string
|
|
48
|
+
activeTaskId?: string
|
|
49
|
+
tasks?: Array<{ taskId: string; lastSubtaskId?: string; category?: string; ageSeconds: number }>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface StoreView {
|
|
53
|
+
sessionKey: string
|
|
54
|
+
activeTaskId?: string
|
|
55
|
+
activeIntentId?: string
|
|
56
|
+
expiresAt?: string
|
|
57
|
+
tasks: Array<{ taskId: string; lastSubtaskId?: string; category?: string; ageSeconds: number }>
|
|
58
|
+
recent: IntentRecord[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 一条可编辑的预置意图分类(与宿主 `IntentCategory` 同形)。 */
|
|
62
|
+
interface RegistryCategory {
|
|
63
|
+
id: string
|
|
64
|
+
label: string
|
|
65
|
+
route: string
|
|
66
|
+
description: string
|
|
67
|
+
examples: string[]
|
|
68
|
+
enabled: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 一条可编辑的 act(与宿主 `IntentActDef` 同形)。 */
|
|
72
|
+
interface RegistryAct {
|
|
73
|
+
id: string
|
|
74
|
+
label: string
|
|
75
|
+
description: string
|
|
76
|
+
enabled: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface Registry { acts: RegistryAct[]; categories: RegistryCategory[]; predicates: string[] }
|
|
80
|
+
|
|
81
|
+
/** 新增 / 编辑分类的草稿;`examples` 在表单里是「一行一条」。 */
|
|
82
|
+
interface CategoryDraft {
|
|
83
|
+
open: boolean
|
|
84
|
+
mode: 'create' | 'edit'
|
|
85
|
+
id: string
|
|
86
|
+
label: string
|
|
87
|
+
route: string
|
|
88
|
+
description: string
|
|
89
|
+
examples: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 新增 / 编辑 act 的草稿。 */
|
|
93
|
+
interface ActDraft {
|
|
94
|
+
open: boolean
|
|
95
|
+
mode: 'create' | 'edit'
|
|
96
|
+
id: string
|
|
97
|
+
label: string
|
|
98
|
+
description: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const EMPTY_ACT_DRAFT: ActDraft = { open: false, mode: 'create', id: '', label: '', description: '' }
|
|
102
|
+
|
|
103
|
+
/** act id 的形状:写进每条记录的稳定标识。 */
|
|
104
|
+
const ACT_ID_PATTERN = /^[a-z][a-z0-9_]{1,40}$/
|
|
105
|
+
|
|
106
|
+
/** 意图页的三个 tab:记录(只读审计)、配置(注册表)、逻辑(说明)。 */
|
|
107
|
+
type IntentSection = 'records' | 'config' | 'logic'
|
|
108
|
+
|
|
109
|
+
const EMPTY_CATEGORY_DRAFT: CategoryDraft = { open: false, mode: 'create', id: '', label: '', route: 'query', description: '', examples: '' }
|
|
110
|
+
|
|
111
|
+
/** 分类 id 的形状:写进 lineage 的稳定标识,小写 + 下划线。 */
|
|
112
|
+
const CATEGORY_ID_PATTERN = /^[a-z][a-z0-9_]{1,40}$/
|
|
113
|
+
|
|
114
|
+
const UNAVAILABLE_SNAPSHOT: SettingsSnapshot = { status: 'unavailable', writable: false, mode: 'memory' }
|
|
115
|
+
|
|
116
|
+
async function getJson<T>(url: string): Promise<T> {
|
|
117
|
+
const response = await fetch(url)
|
|
118
|
+
const text = await response.text()
|
|
119
|
+
if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
|
|
120
|
+
const payload = JSON.parse(text) as { ok?: boolean; data?: T; error?: { message?: string } | null }
|
|
121
|
+
if (!response.ok || payload.ok === false) throw new Error(payload.error?.message ?? `请求失败(${response.status})`)
|
|
122
|
+
if (payload.data === undefined) throw new Error('响应缺少 data')
|
|
123
|
+
return payload.data
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function postJson<T>(url: string, body: unknown): Promise<T> {
|
|
127
|
+
const response = await fetch(url, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'content-type': 'application/json' },
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
})
|
|
132
|
+
const text = await response.text()
|
|
133
|
+
if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
|
|
134
|
+
const payload = JSON.parse(text) as { ok?: boolean; data?: T; error?: { message?: string } | null }
|
|
135
|
+
// 400 也带着 data(当前生效的注册表 + error),所以先看 data 再判状态码 ——
|
|
136
|
+
// 否则「写被拒」会丢掉「现在生效的是什么」这条最该显示的信息。
|
|
137
|
+
if (payload.data !== undefined) return payload.data
|
|
138
|
+
if (!response.ok || payload.ok === false) throw new Error(payload.error?.message ?? `请求失败(${response.status})`)
|
|
139
|
+
throw new Error('响应缺少 data')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatTime(value: string | undefined): string {
|
|
143
|
+
if (!value) return '—'
|
|
144
|
+
const date = new Date(value)
|
|
145
|
+
if (Number.isNaN(date.getTime())) return '—'
|
|
146
|
+
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatAge(seconds: number): string {
|
|
150
|
+
if (seconds < 60) return `${seconds}s`
|
|
151
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`
|
|
152
|
+
return `${Math.round(seconds / 3600)}h`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const SOURCE_LABEL: Record<IntentRecord['source'], string> = { model: '模型记录', fallback: 'Host 兜底' }
|
|
156
|
+
|
|
157
|
+
/** `user` 层里存在该字段 = 用户改过,可以「恢复默认」。 */
|
|
158
|
+
function hasOverride(user: unknown, key: string): boolean {
|
|
159
|
+
return user !== null && typeof user === 'object' && Object.prototype.hasOwnProperty.call(user, key)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Top-N 分布:记录本身是流水,这里把流水压成「这段会话主要在干什么」。 */
|
|
163
|
+
function Distribution({ title, rows, tone }: { title: string; rows: Array<[string, number]>; tone?: 'violet' | 'blue' | 'green' }): ReactNode {
|
|
164
|
+
const max = Math.max(1, ...rows.map(([, count]) => count))
|
|
165
|
+
const barClass = tone === 'blue' ? styles.distBarFts : tone === 'green' ? styles.distBarGraph : undefined
|
|
166
|
+
return <div>
|
|
167
|
+
<h4>{title}</h4>
|
|
168
|
+
{rows.length === 0
|
|
169
|
+
? <p className={styles.kbHint}>还没有记录。</p>
|
|
170
|
+
: <div className={styles.distList}>
|
|
171
|
+
{rows.map(([label, count]) => <div className={styles.distRow} key={label}>
|
|
172
|
+
<span title={label}>{label}</span>
|
|
173
|
+
<span className={cn(styles.distBar, barClass)}><i style={{ width: `${Math.round((count / max) * 100)}%` }} /></span>
|
|
174
|
+
<span className={styles.distCount}>{count}</span>
|
|
175
|
+
</div>)}
|
|
176
|
+
</div>}
|
|
177
|
+
</div>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function IntentTab({ visible, intent }: { visible: boolean; intent?: SettingsScopeLike }): ReactNode {
|
|
181
|
+
const [section, setSection] = useState<IntentSection>('records')
|
|
182
|
+
const [sessions, setSessions] = useState<SessionSummary[]>([])
|
|
183
|
+
const [sessionKey, setSessionKey] = useState('')
|
|
184
|
+
const [store, setStore] = useState<StoreView | null>(null)
|
|
185
|
+
const [registry, setRegistry] = useState<Registry | null>(null)
|
|
186
|
+
/** 真实注入 system prompt 的那段文本:让「预置分类怎么起作用」看得见。 */
|
|
187
|
+
const [prompt, setPrompt] = useState('')
|
|
188
|
+
const [loading, setLoading] = useState(false)
|
|
189
|
+
const [error, setError] = useState<string | null>(null)
|
|
190
|
+
const [categoryDraft, setCategoryDraft] = useState<CategoryDraft>(EMPTY_CATEGORY_DRAFT)
|
|
191
|
+
const [actDraft, setActDraft] = useState<ActDraft>(EMPTY_ACT_DRAFT)
|
|
192
|
+
const [categoryBusy, setCategoryBusy] = useState<string | null>(null)
|
|
193
|
+
const [categoryNotice, setCategoryNotice] = useState<{ tone: 'success' | 'error'; text: string } | null>(null)
|
|
194
|
+
|
|
195
|
+
const snapshot = useSyncExternalStore(
|
|
196
|
+
listener => intent?.subscribe(listener) ?? (() => undefined),
|
|
197
|
+
() => intent?.getSnapshot() ?? UNAVAILABLE_SNAPSHOT,
|
|
198
|
+
() => intent?.getSnapshot() ?? UNAVAILABLE_SNAPSHOT,
|
|
199
|
+
)
|
|
200
|
+
/**
|
|
201
|
+
* 词表能不能编辑。
|
|
202
|
+
*
|
|
203
|
+
* **判据是「文件接口能不能写」,不是「设置作用域可不可写」**(0033):分类与 act
|
|
204
|
+
* 现在写 `<agentRoot>/intent/registry.json`,与宿主的 settings 文档无关。继续用
|
|
205
|
+
* `snapshot.writable` 会让「设置服务不可用」连带把词表编辑也锁死 —— 而那两件事
|
|
206
|
+
* 的可用性本来就没有关系。
|
|
207
|
+
*/
|
|
208
|
+
const [registryWritable, setRegistryWritable] = useState(true)
|
|
209
|
+
const canEdit = registryWritable && (Boolean(intent) ? snapshot.status !== 'loading' : true)
|
|
210
|
+
/**
|
|
211
|
+
* 「已自定义」的判据来自**服务端**(0033)。
|
|
212
|
+
*
|
|
213
|
+
* 原来读 `settings.user.categories` —— 那是「宿主设置文档里有没有用户层覆盖」。
|
|
214
|
+
* 现在词表的真源是 `<agentRoot>/intent/registry.json`,与 settings 无关,所以
|
|
215
|
+
* 那个判据会在搬完家之后永远显示「未自定义」,而「恢复出厂」按钮也跟着灰掉。
|
|
216
|
+
*/
|
|
217
|
+
const [customized, setCustomized] = useState<{ categories: boolean; acts: boolean }>({ categories: false, acts: false })
|
|
218
|
+
const categoriesCustomized = customized.categories
|
|
219
|
+
const actsCustomized = customized.acts
|
|
220
|
+
|
|
221
|
+
const load = useCallback(async (preferred?: string): Promise<void> => {
|
|
222
|
+
setLoading(true)
|
|
223
|
+
try {
|
|
224
|
+
const [overview, reg] = await Promise.all([
|
|
225
|
+
getJson<{ sessions: SessionSummary[] }>('/intent/overview'),
|
|
226
|
+
getJson<{ registry: Registry; prompt: string; customized?: { categories: boolean; acts: boolean }; path?: string }>('/intent/registry'),
|
|
227
|
+
])
|
|
228
|
+
const list = overview.sessions ?? []
|
|
229
|
+
setSessions(list)
|
|
230
|
+
setRegistry(reg.registry)
|
|
231
|
+
setPrompt(reg.prompt ?? '')
|
|
232
|
+
if (reg.customized) setCustomized(reg.customized)
|
|
233
|
+
setRegistryWritable(Boolean(reg.path))
|
|
234
|
+
const key = preferred && list.some(item => item.sessionKey === preferred)
|
|
235
|
+
? preferred
|
|
236
|
+
: list[0]?.sessionKey ?? ''
|
|
237
|
+
setSessionKey(key)
|
|
238
|
+
setStore(key ? await getJson<{ store: StoreView }>(`/intent/store?session_key=${encodeURIComponent(key)}`).then(payload => payload.store) : null)
|
|
239
|
+
setError(null)
|
|
240
|
+
} catch (cause) {
|
|
241
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
242
|
+
} finally {
|
|
243
|
+
setLoading(false)
|
|
244
|
+
}
|
|
245
|
+
}, [])
|
|
246
|
+
|
|
247
|
+
useEffect(() => { if (visible) void load(sessionKey || undefined) }, [visible]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
248
|
+
|
|
249
|
+
const selectSession = async (key: string): Promise<void> => {
|
|
250
|
+
setSessionKey(key)
|
|
251
|
+
setLoading(true)
|
|
252
|
+
try {
|
|
253
|
+
setStore((await getJson<{ store: StoreView }>(`/intent/store?session_key=${encodeURIComponent(key)}`)).store)
|
|
254
|
+
setError(null)
|
|
255
|
+
} catch (cause) {
|
|
256
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
257
|
+
} finally {
|
|
258
|
+
setLoading(false)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ------------------------------------------------- 预置分类编辑(issue 0023)
|
|
263
|
+
|
|
264
|
+
/** 只重读注册表与提示词:编辑分类后用它确认写入是否真的落地。 */
|
|
265
|
+
const reloadRegistry = useCallback(async (): Promise<Registry | null> => {
|
|
266
|
+
try {
|
|
267
|
+
const fresh = await getJson<{ registry: Registry; prompt: string; customized?: { categories: boolean; acts: boolean }; path?: string }>('/intent/registry')
|
|
268
|
+
setRegistry(fresh.registry)
|
|
269
|
+
setPrompt(fresh.prompt ?? '')
|
|
270
|
+
if (fresh.customized) setCustomized(fresh.customized)
|
|
271
|
+
// 服务端给得出文件路径 = 意图目录已就绪,写就能落地。
|
|
272
|
+
setRegistryWritable(Boolean(fresh.path))
|
|
273
|
+
return fresh.registry
|
|
274
|
+
} catch (cause) {
|
|
275
|
+
setCategoryNotice({ tone: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
}, [])
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 写注册表字段(categories / acts),然后**按生效结果**回话。
|
|
282
|
+
*
|
|
283
|
+
* 0033 起写的是 `<agentRoot>/intent/registry.json`(POST `/intent/registry`),
|
|
284
|
+
* 不再走设置作用域 —— 词表是这个 Agent 自己的文件,不是宿主的配置。
|
|
285
|
+
*
|
|
286
|
+
* 「写完必须重读比对」这条纪律保留:服务端可能拒绝(schema 不过、目录不可写),
|
|
287
|
+
* 而**报成功但没生效**比报错更糟。所以这里比对的是重读回来的签名。
|
|
288
|
+
*/
|
|
289
|
+
const applyRegistry = useCallback(async (
|
|
290
|
+
field: 'categories' | 'acts',
|
|
291
|
+
value: Array<{ id: string; enabled: boolean }>,
|
|
292
|
+
describe: string,
|
|
293
|
+
): Promise<void> => {
|
|
294
|
+
setCategoryBusy(field)
|
|
295
|
+
try {
|
|
296
|
+
const written = await postJson<{ registry: Registry; prompt: string; customized?: { categories: boolean; acts: boolean }; error?: string }>(
|
|
297
|
+
'/intent/registry', { [field]: value })
|
|
298
|
+
if (written.error) throw new Error(written.error)
|
|
299
|
+
const fresh = await reloadRegistry()
|
|
300
|
+
const signature = (list: Array<{ id: string; enabled: boolean }>): string => list.map(item => `${item.id}:${item.enabled ? 1 : 0}`).join(',')
|
|
301
|
+
if (!fresh || signature(fresh[field]) !== signature(value)) {
|
|
302
|
+
setCategoryNotice({ tone: 'error', text: '写入没有被接受,下面显示的仍是当前生效的配置。' })
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
setCategoryNotice({ tone: 'success', text: describe })
|
|
306
|
+
} catch (cause) {
|
|
307
|
+
setCategoryNotice({ tone: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
308
|
+
} finally {
|
|
309
|
+
setCategoryBusy(null)
|
|
310
|
+
}
|
|
311
|
+
}, [reloadRegistry])
|
|
312
|
+
|
|
313
|
+
const applyCategories = useCallback((next: RegistryCategory[], describe: string): Promise<void> => applyRegistry('categories', next, describe), [applyRegistry])
|
|
314
|
+
const applyActs = useCallback((next: RegistryAct[], describe: string): Promise<void> => applyRegistry('acts', next, describe), [applyRegistry])
|
|
315
|
+
|
|
316
|
+
/** 回到出厂预置:把出厂词表**写回文件**(而不是删掉文件)。`field` 省略时两样都重置。 */
|
|
317
|
+
const resetRegistry = async (field?: 'categories' | 'acts'): Promise<void> => {
|
|
318
|
+
setCategoryBusy('reset')
|
|
319
|
+
try {
|
|
320
|
+
await postJson('/intent/registry', { reset: field ?? 'all' })
|
|
321
|
+
const fresh = await reloadRegistry()
|
|
322
|
+
setCategoryNotice(fresh
|
|
323
|
+
? { tone: 'success', text: `已恢复出厂预置(${fresh.categories.length} 个分类 / ${fresh.acts.length} 个 act)` }
|
|
324
|
+
: { tone: 'error', text: '恢复默认后未能读回注册表' })
|
|
325
|
+
} catch (cause) {
|
|
326
|
+
setCategoryNotice({ tone: 'error', text: cause instanceof Error ? cause.message : String(cause) })
|
|
327
|
+
} finally {
|
|
328
|
+
setCategoryBusy(null)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const openCategoryEditor = (category?: RegistryCategory): void => {
|
|
333
|
+
setCategoryNotice(null)
|
|
334
|
+
setCategoryDraft(category
|
|
335
|
+
? { open: true, mode: 'edit', id: category.id, label: category.label, route: category.route, description: category.description, examples: (category.examples ?? []).join('\n') }
|
|
336
|
+
: { ...EMPTY_CATEGORY_DRAFT, open: true, route: registry?.acts.find(act => act.enabled)?.id ?? 'query' })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const saveCategory = async (): Promise<void> => {
|
|
340
|
+
const current = registry?.categories ?? []
|
|
341
|
+
const id = categoryDraft.id.trim()
|
|
342
|
+
const label = categoryDraft.label.trim()
|
|
343
|
+
const description = categoryDraft.description.trim()
|
|
344
|
+
const examples = categoryDraft.examples.split('\n').map(line => line.trim()).filter(Boolean)
|
|
345
|
+
if (!CATEGORY_ID_PATTERN.test(id)) { setCategoryNotice({ tone: 'error', text: '分类 id 必须是小写字母开头的 2–41 位小写字母 / 数字 / 下划线,例如 test_review' }); return }
|
|
346
|
+
if (categoryDraft.mode === 'create' && current.some(item => item.id === id)) { setCategoryNotice({ tone: 'error', text: `分类 id 已存在:${id}` }); return }
|
|
347
|
+
if (!label) { setCategoryNotice({ tone: 'error', text: '显示名不能为空' }); return }
|
|
348
|
+
if (description.length < 4) { setCategoryNotice({ tone: 'error', text: '定义必填:写清这一类「是什么」,模型靠它匹配' }); return }
|
|
349
|
+
if (examples.length === 0) { setCategoryNotice({ tone: 'error', text: '至少写一条"例如",否则模型只能靠猜' }); return }
|
|
350
|
+
const entry: RegistryCategory = { id, label, route: categoryDraft.route, description, examples, enabled: current.find(item => item.id === id)?.enabled ?? true }
|
|
351
|
+
const next = categoryDraft.mode === 'create'
|
|
352
|
+
? [...current, entry]
|
|
353
|
+
: current.map(item => item.id === id ? { ...item, ...entry } : item)
|
|
354
|
+
setCategoryDraft(EMPTY_CATEGORY_DRAFT)
|
|
355
|
+
await applyCategories(next, categoryDraft.mode === 'create' ? `已新增分类「${label}」` : `分类「${label}」已更新`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const toggleCategory = async (category: RegistryCategory, enabled: boolean): Promise<void> => {
|
|
359
|
+
await applyCategories((registry?.categories ?? []).map(item => item.id === category.id ? { ...item, enabled } : item), `${category.label} 已${enabled ? '启用' : '停用'}`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const removeCategory = async (category: RegistryCategory): Promise<void> => {
|
|
363
|
+
await applyCategories((registry?.categories ?? []).filter(item => item.id !== category.id), `已删除分类「${category.label}」`)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ------------------------------------------------- act 词表编辑(issue 0023)
|
|
367
|
+
|
|
368
|
+
const openActEditor = (act?: RegistryAct): void => {
|
|
369
|
+
setCategoryNotice(null)
|
|
370
|
+
setActDraft(act
|
|
371
|
+
? { open: true, mode: 'edit', id: act.id, label: act.label, description: act.description }
|
|
372
|
+
: { ...EMPTY_ACT_DRAFT, open: true })
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const saveAct = async (): Promise<void> => {
|
|
376
|
+
const current = registry?.acts ?? []
|
|
377
|
+
const id = actDraft.id.trim()
|
|
378
|
+
const label = actDraft.label.trim()
|
|
379
|
+
const description = actDraft.description.trim()
|
|
380
|
+
if (!ACT_ID_PATTERN.test(id)) { setCategoryNotice({ tone: 'error', text: 'act id 必须是小写字母开头的 2–41 位小写字母 / 数字 / 下划线,例如 audit' }); return }
|
|
381
|
+
if (actDraft.mode === 'create' && current.some(item => item.id === id)) { setCategoryNotice({ tone: 'error', text: `act id 已存在:${id}` }); return }
|
|
382
|
+
if (!label) { setCategoryNotice({ tone: 'error', text: 'act 显示名不能为空' }); return }
|
|
383
|
+
if (description.length < 4) { setCategoryNotice({ tone: 'error', text: 'act 说明必填:写清这个动作是什么,模型靠它区分 query 与 request' }); return }
|
|
384
|
+
const entry: RegistryAct = { id, label, description, enabled: current.find(item => item.id === id)?.enabled ?? true }
|
|
385
|
+
const next = actDraft.mode === 'create'
|
|
386
|
+
? [...current, entry]
|
|
387
|
+
: current.map(item => item.id === id ? { ...item, ...entry } : item)
|
|
388
|
+
setActDraft(EMPTY_ACT_DRAFT)
|
|
389
|
+
await applyActs(next, actDraft.mode === 'create' ? `已新增 act「${label}」` : `act「${label}」已更新`)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const toggleAct = async (act: RegistryAct, enabled: boolean): Promise<void> => {
|
|
393
|
+
await applyActs((registry?.acts ?? []).map(item => item.id === act.id ? { ...item, enabled } : item), `${act.label} 已${enabled ? '启用' : '停用'}`)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const removeAct = async (act: RegistryAct): Promise<void> => {
|
|
397
|
+
const used = (registry?.categories ?? []).some(category => category.route === act.id)
|
|
398
|
+
if (used) { setCategoryNotice({ tone: 'error', text: `还有分类把 ${act.id} 当作默认 act,先改掉那些分类再删。` }); return }
|
|
399
|
+
await applyActs((registry?.acts ?? []).filter(item => item.id !== act.id), `已删除 act「${act.label}」`)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const active = sessions.find(item => item.sessionKey === sessionKey)
|
|
403
|
+
|
|
404
|
+
/** 从最近记录里归纳:这段会话主要在做什么,有多少低置信度需要人看。 */
|
|
405
|
+
const summary = useMemo(() => {
|
|
406
|
+
const records = store?.recent ?? []
|
|
407
|
+
const top = (pick: (record: IntentRecord) => string | undefined): Array<[string, number]> => {
|
|
408
|
+
const counts = new Map<string, number>()
|
|
409
|
+
for (const record of records) {
|
|
410
|
+
const key = pick(record)
|
|
411
|
+
if (!key) continue
|
|
412
|
+
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
413
|
+
}
|
|
414
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5)
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
total: records.length,
|
|
418
|
+
byAct: top(record => record.act),
|
|
419
|
+
byCategory: top(record => record.category),
|
|
420
|
+
byPredicate: top(record => record.predicate),
|
|
421
|
+
lowConfidence: records.filter(record => record.lowConfidence).length,
|
|
422
|
+
fallback: records.filter(record => record.source === 'fallback').length,
|
|
423
|
+
subtasks: records.filter(record => record.relation === 'subtask_of').length,
|
|
424
|
+
}
|
|
425
|
+
}, [store])
|
|
426
|
+
|
|
427
|
+
return <div className={styles.viewStack}>
|
|
428
|
+
{/* 三个 tab 而不是上下堆叠:记录是"看",配置是"改",逻辑是"为什么这么设计"。 */}
|
|
429
|
+
<Tabs className={styles.intentSections} value={section} onValueChange={value => setSection(value as IntentSection)}>
|
|
430
|
+
<div className={styles.intentSectionBar}>
|
|
431
|
+
<TabsList className={styles.tabsList}>
|
|
432
|
+
<TabsTrigger value="records"><Clock3 size={13} />意图记录</TabsTrigger>
|
|
433
|
+
<TabsTrigger value="config"><Settings2 size={13} />意图配置</TabsTrigger>
|
|
434
|
+
<TabsTrigger value="logic"><Lightbulb size={13} />意图的逻辑</TabsTrigger>
|
|
435
|
+
</TabsList>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{error && <p className={styles.kbNotice}><AlertCircle size={13} /><span>{error}</span></p>}
|
|
439
|
+
|
|
440
|
+
<TabsContent value="records">
|
|
441
|
+
<div className={styles.viewStack}>
|
|
442
|
+
{/* 会话切换属于"看记录",所以它跟记录内容在一起,不占 tab 栏。 */}
|
|
443
|
+
<div className={styles.intentHead}>
|
|
444
|
+
<SelectMenu
|
|
445
|
+
className="max-w-[320px]"
|
|
446
|
+
value={sessionKey}
|
|
447
|
+
onValueChange={next => void selectSession(next)}
|
|
448
|
+
disabled={sessions.length === 0}
|
|
449
|
+
aria-label="选择意图记录会话"
|
|
450
|
+
placeholder={sessions.length === 0 ? '还没有会话记录' : '选择会话'}
|
|
451
|
+
options={sessions.map(item => ({ value: item.sessionKey, label: `${item.sessionKey.slice(0, 18)} · ${item.recordCount} 条 · ${formatTime(item.lastAt)}` }))}
|
|
452
|
+
/>
|
|
453
|
+
<Button variant="ghost" size="icon" title="刷新" aria-label="刷新意图记录" onClick={() => void load(sessionKey)} disabled={loading}><RefreshCw size={14} /></Button>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div className={styles.statsRow}>
|
|
457
|
+
<Card className={styles.intentMetric}>
|
|
458
|
+
<small>当前 act</small>
|
|
459
|
+
<strong>{active?.lastAct ?? '—'}</strong>
|
|
460
|
+
<em>{active?.lastCategory ?? '未分类'}</em>
|
|
461
|
+
</Card>
|
|
462
|
+
<Card className={styles.intentMetric}>
|
|
463
|
+
<small>置信度</small>
|
|
464
|
+
<strong>{active?.lastConfidence !== undefined ? active.lastConfidence.toFixed(2) : '—'}</strong>
|
|
465
|
+
<em>{active?.lastRelation ?? '—'}</em>
|
|
466
|
+
</Card>
|
|
467
|
+
<Card className={styles.intentMetric}>
|
|
468
|
+
<small>记录数</small>
|
|
469
|
+
<strong>{active?.recordCount ?? 0}</strong>
|
|
470
|
+
<em>TTL 内有效</em>
|
|
471
|
+
</Card>
|
|
472
|
+
<Card className={styles.intentMetric}>
|
|
473
|
+
<small>活动任务</small>
|
|
474
|
+
<strong>{store?.tasks.length ?? 0}</strong>
|
|
475
|
+
<em>{store?.activeTaskId ? store.activeTaskId.slice(0, 14) : '无'}</em>
|
|
476
|
+
</Card>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<Card className={styles.kbCard}>
|
|
480
|
+
<div className={styles.kbCardHead}>
|
|
481
|
+
<div><h3><BarChart3 size={13} />记录分布</h3></div>
|
|
482
|
+
<span className={styles.kbCount}>{summary.total} 条记录</span>
|
|
483
|
+
</div>
|
|
484
|
+
<p className={styles.kbHint}>
|
|
485
|
+
TTL 内的记录在这里被归纳:低置信度 {summary.lowConfidence} 条、Host 兜底 {summary.fallback} 条、子任务 {summary.subtasks} 条。
|
|
486
|
+
低置信度只标注、不拦截 —— 记录器的契约就是不阻塞请求。
|
|
487
|
+
</p>
|
|
488
|
+
<div className={styles.intentRegistry}>
|
|
489
|
+
<Distribution title="act" rows={summary.byAct} tone="blue" />
|
|
490
|
+
<Distribution title="分类" rows={summary.byCategory} tone="violet" />
|
|
491
|
+
<Distribution title="predicate" rows={summary.byPredicate} tone="green" />
|
|
492
|
+
</div>
|
|
493
|
+
</Card>
|
|
494
|
+
|
|
495
|
+
<Card className={styles.kbCard}>
|
|
496
|
+
<div className={styles.kbCardHead}>
|
|
497
|
+
<div><h3><GitBranch size={13} />活动任务</h3></div>
|
|
498
|
+
<span className={styles.kbCount}>{store?.tasks.length ?? 0} 个</span>
|
|
499
|
+
</div>
|
|
500
|
+
{(store?.tasks.length ?? 0) === 0 && <p className={styles.kbEmpty}>这个会话还没有绑定任务。</p>}
|
|
501
|
+
<ul className={styles.intentTaskList}>
|
|
502
|
+
{(store?.tasks ?? []).map(task => <li key={task.taskId}>
|
|
503
|
+
<span className={styles.intentTaskId}><GitBranch size={12} />{task.taskId}</span>
|
|
504
|
+
{task.lastSubtaskId && <span className={styles.kbCode}>{task.lastSubtaskId}</span>}
|
|
505
|
+
{task.category && <Badge tone="violet">{task.category}</Badge>}
|
|
506
|
+
<span className={styles.kbTypeCount}>{formatAge(task.ageSeconds)} 前</span>
|
|
507
|
+
</li>)}
|
|
508
|
+
</ul>
|
|
509
|
+
</Card>
|
|
510
|
+
|
|
511
|
+
<Card className={styles.kbCard}>
|
|
512
|
+
<div className={styles.kbCardHead}>
|
|
513
|
+
<div><h3><BrainCircuit size={13} />最近意图记录</h3></div>
|
|
514
|
+
<span className={styles.kbCount}>{store?.recent.length ?? 0} 条</span>
|
|
515
|
+
</div>
|
|
516
|
+
<p className={styles.kbHint}>
|
|
517
|
+
每轮记录一条,只做审计:默认不阻塞、不确认。{store?.expiresAt ? `本会话记录 ${formatTime(store.expiresAt)} 前有效。` : ''}
|
|
518
|
+
</p>
|
|
519
|
+
{(store?.recent.length ?? 0) === 0 && <p className={styles.kbEmpty}>还没有意图记录。</p>}
|
|
520
|
+
<ul className={styles.intentList}>
|
|
521
|
+
{(store?.recent ?? []).map(record => <li key={record.id} className={styles.intentRow}>
|
|
522
|
+
<div className={styles.intentRowHead}>
|
|
523
|
+
<Badge tone={record.lowConfidence ? 'warning' : 'neutral'}>{record.act}</Badge>
|
|
524
|
+
{record.category && <Badge tone="violet">{record.category}</Badge>}
|
|
525
|
+
{record.predicate && <span className={styles.kbCode}>{record.predicate}</span>}
|
|
526
|
+
<span className={styles.intentRelation}>{record.relation === 'subtask_of' ? '子任务' : record.relation === 'new_task' ? '新任务' : '未判定'}</span>
|
|
527
|
+
<span className={styles.intentTime}><Clock3 size={11} />{formatTime(record.updatedAt)}</span>
|
|
528
|
+
</div>
|
|
529
|
+
<p className={styles.intentInput}>{record.userInput.slice(0, 220) || '(无用户原文)'}</p>
|
|
530
|
+
<div className={styles.intentRowFoot}>
|
|
531
|
+
<span>{SOURCE_LABEL[record.source]}</span>
|
|
532
|
+
{record.confidence !== undefined && <span>confidence {record.confidence.toFixed(2)}</span>}
|
|
533
|
+
{record.lineage.taskId && <span className={styles.kbCode}>{record.lineage.taskId}</span>}
|
|
534
|
+
{record.lineage.subtaskId && <span className={styles.kbCode}>{record.lineage.subtaskId}</span>}
|
|
535
|
+
</div>
|
|
536
|
+
</li>)}
|
|
537
|
+
</ul>
|
|
538
|
+
</Card>
|
|
539
|
+
</div>
|
|
540
|
+
</TabsContent>
|
|
541
|
+
|
|
542
|
+
<TabsContent value="config">
|
|
543
|
+
<div className={styles.viewStack}>
|
|
544
|
+
{/* 标量设置复用架构节点那套表单:同一个命名空间、同一个组件,两处不会漂移。 */}
|
|
545
|
+
<NodeSettingsPanel node="intent" intent={intent} />
|
|
546
|
+
|
|
547
|
+
<Card className={styles.kbCard}>
|
|
548
|
+
<div className={styles.kbCardHead}>
|
|
549
|
+
<div><h3><ShieldCheck size={13} />意图分类(预置)</h3></div>
|
|
550
|
+
<div className={styles.kbCardActions}>
|
|
551
|
+
<span className={styles.kbCount}>
|
|
552
|
+
{registry?.categories.filter(category => category.enabled).length ?? 0} / {registry?.categories.length ?? 0} 启用
|
|
553
|
+
{categoriesCustomized ? ' · 已自定义' : ''}
|
|
554
|
+
</span>
|
|
555
|
+
{canEdit && <Button
|
|
556
|
+
variant="ghost" size="sm"
|
|
557
|
+
disabled={categoryBusy !== null || !categoriesCustomized}
|
|
558
|
+
title={categoriesCustomized ? '清除自定义,回到出厂预置' : '当前就是出厂预置'}
|
|
559
|
+
onClick={() => void resetRegistry('categories')}
|
|
560
|
+
><RotateCcw size={13} />恢复默认</Button>}
|
|
561
|
+
<Button variant="outline" size="sm" disabled={!canEdit || categoryBusy !== null} onClick={() => openCategoryEditor()}><Plus size={13} />新增分类</Button>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
<p className={styles.kbHint}>
|
|
565
|
+
判定是<b>两段式</b>的:模型先拿用户的话比对这里的<b>定义</b>与<b>例如</b>,命中就直接用该分类 id;
|
|
566
|
+
全不命中才自行判断(新起一个 id 或 unknown)。预置写得越具体,模型越不需要每轮从零分类。
|
|
567
|
+
{canEdit ? '' : '(意图目录还没就绪,只能查看。)'}
|
|
568
|
+
</p>
|
|
569
|
+
{categoryNotice && <p className={cn(styles.kbNotice, categoryNotice.tone === 'error' && styles.kbNoticeError)} role="status">
|
|
570
|
+
<AlertCircle size={13} /><span>{categoryNotice.text}</span>
|
|
571
|
+
<button type="button" className={styles.kbNoticeClose} aria-label="关闭提示" onClick={() => setCategoryNotice(null)}><X size={12} /></button>
|
|
572
|
+
</p>}
|
|
573
|
+
<ul className={styles.intentCategoryList}>
|
|
574
|
+
{(registry?.categories ?? []).map(category => <li key={category.id} className={styles.intentCategoryRow}>
|
|
575
|
+
<span className={styles.intentCategoryName}>
|
|
576
|
+
<span className={styles.intentCategoryLabel}>{category.label}</span>
|
|
577
|
+
<code className={styles.kbSourceId}>{category.id}</code>
|
|
578
|
+
<span className={styles.kbCode}>{category.route}</span>
|
|
579
|
+
</span>
|
|
580
|
+
<span className={styles.intentCategoryExample}>
|
|
581
|
+
{category.description || '(没有定义:模型只能靠猜)'}
|
|
582
|
+
{(category.examples ?? []).length > 0 ? ` 例如:${(category.examples ?? []).join(' / ')}` : ' (没有例如)'}
|
|
583
|
+
</span>
|
|
584
|
+
<Switch checked={category.enabled} disabled={!canEdit || categoryBusy !== null} onCheckedChange={checked => void toggleCategory(category, checked)} />
|
|
585
|
+
<Button variant="ghost" size="icon" title={`编辑 ${category.label}`} aria-label={`编辑分类 ${category.label}`} disabled={!canEdit || categoryBusy !== null} onClick={() => openCategoryEditor(category)}><Pencil size={13} /></Button>
|
|
586
|
+
<Button variant="ghost" size="icon" title={`删除 ${category.label}`} aria-label={`删除分类 ${category.label}`} disabled={!canEdit || categoryBusy !== null} onClick={() => void removeCategory(category)}><Trash2 size={13} /></Button>
|
|
587
|
+
</li>)}
|
|
588
|
+
</ul>
|
|
589
|
+
|
|
590
|
+
{prompt && <details className={styles.kbPrompt}>
|
|
591
|
+
<summary><Eye size={12} /> 模型看到的提示词(只读,随上面的配置实时生成;它就是 intent_record 的工具描述)</summary>
|
|
592
|
+
<pre className={styles.kbPre}>{prompt}</pre>
|
|
593
|
+
</details>}
|
|
594
|
+
</Card>
|
|
595
|
+
|
|
596
|
+
<Card className={styles.kbCard}>
|
|
597
|
+
<div className={styles.kbCardHead}>
|
|
598
|
+
<div><h3><Zap size={13} />act 词表(这一轮在做什么)</h3></div>
|
|
599
|
+
<div className={styles.kbCardActions}>
|
|
600
|
+
<span className={styles.kbCount}>
|
|
601
|
+
{registry?.acts.filter(act => act.enabled).length ?? 0} / {registry?.acts.length ?? 0} 启用
|
|
602
|
+
{actsCustomized ? ' · 已自定义' : ''}
|
|
603
|
+
</span>
|
|
604
|
+
{canEdit && <Button
|
|
605
|
+
variant="ghost" size="sm"
|
|
606
|
+
disabled={categoryBusy !== null || !actsCustomized}
|
|
607
|
+
title={actsCustomized ? '清除自定义,回到出厂词表' : '当前就是出厂词表'}
|
|
608
|
+
onClick={() => void resetRegistry('acts')}
|
|
609
|
+
><RotateCcw size={13} />恢复默认</Button>}
|
|
610
|
+
<Button variant="outline" size="sm" disabled={!canEdit || categoryBusy !== null} onClick={() => openActEditor()}><Plus size={13} />新增 act</Button>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
<p className={styles.kbHint}>
|
|
614
|
+
act 回答"用户这一轮在<b>做什么</b>",与分类(哪一类业务)正交:<b>query</b> 是提问要答案,
|
|
615
|
+
<b>request</b> 是让 Agent 做事并产生副作用。说明会进工具描述,所以写清两者的差别比加动作更重要。
|
|
616
|
+
</p>
|
|
617
|
+
<ul className={styles.intentCategoryList}>
|
|
618
|
+
{(registry?.acts ?? []).map(act => <li key={act.id} className={styles.intentCategoryRow}>
|
|
619
|
+
<span className={styles.intentCategoryName}>
|
|
620
|
+
<span className={styles.intentCategoryLabel}>{act.label}</span>
|
|
621
|
+
<code className={styles.kbSourceId}>{act.id}</code>
|
|
622
|
+
</span>
|
|
623
|
+
<span className={styles.intentCategoryExample}>{act.description || '(没有说明:模型只能靠猜)'}</span>
|
|
624
|
+
<Switch checked={act.enabled} disabled={!canEdit || categoryBusy !== null} onCheckedChange={checked => void toggleAct(act, checked)} />
|
|
625
|
+
<Button variant="ghost" size="icon" title={`编辑 ${act.label}`} aria-label={`编辑 act ${act.label}`} disabled={!canEdit || categoryBusy !== null} onClick={() => openActEditor(act)}><Pencil size={13} /></Button>
|
|
626
|
+
<Button variant="ghost" size="icon" title={`删除 ${act.label}`} aria-label={`删除 act ${act.label}`} disabled={!canEdit || categoryBusy !== null} onClick={() => void removeAct(act)}><Trash2 size={13} /></Button>
|
|
627
|
+
</li>)}
|
|
628
|
+
</ul>
|
|
629
|
+
|
|
630
|
+
<div className={styles.intentRegistry}>
|
|
631
|
+
<div>
|
|
632
|
+
<h4>predicate(只读)</h4>
|
|
633
|
+
<div className={styles.intentChips}>{(registry?.predicates ?? []).map(predicate => <span key={predicate} className={cn(styles.kbCode, styles.intentPredicate)}>{predicate}</span>)}</div>
|
|
634
|
+
<p className={styles.kbHint}>predicate 是真实工具名,应来自工具注册表本身,所以不在这里手写。</p>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</Card>
|
|
638
|
+
</div>
|
|
639
|
+
</TabsContent>
|
|
640
|
+
|
|
641
|
+
<TabsContent value="logic">
|
|
642
|
+
<IntentLogicTab />
|
|
643
|
+
</TabsContent>
|
|
644
|
+
</Tabs>
|
|
645
|
+
|
|
646
|
+
<Dialog open={categoryDraft.open} onOpenChange={open => { if (!open) setCategoryDraft(EMPTY_CATEGORY_DRAFT) }}>
|
|
647
|
+
{categoryDraft.open && <DialogContent>
|
|
648
|
+
<DialogHeader><DialogTitle>{categoryDraft.mode === 'create' ? '新增意图分类' : `编辑意图分类 · ${categoryDraft.label}`}</DialogTitle></DialogHeader>
|
|
649
|
+
<div className={styles.kbForm}>
|
|
650
|
+
<label className={styles.kbField}><Label>分类 id</Label>
|
|
651
|
+
<Input value={categoryDraft.id} disabled={categoryDraft.mode === 'edit'} spellCheck={false} placeholder="test_review"
|
|
652
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setCategoryDraft({ ...categoryDraft, id: event.target.value })} />
|
|
653
|
+
<span className={styles.kbHint}>小写字母 / 数字 / 下划线,创建后不可修改——它会被写进每轮意图记录。</span>
|
|
654
|
+
</label>
|
|
655
|
+
<label className={styles.kbField}><Label>显示名</Label>
|
|
656
|
+
<Input value={categoryDraft.label} placeholder="测试评审"
|
|
657
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setCategoryDraft({ ...categoryDraft, label: event.target.value })} />
|
|
658
|
+
</label>
|
|
659
|
+
<label className={styles.kbField}><Label>对应 act</Label>
|
|
660
|
+
<SelectMenu
|
|
661
|
+
value={categoryDraft.route}
|
|
662
|
+
onValueChange={next => setCategoryDraft({ ...categoryDraft, route: next })}
|
|
663
|
+
aria-label="对应 act"
|
|
664
|
+
options={(registry?.acts ?? []).filter(act => act.enabled || act.id === categoryDraft.route).map(act => ({ value: act.id, label: `${act.label}(${act.id})` }))}
|
|
665
|
+
/>
|
|
666
|
+
<span className={styles.kbHint}>命中这个分类时记录用的 act。</span>
|
|
667
|
+
</label>
|
|
668
|
+
<label className={styles.kbField}><Label>定义(必填)</Label>
|
|
669
|
+
<textarea className={styles.kbTextarea} value={categoryDraft.description} spellCheck={false}
|
|
670
|
+
placeholder="审查测试用例覆盖度、质量与遗漏场景的请求。"
|
|
671
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setCategoryDraft({ ...categoryDraft, description: event.target.value })} />
|
|
672
|
+
<span className={styles.kbHint}>写清这一类「是什么」,而不是「包含哪些词」——这是模型匹配的第一依据。</span>
|
|
673
|
+
</label>
|
|
674
|
+
<label className={styles.kbField}><Label>例如 · 用户会怎么说(每行一条,至少 1 条)</Label>
|
|
675
|
+
<textarea className={styles.kbTextarea} value={categoryDraft.examples} spellCheck={false}
|
|
676
|
+
placeholder={'帮我看看这批用例覆盖全了没\n这个需求的测试用例补一下'}
|
|
677
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setCategoryDraft({ ...categoryDraft, examples: event.target.value })} />
|
|
678
|
+
<span className={styles.kbHint}>按用户的真实说法写,不要只写关键词:模型靠它们判断「这句话属不属于这一类」。</span>
|
|
679
|
+
</label>
|
|
680
|
+
</div>
|
|
681
|
+
<DialogFooter>
|
|
682
|
+
<Button variant="ghost" onClick={() => setCategoryDraft(EMPTY_CATEGORY_DRAFT)}>取消</Button>
|
|
683
|
+
<Button variant="default" disabled={categoryBusy !== null || !categoryDraft.id.trim() || !categoryDraft.label.trim()} onClick={() => void saveCategory()}><Check size={13} />保存</Button>
|
|
684
|
+
</DialogFooter>
|
|
685
|
+
</DialogContent>}
|
|
686
|
+
</Dialog>
|
|
687
|
+
|
|
688
|
+
<Dialog open={actDraft.open} onOpenChange={open => { if (!open) setActDraft(EMPTY_ACT_DRAFT) }}>
|
|
689
|
+
{actDraft.open && <DialogContent>
|
|
690
|
+
<DialogHeader><DialogTitle>{actDraft.mode === 'create' ? '新增 act' : `编辑 act · ${actDraft.label}`}</DialogTitle></DialogHeader>
|
|
691
|
+
<div className={styles.kbForm}>
|
|
692
|
+
<label className={styles.kbField}><Label>act id</Label>
|
|
693
|
+
<Input value={actDraft.id} disabled={actDraft.mode === 'edit'} spellCheck={false} placeholder="audit"
|
|
694
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setActDraft({ ...actDraft, id: event.target.value })} />
|
|
695
|
+
<span className={styles.kbHint}>小写字母 / 数字 / 下划线,创建后不可修改——它会被写进每轮意图记录。</span>
|
|
696
|
+
</label>
|
|
697
|
+
<label className={styles.kbField}><Label>显示名</Label>
|
|
698
|
+
<Input value={actDraft.label} placeholder="审计"
|
|
699
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setActDraft({ ...actDraft, label: event.target.value })} />
|
|
700
|
+
</label>
|
|
701
|
+
<label className={styles.kbField}><Label>说明(必填)</Label>
|
|
702
|
+
<textarea className={styles.kbTextarea} value={actDraft.description} spellCheck={false}
|
|
703
|
+
placeholder="要求对已有数据做合规或口径审计。"
|
|
704
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setActDraft({ ...actDraft, description: event.target.value })} />
|
|
705
|
+
<span className={styles.kbHint}>写清"这个动作是什么",并且要和相邻 act 的差别可见(例如 query 与 request)。</span>
|
|
706
|
+
</label>
|
|
707
|
+
</div>
|
|
708
|
+
<DialogFooter>
|
|
709
|
+
<Button variant="ghost" onClick={() => setActDraft(EMPTY_ACT_DRAFT)}>取消</Button>
|
|
710
|
+
<Button variant="default" disabled={categoryBusy !== null || !actDraft.id.trim() || !actDraft.label.trim()} onClick={() => void saveAct()}><Check size={13} />保存</Button>
|
|
711
|
+
</DialogFooter>
|
|
712
|
+
</DialogContent>}
|
|
713
|
+
</Dialog>
|
|
714
|
+
</div>
|
|
715
|
+
}
|