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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/cordis.patch.yml +6 -0
  4. package/lib/advisor-prompt.d.ts +28 -0
  5. package/lib/advisor-prompt.js +91 -0
  6. package/lib/client/AdvisorCard.d.ts +15 -0
  7. package/lib/client/AdvisorCard.js +60 -0
  8. package/lib/client/AdvisorToolRow.d.ts +40 -0
  9. package/lib/client/AdvisorToolRow.js +52 -0
  10. package/lib/client/PatrolNodeView.d.ts +18 -0
  11. package/lib/client/PatrolNodeView.js +57 -0
  12. package/lib/client/controller.d.ts +161 -0
  13. package/lib/client/controller.js +287 -0
  14. package/lib/client/index.d.ts +20 -0
  15. package/lib/client/index.js +841 -0
  16. package/lib/client/locales.d.ts +54 -0
  17. package/lib/client/locales.js +101 -0
  18. package/lib/client/patrol-chat.d.ts +78 -0
  19. package/lib/client/patrol-chat.js +54 -0
  20. package/lib/client/store.d.ts +11 -0
  21. package/lib/client/store.js +23 -0
  22. package/lib/command.d.ts +7 -0
  23. package/lib/command.js +52 -0
  24. package/lib/config.d.ts +37 -0
  25. package/lib/config.js +83 -0
  26. package/lib/emission-guard.d.ts +32 -0
  27. package/lib/emission-guard.js +74 -0
  28. package/lib/gating.d.ts +8 -0
  29. package/lib/gating.js +48 -0
  30. package/lib/history.d.ts +20 -0
  31. package/lib/history.js +95 -0
  32. package/lib/index.d.ts +25 -0
  33. package/lib/index.js +96 -0
  34. package/lib/llm-call.d.ts +69 -0
  35. package/lib/llm-call.js +483 -0
  36. package/lib/patrol-event.d.ts +88 -0
  37. package/lib/patrol-event.js +68 -0
  38. package/lib/patrol.d.ts +51 -0
  39. package/lib/patrol.js +202 -0
  40. package/lib/prompt-section.d.ts +7 -0
  41. package/lib/prompt-section.js +16 -0
  42. package/lib/settings.d.ts +18 -0
  43. package/lib/settings.js +50 -0
  44. package/lib/tool.d.ts +22 -0
  45. package/lib/tool.js +91 -0
  46. package/package.json +91 -0
  47. package/src/advisor-prompt.ts +102 -0
  48. package/src/client/AdvisorCard.tsx +253 -0
  49. package/src/client/AdvisorToolRow.tsx +83 -0
  50. package/src/client/PatrolNodeView.tsx +85 -0
  51. package/src/client/controller.ts +399 -0
  52. package/src/client/index.ts +100 -0
  53. package/src/client/locales.ts +105 -0
  54. package/src/client/patrol-chat.ts +109 -0
  55. package/src/client/store.ts +29 -0
  56. package/src/command.ts +61 -0
  57. package/src/config.ts +113 -0
  58. package/src/emission-guard.ts +76 -0
  59. package/src/gating.ts +54 -0
  60. package/src/history.ts +102 -0
  61. package/src/index.ts +106 -0
  62. package/src/llm-call.ts +552 -0
  63. package/src/patrol-event.ts +115 -0
  64. package/src/patrol.ts +229 -0
  65. package/src/prompt-section.ts +21 -0
  66. package/src/settings.ts +74 -0
  67. package/src/tool.ts +114 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 leeyoung1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # dsh-advisor-plugin
