dsh-advisor-plugin 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,8 @@
20
20
 
21
21
  ## 特性
22
22
 
23
+ - **总开关**:设置页一键启用/关闭;关闭时不注册 advisor 工具与升级守则,已保存的模型配置保留。
24
+ - **可折叠配置**:模型与推理档位、巡逻模式两个区块可分别收起/展开。
23
25
  - **零参数 `advisor()` 工具**:调用即自动转发整段会话,模型不需要组织上下文。
24
26
  - **plan / correction / stop 契约**:审查模型只做判断,不碰代码、不直接改状态。
25
27
  - **compaction 感知**:转发的是 `session.deriveMessages()` 的真实模型可见面,而不是过期原始历史。
@@ -116,6 +118,7 @@ dsh plugin --profile web add github:leeyoung1/dsh-advisor-plugin
116
118
 
117
119
  ```yaml
118
120
  advisor:
121
+ enabled: true
119
122
  provider: example-provider
120
123
  model: reviewer-model
121
124
  effort: high
@@ -166,10 +169,17 @@ agent/request ──每 N 步──▶ 快照 ──▶ 审查模型
166
169
 
167
170
  ## 依赖要求
168
171
 
169
- - Node.js >= 20
172
+ - Node.js `^22.19` 或 `>=24`
170
173
  - DeepSeek Harness(web profile)
171
174
  - 已配置至少一个可用的 provider / model 作为审查模型
172
175
 
176
+ ## 兼容性
177
+
178
+ - **DSH 版本**:peer 范围覆盖 `0.1.0-rc.6`、`0.1.1-rc.2`、`0.1.2-rc.1`、`0.1.3-alpha.1`;已在 `0.1.1-rc.2` 上实机验证。
179
+ - **配置向后兼容**:旧配置没有 `enabled` 字段时默认 `true`,不影响升级。
180
+ - **关闭总开关**:`enabled: false` 时保留已保存的模型配置,但不注册工具与升级守则。
181
+ - **安装方式**:请使用 `dsh plugin --profile web add dsh-advisor-plugin`;裸 `npm install` 会撞 DSH 官方包的 peer 依赖,这不是插件本身的问题。
182
+
173
183
  开发验证:
174
184
 
175
185
  ```bash
@@ -1,9 +1,8 @@
1
1
  /**
2
- * AdvisorCard —— 设置页"插件"栏里的 Advisor 卡片(浏览器半)。
2
+ * AdvisorCard —— 设置页“插件”栏里的 Advisor 卡片(浏览器半)。
3
3
  *
4
- * 交互:provider 分组的模型单选列表(再次点击取消选择)+ 推理档位下拉 +
5
- * 保存/放弃。数据与动作全部由 controller 通过 slot inject 面注入;
6
- * 本组件纯渲染,样式内联(不依赖 css module 构建)。
4
+ * 交互:总开关 + 可折叠的模型/档位区块与巡逻区块;数据与动作全部由
5
+ * controller 通过 slot inject 面注入;本组件纯渲染,样式内联。
7
6
  */
8
7
  import type { AdvisorCardFace, AdvisorCardState } from './controller.js';
9
8
  import type { LocaleKey } from './locales.js';
@@ -1,23 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * AdvisorCard —— 设置页“插件”栏里的 Advisor 卡片(浏览器半)。
4
+ *
5
+ * 交互:总开关 + 可折叠的模型/档位区块与巡逻区块;数据与动作全部由
6
+ * controller 通过 slot inject 面注入;本组件纯渲染,样式内联。
7
+ */
8
+ import { useState } from 'react';
9
+ /** 通用开关 */
10
+ function ToggleSwitch(props) {
11
+ return (_jsx("button", { type: "button", role: "switch", "aria-checked": props.checked, "aria-label": props.label, disabled: props.disabled, onClick: props.onToggle, style: {
12
+ ...style.button,
13
+ width: 40, height: 22, borderRadius: 11, padding: 0, position: 'relative',
14
+ background: props.checked ? '#16a34a' : 'rgba(128,128,128,.4)', border: 'none',
15
+ }, children: _jsx("span", { style: {
16
+ position: 'absolute', top: 2, left: props.checked ? 20 : 2,
17
+ width: 18, height: 18, borderRadius: 9, background: '#fff', transition: 'left .15s',
18
+ } }) }));
19
+ }
20
+ /** 可折叠区块:点击标题行展开/收起 */
21
+ function Section(props) {
22
+ const [open, setOpen] = useState(props.defaultOpen ?? false);
23
+ return (_jsxs("div", { style: style.section, children: [_jsxs("button", { type: "button", style: style.sectionHead, "aria-expanded": open, onClick: () => setOpen(v => !v), children: [_jsx("span", { style: style.sectionTitle, children: props.title }), props.subtitle === undefined ? null : _jsx("span", { style: style.sectionSubtitle, children: props.subtitle }), _jsx("span", { style: style.sectionState, children: open ? props.t('sectionCollapse') : props.t('sectionExpand') })] }), open ? _jsx("div", { style: style.sectionBody, children: props.children }) : null] }));
24
+ }
2
25
  /** 巡逻模式区块(自动检测跑偏) */
3
26
  function PatrolSection(props) {
4
27
  const { t } = props;
5
- return (_jsxs("div", { style: { ...style.card, background: 'rgba(128,128,128,.06)' }, children: [_jsx("h4", { style: { ...style.title, fontSize: 13 }, children: t('patrolTitle') }), _jsx("p", { style: style.description, children: t('patrolDescription') }), _jsxs("div", { style: style.effortRow, children: [_jsx("span", { children: t('patrolToggle') }), _jsx("button", { type: "button", role: "switch", "aria-checked": props.enabled, "aria-label": t('patrolToggle'), disabled: props.disabled, onClick: props.togglePatrol, style: {
6
- ...style.button,
7
- width: 40, height: 22, borderRadius: 11, padding: 0, position: 'relative',
8
- background: props.enabled ? '#16a34a' : 'rgba(128,128,128,.4)', border: 'none',
9
- }, children: _jsx("span", { style: {
10
- position: 'absolute', top: 2, left: props.enabled ? 20 : 2,
11
- width: 18, height: 18, borderRadius: 9, background: '#fff', transition: 'left .15s',
12
- } }) })] }), props.enabled
13
- ? (_jsxs("div", { children: [_jsxs("div", { style: style.effortRow, children: [_jsx("label", { htmlFor: "advisor-patrol-steps", children: t('patrolEverySteps') }), _jsx("input", { id: "advisor-patrol-steps", type: "number", min: 2, max: 500, value: props.everySteps, disabled: props.disabled, onChange: (event) => { props.setPatrolEverySteps(Number(event.target.value)); }, style: { width: 64 } })] }), _jsxs("div", { style: style.effortRow, children: [_jsx("label", { htmlFor: "advisor-patrol-immune", children: t('patrolImmuneTurns') }), _jsx("input", { id: "advisor-patrol-immune", type: "number", min: 0, max: 20, value: props.immuneTurns, disabled: props.disabled, onChange: (event) => { props.setPatrolImmuneTurns(Number(event.target.value)); }, style: { width: 64 } })] }), _jsxs("div", { style: { ...style.effortRow, marginTop: 8 }, children: [_jsx("span", { children: t('investigateToggle') }), _jsx("button", { type: "button", role: "switch", "aria-checked": props.investigate, "aria-label": t('investigateToggle'), disabled: props.disabled, onClick: props.toggleInvestigate, style: {
14
- ...style.button,
15
- width: 40, height: 22, borderRadius: 11, padding: 0, position: 'relative',
16
- background: props.investigate ? '#16a34a' : 'rgba(128,128,128,.4)', border: 'none',
17
- }, children: _jsx("span", { style: {
18
- position: 'absolute', top: 2, left: props.investigate ? 20 : 2,
19
- width: 18, height: 18, borderRadius: 9, background: '#fff', transition: 'left .15s',
20
- } }) })] }), _jsx("p", { style: { ...style.notice, marginTop: -2 }, children: t('investigateHint') })] }))
28
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 10 }, children: [_jsx("p", { style: style.description, children: t('patrolDescription') }), _jsxs("div", { style: style.effortRow, children: [_jsx("span", { children: t('patrolToggle') }), _jsx(ToggleSwitch, { checked: props.enabled, disabled: props.disabled, label: t('patrolToggle'), onToggle: props.togglePatrol })] }), props.enabled
29
+ ? (_jsxs("div", { children: [_jsxs("div", { style: style.effortRow, children: [_jsx("label", { htmlFor: "advisor-patrol-steps", children: t('patrolEverySteps') }), _jsx("input", { id: "advisor-patrol-steps", type: "number", min: 2, max: 500, value: props.everySteps, disabled: props.disabled, onChange: (event) => { props.setPatrolEverySteps(Number(event.target.value)); }, style: { width: 64 } })] }), _jsxs("div", { style: style.effortRow, children: [_jsx("label", { htmlFor: "advisor-patrol-immune", children: t('patrolImmuneTurns') }), _jsx("input", { id: "advisor-patrol-immune", type: "number", min: 0, max: 20, value: props.immuneTurns, disabled: props.disabled, onChange: (event) => { props.setPatrolImmuneTurns(Number(event.target.value)); }, style: { width: 64 } })] }), _jsxs("div", { style: { ...style.effortRow, marginTop: 8 }, children: [_jsx("span", { children: t('investigateToggle') }), _jsx(ToggleSwitch, { checked: props.investigate, disabled: props.disabled, label: t('investigateToggle'), onToggle: props.toggleInvestigate })] }), _jsx("p", { style: { ...style.notice, marginTop: -2 }, children: t('investigateHint') })] }))
21
30
  : null, _jsx("p", { style: { ...style.notice, marginTop: -2 }, children: t('patrolEveryStepsHint') })] }));
22
31
  }
