@harmonyos-arkts/opencode-acp 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +100 -38
- package/README.zh.md +41 -39
- package/dist/index.cjs +1420 -215
- package/dist/index.cjs.map +4 -4
- package/docs/codebase-overview.md +191 -0
- package/docs/mcp-extmethod-design.md +366 -0
- package/docs/provider-config-flow.md +312 -0
- package/docs/question-asked.md +35 -18
- package/docs/session-stats-to-vscode-design.md +217 -0
- package/docs/subagent-visibility.md +26 -25
- package/docs/tui-vs-acp-analysis.md +36 -36
- package/package.json +10 -4
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# VSCode → ACP → OpenCode Provider 配置全链路
|
|
2
|
+
|
|
3
|
+
## 核心概念:OpenCode 的 Provider 可见性机制
|
|
4
|
+
|
|
5
|
+
OpenCode 有两个层面的 provider 数据:
|
|
6
|
+
|
|
7
|
+
| 数据源 | 接口 | 内容 | 用途 |
|
|
8
|
+
|--------|------|------|------|
|
|
9
|
+
| **数据库全量** | `GET /provider` | models.dev 中的所有 provider(70+) | 发现哪些 provider 可配置 |
|
|
10
|
+
| **运行时可用** | `GET /config/providers` | 有凭证或 autoload 的 provider + 模型列表 | 模型选择下拉框 |
|
|
11
|
+
|
|
12
|
+
**关键**:`GET /config/providers` 返回的不是"有 API Key 的 provider",而是 `s.providers` 这个内部状态。进入 `s.providers` 的条件(`provider/provider.ts:1076-1285`):
|
|
13
|
+
|
|
14
|
+
| 条件 | 代码位置 | 说明 |
|
|
15
|
+
|------|---------|------|
|
|
16
|
+
| 配置文件声明了 provider | 1124 行 | `opencode.json` 中 `provider.<id>` 有配置 |
|
|
17
|
+
| 环境变量有 key | 1211 行 | 如 `ANTHROPIC_API_KEY=xxx` |
|
|
18
|
+
| auth.json 有 key | 1224 行 | 通过 `PUT /auth/:providerID` 存入的 |
|
|
19
|
+
| Plugin auth loader | 1237 行 | Plugin 提供的认证加载器 |
|
|
20
|
+
| **Custom loader autoload** | 1258 行 | **`opencode` provider 始终 autoload** |
|
|
21
|
+
|
|
22
|
+
### `opencode` Provider 的特殊性
|
|
23
|
+
|
|
24
|
+
`opencode` provider 有一个特殊的 custom loader(`provider/provider.ts:152-174`):
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// 即使没有任何 key,只要还有免费模型,就 autoload
|
|
28
|
+
if (!ok) {
|
|
29
|
+
// 删除付费模型,保留免费模型(cost.input === 0)
|
|
30
|
+
for (const [key, value] of Object.entries(input.models)) {
|
|
31
|
+
if (value.cost.input === 0) continue
|
|
32
|
+
delete input.models[key]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
autoload: Object.keys(input.models).length > 0, // 有免费模型就 autoload
|
|
37
|
+
options: ok ? {} : { apiKey: "public" }, // 无 key 用 "public"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**这意味着**:即使零配置,`GET /config/providers` 也会返回 `opencode` provider,包含所有免费模型(如 `glm-4.6`、`big-pickle` 等)。TUI 的 `/models` 能看到 GLM 模型就是这个原因。
|
|
42
|
+
|
|
43
|
+
### 所以:配置 GLM 的场景分两种
|
|
44
|
+
|
|
45
|
+
**场景 A:使用 OpenCode Zen 的免费 GLM 模型**
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
opencode provider 下的 glm-4.6 是免费模型
|
|
49
|
+
→ 无需任何配置,直接可用
|
|
50
|
+
→ providerID: "opencode", modelID: "glm-4.6"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**场景 B:使用智谱 AI 官方 API**
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
需要配置 zhipuai provider 的 API Key
|
|
57
|
+
→ extMethod("provider/auth/set", { providerID: "zhipuai", auth: { type: "api", key: "xxx" } })
|
|
58
|
+
→ 配置后 zhipuai provider 进入 s.providers
|
|
59
|
+
→ 其下所有 GLM 模型可用
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 阶段一:启动 & 发现
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
VSCode 启动 harmony-acp 进程
|
|
66
|
+
│
|
|
67
|
+
├─ harmony-acp 连接 OpenCode server (http://localhost:4096)
|
|
68
|
+
│ └─ sdk.global.health() 验证连通
|
|
69
|
+
│
|
|
70
|
+
└─ VSCode 调用 initialize()
|
|
71
|
+
│
|
|
72
|
+
└─ harmony-acp 返回:
|
|
73
|
+
{
|
|
74
|
+
authMethods: [{ id: "opencode-login", name: "Login with opencode" }],
|
|
75
|
+
_meta: {
|
|
76
|
+
providers: [
|
|
77
|
+
{ id: "opencode", name: "OpenCode", connected: true }, ← 始终连接(免费模型)
|
|
78
|
+
{ id: "anthropic", name: "Anthropic", connected: false },
|
|
79
|
+
{ id: "openai", name: "OpenAI", connected: false },
|
|
80
|
+
{ id: "zhipuai", name: "ZhipuAI", connected: false },
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**VSCode 此时知道**:哪些 provider 可用、哪些已连接。
|
|
87
|
+
|
|
88
|
+
## 阶段二:创建 Session & 发现可用模型
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
VSCode 调用 newSession({ cwd })
|
|
92
|
+
│
|
|
93
|
+
├─ harmony-acp 调用 sdk.config.providers()
|
|
94
|
+
│ → 返回 [opencode provider] ← 因为 opencode 始终 autoload
|
|
95
|
+
│ → opencode.models = { "glm-4.6", "big-pickle", ... } ← 免费模型
|
|
96
|
+
│
|
|
97
|
+
├─ harmony-acp 调用 defaultModel()
|
|
98
|
+
│ → 找到 opencode provider → 选择第一个模型
|
|
99
|
+
│
|
|
100
|
+
├─ sdk.session.create() → 成功创建 session
|
|
101
|
+
│
|
|
102
|
+
└─ 返回给 VSCode:
|
|
103
|
+
{
|
|
104
|
+
sessionId: "xxx",
|
|
105
|
+
models: {
|
|
106
|
+
currentModelId: "opencode/glm-4.6",
|
|
107
|
+
availableModels: [
|
|
108
|
+
{ modelId: "opencode/glm-4.6", name: "OpenCode/GLM-4.6" },
|
|
109
|
+
{ modelId: "opencode/big-pickle", name: "OpenCode/Big Pickle" },
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
configOptions: [
|
|
113
|
+
{ id: "model", currentValue: "opencode/glm-4.6", options: [...] },
|
|
114
|
+
{ id: "mode", currentValue: "build", options: [...] }
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**此时**:VSCode 可以用免费模型直接对话,但只有 `opencode` provider 的免费模型可选。
|
|
120
|
+
|
|
121
|
+
## 阶段三:用户配置新 Provider 的 API Key
|
|
122
|
+
|
|
123
|
+
用户想使用 Claude、GPT 等付费模型,需要配置对应 provider 的 API Key。
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
用户在 VSCode 中选择 "配置 Provider"
|
|
127
|
+
│
|
|
128
|
+
├─① extMethod("provider/list", { sessionId })
|
|
129
|
+
│ │
|
|
130
|
+
│ └─ harmony-acp → sdk.provider.list()
|
|
131
|
+
│ → GET /provider → 返回全量 provider 列表
|
|
132
|
+
│ → { all: [opencode, anthropic, openai, zhipuai, ...], connected: ["opencode"] }
|
|
133
|
+
│ → VSCode 展示 "未连接" 的 provider 列表让用户选择
|
|
134
|
+
│
|
|
135
|
+
├─② 用户选择 anthropic,VSCode 查询认证方式:
|
|
136
|
+
│ extMethod("provider/auth/methods", { sessionId })
|
|
137
|
+
│ │
|
|
138
|
+
│ └─ harmony-acp → sdk.provider.auth()
|
|
139
|
+
│ → GET /provider/auth
|
|
140
|
+
│ → { anthropic: [{ type: "api", label: "API Key" }] }
|
|
141
|
+
│ → VSCode 知道需要输入 API Key,展示输入框
|
|
142
|
+
│
|
|
143
|
+
├─③ 用户输入 API Key,VSCode 调用:
|
|
144
|
+
│ extMethod("provider/auth/set", {
|
|
145
|
+
│ providerID: "anthropic",
|
|
146
|
+
│ auth: { type: "api", key: "sk-ant-xxx" }
|
|
147
|
+
│ })
|
|
148
|
+
│ │
|
|
149
|
+
│ └─ harmony-acp → sdk.auth.set()
|
|
150
|
+
│ → PUT /auth/anthropic → 写入 ~/.opencode/data/auth.json
|
|
151
|
+
│ → 返回 { success: true }
|
|
152
|
+
│
|
|
153
|
+
└─④ 验证是否生效(可选):
|
|
154
|
+
extMethod("provider/list", { sessionId })
|
|
155
|
+
│
|
|
156
|
+
└─ harmony-acp → sdk.provider.list()
|
|
157
|
+
→ 返回: { connected: ["opencode", "anthropic"] } ← anthropic 已连接
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 阶段四:切换到新模型
|
|
161
|
+
|
|
162
|
+
配置 API Key 后,**不需要重新创建 session**,直接通过 `setSessionConfigOption` 切换模型:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
VSCode 调用 setSessionConfigOption({
|
|
166
|
+
sessionId, configId: "model", value: "anthropic/claude-sonnet-4"
|
|
167
|
+
})
|
|
168
|
+
│
|
|
169
|
+
├─ harmony-acp → sdk.config.providers()
|
|
170
|
+
│ → 现在返回 [opencode, anthropic] ← anthropic 有 key 了
|
|
171
|
+
│ → anthropic.models = { "claude-sonnet-4", "claude-opus-4", ... }
|
|
172
|
+
│
|
|
173
|
+
├─ parseModelSelection("anthropic/claude-sonnet-4")
|
|
174
|
+
│ → { providerID: "anthropic", modelID: "claude-sonnet-4" }
|
|
175
|
+
│
|
|
176
|
+
└─ 更新 session model → 返回新的 configOptions(包含 anthropic 的所有模型)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
或者用 `setSessionModel`:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
VSCode 调用 unstable_setSessionModel({
|
|
183
|
+
sessionId, modelId: "anthropic/claude-sonnet-4/high"
|
|
184
|
+
})
|
|
185
|
+
│
|
|
186
|
+
└─ 同样效果,支持 variant 选择
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 阶段五:开始对话
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
VSCode 调用 prompt({ sessionId, prompt: [...] })
|
|
193
|
+
│
|
|
194
|
+
└─ harmony-acp → sdk.session.prompt({
|
|
195
|
+
sessionID,
|
|
196
|
+
model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
|
197
|
+
parts: [...]
|
|
198
|
+
})
|
|
199
|
+
│
|
|
200
|
+
└─ OpenCode 用该模型发起 API 调用
|
|
201
|
+
→ 如果 key 无效,此时才报错 → harmony-acp 捕获 → RequestError.authRequired()
|
|
202
|
+
→ 如果 key 有效,正常流式返回
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 完整数据流图
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
VSCode harmony-acp OpenCode Server
|
|
209
|
+
│ │ │
|
|
210
|
+
│── initialize() ──────────>│ │
|
|
211
|
+
│ │── sdk.provider.list() ───────>│ GET /provider
|
|
212
|
+
│ │<── {all, connected} ──────────│
|
|
213
|
+
│<── {authMethods, _meta} ──│ │
|
|
214
|
+
│ │ │
|
|
215
|
+
│── newSession() ──────────>│ │
|
|
216
|
+
│ │── sdk.config.providers() ────>│ GET /config/providers
|
|
217
|
+
│ │<── [opencode + 免费模型] ─────│ (opencode 始终 autoload)
|
|
218
|
+
│ │── sdk.session.create() ──────>│ POST /session
|
|
219
|
+
│ │<── { id } ────────────────────│
|
|
220
|
+
│<── { sessionId, models } ─│ │
|
|
221
|
+
│ │ │
|
|
222
|
+
│── prompt(免费模型) ───────>│ │
|
|
223
|
+
│ │── sdk.session.prompt() ──────>│ POST /session/xxx/prompt
|
|
224
|
+
│ │<── SSE events ────────────────│
|
|
225
|
+
│<── streaming response ────│ │
|
|
226
|
+
│ │ │
|
|
227
|
+
│== 用户想用 Claude =======│ │
|
|
228
|
+
│ │ │
|
|
229
|
+
│── extMethod("provider/list") ─────────>│ │
|
|
230
|
+
│ │── sdk.provider.list() ───────>│ GET /provider
|
|
231
|
+
│ │<── {all, connected} ──────────│
|
|
232
|
+
│<── 全量 provider 列表 ────│ │
|
|
233
|
+
│ │ │
|
|
234
|
+
│── extMethod("provider/auth/methods") ──>│ │
|
|
235
|
+
│ │── sdk.provider.auth() ───────>│ GET /provider/auth
|
|
236
|
+
│ │<── {anthropic: [{type:"api"}]}│
|
|
237
|
+
│<── 认证方式 ──────────────│ │
|
|
238
|
+
│ │ │
|
|
239
|
+
│── extMethod("provider/auth/set") ──────>│ │
|
|
240
|
+
│ { providerID, auth } │── sdk.auth.set() ────────────>│ PUT /auth/anthropic
|
|
241
|
+
│ │ │ → 写入 auth.json
|
|
242
|
+
│ │<── true ──────────────────────│
|
|
243
|
+
│<── { success: true } ─────│ │
|
|
244
|
+
│ │ │
|
|
245
|
+
│── setSessionConfigOption ─>│ │
|
|
246
|
+
│ { "anthropic/claude-sonnet-4" } │
|
|
247
|
+
│ │── sdk.config.providers() ────>│ GET /config/providers
|
|
248
|
+
│ │<── [opencode, anthropic] ────│
|
|
249
|
+
│<── 更新后的模型列表 ──────│ │
|
|
250
|
+
│ │ │
|
|
251
|
+
│── prompt(claude) ────────>│ │
|
|
252
|
+
│ │── sdk.session.prompt() ──────>│ POST /session/xxx/prompt
|
|
253
|
+
│ │<── SSE events ────────────────│
|
|
254
|
+
│<── streaming response ────│ │
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 涉及的 OpenCode 接口
|
|
258
|
+
|
|
259
|
+
### 控制面路由(`routes/control/index.ts`)
|
|
260
|
+
|
|
261
|
+
| 方法 | 路径 | 用途 |
|
|
262
|
+
|------|------|------|
|
|
263
|
+
| `PUT` | `/auth/:providerID` | 设置 API Key,写入 `~/.opencode/data/auth.json` |
|
|
264
|
+
| `DELETE` | `/auth/:providerID` | 移除凭证 |
|
|
265
|
+
|
|
266
|
+
### 实例面路由(`routes/instance/provider.ts`)
|
|
267
|
+
|
|
268
|
+
| 方法 | 路径 | 用途 |
|
|
269
|
+
|------|------|------|
|
|
270
|
+
| `GET` | `/provider` | 返回全量 provider 列表 + connected 状态 |
|
|
271
|
+
| `GET` | `/provider/auth` | 返回每个 provider 的认证方式(API Key / OAuth) |
|
|
272
|
+
| `POST` | `/:providerID/oauth/authorize` | OAuth 授权(仅 Plugin 注册了 OAuth hook 的 provider) |
|
|
273
|
+
| `POST` | `/:providerID/oauth/callback` | OAuth 回调 |
|
|
274
|
+
|
|
275
|
+
### 实例面路由(`routes/instance/config.ts`)
|
|
276
|
+
|
|
277
|
+
| 方法 | 路径 | 用途 |
|
|
278
|
+
|------|------|------|
|
|
279
|
+
| `GET` | `/config/providers` | 返回 `s.providers`(有凭证或 autoload 的 provider + 模型列表) |
|
|
280
|
+
|
|
281
|
+
## 两个 Provider 接口的区别
|
|
282
|
+
|
|
283
|
+
| | `GET /provider` | `GET /config/providers` |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| 数据源 | models.dev 全量数据库 + connected 状态 | `s.providers` 内部状态(有凭证/autoload) |
|
|
286
|
+
| 返回范围 | **所有** provider(70+) | 仅"可用"的 provider |
|
|
287
|
+
| 用途 | 发现可配置的 provider、展示配置入口 | 模型选择下拉框的数据源 |
|
|
288
|
+
| 返回格式 | `{ all, default, connected }` | `{ providers, default }` |
|
|
289
|
+
| ACP extMethod | `provider/list` | 无(harmony-acp 内部调用) |
|
|
290
|
+
| TUI 使用 | `store.provider_next` | `store.provider`(`/models` 下拉框) |
|
|
291
|
+
|
|
292
|
+
## API Key 有效性判定
|
|
293
|
+
|
|
294
|
+
- **配置阶段**:只判断"存不存在",不判断"对不对"
|
|
295
|
+
- **使用阶段**:首次 `prompt()` 调用时才真正验证,key 无效则抛 `AI_LoadAPIKeyError`
|
|
296
|
+
- harmony-acp 在 `newSession`/`loadSession`/`forkSession`/`resumeSession` 中捕获该错误,返回 `RequestError.authRequired()`
|
|
297
|
+
|
|
298
|
+
## OAuth 接口的调用条件
|
|
299
|
+
|
|
300
|
+
OAuth 流程**不是通用的**,只有 Plugin 注册了 `auth.provider` hook 的 provider 才支持。
|
|
301
|
+
|
|
302
|
+
流程(参考 `packages/app/src/components/dialog-connect-provider.tsx`):
|
|
303
|
+
|
|
304
|
+
1. `GET /provider/auth` 获取认证方式列表
|
|
305
|
+
2. 如果 `type === "oauth"` 且有 `prompts` → 展示表单让用户填写
|
|
306
|
+
3. `POST /:providerID/oauth/authorize` 获取授权 URL
|
|
307
|
+
4. 根据 `method` 字段:
|
|
308
|
+
- `"code"` → 用户从浏览器复制 code → `POST /:providerID/oauth/callback({ code })`
|
|
309
|
+
- `"auto"` → 自动回调 → `POST /:providerID/oauth/callback`(无 code)
|
|
310
|
+
5. 回调成功后 OpenCode 自动写入 auth.json
|
|
311
|
+
|
|
312
|
+
绝大多数用户(Anthropic、OpenAI 等)只需 API Key 方式,不走 OAuth。
|
package/docs/question-asked.md
CHANGED
|
@@ -3,11 +3,28 @@
|
|
|
3
3
|
## 背景
|
|
4
4
|
|
|
5
5
|
OpenCode 的 `UserQuestionTool` 允许 agent 向用户提问并阻塞等待回答。典型场景:
|
|
6
|
+
|
|
6
7
|
- agent 需要用户确认技术选型(React vs Vue)
|
|
7
8
|
- agent 需要用户提供额外信息
|
|
8
9
|
- agent 遇到歧义需要用户裁决
|
|
9
10
|
|
|
10
|
-
**问题**:OpenCode 内置 ACP
|
|
11
|
+
**问题**:OpenCode 内置 ACP 忽略了 `question.asked` SSE 事件,导致 agent 调用 UserQuestionTool 时 Deferred promise 永远不 resolve,agent 挂起。
|
|
12
|
+
|
|
13
|
+
opencode question.asked流程:
|
|
14
|
+
|
|
15
|
+
1. Agent 调用 QuestionTool → Question.ask() → 创建 deferred,发布 question.asked 事件到 bus → await deferred
|
|
16
|
+
2. TUI 通过 SSE 接收 question.asked 事件,向用户显示问题
|
|
17
|
+
3. 用户回答 → TUI 调用 HTTP POST /question/:requestID/reply → Question.reply() → 解析 deferred
|
|
18
|
+
4. Agent 继续
|
|
19
|
+
|
|
20
|
+
opencode acp 缺少对 "question.asked" 的处理。 当 agent 通过 UserQuestionTool (即 QuestionTool 在 src/tool/question.ts 中) 调用 Question.ask() 时:
|
|
21
|
+
|
|
22
|
+
1. Question.ask() 创建一个 Deferred,发布 question.asked 到总线,并 await 延迟处理
|
|
23
|
+
2. runEventSubscription() 通过全局 SSE 流接收到事件
|
|
24
|
+
3. handleEvent() 调用并使用 switch(event.type) — 没有 case "question.asked" 分支
|
|
25
|
+
4. 该事件被静默丢弃
|
|
26
|
+
5. ACP 客户端 (例如 Claude Code、Copilot 等) 从未收到关于问题的通知,无法显示 UI,也无法调用 POST /question/:requestID/reply
|
|
27
|
+
6. Deferred 永远不会解决 → agent 永久挂起
|
|
11
28
|
|
|
12
29
|
**解决方案**:Harmony-ACP 拦截 `question.asked` SSE 事件,通过 ACP 扩展机制 `extMethod("questionAsked")` 转发给客户端,用户回答后通过 OpenCode SDK 回复,解除 agent 阻塞。
|
|
13
30
|
|
|
@@ -15,12 +32,12 @@ OpenCode 的 `UserQuestionTool` 允许 agent 向用户提问并阻塞等待回
|
|
|
15
32
|
|
|
16
33
|
ACP 协议唯一的 agent→client request-response 机制是 `requestPermission`,但它只支持预定义选项(optionId),不支持自由文本。`extMethod` 的参数和返回值都是 `Record<string, unknown>`,天然支持任意结构。
|
|
17
34
|
|
|
18
|
-
| 方面
|
|
19
|
-
|
|
20
|
-
| 返回值
|
|
21
|
-
| 自由文本 | 不支持
|
|
22
|
-
| 多问题
|
|
23
|
-
| 语义
|
|
35
|
+
| 方面 | requestPermission | extMethod("questionAsked") |
|
|
36
|
+
| -------- | -------------------- | ------------------------------- |
|
|
37
|
+
| 返回值 | 只有 optionId | 自由结构,直接返回 answers 数组 |
|
|
38
|
+
| 自由文本 | 不支持 | 天然支持 |
|
|
39
|
+
| 多问题 | 需 hack 合并 | 一次性传递完整问题列表 |
|
|
40
|
+
| 语义 | 扭曲 permission 语义 | 干净的 question 语义 |
|
|
24
41
|
|
|
25
42
|
## 完整流程
|
|
26
43
|
|
|
@@ -244,14 +261,14 @@ OpenCode → Question.Service.reject()
|
|
|
244
261
|
await sdk.question.reply({
|
|
245
262
|
requestID: "q_1",
|
|
246
263
|
directory: "/path/to/project",
|
|
247
|
-
answers: [["React"]]
|
|
248
|
-
})
|
|
264
|
+
answers: [["React"]], // QuestionAnswer = Array<string>
|
|
265
|
+
});
|
|
249
266
|
|
|
250
267
|
// 拒绝
|
|
251
268
|
await sdk.question.reject({
|
|
252
269
|
requestID: "q_1",
|
|
253
|
-
directory: "/path/to/project"
|
|
254
|
-
})
|
|
270
|
+
directory: "/path/to/project",
|
|
271
|
+
});
|
|
255
272
|
```
|
|
256
273
|
|
|
257
274
|
## 序列化设计
|
|
@@ -278,10 +295,10 @@ question 2 到达 → questionQueues["ses_1"] = Promise<处理 Q1>.then(() =>
|
|
|
278
295
|
|
|
279
296
|
## 涉及的文件
|
|
280
297
|
|
|
281
|
-
| 文件
|
|
282
|
-
|
|
283
|
-
| `src/event-handler.ts`
|
|
284
|
-
| `scripts/view-log.mjs`
|
|
285
|
-
| `vscode-acp/src/handlers/QuestionHandler.ts` | 新建
|
|
286
|
-
| `vscode-acp/src/core/AcpClientImpl.ts`
|
|
287
|
-
| `vscode-acp/src/core/ConnectionManager.ts`
|
|
298
|
+
| 文件 | 改动 | 作用 |
|
|
299
|
+
| -------------------------------------------- | --------------------------------------------- | -------------------------------------------- |
|
|
300
|
+
| `src/event-handler.ts` | 新增 `question.asked` case + `questionQueues` | 拦截 SSE 事件、调用 extMethod、回复 OpenCode |
|
|
301
|
+
| `scripts/view-log.mjs` | 新增 `question.asked/reply/rejected` 格式化 | 日志查看器展示问题流程 |
|
|
302
|
+
| `vscode-acp/src/handlers/QuestionHandler.ts` | 新建 | VSCode 客户端问题 UI(QuickPick / InputBox) |
|
|
303
|
+
| `vscode-acp/src/core/AcpClientImpl.ts` | 新增 `extMethod()` | 客户端 extMethod 路由 |
|
|
304
|
+
| `vscode-acp/src/core/ConnectionManager.ts` | 创建 QuestionHandler | 注入 handler |
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# 方案:将 Session 统计信息暴露给 harmony-acp 并呈现到 VSCode
|
|
2
|
+
|
|
3
|
+
> 日期:2026-04-29
|
|
4
|
+
> 状态:方案设计
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
OpenCode session 表已持久化 `summary_additions`/`summary_deletions`/`summary_files`/`summary_diffs`,SDK 类型已包含 `summary` 字段。但 harmony-acp 目前只通过 `usage_update` 发送 token/cost,未暴露代码行变更信息。
|
|
11
|
+
|
|
12
|
+
**目标:** VSCode 能实时/加载时看到 session 的代码修改统计(additions/deletions/files)。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 架构决策:`usage_update._meta` 扩展
|
|
17
|
+
|
|
18
|
+
选择 `usage_update._meta` 而非 `session_info_update`,理由:
|
|
19
|
+
|
|
20
|
+
1. **语义一致** — 代码行变更与 token/cost 同属"资源消耗"维度,`usage_update` 即"这次消耗了什么"
|
|
21
|
+
2. **减少事件流** — 不额外增加通知,VSCode 一个回调拿到所有统计,避免 UI 抖动
|
|
22
|
+
3. **`_meta` 为扩展设计** — ACP 协议文档明确 `_meta` 用于附加数据
|
|
23
|
+
4. **时序对齐** — token 和代码变更在同一事件中,无中间态
|
|
24
|
+
5. **文件级 diff 已有通道** — edit tool 的 tool_call_update 已返回 diff,无需重复传输
|
|
25
|
+
|
|
26
|
+
**数据格式:**
|
|
27
|
+
```typescript
|
|
28
|
+
usage_update._meta.codeChange = {
|
|
29
|
+
additions: number
|
|
30
|
+
deletions: number
|
|
31
|
+
files: number
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 修改范围
|
|
38
|
+
|
|
39
|
+
仅修改 harmony-acp,**不改 OpenCode、不改 ACP 协议**:
|
|
40
|
+
|
|
41
|
+
- OpenCode 已有 `session.diff` SSE 事件 + `EventSessionDiff` SDK 类型
|
|
42
|
+
- ACP `UsageUpdate._meta` 是 `{ [key: string]: unknown }` 扩展字段
|
|
43
|
+
- harmony-acp 已在 `_meta` 传递 `parentCost`/`childCost`,模式一致
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 数据流
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
51
|
+
│ OpenCode Server │
|
|
52
|
+
│ │
|
|
53
|
+
│ Session 完成时 → SSE 事件 session.diff {sessionID, diff[]} │
|
|
54
|
+
│ Session 加载时 → API session.get() 返回 summary 字段 │
|
|
55
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
56
|
+
│ SSE / HTTP
|
|
57
|
+
▼
|
|
58
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
59
|
+
│ harmony-acp (EventHandler) │
|
|
60
|
+
│ │
|
|
61
|
+
│ 1. 监听 session.diff → 聚合 additions/deletions/files │
|
|
62
|
+
│ 2. 存入 diffStats Map<sessionID, CodeChangeStats> │
|
|
63
|
+
│ 3. loadSession 时从 session.summary 回填 │
|
|
64
|
+
│ 4. sendUsageUpdate() 时读取并附加到 _meta.codeChange │
|
|
65
|
+
└───────────────────────────┬─────────────────────────────────────┘
|
|
66
|
+
│ ACP JSON-RPC (usage_update)
|
|
67
|
+
▼
|
|
68
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
69
|
+
│ VSCode Extension │
|
|
70
|
+
│ │
|
|
71
|
+
│ usage_update._meta.codeChange = { additions, deletions, files }│
|
|
72
|
+
│ → 状态栏展示 "+123 -45 (3 files)" │
|
|
73
|
+
│ → Session 面板展示详细统计 │
|
|
74
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 实现步骤
|
|
80
|
+
|
|
81
|
+
### Step 1: 新增 CodeChangeStats 类型
|
|
82
|
+
|
|
83
|
+
**文件:** `src/types.ts`
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
export interface CodeChangeStats {
|
|
87
|
+
additions: number
|
|
88
|
+
deletions: number
|
|
89
|
+
files: number
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Step 2: EventHandler 监听 `session.diff` 事件
|
|
94
|
+
|
|
95
|
+
**文件:** `src/event-handler.ts`
|
|
96
|
+
|
|
97
|
+
在 `handleEvent()` 的 switch 中新增 `session.diff` 分支:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
case "session.diff": {
|
|
101
|
+
const { sessionID, diff } = event.properties
|
|
102
|
+
const stats = diff.reduce(
|
|
103
|
+
(acc, d) => ({
|
|
104
|
+
additions: acc.additions + d.additions,
|
|
105
|
+
deletions: acc.deletions + d.deletions,
|
|
106
|
+
files: acc.files + 1,
|
|
107
|
+
}),
|
|
108
|
+
{ additions: 0, deletions: 0, files: 0 },
|
|
109
|
+
)
|
|
110
|
+
this.diffStats.set(sessionID, stats)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
新增实例变量和方法:
|
|
116
|
+
- `diffStats: Map<string, CodeChangeStats>` — 存储 session 级 diff 聚合
|
|
117
|
+
- `getDiffStats(sessionId)` — 供 agent.ts 读取
|
|
118
|
+
- `setDiffStats(sessionId, stats)` — 供 agent.ts 回填历史数据
|
|
119
|
+
- `clearDiffStats(sessionId)` — session 结束时清理
|
|
120
|
+
|
|
121
|
+
### Step 3: `sendUsageUpdate()` 附加 codeChange
|
|
122
|
+
|
|
123
|
+
**文件:** `src/agent.ts` 的 `sendUsageUpdate()` 方法
|
|
124
|
+
|
|
125
|
+
在现有 `_meta` 构造中增加 `codeChange` 字段:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const stats = this.eventHandler.getDiffStats(sessionId)
|
|
129
|
+
// 聚合子 session 的 diff
|
|
130
|
+
let childStats: CodeChangeStats | undefined
|
|
131
|
+
for (const childId of children) {
|
|
132
|
+
const cs = this.eventHandler.getDiffStats(childId)
|
|
133
|
+
if (cs) {
|
|
134
|
+
childStats = childStats
|
|
135
|
+
? { additions: childStats.additions + cs.additions, deletions: childStats.deletions + cs.deletions, files: childStats.files + cs.files }
|
|
136
|
+
: cs
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const totalStats = stats || childStats
|
|
141
|
+
? {
|
|
142
|
+
additions: (stats?.additions ?? 0) + (childStats?.additions ?? 0),
|
|
143
|
+
deletions: (stats?.deletions ?? 0) + (childStats?.deletions ?? 0),
|
|
144
|
+
files: (stats?.files ?? 0) + (childStats?.files ?? 0),
|
|
145
|
+
}
|
|
146
|
+
: undefined
|
|
147
|
+
|
|
148
|
+
// 在 _meta 中附加
|
|
149
|
+
_meta: {
|
|
150
|
+
...existingMeta,
|
|
151
|
+
...totalStats && { codeChange: totalStats },
|
|
152
|
+
...childStats && {
|
|
153
|
+
childCodeChange: childStats,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Step 4: 加载历史 session 时读取 summary
|
|
159
|
+
|
|
160
|
+
**文件:** `src/agent.ts`
|
|
161
|
+
|
|
162
|
+
在 `loadSession()` 和 `unstable_resumeSession()` 中:
|
|
163
|
+
|
|
164
|
+
这两个方法已通过 `sdk.session.get()` 获取 session 数据。SDK 返回的 `Session` 类型包含 `summary?: { additions, deletions, files, diffs }`。
|
|
165
|
+
|
|
166
|
+
从返回数据中提取 summary,存入 EventHandler 的 `diffStats` map:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const session = await this.sdk.session.get({ sessionID, directory })
|
|
170
|
+
if (session.data?.summary) {
|
|
171
|
+
this.eventHandler.setDiffStats(sessionID, {
|
|
172
|
+
additions: session.data.summary.additions,
|
|
173
|
+
deletions: session.data.summary.deletions,
|
|
174
|
+
files: session.data.summary.files,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
这样已有的 `sendUsageUpdate()` 调用会自动携带历史统计数据。
|
|
180
|
+
|
|
181
|
+
### Step 5: Session 清理
|
|
182
|
+
|
|
183
|
+
**文件:** `src/event-handler.ts`
|
|
184
|
+
|
|
185
|
+
在子 session 完成(`sendChildSessionCompleted`)和 session 关闭时,清理 `diffStats` 中对应条目避免内存泄漏。
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 涉及文件
|
|
190
|
+
|
|
191
|
+
| 文件 | 修改内容 |
|
|
192
|
+
|------|---------|
|
|
193
|
+
| `src/types.ts` | 新增 `CodeChangeStats` 接口 |
|
|
194
|
+
| `src/event-handler.ts` | 新增 `session.diff` 事件处理 + `diffStats` map + get/set/clear 方法 |
|
|
195
|
+
| `src/agent.ts` | `sendUsageUpdate()` 附加 `_meta.codeChange`;`loadSession()`/`resumeSession()` 读取 `session.summary` |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 依赖的 OpenCode 现有能力
|
|
200
|
+
|
|
201
|
+
| 能力 | 来源 | 说明 |
|
|
202
|
+
|------|------|------|
|
|
203
|
+
| `session.diff` SSE 事件 | OpenCode SDK `EventSessionDiff` | session 代码变更时实时推送 `SnapshotFileDiff[]` |
|
|
204
|
+
| `Session.summary` 字段 | OpenCode SDK `Session` 类型 | 含 `additions`/`deletions`/`files`/`diffs` |
|
|
205
|
+
| `UsageUpdate._meta` | ACP 协议 | `{ [key: string]: unknown }` 扩展字段 |
|
|
206
|
+
| 已有 `_meta` 扩展模式 | harmony-acp `sendUsageUpdate()` | 已传递 `parentCost`/`childCost`/`childSessionCount` |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 验证
|
|
211
|
+
|
|
212
|
+
1. 启动 OpenCode server(`bun dev serve`)
|
|
213
|
+
2. 启动 harmony-acp 连接 VSCode
|
|
214
|
+
3. 创建 session,让 agent 修改几个文件
|
|
215
|
+
4. 检查 VSCode 收到的 `usage_update._meta.codeChange` 是否正确
|
|
216
|
+
5. `loadSession()` 加载已有 session,验证历史 summary 也正确传递
|
|
217
|
+
6. 创建含子 session 的任务,验证子 session 的 diff 也被聚合
|