2
+
3
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+ ![node](https://img.shields.io/badge/node-%3E%3D20-339933)
5
+ ![dsh](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4B32C3)
6
+
7
+ > DeepSeek Harness 的 advisor 策略模式插件:让便宜快速的执行模型继续跑主线,在关键节点把整段会话交给更强的审查模型,拿回 **plan / correction / stop signal** 后继续执行。
8
+
9
+ `dsh-advisor-plugin` 解决的是一个结构性问题:**同一个模型既写代码又验代码,很容易“证明自己是对的”**。长链路 Coding Agent 常见的三类失效——自验证盲区、长程漂移、过早宣告完成——都可以用“独立第二意见”来缓解。
10
+
11
+ 它提供两条互补通道:
12
+
13
+ - **显式咨询**:执行模型零参数调用 `advisor()`,整段会话与工具清单自动转发给审查模型;
14
+ - **自动巡逻**:每 N 步把会话快照发给审查模型,跑偏时在下一个请求注入纠偏,STOP 时要求执行模型停下向用户报告。
15
+
16
+ 所有审查配置都在 DSH 设置页完成,不需要手改配置文件。
17
+
18
+ ---
19
+
20
+ ## 特性
21
+
22
+ - **零参数 `advisor()` 工具**:调用即自动转发整段会话,模型不需要组织上下文。
23
+ - **plan / correction / stop 契约**:审查模型只做判断,不碰代码、不直接改状态。
24
+ - **compaction 感知**:转发的是 `session.deriveMessages()` 的真实模型可见面,而不是过期原始历史。
25
+ - **稳定的工具清单前缀**:按 agent scope 序列化,目标是命中 DeepSeek 前缀缓存。
26
+ - **巡逻模式**:步数 + 90 秒墙钟节流 + `patrolImmuneTurns` 冷却 + emission-guard 去重,防止过度打扰。
27
+ - **只读调查工具**:审查模型可先在工作区内 `search_files` / `read_file` 核实事实,再给建议。
28
+ - **失败隔离**:审查调用失败、超时、空正文、上下文溢出都不会炸掉执行模型的 turn。
29
+ - **双降级**:推理档位不支持时走模型默认档;上下文溢出时做配对安全截断后重试。
30
+ - **执行模型黑名单**:`disabledForModels` 支持 `model`、`provider/model`、`provider/model@effort` 三种语法。
31
+ - **未武装零成本**:没有配置审查模型时,工具和提示段都不注册。
32
+ - **可视化**:设置页卡片、`advisor()` 咨询卡片、巡逻裁决灰/琥珀/红三形态卡片。
33
+
34
+ ---
35
+
36
+ ## 架构
37
+
38
+ ```text
39
+ src/ # host 半(Node / Cordis 插件)
40
+ ├── index.ts # apply:组装工具/提示段/门控/巡逻/命令/设置
41
+ ├── advisor-prompt.ts # 咨询/巡逻两套系统提示 + 干预文本
42
+ ├── config.ts # schemastery schema + 武装解析 + 黑名单语法
43
+ ├── tool.ts # 零参 advisor 工具 + 专属卡片 + 结果信封
44
+ ├── history.ts # 工具清单 + deriveMessages + 尾部规则
45
+ ├── llm-call.ts # 审查调用 / sessionId 透传 / 降级 / 调查循环 / 回显剥离
46
+ ├── patrol.ts # 巡逻调度 / 裁决解析 / agent.inject 干预
47
+ ├── patrol-event.ts # 巡逻裁决写入会话事件
48
+ ├── emission-guard.ts # 归一化 / 空话过滤 / FIFO 去重
49
+ ├── settings.ts # settings 命名空间接入与活编辑
50
+ ├── gating.ts # 按 agent 隐藏 advisor 工具
51
+ ├── prompt-section.ts # 升级守则提示段
52
+ ├── command.ts # /advisor 命令
53
+ └── client/ # 浏览器半
54
+ ├── index.ts # 注册设置卡片 / 工具行 / 巡逻卡片
55
+ ├── controller.ts # 设置暂存编辑 + 模型目录
56
+ ├── AdvisorCard.tsx # 设置页卡片
57
+ ├── AdvisorToolRow.tsx
58
+ ├── PatrolNodeView.tsx
59
+ ├── patrol-chat.ts
60
+ ├── locales.ts
61
+ └── store.ts
62
+
63
+ docs/ # 技术文章、架构图、生图提示词
64
+ scripts/ # 本地开发辅助脚本
65
+ ```
66
+
67
+ 完整模块说明见 `docs/`。
68
+
69
+ ---
70
+
71
+ ## 安装
72
+
73
+ ### 1. npm 安装(推荐)
74
+
75
+ ```bash
76
+ dsh plugin --profile web add dsh-advisor-plugin
77
+ ```
78
+
79
+ 安装后重启 `dsh web`。
80
+
81
+ ### 2. 本地开发安装
82
+
83
+ ```bash
84
+ git clone https://github.com/leeyoung1/dsh-advisor-plugin.git
85
+ cd dsh-advisor-plugin
86
+ npm install
87
+ ./scripts/link-deps.sh
88
+ ./scripts/mount.sh
89
+ ```
90
+
91
+ `link-deps.sh` 会把本机 DSH 携带的官方包链接进 `node_modules`,供类型检查和测试使用;`mount.sh` 会构建并挂载到本地 web profile。
92
+
93
+ > 注意:`npm install` 会清掉手工链接的 `@deepseek-ai/*` 包,重装依赖后需要重新执行 `./scripts/link-deps.sh`。
94
+
95
+ ### 3. Git 直接安装(开发)
96
+
97
+ ```bash
98
+ dsh plugin --profile web add github:leeyoung1/dsh-advisor-plugin
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 快速开始
104
+
105
+ 1. 打开 **设置 → 插件**,找到 **Advisor 审查模型** 卡片;
106
+ 2. 选择一个比执行模型更强的审查模型,按需设置推理档位;
107
+ 3. 按任务类型决定是否开启巡逻模式;
108
+ 4. 保存后,执行模型在任务开始前、卡住时、宣告完成前会自动调用 `advisor()`。
109
+
110
+ 也可以在会话里执行 `/advisor` 查看当前配置和可用 provider/model。
111
+
112
+ ### 配置示例
113
+
114
+ `~/.dsh/settings.yaml`:
115
+
116
+ ```yaml
117
+ advisor:
118
+ provider: example-provider
119
+ model: reviewer-model
120
+ effort: high
121
+ patrolEnabled: true
122
+ patrolEverySteps: 6
123
+ patrolImmuneTurns: 3
124
+ investigate: true
125
+ disabledForModels:
126
+ - deepseek-v4-pro
127
+ - gpt-5.2@high
128
+ guidelines: []
129
+ ```
130
+
131
+ `provider` + `model` 齐备才会武装;未配置时 advisor 工具对模型不可见,零提示词成本。
132
+
133
+ ---
134
+
135
+ ## 两条通道
136
+
137
+ ### 显式咨询
138
+
139
+ ```text
140
+ 执行模型 ──advisor()──▶ 上下文组装 ──▶ 审查模型
141
+ ▲ │
142
+ └──── plan / correction / stop ◀───────┘
143
+ ```
144
+
145
+ 适用时机:
146
+
147
+ - 实质性工作开始前;
148
+ - 反复卡住、方向不收敛时;
149
+ - 证据与建议矛盾时;
150
+ - 宣告完成前。
151
+
152
+ ### 自动巡逻
153
+
154
+ ```text
155
+ agent/request ──每 N 步──▶ 快照 ──▶ 审查模型
156
+
157
+ ON-TRACK / CORRECTION / STOP
158
+
159
+ 会话事件卡片 + agent.inject() 注入下一请求
160
+ ```
161
+
162
+ 巡逻不会打断在飞 turn,干预最早落在下一个模型请求。
163
+
164
+ ---
165
+
166
+ ## 依赖要求
167
+
168
+ - Node.js >= 20
169
+ - DeepSeek Harness(web profile)
170
+ - 已配置至少一个可用的 provider / model 作为审查模型
171
+
172
+ 开发验证:
173
+
174
+ ```bash
175
+ npm run typecheck
176
+ npm test
177
+ npm run build
178
+ ```
179
+
180
+ 当前测试:**7 个测试文件、59 个用例通过**。
181
+
182
+ ---
183
+
184
+ ## 文档
185
+
186
+ - [文档索引](docs/README.md)
187
+ - [技术文章:DeepSeek Harness 插件实战:我给 Coding Agent 请了个“随车专家”](docs/advisor-deep-dive.md)
188
+ - [架构图](docs/diagrams/architecture.svg)
189
+ - [双通道时序图](docs/diagrams/advisor-dual-channel-sequence.svg)
190
+ - [生图提示词](docs/diagrams/image-generation-prompts.md)
191
+
192
+ ---
193
+
194
+ ## 设计取向
195
+
196
+ - **执行与判断分离**:执行模型负责推进,审查模型负责判断。
197
+ - **建议权不等于执行权**:advisor 只输出建议,不替执行模型做决定。
198
+ - **按需升级**:强模型只在关键节点出现。
199
+ - **防御性工程**:降级、截断、重试、防噪、冷却、失败隔离。
200
+ - **宿主原生**:复用 DSH 的模型路由、凭据、设置页、会话事件与 UI 槽位。
201
+
202
+ ## 灵感来源
203
+
204
+ - [Anthropic: The advisor strategy](https://claude.com/blog/the-advisor-strategy)
205
+ - [oh-my-pi advisor/watchdog](https://github.com/can1357/oh-my-pi/blob/main/docs/advisor-watchdog.md)
206
+ - [rpiv-advisor](https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-advisor)
207
+
208
+ ## License
209
+
210
+ [MIT](LICENSE)
@@ -0,0 +1,6 @@
1
+ # dsh-advisor 挂载补丁(bundle patch 形态)。
2
+ # 本机 profile 采用 package.json bundles 数组挂载时无需此文件;
3
+ # 保留它以兼容 `dsh plugin add` 工作流。
4
+ - insert:
5
+ - id: dsh-advisor
6
+ name: dsh-advisor
@@ -0,0 +1,28 @@
1
+ /**
2
+ * advisor-prompt —— 插件全部固定文案与提示词常量(纯声明,无逻辑)。
3
+ *
4
+ * 面向模型的文本用英文(模型侧语义稳定),面向用户的日志用中文。
5
+ * 三个提示词共同构成 advisor 策略模式契约:审查模型三选一回文、
6
+ * 执行模型按守则升级、尾部 nudge 保证 user 结尾。
7
+ */
8
+ export declare const ADVISOR_TOOL_NAME = "advisor";
9
+ /** 发给审查模型的系统提示词:plan / correction / stop signal 三选一契约 */
10
+ export declare const ADVISOR_SYSTEM_PROMPT: string;
11
+ /** 零参数工具 description —— 执行模型判断"何时升级"的主要依据 */
12
+ export declare const ADVISOR_TOOL_DESCRIPTION: string;
13
+ /** 注入系统提示的升级守则(未配置审查模型时不注册,零提示词成本) */
14
+ export declare const DEFAULT_GUIDELINES: readonly string[];
15
+ /** 尾部 nudge:部分 provider 拒绝 assistant 结尾的载荷 */
16
+ export declare const MSG_USER_TAIL_NUDGE = "Please advise on the executor's situation above.";
17
+ /**
18
+ * 巡逻模式系统提示:审查模型对执行过程做中段检查,回三选一裁决。
19
+ * 首行前缀是机器可解析契约(patrol.parsePatrolVerdict),细节跟在后面。
20
+ */
21
+ export declare const PATROL_SYSTEM_PROMPT: string;
22
+ /** 巡逻问题(挂在快照尾部,随整段会话发给审查模型) */
23
+ export declare const PATROL_NUDGE = "Patrol check: is the executor still on track for the user's task? Reply per the patrol contract.";
24
+ /** 把巡逻裁决转成注入执行模型的干预文本(llm/stream 尾部 user 消息) */
25
+ export declare function buildInterventionText(kind: 'correction' | 'stop', detail: string): string;
26
+ /** 用户可见文案(中文) */
27
+ export declare const MSG_NO_MODEL = "\u672A\u914D\u7F6E advisor \u5BA1\u67E5\u6A21\u578B\u2014\u2014\u5728\u8BBE\u7F6E \u2192 \u63D2\u4EF6\u91CC\u9009\u62E9\u4E00\u4E2A\u5BA1\u67E5\u6A21\u578B\uFF08\u6216 /advisor \u67E5\u770B\u53EF\u7528\u8DEF\u7531\uFF09\u3002";
28
+ export declare const MSG_NO_AGENT_SCOPE = "advisor \u65E0\u6CD5\u8BBF\u95EE\u5F53\u524D\u4F1A\u8BDD\uFF08\u672A\u62FF\u5230 agent \u4E0A\u4E0B\u6587\uFF09\u2014\u2014\u8BF7\u4ECE\u4F1A\u8BDD\u5185\u8C03\u7528\u3002";
@@ -0,0 +1,91 @@
1
+ /**
2
+ * advisor-prompt —— 插件全部固定文案与提示词常量(纯声明,无逻辑)。
3
+ *
4
+ * 面向模型的文本用英文(模型侧语义稳定),面向用户的日志用中文。
5
+ * 三个提示词共同构成 advisor 策略模式契约:审查模型三选一回文、
6
+ * 执行模型按守则升级、尾部 nudge 保证 user 结尾。
7
+ */
8
+ export const ADVISOR_TOOL_NAME = 'advisor';
9
+ /** 发给审查模型的系统提示词:plan / correction / stop signal 三选一契约 */
10
+ export const ADVISOR_SYSTEM_PROMPT = [
11
+ 'You are an advisor model in an advisor-strategy pattern.',
12
+ 'An executor model is running a task end-to-end — calling tools, reading',
13
+ 'results, iterating toward a solution. When the executor hits a decision it',
14
+ 'cannot reasonably solve alone, it consults you for guidance. The executor\'s',
15
+ 'full tool inventory is prepended before the conversation so you can judge',
16
+ 'tool-choice correctness.',
17
+ '',
18
+ 'You read the shared conversation context and return ONE of:',
19
+ '- a plan (concrete next steps the executor should take),',
20
+ '- a correction (the executor is going down a wrong path — redirect it),',
21
+ '- a stop signal (the executor should halt and escalate to the user).',
22
+ '',
23
+ 'You NEVER call tools. You NEVER produce user-facing output. Be concise,',
24
+ 'directive, and grounded in the shared context. Name files, functions, and',
25
+ 'line numbers where possible. No preamble, no apologies, no meta-commentary',
26
+ '— just the guidance the executor needs.',
27
+ ].join('\n');
28
+ /** 零参数工具 description —— 执行模型判断"何时升级"的主要依据 */
29
+ export const ADVISOR_TOOL_DESCRIPTION = [
30
+ 'Escalate to a stronger reviewer model for guidance. When you need',
31
+ 'stronger judgment — a complex decision, an ambiguous failure, a problem',
32
+ "you're circling without progress — escalate to the advisor model for",
33
+ 'guidance, then resume. Takes NO parameters — when you call advisor(),',
34
+ 'your entire conversation history (task, every tool call, every result)',
35
+ 'is automatically forwarded. When to call:',
36
+ '(1) BEFORE substantive work — before writing, before committing to an',
37
+ 'interpretation, before building on an assumption;',
38
+ '(2) BEFORE declaring done — and make your deliverable durable first;',
39
+ '(3) when stuck — recurring errors, approach not converging;',
40
+ '(4) when your evidence contradicts previous advice, reconcile in one',
41
+ 'more call instead of silently switching.',
42
+ 'After each advisor result, restate its key guidance (plan / correction',
43
+ '/ stop signal) in your next visible reply to the user before continuing.',
44
+ ].join(' ');
45
+ /** 注入系统提示的升级守则(未配置审查模型时不注册,零提示词成本) */
46
+ export const DEFAULT_GUIDELINES = [
47
+ 'Call `advisor` BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. Orientation (finding files, fetching a source, seeing what is there) is not substantive work; writing, editing, and declaring an answer are.',
48
+ 'Also call `advisor` when you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one does not.',
49
+ 'Also call `advisor` when stuck — errors recurring, approach not converging, results that do not fit — or when considering a change of approach.',
50
+ 'On tasks longer than a few steps, call `advisor` at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don\'t need to keep calling.',
51
+ 'Give the advisor\'s advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim, adapt — a passing self-test is not evidence the advice is wrong.',
52
+ 'If you\'ve already retrieved data pointing one way and the advisor points another, don\'t silently switch — surface the conflict in one more `advisor` call. A reconcile call is cheaper than committing to the wrong branch.',
53
+ 'After each `advisor` result, put the advisor\'s key guidance into your next visible reply to the user before continuing — quote or paraphrase the plan, correction, or stop signal. The user often cannot see collapsed tool results.',
54
+ ];
55
+ /** 尾部 nudge:部分 provider 拒绝 assistant 结尾的载荷 */
56
+ export const MSG_USER_TAIL_NUDGE = 'Please advise on the executor\'s situation above.';
57
+ /**
58
+ * 巡逻模式系统提示:审查模型对执行过程做中段检查,回三选一裁决。
59
+ * 首行前缀是机器可解析契约(patrol.parsePatrolVerdict),细节跟在后面。
60
+ */
61
+ export const PATROL_SYSTEM_PROMPT = [
62
+ 'You are the advisor model running a PATROL check on an executor model mid-task.',
63
+ 'You see the executor\'s tool inventory and its conversation so far.',
64
+ 'Reply with EXACTLY one verdict on the first line, detail after it if applicable:',
65
+ '- "ON-TRACK" — the executor is progressing correctly; reply with just this line',
66
+ '- "CORRECTION: <one-paragraph redirect>" — the executor has drifted; state what is wrong and the concrete next step',
67
+ '- "STOP: <reason>" — the executor should halt and report to the user',
68
+ 'Flag only substantive drift: wrong files or approach, hallucinated progress,',
69
+ 'destructive operations, violating the user\'s constraints, or the task already',
70
+ 'being satisfied while the executor keeps working. Do not nitpick style.',
71
+ ].join('\n');
72
+ /** 巡逻问题(挂在快照尾部,随整段会话发给审查模型) */
73
+ export const PATROL_NUDGE = 'Patrol check: is the executor still on track for the user\'s task? Reply per the patrol contract.';
74
+ /** 把巡逻裁决转成注入执行模型的干预文本(llm/stream 尾部 user 消息) */
75
+ export function buildInterventionText(kind, detail) {
76
+ if (kind === 'stop') {
77
+ return [
78
+ '[advisor patrol] STOP signal from the reviewer model that monitors this execution:',
79
+ detail,
80
+ 'Halt the current approach immediately and report the situation to the user.',
81
+ ].join('\n');
82
+ }
83
+ return [
84
+ '[advisor patrol] The reviewer model monitoring this execution flagged a drift. Its correction:',
85
+ detail,
86
+ 'Adjust your approach now. Briefly acknowledge this correction in your next visible reply, then continue.',
87
+ ].join('\n');
88
+ }
89
+ /** 用户可见文案(中文) */
90
+ export const MSG_NO_MODEL = '未配置 advisor 审查模型——在设置 → 插件里选择一个审查模型(或 /advisor 查看可用路由)。';
91
+ export const MSG_NO_AGENT_SCOPE = 'advisor 无法访问当前会话(未拿到 agent 上下文)——请从会话内调用。';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * AdvisorCard —— 设置页"插件"栏里的 Advisor 卡片(浏览器半)。
3
+ *
4
+ * 交互:provider 分组的模型单选列表(再次点击取消选择)+ 推理档位下拉 +
5
+ * 保存/放弃。数据与动作全部由 controller 通过 slot inject 面注入;
6
+ * 本组件纯渲染,样式内联(不依赖 css module 构建)。
7
+ */
8
+ import type { AdvisorCardFace, AdvisorCardState } from './controller.js';
9
+ import type { LocaleKey } from './locales.js';
10
+ /** 组件收到的 props:face 里的 hooks 被渲染器绑定成 use 选择器 hook */
11
+ export type AdvisorCardProps = Omit<AdvisorCardFace, 'hooks'> & {
12
+ useAdvisorCard: <S>(selector: (s: AdvisorCardState) => S) => S;
13
+ t: (key: LocaleKey) => string;
14
+ };
15
+ export declare function AdvisorCard(props: AdvisorCardProps): import("react").JSX.Element;
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** 巡逻模式区块(自动检测跑偏) */
3
+ function PatrolSection(props) {
4
+ 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') })] }))
21
+ : null, _jsx("p", { style: { ...style.notice, marginTop: -2 }, children: t('patrolEveryStepsHint') })] }));
22
+ }
23
+ const EFFORT_OPTIONS = ['', 'off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
24
+ // 内联样式(跟随设置页朴素风格,避免 css module 构建依赖)
25
+ const style = {
26
+ card: { border: '1px solid rgba(128,128,128,.35)', borderRadius: 8, padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 10 },
27
+ title: { fontSize: 14, fontWeight: 600, margin: 0 },
28
+ description: { fontSize: 12, opacity: 0.75, margin: 0, lineHeight: 1.6 },
29
+ current: { fontSize: 12, margin: 0 },
30
+ groupTitle: { fontSize: 12, fontWeight: 600, opacity: 0.85, margin: '8px 0 2px' },
31
+ row: { display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0', fontSize: 13 },
32
+ modelName: { display: 'flex', flexDirection: 'column' },
33
+ route: { fontSize: 11, opacity: 0.65 },
34
+ badge: { fontSize: 11, color: '#b45309' },
35
+ notice: { fontSize: 12, opacity: 0.7, margin: 0 },
36
+ error: { fontSize: 12, color: '#b91c1c', display: 'flex', gap: 8, alignItems: 'center' },
37
+ effortRow: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, marginTop: 6 },
38
+ actions: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 4 },
39
+ button: { fontSize: 13, padding: '4px 14px', borderRadius: 6, cursor: 'pointer' },
40
+ primary: { background: '#2563eb', color: '#fff', border: 'none' },
41
+ secondary: { background: 'transparent', border: '1px solid rgba(128,128,128,.5)' },
42
+ status: { fontSize: 12 },
43
+ list: { maxHeight: 260, overflowY: 'auto', border: '1px solid rgba(128,128,128,.25)', borderRadius: 6, padding: '4px 10px' },
44
+ };
45
+ export function AdvisorCard(props) {
46
+ const state = props.useAdvisorCard(s => s);
47
+ // 档位选项:目录声明了支持面 → 只列支持的;未声明 → 全档位(host 自动降级兜底)
48
+ const effortOptions = state.efforts.known
49
+ ? ['', ...state.efforts.levels]
50
+ : EFFORT_OPTIONS;
51
+ 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
54
+ ? _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] })] }));
60
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * AdvisorToolRow —— `advisor` 工具在会话消息流里的专属行(keyed
3
+ * `tool.call.toolview` 槽,官方给插件自有工具预留的定制点)。
4
+ *
5
+ * 折叠态:🧭 标题 + 进行中/完成/失败状态 + 审查模型路由;
6
+ * 展开态:审查建议全文(或错误说明)——用户点开即见"真的调了
7
+ * advisor、建议是什么"。
8
+ */
9
+ import type { LocaleKey } from './locales.js';
10
+ /** 会话工具块的最小结构(RunningToolCall / settled result 的并集子集) */
11
+ interface AdvisorBlock {
12
+ kind?: unknown;
13
+ callId?: unknown;
14
+ call?: {
15
+ argsRaw?: unknown;
16
+ } | undefined;
17
+ argsRaw?: unknown;
18
+ content?: readonly {
19
+ type?: unknown;
20
+ text?: unknown;
21
+ }[] | undefined;
22
+ isError?: boolean | undefined;
23
+ error?: {
24
+ name?: unknown;
25
+ code?: unknown;
26
+ message?: unknown;
27
+ } | undefined;
28
+ }
29
+ export type AdvisorToolRowProps = {
30
+ callId?: string;
31
+ toolName?: string;
32
+ block?: AdvisorBlock;
33
+ cwd?: string;
34
+ home?: string;
35
+ openFile?: (path: string) => void;
36
+ inspect?: () => void;
37
+ t?: (key: LocaleKey) => string;
38
+ };
39
+ export declare function AdvisorToolRow(props: AdvisorToolRowProps): import("react").JSX.Element;
40
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * AdvisorToolRow —— `advisor` 工具在会话消息流里的专属行(keyed
4
+ * `tool.call.toolview` 槽,官方给插件自有工具预留的定制点)。
5
+ *
6
+ * 折叠态:🧭 标题 + 进行中/完成/失败状态 + 审查模型路由;
7
+ * 展开态:审查建议全文(或错误说明)——用户点开即见"真的调了
8
+ * advisor、建议是什么"。
9
+ */
10
+ import { useState } from 'react';
11
+ const style = {
12
+ row: { display: 'flex', flexDirection: 'column', gap: 4, margin: '6px 0', border: '1px solid rgba(37,99,235,.35)', borderRadius: 8, overflow: 'hidden' },
13
+ 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%' },
14
+ title: { fontWeight: 600, fontSize: 13, color: '#1d4ed8' },
15
+ state: { fontSize: 12, opacity: 0.75 },
16
+ badge: { fontSize: 11, padding: '1px 8px', borderRadius: 8, flex: 'none' },
17
+ badgeRun: { background: 'rgba(37,99,235,.15)', color: '#1d4ed8' },
18
+ badgeOk: { background: 'rgba(22,163,74,.15)', color: '#15803d' },
19
+ badgeErr: { background: 'rgba(185,28,28,.12)', color: '#b91c1c' },
20
+ body: { padding: '8px 12px', fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', borderTop: '1px solid rgba(37,99,235,.2)', maxHeight: 320, overflowY: 'auto' },
21
+ };
22
+ function blockOutput(block) {
23
+ if (block === undefined)
24
+ return { text: '', isError: false };
25
+ const parts = [];
26
+ for (const b of block.content ?? []) {
27
+ if (b?.type === 'text' && typeof b.text === 'string')
28
+ parts.push(b.text);
29
+ else if (b !== undefined && b !== null)
30
+ parts.push(JSON.stringify(b, null, 2));
31
+ }
32
+ if (parts.length === 0 && block.error !== undefined) {
33
+ const e = block.error;
34
+ parts.push(`${typeof e.name === 'string' ? e.name : 'Error'}: ${typeof e.code === 'string' ? e.code : ''} ${typeof e.message === 'string' ? e.message : ''}`.trim());
35
+ }
36
+ return { text: parts.join('\n').trim(), isError: block.isError === true };
37
+ }
38
+ export function AdvisorToolRow(props) {
39
+ const t = props.t ?? ((key) => key);
40
+ const [expanded, setExpanded] = useState(false);
41
+ const block = props.block ?? {};
42
+ const done = block.kind !== undefined;
43
+ const { text, isError } = done ? blockOutput(block) : { text: '', isError: false };
44
+ const badge = !done
45
+ ? { label: t('toolRunning'), css: style.badgeRun }
46
+ : isError
47
+ ? { label: t('toolFailed'), css: style.badgeErr }
48
+ : { label: t('toolDone'), css: style.badgeOk };
49
+ const canExpand = done && text !== '';
50
+ return (_jsxs("div", { style: style.row, children: [_jsxs("button", { type: "button", style: style.head, onClick: () => { if (canExpand)
51
+ setExpanded(v => !v); }, children: [_jsxs("span", { style: style.title, children: ["\uD83E\uDDED Advisor ", t('toolTitle')] }), _jsx("span", { style: { ...style.badge, ...badge.css }, children: badge.label }), _jsx("span", { style: style.state, children: !done ? t('toolConsulting') : canExpand ? (expanded ? t('toolCollapse') : t('toolExpand')) : '' })] }), expanded && canExpand ? _jsx("div", { style: style.body, children: text }) : null] }));
52
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PatrolNodeView —— 巡逻裁决在会话消息流里的 🧭 卡片(keyed
3
+ * `conversation.chat.node` 槽,key 'advisor-patrol'——该 kind 只有本
4
+ * 插件的 definition 会物化,槽格子无官方占位)。
5
+ *
6
+ * 三种形态:ON-TRACK 低调灰卡(默认收起,视野完整但不打扰)、
7
+ * 纠偏琥珀卡、叫停红卡。后两者展开即裁决全文——用户点开就见
8
+ * "巡逻真的判了什么、为什么"。unclear 不写事件,天然不出卡片。
9
+ */
10
+ import type { PatrolNodeState } from './patrol-chat.js';
11
+ import type { LocaleKey } from './locales.js';
12
+ export type PatrolNodeViewProps = {
13
+ node?: {
14
+ data?: PatrolNodeState;
15
+ } | undefined;
16
+ t?: (key: LocaleKey) => string;
17
+ };
18
+ export declare function PatrolNodeView(props: PatrolNodeViewProps): import("react").JSX.Element | null;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * PatrolNodeView —— 巡逻裁决在会话消息流里的 🧭 卡片(keyed
4
+ * `conversation.chat.node` 槽,key 'advisor-patrol'——该 kind 只有本
5
+ * 插件的 definition 会物化,槽格子无官方占位)。
6
+ *
7
+ * 三种形态:ON-TRACK 低调灰卡(默认收起,视野完整但不打扰)、
8
+ * 纠偏琥珀卡、叫停红卡。后两者展开即裁决全文——用户点开就见
9
+ * "巡逻真的判了什么、为什么"。unclear 不写事件,天然不出卡片。
10
+ */
11
+ import { useState } from 'react';
12
+ const ACCENT_ON_TRACK = {
13
+ border: 'rgba(107,114,128,.35)',
14
+ head: 'rgba(107,114,128,.06)',
15
+ title: '#4b5563',
16
+ badge: { background: 'rgba(107,114,128,.14)', color: '#4b5563' },
17
+ };
18
+ const ACCENT_CORRECTION = {
19
+ border: 'rgba(217,119,6,.45)',
20
+ head: 'rgba(217,119,6,.09)',
21
+ title: '#b45309',
22
+ badge: { background: 'rgba(217,119,6,.16)', color: '#b45309' },
23
+ };
24
+ const ACCENT_STOP = {
25
+ border: 'rgba(185,28,28,.4)',
26
+ head: 'rgba(185,28,28,.07)',
27
+ title: '#b91c1c',
28
+ badge: { background: 'rgba(185,28,28,.12)', color: '#b91c1c' },
29
+ };
30
+ const style = {
31
+ row: (accent) => ({ display: 'flex', flexDirection: 'column', gap: 4, margin: '6px 0', border: `1px solid ${accent.border}`, borderRadius: 8, overflow: 'hidden' }),
32
+ head: (accent) => ({ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', cursor: 'pointer', background: accent.head, border: 'none', font: 'inherit', textAlign: 'left', width: '100%' }),
33
+ title: (accent) => ({ fontWeight: 600, fontSize: 13, color: accent.title }),
34
+ badge: { fontSize: 11, padding: '1px 8px', borderRadius: 8, flex: 'none' },
35
+ meta: { fontSize: 12, opacity: 0.7 },
36
+ body: { padding: '8px 12px', fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', borderTop: '1px solid rgba(120,113,108,.25)', maxHeight: 320, overflowY: 'auto' },
37
+ };
38
+ export function PatrolNodeView(props) {
39
+ const t = props.t ?? ((key) => key);
40
+ const [expanded, setExpanded] = useState(false);
41
+ const data = props.node?.data;
42
+ if (data === undefined)
43
+ return null;
44
+ const onTrack = data.verdict === 'on-track';
45
+ const accent = onTrack ? ACCENT_ON_TRACK : data.verdict === 'stop' ? ACCENT_STOP : ACCENT_CORRECTION;
46
+ const badge = onTrack ? t('patrolVerdictOnTrack') : data.verdict === 'stop' ? t('patrolVerdictStop') : t('patrolVerdictCorrection');
47
+ const title = onTrack ? t('patrolCheckTitle') : t('patrolCardTitle');
48
+ const canExpand = data.detail.trim() !== '';
49
+ const fmtTok = (n) => (n >= 1000 ? `${Math.round(n / 100) / 10}k` : String(n));
50
+ const usageText = data.usage === undefined
51
+ ? ''
52
+ : ` · ↓${fmtTok(data.usage.input)}${data.usage.cacheRead !== undefined && data.usage.cacheRead > 0 ? `(缓存${fmtTok(data.usage.cacheRead)})` : ''} ↑${fmtTok(data.usage.output)} tok`;
53
+ // 冷却降级标记:concern 在 immuneTurns 窗口内只出了卡片、未注入执行模型
54
+ const downgradedTag = data.downgraded === true ? ` · ${t('patrolDowngradedTag')}` : '';
55
+ return (_jsxs("div", { style: style.row(accent), children: [_jsxs("button", { type: "button", style: style.head(accent), onClick: () => { if (canExpand)
56
+ setExpanded(v => !v); }, children: [_jsx("span", { style: style.title(accent), children: `🧭 Advisor ${title}` }), _jsx("span", { style: { ...style.badge, ...accent.badge }, children: badge }), _jsx("span", { style: style.meta, children: `${data.reviewer} · ${t('patrolCardStepPrefix')}${data.step}${t('patrolCardStepSuffix')} · ${Math.round(data.patrolMs / 100) / 10}s${usageText}${downgradedTag}` }), _jsx("span", { style: style.meta, children: canExpand ? (expanded ? t('patrolCollapse') : t('patrolExpand')) : '' })] }), expanded && canExpand ? _jsx("div", { style: style.body, children: data.detail }) : null] }));
57
+ }