23
32
  const EFFORT_OPTIONS = ['', 'off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
@@ -41,20 +50,29 @@ const style = {
41
50
  secondary: { background: 'transparent', border: '1px solid rgba(128,128,128,.5)' },
42
51
  status: { fontSize: 12 },
43
52
  list: { maxHeight: 260, overflowY: 'auto', border: '1px solid rgba(128,128,128,.25)', borderRadius: 6, padding: '4px 10px' },
53
+ switchRow: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, marginTop: 2 },
54
+ switchLabel: { fontWeight: 600 },
55
+ section: { border: '1px solid rgba(128,128,128,.25)', borderRadius: 8, overflow: 'hidden' },
56
+ sectionHead: { display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 10px', background: 'rgba(128,128,128,.06)', border: 'none', font: 'inherit', textAlign: 'left', cursor: 'pointer' },
57
+ sectionTitle: { fontSize: 13, fontWeight: 600 },
58
+ sectionSubtitle: { fontSize: 11, opacity: 0.65, flex: 1 },
59
+ sectionState: { fontSize: 11, opacity: 0.65, marginLeft: 'auto' },
60
+ sectionBody: { padding: 10, display: 'flex', flexDirection: 'column', gap: 8 },
44
61
  };
45
62
  export function AdvisorCard(props) {
46
63
  const state = props.useAdvisorCard(s => s);
64
+ const enabled = state.enabled;
47
65
  // 档位选项:目录声明了支持面 → 只列支持的;未声明 → 全档位(host 自动降级兜底)
48
66
  const effortOptions = state.efforts.known
49
67
  ? ['', ...state.efforts.levels]
50
68
  : EFFORT_OPTIONS;
51
69
  const disabled = !state.writable || state.saving;
52
- const armed = state.effective.provider !== '' && state.effective.model !== '';
53
- return (_jsxs("div", { style: style.card, children: [_jsx("h3", { style: style.title, children: props.t('title') }), _jsx("p", { style: style.description, children: props.t('description') }), _jsxs("p", { style: style.current, children: [props.t('armed'), "\uFF1A", armed
70
+ const armed = enabled && state.effective.provider !== '' && state.effective.model !== '';
71
+ return (_jsxs("div", { style: style.card, children: [_jsx("h3", { style: style.title, children: props.t('title') }), _jsx("p", { style: style.description, children: props.t('description') }), _jsxs("div", { style: style.switchRow, children: [_jsx("span", { style: style.switchLabel, children: props.t('enabledToggle') }), _jsx(ToggleSwitch, { checked: enabled, disabled: disabled, label: props.t('enabledToggle'), onToggle: props.toggleEnabled })] }), _jsx("p", { style: style.notice, children: enabled ? props.t('enabledHintOn') : props.t('enabledHintOff') }), _jsxs("p", { style: style.current, children: [props.t('armed'), "\uFF1A", armed
54
72
  ? _jsx("strong", { children: `${state.effective.provider}/${state.effective.model}${state.effective.effort === '' ? '' : ` (${state.effective.effort})`}` })
55
- : props.t('offCurrent')] }), !state.writable ? _jsx("p", { style: style.notice, children: props.t('readonly') }) : null, state.catalogStatus === 'loading' ? _jsx("p", { style: style.notice, children: props.t('loading') }) : null, state.catalogStatus === 'error'
56
- ? (_jsxs("div", { style: style.error, children: [_jsx("span", { children: props.t('loadFailed') }), _jsx("button", { type: "button", style: { ...style.button, ...style.secondary }, disabled: disabled, onClick: props.retryCatalog, children: props.t('retry') })] }))
57
- : null, state.catalogPartial ? _jsx("p", { style: style.notice, children: props.t('partial') }) : null, state.groups.length > 0
58
- ? (_jsxs("div", { children: [_jsx("p", { style: style.notice, children: props.t('choose') }), _jsx("div", { style: style.list, children: state.groups.map(group => (_jsxs("div", { children: [_jsx("div", { style: style.groupTitle, children: group.name }), group.candidates.map(candidate => (_jsxs("label", { style: style.row, children: [_jsx("input", { type: "radio", name: "advisor-route", checked: candidate.selected, disabled: disabled, onChange: () => { props.pickRoute(candidate.key); } }), _jsxs("span", { style: style.modelName, children: [_jsx("span", { children: candidate.modelName }), _jsx("span", { style: style.route, children: `${candidate.provider}/${candidate.model}` })] }), !candidate.available ? _jsx("span", { style: style.badge, children: props.t('unavailable') }) : null] }, candidate.key)))] }, group.id))) })] }))
59
- : state.catalogStatus === 'ready' ? _jsx("p", { style: style.notice, children: props.t('empty') }) : null, _jsxs("div", { style: style.effortRow, children: [_jsx("label", { htmlFor: "advisor-effort", children: props.t('effort') }), _jsx("select", { id: "advisor-effort", value: state.effective.effort, disabled: disabled, onChange: (event) => { props.setEffort(event.target.value); }, children: effortOptions.map(option => (_jsx("option", { value: option, children: option === '' ? props.t('effortDefault') : option }, option))) })] }), _jsx("p", { style: { ...style.notice, marginTop: -4 }, children: state.efforts.known ? props.t('effortHintKnown') : props.t('effortHint') }), _jsx(PatrolSection, { ...props, enabled: state.patrol.enabled, everySteps: state.patrol.everySteps, immuneTurns: state.patrol.immuneTurns, investigate: state.patrol.investigate, disabled: disabled }), _jsxs("div", { style: style.actions, children: [_jsx("button", { type: "button", style: { ...style.button, ...style.primary }, disabled: disabled || !state.dirty, onClick: props.save, children: props.t('save') }), _jsx("button", { type: "button", style: { ...style.button, ...style.secondary }, disabled: disabled || !state.dirty, onClick: props.discard, children: props.t('discard') }), state.saving ? _jsx("span", { style: style.status, children: props.t('saving') }) : null, state.failed ? _jsx("span", { style: { ...style.status, color: '#b91c1c' }, children: props.t('saveFailed') }) : null] })] }));
73
+ : props.t('offCurrent')] }), !state.writable ? _jsx("p", { style: style.notice, children: props.t('readonly') }) : null, _jsxs(Section, { t: props.t, title: props.t('modelSection'), subtitle: props.t('modelSectionHint'), defaultOpen: true, children: [state.catalogStatus === 'loading' ? _jsx("p", { style: style.notice, children: props.t('loading') }) : null, state.catalogStatus === 'error'
74
+ ? (_jsxs("div", { style: style.error, children: [_jsx("span", { children: props.t('loadFailed') }), _jsx("button", { type: "button", style: { ...style.button, ...style.secondary }, disabled: disabled, onClick: props.retryCatalog, children: props.t('retry') })] }))
75
+ : null, state.catalogPartial ? _jsx("p", { style: style.notice, children: props.t('partial') }) : null, state.groups.length > 0
76
+ ? (_jsxs("div", { children: [_jsx("p", { style: style.notice, children: props.t('choose') }), _jsx("div", { style: style.list, children: state.groups.map(group => (_jsxs("div", { children: [_jsx("div", { style: style.groupTitle, children: group.name }), group.candidates.map(candidate => (_jsxs("label", { style: style.row, children: [_jsx("input", { type: "radio", name: "advisor-route", checked: candidate.selected, disabled: disabled, onChange: () => { props.pickRoute(candidate.key); } }), _jsxs("span", { style: style.modelName, children: [_jsx("span", { children: candidate.modelName }), _jsx("span", { style: style.route, children: `${candidate.provider}/${candidate.model}` })] }), !candidate.available ? _jsx("span", { style: style.badge, children: props.t('unavailable') }) : null] }, candidate.key)))] }, group.id))) })] }))
77
+ : state.catalogStatus === 'ready' ? _jsx("p", { style: style.notice, children: props.t('empty') }) : null, _jsxs("div", { style: style.effortRow, children: [_jsx("label", { htmlFor: "advisor-effort", children: props.t('effort') }), _jsx("select", { id: "advisor-effort", value: state.effective.effort, disabled: disabled, onChange: (event) => { props.setEffort(event.target.value); }, children: effortOptions.map(option => (_jsx("option", { value: option, children: option === '' ? props.t('effortDefault') : option }, option))) })] }), _jsx("p", { style: { ...style.notice, marginTop: -4 }, children: state.efforts.known ? props.t('effortHintKnown') : props.t('effortHint') })] }), _jsx(Section, { t: props.t, title: props.t('patrolTitle'), defaultOpen: false, children: _jsx(PatrolSection, { ...props, enabled: state.patrol.enabled, everySteps: state.patrol.everySteps, immuneTurns: state.patrol.immuneTurns, investigate: state.patrol.investigate, disabled: disabled }) }), _jsxs("div", { style: style.actions, children: [_jsx("button", { type: "button", style: { ...style.button, ...style.primary }, disabled: disabled || !state.dirty, onClick: props.save, children: props.t('save') }), _jsx("button", { type: "button", style: { ...style.button, ...style.secondary }, disabled: disabled || !state.dirty, onClick: props.discard, children: props.t('discard') }), state.saving ? _jsx("span", { style: style.status, children: props.t('saving') }) : null, state.failed ? _jsx("span", { style: { ...style.status, color: '#b91c1c' }, children: props.t('saveFailed') }) : null] })] }));
60
78
  }
@@ -71,6 +71,8 @@ export interface AdvisorCardState {
71
71
  dirty: boolean;
72
72
  catalogStatus: 'idle' | 'loading' | 'ready' | 'error';
73
73
  catalogPartial: boolean;
74
+ /** 总开关的生效值(draft 优先) */
75
+ enabled: boolean;
74
76
  /** 生效值(draft 优先,其次已保存值) */
75
77
  effective: AdvisorRoute;
76
78
  /** 巡逻模式的生效值(同 draft 优先) */
@@ -96,6 +98,7 @@ export interface AdvisorCardFace {
96
98
  advisorCard: SnapshotStore<AdvisorCardState>;
97
99
  };
98
100
  pickRoute: (key: string) => void;
101
+ toggleEnabled: () => void;
99
102
  setEffort: (effort: string) => void;
100
103
  togglePatrol: () => void;
101
104
  setPatrolEverySteps: (steps: number) => void;
@@ -120,6 +123,7 @@ export declare function currentEffortSupport(groups: readonly CatalogGroup[], ro
120
123
  levels: readonly string[];
121
124
  };
122
125
  export interface AdvisorSection {
126
+ enabled?: boolean;
123
127
  provider?: string;
124
128
  model?: string;
125
129
  effort?: string;
@@ -149,6 +153,7 @@ export declare class AdvisorCardController {
149
153
  private effective;
150
154
  private pickRoute;
151
155
  private setEffort;
156
+ private toggleEnabled;
152
157
  private togglePatrol;
153
158
  private setPatrolEverySteps;
154
159
  private setPatrolImmuneTurns;
@@ -103,6 +103,7 @@ export class AdvisorCardController {
103
103
  return {
104
104
  hooks: { advisorCard: this.store },
105
105
  pickRoute: (key) => this.pickRoute(key),
106
+ toggleEnabled: () => this.toggleEnabled(),
106
107
  setEffort: (effort) => this.setEffort(effort),
107
108
  togglePatrol: () => this.togglePatrol(),
108
109
  setPatrolEverySteps: (steps) => this.setPatrolEverySteps(steps),
@@ -116,6 +117,7 @@ export class AdvisorCardController {
116
117
  saved() {
117
118
  const value = this.scope.getSnapshot().value;
118
119
  return {
120
+ enabled: value?.enabled ?? true,
119
121
  provider: value?.provider ?? '',
120
122
  model: value?.model ?? '',
121
123
  effort: value?.effort ?? '',
@@ -167,6 +169,14 @@ export class AdvisorCardController {
167
169
  this.failed = false;
168
170
  this.publish();
169
171
  }
172
+ toggleEnabled() {
173
+ if (!this.scope.getSnapshot().writable || this.saving)
174
+ return;
175
+ const current = this.effective();
176
+ this.draft = { ...current, enabled: !(current.enabled ?? true) };
177
+ this.failed = false;
178
+ this.publish();
179
+ }
170
180
  togglePatrol() {
171
181
  if (!this.scope.getSnapshot().writable || this.saving)
172
182
  return;
@@ -214,7 +224,8 @@ export class AdvisorCardController {
214
224
  this.failed = false;
215
225
  this.publish();
216
226
  try {
217
- // rc.6 scope 是逐字段写:五个字段顺序写入('' 也写,保证旧值被清掉)
227
+ // rc.6 scope 是逐字段写:顺序写入('' / false 也写,保证旧值被清掉)
228
+ await this.scope.set('enabled', desired.enabled ?? true);
218
229
  await this.scope.set('provider', desired.provider);
219
230
  await this.scope.set('model', desired.model);
220
231
  await this.scope.set('effort', desired.effort);
@@ -270,6 +281,7 @@ export class AdvisorCardController {
270
281
  dirty: this.draft !== undefined,
271
282
  catalogStatus: this.catalogStatus,
272
283
  catalogPartial: this.catalogPartial,
284
+ enabled: effective.enabled ?? true,
273
285
  effective,
274
286
  patrol: {
275
287
  enabled: effective.patrolEnabled ?? true,
@@ -31,43 +31,74 @@ __export(index_exports, {
31
31
  module.exports = __toCommonJS(index_exports);
32
32
 
33
33
  // src/client/AdvisorCard.tsx
34
+ var import_react = require("react");
34
35
  var import_jsx_runtime = require("react/jsx-runtime");
36
+ function ToggleSwitch(props) {
37
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
38
+ "button",
39
+ {
40
+ type: "button",
41
+ role: "switch",
42
+ "aria-checked": props.checked,
43
+ "aria-label": props.label,
44
+ disabled: props.disabled,
45
+ onClick: props.onToggle,
46
+ style: {
47
+ ...style.button,
48
+ width: 40,
49
+ height: 22,
50
+ borderRadius: 11,
51
+ padding: 0,
52
+ position: "relative",
53
+ background: props.checked ? "#16a34a" : "rgba(128,128,128,.4)",
54
+ border: "none"
55
+ },
56
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
57
+ position: "absolute",
58
+ top: 2,
59
+ left: props.checked ? 20 : 2,
60
+ width: 18,
61
+ height: 18,
62
+ borderRadius: 9,
63
+ background: "#fff",
64
+ transition: "left .15s"
65
+ } })
66
+ }
67
+ );
68
+ }
69
+ function Section(props) {
70
+ const [open, setOpen] = (0, import_react.useState)(props.defaultOpen ?? false);
71
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.section, children: [
72
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ style: style.sectionHead,
77
+ "aria-expanded": open,
78
+ onClick: () => setOpen((v) => !v),
79
+ children: [
80
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.sectionTitle, children: props.title }),
81
+ props.subtitle === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.sectionSubtitle, children: props.subtitle }),
82
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.sectionState, children: open ? props.t("sectionCollapse") : props.t("sectionExpand") })
83
+ ]
84
+ }
85
+ ),
86
+ open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: style.sectionBody, children: props.children }) : null
87
+ ] });
88
+ }
35
89
  function PatrolSection(props) {
36
90
  const { t } = props;
37
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...style.card, background: "rgba(128,128,128,.06)" }, children: [
38
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { style: { ...style.title, fontSize: 13 }, children: t("patrolTitle") }),
91
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
39
92
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.description, children: t("patrolDescription") }),
40
93
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.effortRow, children: [
41
94
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("patrolToggle") }),
42
95
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
43
- "button",
96
+ ToggleSwitch,
44
97
  {
45
- type: "button",
46
- role: "switch",
47
- "aria-checked": props.enabled,
48
- "aria-label": t("patrolToggle"),
98
+ checked: props.enabled,
49
99
  disabled: props.disabled,
50
- onClick: props.togglePatrol,
51
- style: {
52
- ...style.button,
53
- width: 40,
54
- height: 22,
55
- borderRadius: 11,
56
- padding: 0,
57
- position: "relative",
58
- background: props.enabled ? "#16a34a" : "rgba(128,128,128,.4)",
59
- border: "none"
60
- },
61
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
62
- position: "absolute",
63
- top: 2,
64
- left: props.enabled ? 20 : 2,
65
- width: 18,
66
- height: 18,
67
- borderRadius: 9,
68
- background: "#fff",
69
- transition: "left .15s"
70
- } })
100
+ label: t("patrolToggle"),
101
+ onToggle: props.togglePatrol
71
102
  }
