dsh-advisor-plugin 0.2.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 +210 -0
- package/cordis.patch.yml +6 -0
- package/lib/advisor-prompt.d.ts +28 -0
- package/lib/advisor-prompt.js +91 -0
- package/lib/client/AdvisorCard.d.ts +15 -0
- package/lib/client/AdvisorCard.js +60 -0
- package/lib/client/AdvisorToolRow.d.ts +40 -0
- package/lib/client/AdvisorToolRow.js +52 -0
- package/lib/client/PatrolNodeView.d.ts +18 -0
- package/lib/client/PatrolNodeView.js +57 -0
- package/lib/client/controller.d.ts +161 -0
- package/lib/client/controller.js +287 -0
- package/lib/client/index.d.ts +20 -0
- package/lib/client/index.js +841 -0
- package/lib/client/locales.d.ts +54 -0
- package/lib/client/locales.js +101 -0
- package/lib/client/patrol-chat.d.ts +78 -0
- package/lib/client/patrol-chat.js +54 -0
- package/lib/client/store.d.ts +11 -0
- package/lib/client/store.js +23 -0
- package/lib/command.d.ts +7 -0
- package/lib/command.js +52 -0
- package/lib/config.d.ts +37 -0
- package/lib/config.js +83 -0
- package/lib/emission-guard.d.ts +32 -0
- package/lib/emission-guard.js +74 -0
- package/lib/gating.d.ts +8 -0
- package/lib/gating.js +48 -0
- package/lib/history.d.ts +20 -0
- package/lib/history.js +95 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +96 -0
- package/lib/llm-call.d.ts +69 -0
- package/lib/llm-call.js +483 -0
- package/lib/patrol-event.d.ts +88 -0
- package/lib/patrol-event.js +68 -0
- package/lib/patrol.d.ts +51 -0
- package/lib/patrol.js +202 -0
- package/lib/prompt-section.d.ts +7 -0
- package/lib/prompt-section.js +16 -0
- package/lib/settings.d.ts +18 -0
- package/lib/settings.js +50 -0
- package/lib/tool.d.ts +22 -0
- package/lib/tool.js +91 -0
- package/package.json +91 -0
- package/src/advisor-prompt.ts +102 -0
- package/src/client/AdvisorCard.tsx +253 -0
- package/src/client/AdvisorToolRow.tsx +83 -0
- package/src/client/PatrolNodeView.tsx +85 -0
- package/src/client/controller.ts +399 -0
- package/src/client/index.ts +100 -0
- package/src/client/locales.ts +105 -0
- package/src/client/patrol-chat.ts +109 -0
- package/src/client/store.ts +29 -0
- package/src/command.ts +61 -0
- package/src/config.ts +113 -0
- package/src/emission-guard.ts +76 -0
- package/src/gating.ts +54 -0
- package/src/history.ts +102 -0
- package/src/index.ts +106 -0
- package/src/llm-call.ts +552 -0
- package/src/patrol-event.ts +115 -0
- package/src/patrol.ts +229 -0
- package/src/prompt-section.ts +21 -0
- package/src/settings.ts +74 -0
- package/src/tool.ts +114 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdvisorCard —— 设置页"插件"栏里的 Advisor 卡片(浏览器半)。
|
|
3
|
+
*
|
|
4
|
+
* 交互:provider 分组的模型单选列表(再次点击取消选择)+ 推理档位下拉 +
|
|
5
|
+
* 保存/放弃。数据与动作全部由 controller 通过 slot inject 面注入;
|
|
6
|
+
* 本组件纯渲染,样式内联(不依赖 css module 构建)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AdvisorCardFace, AdvisorCardState } from './controller.js'
|
|
10
|
+
import type { LocaleKey } from './locales.js'
|
|
11
|
+
|
|
12
|
+
/** 组件收到的 props:face 里的 hooks 被渲染器绑定成 use 选择器 hook */
|
|
13
|
+
export type AdvisorCardProps = Omit<AdvisorCardFace, 'hooks'> & {
|
|
14
|
+
useAdvisorCard: <S>(selector: (s: AdvisorCardState) => S) => S
|
|
15
|
+
t: (key: LocaleKey) => string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 巡逻模式区块(自动检测跑偏) */
|
|
19
|
+
function PatrolSection(props: AdvisorCardProps & {
|
|
20
|
+
enabled: boolean
|
|
21
|
+
everySteps: number
|
|
22
|
+
immuneTurns: number
|
|
23
|
+
investigate: boolean
|
|
24
|
+
disabled: boolean
|
|
25
|
+
}) {
|
|
26
|
+
const { t } = props
|
|
27
|
+
return (
|
|
28
|
+
<div style={{ ...style.card, background: 'rgba(128,128,128,.06)' }}>
|
|
29
|
+
<h4 style={{ ...style.title, fontSize: 13 }}>{t('patrolTitle')}</h4>
|
|
30
|
+
<p style={style.description}>{t('patrolDescription')}</p>
|
|
31
|
+
<div style={style.effortRow}>
|
|
32
|
+
<span>{t('patrolToggle')}</span>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
role="switch"
|
|
36
|
+
aria-checked={props.enabled}
|
|
37
|
+
aria-label={t('patrolToggle')}
|
|
38
|
+
disabled={props.disabled}
|
|
39
|
+
onClick={props.togglePatrol}
|
|
40
|
+
style={{
|
|
41
|
+
...style.button,
|
|
42
|
+
width: 40, height: 22, borderRadius: 11, padding: 0, position: 'relative',
|
|
43
|
+
background: props.enabled ? '#16a34a' : 'rgba(128,128,128,.4)', border: 'none',
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<span style={{
|
|
47
|
+
position: 'absolute', top: 2, left: props.enabled ? 20 : 2,
|
|
48
|
+
width: 18, height: 18, borderRadius: 9, background: '#fff', transition: 'left .15s',
|
|
49
|
+
}} />
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
{props.enabled
|
|
53
|
+
? (
|
|
54
|
+
<div>
|
|
55
|
+
<div style={style.effortRow}>
|
|
56
|
+
<label htmlFor="advisor-patrol-steps">{t('patrolEverySteps')}</label>
|
|
57
|
+
<input
|
|
58
|
+
id="advisor-patrol-steps"
|
|
59
|
+
type="number"
|
|
60
|
+
min={2}
|
|
61
|
+
max={500}
|
|
62
|
+
value={props.everySteps}
|
|
63
|
+
disabled={props.disabled}
|
|
64
|
+
onChange={(event) => { props.setPatrolEverySteps(Number(event.target.value)) }}
|
|
65
|
+
style={{ width: 64 }}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div style={style.effortRow}>
|
|
69
|
+
<label htmlFor="advisor-patrol-immune">{t('patrolImmuneTurns')}</label>
|
|
70
|
+
<input
|
|
71
|
+
id="advisor-patrol-immune"
|
|
72
|
+
type="number"
|
|
73
|
+
min={0}
|
|
74
|
+
max={20}
|
|
75
|
+
value={props.immuneTurns}
|
|
76
|
+
disabled={props.disabled}
|
|
77
|
+
onChange={(event) => { props.setPatrolImmuneTurns(Number(event.target.value)) }}
|
|
78
|
+
style={{ width: 64 }}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div style={{ ...style.effortRow, marginTop: 8 }}>
|
|
82
|
+
<span>{t('investigateToggle')}</span>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
role="switch"
|
|
86
|
+
aria-checked={props.investigate}
|
|
87
|
+
aria-label={t('investigateToggle')}
|
|
88
|
+
disabled={props.disabled}
|
|
89
|
+
onClick={props.toggleInvestigate}
|
|
90
|
+
style={{
|
|
91
|
+
...style.button,
|
|
92
|
+
width: 40, height: 22, borderRadius: 11, padding: 0, position: 'relative',
|
|
93
|
+
background: props.investigate ? '#16a34a' : 'rgba(128,128,128,.4)', border: 'none',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<span style={{
|
|
97
|
+
position: 'absolute', top: 2, left: props.investigate ? 20 : 2,
|
|
98
|
+
width: 18, height: 18, borderRadius: 9, background: '#fff', transition: 'left .15s',
|
|
99
|
+
}} />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<p style={{ ...style.notice, marginTop: -2 }}>{t('investigateHint')}</p>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
: null}
|
|
106
|
+
<p style={{ ...style.notice, marginTop: -2 }}>{t('patrolEveryStepsHint')}</p>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const EFFORT_OPTIONS = ['', 'off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
|
|
112
|
+
|
|
113
|
+
// 内联样式(跟随设置页朴素风格,避免 css module 构建依赖)
|
|
114
|
+
const style = {
|
|
115
|
+
card: { border: '1px solid rgba(128,128,128,.35)', borderRadius: 8, padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 10 } as const,
|
|
116
|
+
title: { fontSize: 14, fontWeight: 600, margin: 0 } as const,
|
|
117
|
+
description: { fontSize: 12, opacity: 0.75, margin: 0, lineHeight: 1.6 } as const,
|
|
118
|
+
current: { fontSize: 12, margin: 0 } as const,
|
|
119
|
+
groupTitle: { fontSize: 12, fontWeight: 600, opacity: 0.85, margin: '8px 0 2px' } as const,
|
|
120
|
+
row: { display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0', fontSize: 13 } as const,
|
|
121
|
+
modelName: { display: 'flex', flexDirection: 'column' } as const,
|
|
122
|
+
route: { fontSize: 11, opacity: 0.65 } as const,
|
|
123
|
+
badge: { fontSize: 11, color: '#b45309' } as const,
|
|
124
|
+
notice: { fontSize: 12, opacity: 0.7, margin: 0 } as const,
|
|
125
|
+
error: { fontSize: 12, color: '#b91c1c', display: 'flex', gap: 8, alignItems: 'center' } as const,
|
|
126
|
+
effortRow: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, marginTop: 6 } as const,
|
|
127
|
+
actions: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 4 } as const,
|
|
128
|
+
button: { fontSize: 13, padding: '4px 14px', borderRadius: 6, cursor: 'pointer' } as const,
|
|
129
|
+
primary: { background: '#2563eb', color: '#fff', border: 'none' } as const,
|
|
130
|
+
secondary: { background: 'transparent', border: '1px solid rgba(128,128,128,.5)' } as const,
|
|
131
|
+
status: { fontSize: 12 } as const,
|
|
132
|
+
list: { maxHeight: 260, overflowY: 'auto', border: '1px solid rgba(128,128,128,.25)', borderRadius: 6, padding: '4px 10px' } as const,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function AdvisorCard(props: AdvisorCardProps) {
|
|
136
|
+
const state = props.useAdvisorCard(s => s)
|
|
137
|
+
// 档位选项:目录声明了支持面 → 只列支持的;未声明 → 全档位(host 自动降级兜底)
|
|
138
|
+
const effortOptions: readonly string[] = state.efforts.known
|
|
139
|
+
? ['', ...state.efforts.levels]
|
|
140
|
+
: EFFORT_OPTIONS
|
|
141
|
+
const disabled = !state.writable || state.saving
|
|
142
|
+
const armed = state.effective.provider !== '' && state.effective.model !== ''
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div style={style.card}>
|
|
146
|
+
<h3 style={style.title}>{props.t('title')}</h3>
|
|
147
|
+
<p style={style.description}>{props.t('description')}</p>
|
|
148
|
+
|
|
149
|
+
<p style={style.current}>
|
|
150
|
+
{props.t('armed')}:
|
|
151
|
+
{armed
|
|
152
|
+
? <strong>{`${state.effective.provider}/${state.effective.model}${state.effective.effort === '' ? '' : ` (${state.effective.effort})`}`}</strong>
|
|
153
|
+
: props.t('offCurrent')}
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
{!state.writable ? <p style={style.notice}>{props.t('readonly')}</p> : null}
|
|
157
|
+
|
|
158
|
+
{state.catalogStatus === 'loading' ? <p style={style.notice}>{props.t('loading')}</p> : null}
|
|
159
|
+
{state.catalogStatus === 'error'
|
|
160
|
+
? (
|
|
161
|
+
<div style={style.error}>
|
|
162
|
+
<span>{props.t('loadFailed')}</span>
|
|
163
|
+
<button type="button" style={{ ...style.button, ...style.secondary }} disabled={disabled} onClick={props.retryCatalog}>
|
|
164
|
+
{props.t('retry')}
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
: null}
|
|
169
|
+
{state.catalogPartial ? <p style={style.notice}>{props.t('partial')}</p> : null}
|
|
170
|
+
|
|
171
|
+
{state.groups.length > 0
|
|
172
|
+
? (
|
|
173
|
+
<div>
|
|
174
|
+
<p style={style.notice}>{props.t('choose')}</p>
|
|
175
|
+
<div style={style.list}>
|
|
176
|
+
{state.groups.map(group => (
|
|
177
|
+
<div key={group.id}>
|
|
178
|
+
<div style={style.groupTitle}>{group.name}</div>
|
|
179
|
+
{group.candidates.map(candidate => (
|
|
180
|
+
<label key={candidate.key} style={style.row}>
|
|
181
|
+
<input
|
|
182
|
+
type="radio"
|
|
183
|
+
name="advisor-route"
|
|
184
|
+
checked={candidate.selected}
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
onChange={() => { props.pickRoute(candidate.key) }}
|
|
187
|
+
/>
|
|
188
|
+
<span style={style.modelName}>
|
|
189
|
+
<span>{candidate.modelName}</span>
|
|
190
|
+
<span style={style.route}>{`${candidate.provider}/${candidate.model}`}</span>
|
|
191
|
+
</span>
|
|
192
|
+
{!candidate.available ? <span style={style.badge}>{props.t('unavailable')}</span> : null}
|
|
193
|
+
</label>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
: state.catalogStatus === 'ready' ? <p style={style.notice}>{props.t('empty')}</p> : null}
|
|
201
|
+
|
|
202
|
+
<div style={style.effortRow}>
|
|
203
|
+
<label htmlFor="advisor-effort">{props.t('effort')}</label>
|
|
204
|
+
<select
|
|
205
|
+
id="advisor-effort"
|
|
206
|
+
value={state.effective.effort}
|
|
207
|
+
disabled={disabled}
|
|
208
|
+
onChange={(event) => { props.setEffort(event.target.value) }}
|
|
209
|
+
>
|
|
210
|
+
{/* 已声明支持面的模型只列支持的档位;未声明的列全档位(host 侧自动降级兜底) */}
|
|
211
|
+
{effortOptions.map(option => (
|
|
212
|
+
<option key={option} value={option}>
|
|
213
|
+
{option === '' ? props.t('effortDefault') : option}
|
|
214
|
+
</option>
|
|
215
|
+
))}
|
|
216
|
+
</select>
|
|
217
|
+
</div>
|
|
218
|
+
<p style={{ ...style.notice, marginTop: -4 }}>
|
|
219
|
+
{state.efforts.known ? props.t('effortHintKnown') : props.t('effortHint')}
|
|
220
|
+
</p>
|
|
221
|
+
|
|
222
|
+
<PatrolSection
|
|
223
|
+
{...props}
|
|
224
|
+
enabled={state.patrol.enabled}
|
|
225
|
+
everySteps={state.patrol.everySteps}
|
|
226
|
+
immuneTurns={state.patrol.immuneTurns}
|
|
227
|
+
investigate={state.patrol.investigate}
|
|
228
|
+
disabled={disabled}
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
<div style={style.actions}>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
style={{ ...style.button, ...style.primary }}
|
|
235
|
+
disabled={disabled || !state.dirty}
|
|
236
|
+
onClick={props.save}
|
|
237
|
+
>
|
|
238
|
+
{props.t('save')}
|
|
239
|
+
</button>
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
style={{ ...style.button, ...style.secondary }}
|
|
243
|
+
disabled={disabled || !state.dirty}
|
|
244
|
+
onClick={props.discard}
|
|
245
|
+
>
|
|
246
|
+
{props.t('discard')}
|
|
247
|
+
</button>
|
|
248
|
+
{state.saving ? <span style={style.status}>{props.t('saving')}</span> : null}
|
|
249
|
+
{state.failed ? <span style={{ ...style.status, color: '#b91c1c' }}>{props.t('saveFailed')}</span> : null}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdvisorToolRow —— `advisor` 工具在会话消息流里的专属行(keyed
|
|
3
|
+
* `tool.call.toolview` 槽,官方给插件自有工具预留的定制点)。
|
|
4
|
+
*
|
|
5
|
+
* 折叠态:🧭 标题 + 进行中/完成/失败状态 + 审查模型路由;
|
|
6
|
+
* 展开态:审查建议全文(或错误说明)——用户点开即见"真的调了
|
|
7
|
+
* advisor、建议是什么"。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState } from 'react'
|
|
11
|
+
import type { LocaleKey } from './locales.js'
|
|
12
|
+
|
|
13
|
+
/** 会话工具块的最小结构(RunningToolCall / settled result 的并集子集) */
|
|
14
|
+
interface AdvisorBlock {
|
|
15
|
+
kind?: unknown
|
|
16
|
+
callId?: unknown
|
|
17
|
+
call?: { argsRaw?: unknown } | undefined
|
|
18
|
+
argsRaw?: unknown
|
|
19
|
+
content?: readonly { type?: unknown, text?: unknown }[] | undefined
|
|
20
|
+
isError?: boolean | undefined
|
|
21
|
+
error?: { name?: unknown, code?: unknown, message?: unknown } | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AdvisorToolRowProps = {
|
|
25
|
+
callId?: string
|
|
26
|
+
toolName?: string
|
|
27
|
+
block?: AdvisorBlock
|
|
28
|
+
cwd?: string
|
|
29
|
+
home?: string
|
|
30
|
+
openFile?: (path: string) => void
|
|
31
|
+
inspect?: () => void
|
|
32
|
+
t?: (key: LocaleKey) => string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const style = {
|
|
36
|
+
row: { display: 'flex', flexDirection: 'column', gap: 4, margin: '6px 0', border: '1px solid rgba(37,99,235,.35)', borderRadius: 8, overflow: 'hidden' } as const,
|
|
37
|
+
head: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', cursor: 'pointer', background: 'rgba(37,99,235,.08)', border: 'none', font: 'inherit', textAlign: 'left', width: '100%' } as const,
|
|
38
|
+
title: { fontWeight: 600, fontSize: 13, color: '#1d4ed8' } as const,
|
|
39
|
+
state: { fontSize: 12, opacity: 0.75 } as const,
|
|
40
|
+
badge: { fontSize: 11, padding: '1px 8px', borderRadius: 8, flex: 'none' } as const,
|
|
41
|
+
badgeRun: { background: 'rgba(37,99,235,.15)', color: '#1d4ed8' } as const,
|
|
42
|
+
badgeOk: { background: 'rgba(22,163,74,.15)', color: '#15803d' } as const,
|
|
43
|
+
badgeErr: { background: 'rgba(185,28,28,.12)', color: '#b91c1c' } as const,
|
|
44
|
+
body: { padding: '8px 12px', fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', borderTop: '1px solid rgba(37,99,235,.2)', maxHeight: 320, overflowY: 'auto' } as const,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function blockOutput(block: AdvisorBlock | undefined): { text: string, isError: boolean } {
|
|
48
|
+
if (block === undefined) return { text: '', isError: false }
|
|
49
|
+
const parts: string[] = []
|
|
50
|
+
for (const b of block.content ?? []) {
|
|
51
|
+
if (b?.type === 'text' && typeof b.text === 'string') parts.push(b.text)
|
|
52
|
+
else if (b !== undefined && b !== null) parts.push(JSON.stringify(b, null, 2))
|
|
53
|
+
}
|
|
54
|
+
if (parts.length === 0 && block.error !== undefined) {
|
|
55
|
+
const e = block.error
|
|
56
|
+
parts.push(`${typeof e.name === 'string' ? e.name : 'Error'}: ${typeof e.code === 'string' ? e.code : ''} ${typeof e.message === 'string' ? e.message : ''}`.trim())
|
|
57
|
+
}
|
|
58
|
+
return { text: parts.join('\n').trim(), isError: block.isError === true }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function AdvisorToolRow(props: AdvisorToolRowProps) {
|
|
62
|
+
const t = props.t ?? ((key: LocaleKey) => key)
|
|
63
|
+
const [expanded, setExpanded] = useState(false)
|
|
64
|
+
const block = props.block ?? {}
|
|
65
|
+
const done = block.kind !== undefined
|
|
66
|
+
const { text, isError } = done ? blockOutput(block) : { text: '', isError: false }
|
|
67
|
+
const badge = !done
|
|
68
|
+
? { label: t('toolRunning'), css: style.badgeRun }
|
|
69
|
+
: isError
|
|
70
|
+
? { label: t('toolFailed'), css: style.badgeErr }
|
|
71
|
+
: { label: t('toolDone'), css: style.badgeOk }
|
|
72
|
+
const canExpand = done && text !== ''
|
|
73
|
+
return (
|
|
74
|
+
<div style={style.row}>
|
|
75
|
+
<button type="button" style={style.head} onClick={() => { if (canExpand) setExpanded(v => !v) }}>
|
|
76
|
+
<span style={style.title}>🧭 Advisor {t('toolTitle')}</span>
|
|
77
|
+
<span style={{ ...style.badge, ...badge.css }}>{badge.label}</span>
|
|
78
|
+
<span style={style.state}>{!done ? t('toolConsulting') : canExpand ? (expanded ? t('toolCollapse') : t('toolExpand')) : ''}</span>
|
|
79
|
+
</button>
|
|
80
|
+
{expanded && canExpand ? <div style={style.body}>{text}</div> : null}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatrolNodeView —— 巡逻裁决在会话消息流里的 🧭 卡片(keyed
|
|
3
|
+
* `conversation.chat.node` 槽,key 'advisor-patrol'——该 kind 只有本
|
|
4
|
+
* 插件的 definition 会物化,槽格子无官方占位)。
|
|
5
|
+
*
|
|
6
|
+
* 三种形态:ON-TRACK 低调灰卡(默认收起,视野完整但不打扰)、
|
|
7
|
+
* 纠偏琥珀卡、叫停红卡。后两者展开即裁决全文——用户点开就见
|
|
8
|
+
* "巡逻真的判了什么、为什么"。unclear 不写事件,天然不出卡片。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
import type { PatrolNodeState } from './patrol-chat.js'
|
|
13
|
+
import type { LocaleKey } from './locales.js'
|
|
14
|
+
|
|
15
|
+
export type PatrolNodeViewProps = {
|
|
16
|
+
node?: { data?: PatrolNodeState } | undefined
|
|
17
|
+
t?: (key: LocaleKey) => string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 卡片配色(正常灰 / 纠偏琥珀 / 叫停红) */
|
|
21
|
+
interface Accent {
|
|
22
|
+
readonly border: string
|
|
23
|
+
readonly head: string
|
|
24
|
+
readonly title: string
|
|
25
|
+
readonly badge: { background: string, color: string }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ACCENT_ON_TRACK: Accent = {
|
|
29
|
+
border: 'rgba(107,114,128,.35)',
|
|
30
|
+
head: 'rgba(107,114,128,.06)',
|
|
31
|
+
title: '#4b5563',
|
|
32
|
+
badge: { background: 'rgba(107,114,128,.14)', color: '#4b5563' },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ACCENT_CORRECTION: Accent = {
|
|
36
|
+
border: 'rgba(217,119,6,.45)',
|
|
37
|
+
head: 'rgba(217,119,6,.09)',
|
|
38
|
+
title: '#b45309',
|
|
39
|
+
badge: { background: 'rgba(217,119,6,.16)', color: '#b45309' },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ACCENT_STOP: Accent = {
|
|
43
|
+
border: 'rgba(185,28,28,.4)',
|
|
44
|
+
head: 'rgba(185,28,28,.07)',
|
|
45
|
+
title: '#b91c1c',
|
|
46
|
+
badge: { background: 'rgba(185,28,28,.12)', color: '#b91c1c' },
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const style = {
|
|
50
|
+
row: (accent: Accent) => ({ display: 'flex', flexDirection: 'column', gap: 4, margin: '6px 0', border: `1px solid ${accent.border}`, borderRadius: 8, overflow: 'hidden' } as const),
|
|
51
|
+
head: (accent: Accent) => ({ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', cursor: 'pointer', background: accent.head, border: 'none', font: 'inherit', textAlign: 'left', width: '100%' } as const),
|
|
52
|
+
title: (accent: Accent) => ({ fontWeight: 600, fontSize: 13, color: accent.title } as const),
|
|
53
|
+
badge: { fontSize: 11, padding: '1px 8px', borderRadius: 8, flex: 'none' } as const,
|
|
54
|
+
meta: { fontSize: 12, opacity: 0.7 } as const,
|
|
55
|
+
body: { padding: '8px 12px', fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', borderTop: '1px solid rgba(120,113,108,.25)', maxHeight: 320, overflowY: 'auto' } as const,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function PatrolNodeView(props: PatrolNodeViewProps) {
|
|
59
|
+
const t = props.t ?? ((key: LocaleKey) => key)
|
|
60
|
+
const [expanded, setExpanded] = useState(false)
|
|
61
|
+
const data = props.node?.data
|
|
62
|
+
if (data === undefined) return null
|
|
63
|
+
const onTrack = data.verdict === 'on-track'
|
|
64
|
+
const accent = onTrack ? ACCENT_ON_TRACK : data.verdict === 'stop' ? ACCENT_STOP : ACCENT_CORRECTION
|
|
65
|
+
const badge = onTrack ? t('patrolVerdictOnTrack') : data.verdict === 'stop' ? t('patrolVerdictStop') : t('patrolVerdictCorrection')
|
|
66
|
+
const title = onTrack ? t('patrolCheckTitle') : t('patrolCardTitle')
|
|
67
|
+
const canExpand = data.detail.trim() !== ''
|
|
68
|
+
const fmtTok = (n: number): string => (n >= 1000 ? `${Math.round(n / 100) / 10}k` : String(n))
|
|
69
|
+
const usageText = data.usage === undefined
|
|
70
|
+
? ''
|
|
71
|
+
: ` · ↓${fmtTok(data.usage.input)}${data.usage.cacheRead !== undefined && data.usage.cacheRead > 0 ? `(缓存${fmtTok(data.usage.cacheRead)})` : ''} ↑${fmtTok(data.usage.output)} tok`
|
|
72
|
+
// 冷却降级标记:concern 在 immuneTurns 窗口内只出了卡片、未注入执行模型
|
|
73
|
+
const downgradedTag = data.downgraded === true ? ` · ${t('patrolDowngradedTag')}` : ''
|
|
74
|
+
return (
|
|
75
|
+
<div style={style.row(accent)}>
|
|
76
|
+
<button type="button" style={style.head(accent)} onClick={() => { if (canExpand) setExpanded(v => !v) }}>
|
|
77
|
+
<span style={style.title(accent)}>{`🧭 Advisor ${title}`}</span>
|
|
78
|
+
<span style={{ ...style.badge, ...accent.badge }}>{badge}</span>
|
|
79
|
+
<span style={style.meta}>{`${data.reviewer} · ${t('patrolCardStepPrefix')}${data.step}${t('patrolCardStepSuffix')} · ${Math.round(data.patrolMs / 100) / 10}s${usageText}${downgradedTag}`}</span>
|
|
80
|
+
<span style={style.meta}>{canExpand ? (expanded ? t('patrolCollapse') : t('patrolExpand')) : ''}</span>
|
|
81
|
+
</button>
|
|
82
|
+
{expanded && canExpand ? <div style={style.body}>{data.detail}</div> : null}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|