72
103
  )
73
104
  ] }),
@@ -111,34 +142,12 @@ function PatrolSection(props) {
111
142
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...style.effortRow, marginTop: 8 }, children: [
112
143
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("investigateToggle") }),
113
144
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
114
- "button",
145
+ ToggleSwitch,
115
146
  {
116
- type: "button",
117
- role: "switch",
118
- "aria-checked": props.investigate,
119
- "aria-label": t("investigateToggle"),
147
+ checked: props.investigate,
120
148
  disabled: props.disabled,
121
- onClick: props.toggleInvestigate,
122
- style: {
123
- ...style.button,
124
- width: 40,
125
- height: 22,
126
- borderRadius: 11,
127
- padding: 0,
128
- position: "relative",
129
- background: props.investigate ? "#16a34a" : "rgba(128,128,128,.4)",
130
- border: "none"
131
- },
132
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
133
- position: "absolute",
134
- top: 2,
135
- left: props.investigate ? 20 : 2,
136
- width: 18,
137
- height: 18,
138
- borderRadius: 9,
139
- background: "#fff",
140
- transition: "left .15s"
141
- } })
149
+ label: t("investigateToggle"),
150
+ onToggle: props.toggleInvestigate
142
151
  }
143
152
  )
144
153
  ] }),
@@ -166,70 +175,103 @@ var style = {
166
175
  primary: { background: "#2563eb", color: "#fff", border: "none" },
167
176
  secondary: { background: "transparent", border: "1px solid rgba(128,128,128,.5)" },
168
177
  status: { fontSize: 12 },
169
- list: { maxHeight: 260, overflowY: "auto", border: "1px solid rgba(128,128,128,.25)", borderRadius: 6, padding: "4px 10px" }
178
+ list: { maxHeight: 260, overflowY: "auto", border: "1px solid rgba(128,128,128,.25)", borderRadius: 6, padding: "4px 10px" },
179
+ switchRow: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, marginTop: 2 },
180
+ switchLabel: { fontWeight: 600 },
181
+ section: { border: "1px solid rgba(128,128,128,.25)", borderRadius: 8, overflow: "hidden" },
182
+ sectionHead: { display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "8px 10px", background: "rgba(128,128,128,.06)", border: "none", font: "inherit", textAlign: "left", cursor: "pointer" },
183
+ sectionTitle: { fontSize: 13, fontWeight: 600 },
184
+ sectionSubtitle: { fontSize: 11, opacity: 0.65, flex: 1 },
185
+ sectionState: { fontSize: 11, opacity: 0.65, marginLeft: "auto" },
186
+ sectionBody: { padding: 10, display: "flex", flexDirection: "column", gap: 8 }
170
187
  };
171
188
  function AdvisorCard(props) {
172
189
  const state = props.useAdvisorCard((s) => s);
190
+ const enabled = state.enabled;
173
191
  const effortOptions = state.efforts.known ? ["", ...state.efforts.levels] : EFFORT_OPTIONS;
174
192
  const disabled = !state.writable || state.saving;
175
- const armed = state.effective.provider !== "" && state.effective.model !== "";
193
+ const armed = enabled && state.effective.provider !== "" && state.effective.model !== "";
176
194
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.card, children: [
177
195
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: style.title, children: props.t("title") }),
178
196
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.description, children: props.t("description") }),
197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.switchRow, children: [
198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.switchLabel, children: props.t("enabledToggle") }),
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
200
+ ToggleSwitch,
201
+ {
202
+ checked: enabled,
203
+ disabled,
204
+ label: props.t("enabledToggle"),
205
+ onToggle: props.toggleEnabled
206
+ }
207
+ )
208
+ ] }),
209
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: enabled ? props.t("enabledHintOn") : props.t("enabledHintOff") }),
179
210
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: style.current, children: [
180
211
  props.t("armed"),
181
212
  "\uFF1A",
182
213
  armed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: `${state.effective.provider}/${state.effective.model}${state.effective.effort === "" ? "" : ` (${state.effective.effort})`}` }) : props.t("offCurrent")
183
214
  ] }),
184
215
  !state.writable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("readonly") }) : null,
185
- state.catalogStatus === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("loading") }) : null,
186
- state.catalogStatus === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.error, children: [
187
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: props.t("loadFailed") }),
188
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...style.button, ...style.secondary }, disabled, onClick: props.retryCatalog, children: props.t("retry") })
189
- ] }) : null,
190
- state.catalogPartial ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("partial") }) : null,
191
- state.groups.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
192
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("choose") }),
193
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: style.list, children: state.groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
194
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: style.groupTitle, children: group.name }),
195
- group.candidates.map((candidate) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: style.row, children: [
196
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
197
- "input",
198
- {
199
- type: "radio",
200
- name: "advisor-route",
201
- checked: candidate.selected,
202
- disabled,
203
- onChange: () => {
204
- props.pickRoute(candidate.key);
216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
217
+ Section,
218
+ {
219
+ t: props.t,
220
+ title: props.t("modelSection"),
221
+ subtitle: props.t("modelSectionHint"),
222
+ defaultOpen: true,
223
+ children: [
224
+ state.catalogStatus === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("loading") }) : null,
225
+ state.catalogStatus === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.error, children: [
226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: props.t("loadFailed") }),
227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...style.button, ...style.secondary }, disabled, onClick: props.retryCatalog, children: props.t("retry") })
228
+ ] }) : null,
229
+ state.catalogPartial ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("partial") }) : null,
230
+ state.groups.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("choose") }),
232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: style.list, children: state.groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
233
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: style.groupTitle, children: group.name }),
234
+ group.candidates.map((candidate) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: style.row, children: [
235
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
236
+ "input",
237
+ {
238
+ type: "radio",
239
+ name: "advisor-route",
240
+ checked: candidate.selected,
241
+ disabled,
242
+ onChange: () => {
243
+ props.pickRoute(candidate.key);
244
+ }
245
+ }
246
+ ),
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: style.modelName, children: [
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: candidate.modelName }),
249
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.route, children: `${candidate.provider}/${candidate.model}` })
250
+ ] }),
251
+ !candidate.available ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.badge, children: props.t("unavailable") }) : null
252
+ ] }, candidate.key))
253
+ ] }, group.id)) })
254
+ ] }) : state.catalogStatus === "ready" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("empty") }) : null,
255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.effortRow, children: [
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "advisor-effort", children: props.t("effort") }),
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
258
+ "select",
259
+ {
260
+ id: "advisor-effort",
261
+ value: state.effective.effort,
262
+ disabled,
263
+ onChange: (event) => {
264
+ props.setEffort(event.target.value);
265
+ },
266
+ children: effortOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option, children: option === "" ? props.t("effortDefault") : option }, option))
205
267
  }
206
- }
207
- ),
208
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: style.modelName, children: [
209
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: candidate.modelName }),
210
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.route, children: `${candidate.provider}/${candidate.model}` })
268
+ )
211
269
  ] }),
212
- !candidate.available ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: style.badge, children: props.t("unavailable") }) : null
213
- ] }, candidate.key))
214
- ] }, group.id)) })
215
- ] }) : state.catalogStatus === "ready" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: style.notice, children: props.t("empty") }) : null,
216
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.effortRow, children: [
217
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "advisor-effort", children: props.t("effort") }),
218
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
219
- "select",
220
- {
221
- id: "advisor-effort",
222
- value: state.effective.effort,
223
- disabled,
224
- onChange: (event) => {
225
- props.setEffort(event.target.value);
226
- },
227
- children: effortOptions.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option, children: option === "" ? props.t("effortDefault") : option }, option))
228
- }
229
- )
230
- ] }),
231
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { ...style.notice, marginTop: -4 }, children: state.efforts.known ? props.t("effortHintKnown") : props.t("effortHint") }),
232
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { ...style.notice, marginTop: -4 }, children: state.efforts.known ? props.t("effortHintKnown") : props.t("effortHint") })
271
+ ]
272
+ }
273
+ ),
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { t: props.t, title: props.t("patrolTitle"), defaultOpen: false, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
233
275
  PatrolSection,
234
276
  {
235
277
  ...props,
@@ -239,7 +281,7 @@ function AdvisorCard(props) {
239
281
  investigate: state.patrol.investigate,
240
282
  disabled
241
283
  }
242
- ),
284
+ ) }),
243
285
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: style.actions, children: [
244
286
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
245
287
  "button",
@@ -268,7 +310,7 @@ function AdvisorCard(props) {
268
310
  }
269
311
 
270
312
  // src/client/AdvisorToolRow.tsx
271
- var import_react = require("react");
313
+ var import_react2 = require("react");
272
314
  var import_jsx_runtime2 = require("react/jsx-runtime");
273
315
  var style2 = {
274
316
  row: { display: "flex", flexDirection: "column", gap: 4, margin: "6px 0", border: "1px solid rgba(37,99,235,.35)", borderRadius: 8, overflow: "hidden" },
@@ -296,7 +338,7 @@ function blockOutput(block) {
296
338
  }
297
339
  function AdvisorToolRow(props) {
298
340
  const t = props.t ?? ((key) => key);
299
- const [expanded, setExpanded] = (0, import_react.useState)(false);
341
+ const [expanded, setExpanded] = (0, import_react2.useState)(false);
300
342
  const block = props.block ?? {};
301
343
  const done = block.kind !== void 0;
302
344
  const { text, isError } = done ? blockOutput(block) : { text: "", isError: false };
@@ -318,7 +360,7 @@ function AdvisorToolRow(props) {
318
360
  }
319
361
 
320
362
  // src/client/PatrolNodeView.tsx
321
- var import_react2 = require("react");
363
+ var import_react3 = require("react");
322
364
  var import_jsx_runtime3 = require("react/jsx-runtime");
323
365
  var ACCENT_ON_TRACK = {
324
366
  border: "rgba(107,114,128,.35)",
@@ -348,7 +390,7 @@ var style3 = {
348
390
  };
349
391
  function PatrolNodeView(props) {
350
392
  const t = props.t ?? ((key) => key);
351
- const [expanded, setExpanded] = (0, import_react2.useState)(false);
393
+ const [expanded, setExpanded] = (0, import_react3.useState)(false);
352
394
  const data = props.node?.data;
353
395
  if (data === void 0) return null;
354
396
  const onTrack = data.verdict === "on-track";
@@ -473,6 +515,7 @@ var AdvisorCardController = class {
473
515
  return {
474
516
  hooks: { advisorCard: this.store },
475
517
  pickRoute: (key) => this.pickRoute(key),
518
+ toggleEnabled: () => this.toggleEnabled(),
476
519
  setEffort: (effort) => this.setEffort(effort),
477
520
  togglePatrol: () => this.togglePatrol(),
478
521
  setPatrolEverySteps: (steps) => this.setPatrolEverySteps(steps),
@@ -490,6 +533,7 @@ var AdvisorCardController = class {
490
533
  saved() {
491
534
  const value = this.scope.getSnapshot().value;
492
535
  return {
536
+ enabled: value?.enabled ?? true,
493
537
  provider: value?.provider ?? "",
494
538
  model: value?.model ?? "",
495
539
  effort: value?.effort ?? "",
@@ -534,6 +578,13 @@ var AdvisorCardController = class {
534
578
  this.failed = false;
535
579
  this.publish();
536
580
  }
581
+ toggleEnabled() {
582
+ if (!this.scope.getSnapshot().writable || this.saving) return;
583
+ const current = this.effective();
584
+ this.draft = { ...current, enabled: !(current.enabled ?? true) };
585
+ this.failed = false;
586
+ this.publish();
587
+ }
537
588
  togglePatrol() {
538
589
  if (!this.scope.getSnapshot().writable || this.saving) return;
539
590
  const current = this.effective();
@@ -575,6 +626,7 @@ var AdvisorCardController = class {
575
626
  this.failed = false;
576
627
  this.publish();
577
628
  try {
629
+ await this.scope.set("enabled", desired.enabled ?? true);
578
630
  await this.scope.set("provider", desired.provider);
579
631
  await this.scope.set("model", desired.model);
580
632
  await this.scope.set("effort", desired.effort);
@@ -624,6 +676,7 @@ var AdvisorCardController = class {
624
676
  dirty: this.draft !== void 0,
625
677
  catalogStatus: this.catalogStatus,
626
678
  catalogPartial: this.catalogPartial,
679
+ enabled: effective.enabled ?? true,
627
680
  effective,
628
681
  patrol: {
629
682
  enabled: effective.patrolEnabled ?? true,
@@ -709,6 +762,13 @@ var zh = {
709
762
  saved: "\u5DF2\u4FDD\u5B58",
710
763
  saveFailed: "\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
711
764
  readonly: "\u8BBE\u7F6E\u6587\u6863\u53EA\u8BFB\uFF0C\u65E0\u6CD5\u5728\u6B64\u4FEE\u6539\u3002",
765
+ enabledToggle: "\u542F\u7528 Advisor \u63D2\u4EF6",
766
+ enabledHintOn: "\u603B\u5F00\u5173\u5DF2\u6253\u5F00\uFF1A\u914D\u7F6E\u5BA1\u67E5\u6A21\u578B\u540E\uFF0Cadvisor \u5DE5\u5177\u4F1A\u5BF9\u6267\u884C\u6A21\u578B\u53EF\u89C1\u3002",
767
+ enabledHintOff: "\u603B\u5F00\u5173\u5DF2\u5173\u95ED\uFF1Aadvisor \u5DE5\u5177\u4E0E\u5347\u7EA7\u5B88\u5219\u4E0D\u4F1A\u6CE8\u518C\uFF0C\u5DF2\u4FDD\u5B58\u7684\u6A21\u578B\u914D\u7F6E\u4F1A\u4FDD\u7559\u3002",
768
+ modelSection: "\u6A21\u578B\u4E0E\u63A8\u7406\u6863\u4F4D",
769
+ modelSectionHint: "\u9009\u62E9\u5BA1\u67E5\u6A21\u578B\u548C\u63A8\u7406\u6863\u4F4D",
770
+ sectionExpand: "\u5C55\u5F00",
771
+ sectionCollapse: "\u6536\u8D77",
712
772
  patrolTitle: "\u5DE1\u903B\u6A21\u5F0F\uFF08\u81EA\u52A8\u68C0\u6D4B\u8DD1\u504F\uFF09",
713
773
  patrolDescription: "\u6267\u884C\u8FC7\u7A0B\u4E2D\u6BCF\u9694\u82E5\u5E72\u6B65\u81EA\u52A8\u628A\u4F1A\u8BDD\u5FEB\u7167\u53D1\u7ED9\u5BA1\u67E5\u6A21\u578B\u68C0\u67E5\u65B9\u5411\uFF1B\u53D1\u73B0\u8DD1\u504F\u4F1A\u5728\u4E0B\u4E00\u6B65\u6CE8\u5165\u7EA0\u504F\uFF0CSTOP \u65F6\u8981\u6C42\u6267\u884C\u6A21\u578B\u505C\u4E0B\u5411\u7528\u6237\u62A5\u544A\u3002\u6BCF\u6B21\u5DE1\u903B\u6574\u6BB5\u4F1A\u8BDD\u8BA1\u8D39\u4E00\u6B21\u5BA1\u67E5\u6A21\u578B\u3002",
714
774
  patrolToggle: "\u542F\u7528\u5DE1\u903B",
@@ -758,6 +818,13 @@ var en = {
758
818
  saved: "Saved",
759
819
  saveFailed: "Save failed, please retry",
760
820
  readonly: "The settings document is read-only here.",
821
+ enabledToggle: "Enable advisor plugin",
822
+ enabledHintOn: "Master switch is on: once a reviewer is configured, the advisor tool becomes visible to the executor.",
823
+ enabledHintOff: "Master switch is off: the advisor tool and guideline prompt section are not registered; saved settings are kept.",
824
+ modelSection: "Reviewer model & effort",
825
+ modelSectionHint: "Pick the reviewer model and reasoning effort",
826
+ sectionExpand: "Expand",
827
+ sectionCollapse: "Collapse",
761
828
  patrolTitle: "Patrol mode (automatic drift detection)",
762
829
  patrolDescription: "Every few steps the conversation snapshot is sent to the reviewer to check direction; on drift a correction is injected into the next request, and STOP asks the executor to halt and report. Each patrol bills the full conversation to the reviewer once.",
763
830
  patrolToggle: "Enable patrol",
@@ -24,6 +24,13 @@ export declare const zh: {
24
24
  readonly saved: "已保存";
25
25
  readonly saveFailed: "保存失败,请重试";
26
26
  readonly readonly: "设置文档只读,无法在此修改。";
27
+ readonly enabledToggle: "启用 Advisor 插件";
28
+ readonly enabledHintOn: "总开关已打开:配置审查模型后,advisor 工具会对执行模型可见。";
29
+ readonly enabledHintOff: "总开关已关闭:advisor 工具与升级守则不会注册,已保存的模型配置会保留。";
30
+ readonly modelSection: "模型与推理档位";
31
+ readonly modelSectionHint: "选择审查模型和推理档位";
32
+ readonly sectionExpand: "展开";
33
+ readonly sectionCollapse: "收起";
27
34
  readonly patrolTitle: "巡逻模式(自动检测跑偏)";
28
35
  readonly patrolDescription: "执行过程中每隔若干步自动把会话快照发给审查模型检查方向;发现跑偏会在下一步注入纠偏,STOP 时要求执行模型停下向用户报告。每次巡逻整段会话计费一次审查模型。";
29
36
  readonly patrolToggle: "启用巡逻";
@@ -24,6 +24,13 @@ export const zh = {
24
24
  saved: '已保存',
25
25
  saveFailed: '保存失败,请重试',
26
26
  readonly: '设置文档只读,无法在此修改。',
27
+ enabledToggle: '启用 Advisor 插件',
28
+ enabledHintOn: '总开关已打开:配置审查模型后,advisor 工具会对执行模型可见。',
29
+ enabledHintOff: '总开关已关闭:advisor 工具与升级守则不会注册,已保存的模型配置会保留。',
30
+ modelSection: '模型与推理档位',
31
+ modelSectionHint: '选择审查模型和推理档位',
32
+ sectionExpand: '展开',
33
+ sectionCollapse: '收起',
27
34
  patrolTitle: '巡逻模式(自动检测跑偏)',
28
35
  patrolDescription: '执行过程中每隔若干步自动把会话快照发给审查模型检查方向;发现跑偏会在下一步注入纠偏,STOP 时要求执行模型停下向用户报告。每次巡逻整段会话计费一次审查模型。',
29
36
  patrolToggle: '启用巡逻',
@@ -73,6 +80,13 @@ export const en = {
73
80
  saved: 'Saved',
74
81
  saveFailed: 'Save failed, please retry',
75
82
  readonly: 'The settings document is read-only here.',
83
+ enabledToggle: 'Enable advisor plugin',
84
+ enabledHintOn: 'Master switch is on: once a reviewer is configured, the advisor tool becomes visible to the executor.',
85
+ enabledHintOff: 'Master switch is off: the advisor tool and guideline prompt section are not registered; saved settings are kept.',
86
+ modelSection: 'Reviewer model & effort',
87
+ modelSectionHint: 'Pick the reviewer model and reasoning effort',
88
+ sectionExpand: 'Expand',
89
+ sectionCollapse: 'Collapse',
76
90
  patrolTitle: 'Patrol mode (automatic drift detection)',
77
91
  patrolDescription: 'Every few steps the conversation snapshot is sent to the reviewer to check direction; on drift a correction is injected into the next request, and STOP asks the executor to halt and report. Each patrol bills the full conversation to the reviewer once.',
78
92
  patrolToggle: 'Enable patrol',
package/lib/config.d.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  * 配置三层合成(rc.6 settings 服务):schema 默认值 → 插件组合层 base →
5
5
  * 用户层(~/.dsh/settings.yaml 的 advisor: 段,设置页卡片写入的就是这一层)。
6
6
  *
7
- * `provider` + `model` 两者齐备 advisor 才武装(对应 rpiv 的 "off costs
8
- * nothing":没有审查模型时工具与提示词段都不注册)。
7
+ * `enabled` 是用户总开关;`provider` + `model` 齐备且总开关打开时 advisor
8
+ * 才武装(对应 rpiv 的 "off costs nothing":关闭或没有审查模型时,工具与
9
+ * 提示词段都不注册)。
9
10
  *
10
11
  * disabledForModels 条目语法(字符串数组,与设置页文案一致):
11
12
  * "model" —— 任意 provider 跑该模型都禁用
@@ -14,6 +15,7 @@
14
15
  */
15
16
  import z from '@deepseek-ai/schemastery';
16
17
  export interface Config {
18
+ enabled?: boolean;
17
19
  provider?: string;
18
20
  model?: string;
19
21
  effort?: string;
package/lib/config.js CHANGED
@@ -4,8 +4,9 @@
4
4
  * 配置三层合成(rc.6 settings 服务):schema 默认值 → 插件组合层 base →
5
5
  * 用户层(~/.dsh/settings.yaml 的 advisor: 段,设置页卡片写入的就是这一层)。
6
6
  *
7
- * `provider` + `model` 两者齐备 advisor 才武装(对应 rpiv 的 "off costs
8
- * nothing":没有审查模型时工具与提示词段都不注册)。
7
+ * `enabled` 是用户总开关;`provider` + `model` 齐备且总开关打开时 advisor
8
+ * 才武装(对应 rpiv 的 "off costs nothing":关闭或没有审查模型时,工具与
9
+ * 提示词段都不注册)。
9
10
  *
10
11
  * disabledForModels 条目语法(字符串数组,与设置页文案一致):
11
12
  * "model" —— 任意 provider 跑该模型都禁用
@@ -14,6 +15,7 @@
14
15
  */
15
16
  import z from '@deepseek-ai/schemastery';
16
17
  export const Config = z.object({
18
+ enabled: z.boolean().default(true).description('advisor 总开关:关闭后不注册 advisor 工具与升级守则提示段,已保存的模型配置保留'),
17
19
  provider: z.string().default('').description('审查模型所在的 provider 路由名(ctx.llm 适配器路由,如 example-provider)'),
18
20
  model: z.string().default('').description('该路由下的审查模型 id(如 deepseek-v4-pro)'),
19
21
  effort: z.string().default('').description('审查模型推理档位;留空走模型默认'),
@@ -25,6 +27,8 @@ export const Config = z.object({
25
27
  investigate: z.boolean().default(true).description('审查者调查工具:允许审查模型在裁决前用只读工具(工作区内搜索/读文件)亲自核实,建议更有据'),
26
28
  });
27
29
  export function resolveSelection(config) {
30
+ if (config.enabled === false)
31
+ return undefined;
28
32
  const { provider, model } = config;
29
33
  if (provider === undefined || provider === '' || model === undefined || model === '')
30
34
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-advisor-plugin",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "DeepSeek Harness advisor plugin: pair a fast executor with a stronger reviewer model for plan / correction / stop guidance.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "LICENSE"
40
40
  ],
41
41
  "engines": {
42
- "node": ">=20"
42
+ "node": "^22.19 || >=24"
43
43
  },
44
44
  "publishConfig": {
45
45
  "registry": "https://registry.npmjs.org",
@@ -63,15 +63,15 @@
63
63
  "coding-agent"
64
64
  ],
65
65
  "peerDependencies": {
66
- "@deepseek-ai/cordis": "*",
67
- "@deepseek-ai/dsh-agent": "*",
68
- "@deepseek-ai/dsh-commands": "*",
69
- "@deepseek-ai/dsh-llm": "*",
70
- "@deepseek-ai/dsh-session": "*",
71
- "@deepseek-ai/dsh-settings": "*",
72
- "@deepseek-ai/dsh-system-prompt": "*",
73
- "@deepseek-ai/dsh-tools": "*",
74
- "@deepseek-ai/schemastery": "*"
66
+ "@deepseek-ai/cordis": "^4.0.1",
67
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
68
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
69
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
70
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
71
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
72
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
73
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.3-alpha.1",
74
+ "@deepseek-ai/schemastery": "^3.18.1"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/node": "^24.0.0",
@@ -1,11 +1,11 @@
1
1
  /**
2
- * AdvisorCard —— 设置页"插件"栏里的 Advisor 卡片(浏览器半)。
2
+ * AdvisorCard —— 设置页“插件”栏里的 Advisor 卡片(浏览器半)。
3
3
  *
4
- * 交互:provider 分组的模型单选列表(再次点击取消选择)+ 推理档位下拉 +
5
- * 保存/放弃。数据与动作全部由 controller 通过 slot inject 面注入;
6
- * 本组件纯渲染,样式内联(不依赖 css module 构建)。
4
+ * 交互:总开关 + 可折叠的模型/档位区块与巡逻区块;数据与动作全部由
5
+ * controller 通过 slot inject 面注入;本组件纯渲染,样式内联。
7
6
  */
8
7
 
8
+ import { useState, type ReactNode } from 'react'
9
9
  import type { AdvisorCardFace, AdvisorCardState } from './controller.js'
10
10
  import type { LocaleKey } from './locales.js'
11
11
 
@@ -15,6 +15,61 @@ export type AdvisorCardProps = Omit<AdvisorCardFace, 'hooks'> & {
15
15
  t: (key: LocaleKey) => string
16
16
  }
17
17
 
18
+ /** 通用开关 */
19
+ function ToggleSwitch(props: {
20
+ checked: boolean
21
+ disabled?: boolean
22
+ label: string
23
+ onToggle: () => void
24
+ }) {
25
+ return (
26
+ <button
27
+ type="button"
28
+ role="switch"
29
+ aria-checked={props.checked}
30
+ aria-label={props.label}
31
+ disabled={props.disabled}
32
+ onClick={props.onToggle}
33
+ style={{
34
+ ...style.button,
35
+ width: 40, height: 22, borderRadius: 11, padding: 0, position: 'relative',
36
+ background: props.checked ? '#16a34a' : 'rgba(128,128,128,.4)', border: 'none',
37
+ }}
38
+ >
39
+ <span style={{
40
+ position: 'absolute', top: 2, left: props.checked ? 20 : 2,
41
+ width: 18, height: 18, borderRadius: 9, background: '#fff', transition: 'left .15s',
42
+ }} />
43
+ </button>
44
+ )
45
+ }
46
+
47
+ /** 可折叠区块:点击标题行展开/收起 */
48
+ function Section(props: {
49
+ t: (key: LocaleKey) => string
50
+ title: string
51
+ subtitle?: string
52
+ defaultOpen?: boolean
53
+ children: ReactNode
54
+ }) {
55
+ const [open, setOpen] = useState(props.defaultOpen ?? false)
56
+ return (
57
+ <div style={style.section}>
58
+ <button
59
+ type="button"
60
+ style={style.sectionHead}
61
+ aria-expanded={open}
62
+ onClick={() => setOpen(v => !v)}
63
+ >
64
+ <span style={style.sectionTitle}>{props.title}</span>
65
+ {props.subtitle === undefined ? null : <span style={style.sectionSubtitle}>{props.subtitle}</span>}
66
+ <span style={style.sectionState}>{open ? props.t('sectionCollapse') : props.t('sectionExpand')}</span>
67
+ </button>
68
+ {open ? <div style={style.sectionBody}>{props.children}</div> : null}
69
+ </div>
70
+ )
71
+ }
72
+
18
73
  /** 巡逻模式区块(自动检测跑偏) */
19
74
  function PatrolSection(props: AdvisorCardProps & {
20
75
  enabled: boolean
@@ -25,29 +80,16 @@ function PatrolSection(props: AdvisorCardProps & {
25
80
  }) {
26
81
  const { t } = props
27
82
  return (
28
- <div style={{ ...style.card, background: 'rgba(128,128,128,.06)' }}>
29
- <h4 style={{ ...style.title, fontSize: 13 }}>{t('patrolTitle')}</h4>
83
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
30
84
  <p style={style.description}>{t('patrolDescription')}</p>
31
85
  <div style={style.effortRow}>
32
86
  <span>{t('patrolToggle')}</span>
33
- <button
34
- type="button"
35
- role="switch"
36
- aria-checked={props.enabled}
37
- aria-label={t('patrolToggle')}
87
+ <ToggleSwitch
88
+ checked={props.enabled}
38
89
  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>
90
+ label={t('patrolToggle')}
91
+ onToggle={props.togglePatrol}
92
+ />
51
93
  </div>
52
94
  {props.enabled
53
95
  ? (
@@ -80,24 +122,12 @@ function PatrolSection(props: AdvisorCardProps & {
80
122
  </div>
81
123
  <div style={{ ...style.effortRow, marginTop: 8 }}>
82
124
  <span>{t('investigateToggle')}</span>
83
- <button
84
- type="button"
85
- role="switch"
86
- aria-checked={props.investigate}
87
- aria-label={t('investigateToggle')}
125
+ <ToggleSwitch
126
+ checked={props.investigate}
88
127
  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>
128
+ label={t('investigateToggle')}
129
+ onToggle={props.toggleInvestigate}
130
+ />
101
131
  </div>
102
132
  <p style={{ ...style.notice, marginTop: -2 }}>{t('investigateHint')}</p>
103
133
  </div>
@@ -130,22 +160,42 @@ const style = {
130
160
  secondary: { background: 'transparent', border: '1px solid rgba(128,128,128,.5)' } as const,
131
161
  status: { fontSize: 12 } as const,
132
162
  list: { maxHeight: 260, overflowY: 'auto', border: '1px solid rgba(128,128,128,.25)', borderRadius: 6, padding: '4px 10px' } as const,
163
+ switchRow: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, marginTop: 2 } as const,
164
+ switchLabel: { fontWeight: 600 } as const,
165
+ section: { border: '1px solid rgba(128,128,128,.25)', borderRadius: 8, overflow: 'hidden' } as const,
166
+ sectionHead: { display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 10px', background: 'rgba(128,128,128,.06)', border: 'none', font: 'inherit', textAlign: 'left', cursor: 'pointer' } as const,
167
+ sectionTitle: { fontSize: 13, fontWeight: 600 } as const,
168
+ sectionSubtitle: { fontSize: 11, opacity: 0.65, flex: 1 } as const,
169
+ sectionState: { fontSize: 11, opacity: 0.65, marginLeft: 'auto' } as const,
170
+ sectionBody: { padding: 10, display: 'flex', flexDirection: 'column', gap: 8 } as const,
133
171
  }
134
172
 
135
173
  export function AdvisorCard(props: AdvisorCardProps) {
136
174
  const state = props.useAdvisorCard(s => s)
175
+ const enabled = state.enabled
137
176
  // 档位选项:目录声明了支持面 → 只列支持的;未声明 → 全档位(host 自动降级兜底)
138
177
  const effortOptions: readonly string[] = state.efforts.known
139
178
  ? ['', ...state.efforts.levels]
140
179
  : EFFORT_OPTIONS
141
180
  const disabled = !state.writable || state.saving
142
- const armed = state.effective.provider !== '' && state.effective.model !== ''
181
+ const armed = enabled && state.effective.provider !== '' && state.effective.model !== ''
143
182
 
144
183
  return (
145
184
  <div style={style.card}>
146
185
  <h3 style={style.title}>{props.t('title')}</h3>
147
186
  <p style={style.description}>{props.t('description')}</p>
148
187
 
188
+ <div style={style.switchRow}>
189
+ <span style={style.switchLabel}>{props.t('enabledToggle')}</span>
190
+ <ToggleSwitch
191
+ checked={enabled}
192
+ disabled={disabled}
193
+ label={props.t('enabledToggle')}
194
+ onToggle={props.toggleEnabled}
195
+ />
196
+ </div>
197
+ <p style={style.notice}>{enabled ? props.t('enabledHintOn') : props.t('enabledHintOff')}</p>
198
+
149
199
  <p style={style.current}>
150
200
  {props.t('armed')}:
151
201
  {armed
@@ -155,78 +205,87 @@ export function AdvisorCard(props: AdvisorCardProps) {
155
205
 
156
206
  {!state.writable ? <p style={style.notice}>{props.t('readonly')}</p> : null}
157
207
 
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}
208
+ <Section
209
+ t={props.t}
210
+ title={props.t('modelSection')}
211
+ subtitle={props.t('modelSectionHint')}
212
+ defaultOpen
213
+ >
214
+ {state.catalogStatus === 'loading' ? <p style={style.notice}>{props.t('loading')}</p> : null}
215
+ {state.catalogStatus === 'error'
216
+ ? (
217
+ <div style={style.error}>
218
+ <span>{props.t('loadFailed')}</span>
219
+ <button type="button" style={{ ...style.button, ...style.secondary }} disabled={disabled} onClick={props.retryCatalog}>
220
+ {props.t('retry')}
221
+ </button>
222
+ </div>
223
+ )
224
+ : null}
225
+ {state.catalogPartial ? <p style={style.notice}>{props.t('partial')}</p> : null}
170
226
 
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
- ))}
227
+ {state.groups.length > 0
228
+ ? (
229
+ <div>
230
+ <p style={style.notice}>{props.t('choose')}</p>
231
+ <div style={style.list}>
232
+ {state.groups.map(group => (
233
+ <div key={group.id}>
234
+ <div style={style.groupTitle}>{group.name}</div>
235
+ {group.candidates.map(candidate => (
236
+ <label key={candidate.key} style={style.row}>
237
+ <input
238
+ type="radio"
239
+ name="advisor-route"
240
+ checked={candidate.selected}
241
+ disabled={disabled}
242
+ onChange={() => { props.pickRoute(candidate.key) }}
243
+ />
244
+ <span style={style.modelName}>
245
+ <span>{candidate.modelName}</span>
246
+ <span style={style.route}>{`${candidate.provider}/${candidate.model}`}</span>
247
+ </span>
248
+ {!candidate.available ? <span style={style.badge}>{props.t('unavailable')}</span> : null}
249
+ </label>
250
+ ))}
251
+ </div>
252
+ ))}
253
+ </div>
197
254
  </div>
198
- </div>
199
- )
200
- : state.catalogStatus === 'ready' ? <p style={style.notice}>{props.t('empty')}</p> : null}
255
+ )
256
+ : state.catalogStatus === 'ready' ? <p style={style.notice}>{props.t('empty')}</p> : null}
201
257
 
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>
258
+ <div style={style.effortRow}>
259
+ <label htmlFor="advisor-effort">{props.t('effort')}</label>
260
+ <select
261
+ id="advisor-effort"
262
+ value={state.effective.effort}
263
+ disabled={disabled}
264
+ onChange={(event) => { props.setEffort(event.target.value) }}
265
+ >
266
+ {/* 已声明支持面的模型只列支持的档位;未声明的列全档位(host 侧自动降级兜底) */}
267
+ {effortOptions.map(option => (
268
+ <option key={option} value={option}>
269
+ {option === '' ? props.t('effortDefault') : option}
270
+ </option>
271
+ ))}
272
+ </select>
273
+ </div>
274
+ <p style={{ ...style.notice, marginTop: -4 }}>
275
+ {state.efforts.known ? props.t('effortHintKnown') : props.t('effortHint')}
276
+ </p>
277
+ </Section>
221
278
 
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
- />
279
+ <Section t={props.t} title={props.t('patrolTitle')} defaultOpen={false}>
280
+ <PatrolSection
281
+ {...props}
282
+ enabled={state.patrol.enabled}
283
+ everySteps={state.patrol.everySteps}
284
+ immuneTurns={state.patrol.immuneTurns}
285
+ investigate={state.patrol.investigate}
286
+ disabled={disabled}
287
+ />
288
+ </Section>
230
289
 
231
290
  <div style={style.actions}>
232
291
  <button
@@ -87,6 +87,8 @@ export interface AdvisorCardState {
87
87
  dirty: boolean
88
88
  catalogStatus: 'idle' | 'loading' | 'ready' | 'error'
89
89
  catalogPartial: boolean
90
+ /** 总开关的生效值(draft 优先) */
91
+ enabled: boolean
90
92
  /** 生效值(draft 优先,其次已保存值) */
91
93
  effective: AdvisorRoute
92
94
  /** 巡逻模式的生效值(同 draft 优先) */
@@ -99,6 +101,7 @@ export interface AdvisorCardState {
99
101
  export interface AdvisorCardFace {
100
102
  hooks: { advisorCard: SnapshotStore<AdvisorCardState> }
101
103
  pickRoute: (key: string) => void
104
+ toggleEnabled: () => void
102
105
  setEffort: (effort: string) => void
103
106
  togglePatrol: () => void
104
107
  setPatrolEverySteps: (steps: number) => void
@@ -175,6 +178,7 @@ export function currentEffortSupport(
175
178
  }
176
179
 
177
180
  export interface AdvisorSection {
181
+ enabled?: boolean
178
182
  provider?: string
179
183
  model?: string
180
184
  effort?: string
@@ -217,6 +221,7 @@ export class AdvisorCardController {
217
221
  return {
218
222
  hooks: { advisorCard: this.store },
219
223
  pickRoute: (key) => this.pickRoute(key),
224
+ toggleEnabled: () => this.toggleEnabled(),
220
225
  setEffort: (effort) => this.setEffort(effort),
221
226
  togglePatrol: () => this.togglePatrol(),
222
227
  setPatrolEverySteps: (steps) => this.setPatrolEverySteps(steps),
@@ -231,6 +236,7 @@ export class AdvisorCardController {
231
236
  private saved(): SectionDraft {
232
237
  const value = this.scope.getSnapshot().value
233
238
  return {
239
+ enabled: value?.enabled ?? true,
234
240
  provider: value?.provider ?? '',
235
241
  model: value?.model ?? '',
236
242
  effort: value?.effort ?? '',
@@ -283,6 +289,14 @@ export class AdvisorCardController {
283
289
  this.publish()
284
290
  }
285
291
 
292
+ private toggleEnabled(): void {
293
+ if (!this.scope.getSnapshot().writable || this.saving) return
294
+ const current = this.effective()
295
+ this.draft = { ...current, enabled: !(current.enabled ?? true) }
296
+ this.failed = false
297
+ this.publish()
298
+ }
299
+
286
300
  private togglePatrol(): void {
287
301
  if (!this.scope.getSnapshot().writable || this.saving) return
288
302
  const current = this.effective()
@@ -329,7 +343,8 @@ export class AdvisorCardController {
329
343
  this.failed = false
330
344
  this.publish()
331
345
  try {
332
- // rc.6 scope 是逐字段写:五个字段顺序写入('' 也写,保证旧值被清掉)
346
+ // rc.6 scope 是逐字段写:顺序写入('' / false 也写,保证旧值被清掉)
347
+ await this.scope.set('enabled', desired.enabled ?? true)
333
348
  await this.scope.set('provider', desired.provider)
334
349
  await this.scope.set('model', desired.model)
335
350
  await this.scope.set('effort', desired.effort)
@@ -381,6 +396,7 @@ export class AdvisorCardController {
381
396
  dirty: this.draft !== undefined,
382
397
  catalogStatus: this.catalogStatus,
383
398
  catalogPartial: this.catalogPartial,
399
+ enabled: effective.enabled ?? true,
384
400
  effective,
385
401
  patrol: {
386
402
  enabled: effective.patrolEnabled ?? true,
@@ -25,6 +25,13 @@ export const zh = {
25
25
  saved: '已保存',
26
26
  saveFailed: '保存失败,请重试',
27
27
  readonly: '设置文档只读,无法在此修改。',
28
+ enabledToggle: '启用 Advisor 插件',
29
+ enabledHintOn: '总开关已打开:配置审查模型后,advisor 工具会对执行模型可见。',
30
+ enabledHintOff: '总开关已关闭:advisor 工具与升级守则不会注册,已保存的模型配置会保留。',
31
+ modelSection: '模型与推理档位',
32
+ modelSectionHint: '选择审查模型和推理档位',
33
+ sectionExpand: '展开',
34
+ sectionCollapse: '收起',
28
35
  patrolTitle: '巡逻模式(自动检测跑偏)',
29
36
  patrolDescription: '执行过程中每隔若干步自动把会话快照发给审查模型检查方向;发现跑偏会在下一步注入纠偏,STOP 时要求执行模型停下向用户报告。每次巡逻整段会话计费一次审查模型。',
30
37
  patrolToggle: '启用巡逻',
@@ -77,6 +84,13 @@ export const en: Record<LocaleKey, string> = {
77
84
  saved: 'Saved',
78
85
  saveFailed: 'Save failed, please retry',
79
86
  readonly: 'The settings document is read-only here.',
87
+ enabledToggle: 'Enable advisor plugin',
88
+ enabledHintOn: 'Master switch is on: once a reviewer is configured, the advisor tool becomes visible to the executor.',
89
+ enabledHintOff: 'Master switch is off: the advisor tool and guideline prompt section are not registered; saved settings are kept.',
90
+ modelSection: 'Reviewer model & effort',
91
+ modelSectionHint: 'Pick the reviewer model and reasoning effort',
92
+ sectionExpand: 'Expand',
93
+ sectionCollapse: 'Collapse',
80
94
  patrolTitle: 'Patrol mode (automatic drift detection)',
81
95
  patrolDescription: 'Every few steps the conversation snapshot is sent to the reviewer to check direction; on drift a correction is injected into the next request, and STOP asks the executor to halt and report. Each patrol bills the full conversation to the reviewer once.',
82
96
  patrolToggle: 'Enable patrol',
package/src/config.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  * 配置三层合成(rc.6 settings 服务):schema 默认值 → 插件组合层 base →
5
5
  * 用户层(~/.dsh/settings.yaml 的 advisor: 段,设置页卡片写入的就是这一层)。
6
6
  *
7
- * `provider` + `model` 两者齐备 advisor 才武装(对应 rpiv 的 "off costs
8
- * nothing":没有审查模型时工具与提示词段都不注册)。
7
+ * `enabled` 是用户总开关;`provider` + `model` 齐备且总开关打开时 advisor
8
+ * 才武装(对应 rpiv 的 "off costs nothing":关闭或没有审查模型时,工具与
9
+ * 提示词段都不注册)。
9
10
  *
10
11
  * disabledForModels 条目语法(字符串数组,与设置页文案一致):
11
12
  * "model" —— 任意 provider 跑该模型都禁用
@@ -16,6 +17,7 @@
16
17
  import z from '@deepseek-ai/schemastery'
17
18
 
18
19
  export interface Config {
20
+ enabled?: boolean
19
21
  provider?: string
20
22
  model?: string
21
23
  effort?: string
@@ -28,6 +30,7 @@ export interface Config {
28
30
  }
29
31
 
30
32
  export const Config: z<Config> = z.object({
33
+ enabled: z.boolean().default(true).description('advisor 总开关:关闭后不注册 advisor 工具与升级守则提示段,已保存的模型配置保留'),
31
34
  provider: z.string().default('').description('审查模型所在的 provider 路由名(ctx.llm 适配器路由,如 example-provider)'),
32
35
  model: z.string().default('').description('该路由下的审查模型 id(如 deepseek-v4-pro)'),
33
36
  effort: z.string().default('').description('审查模型推理档位;留空走模型默认'),
@@ -47,6 +50,7 @@ export interface Selection {
47
50
  }
48
51
 
49
52
  export function resolveSelection(config: Config): Selection | undefined {
53
+ if (config.enabled === false) return undefined
50
54
  const { provider, model } = config
51
55
  if (provider === undefined || provider === '' || model === undefined || model === '') return undefined
52
56
  return config.effort === undefined || config.effort === ''