@harmonyos-arkts/opencode-acp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -0
- package/README.zh.md +223 -0
- package/dist/index.cjs +21010 -0
- package/dist/index.cjs.map +7 -0
- package/docs/question-asked.md +287 -0
- package/docs/subagent-visibility.md +170 -0
- package/docs/tui-vs-acp-analysis.md +61 -0
- package/package.json +38 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# 问题回答(UserQuestionTool)实现说明
|
|
2
|
+
|
|
3
|
+
## 背景
|
|
4
|
+
|
|
5
|
+
OpenCode 的 `UserQuestionTool` 允许 agent 向用户提问并阻塞等待回答。典型场景:
|
|
6
|
+
- agent 需要用户确认技术选型(React vs Vue)
|
|
7
|
+
- agent 需要用户提供额外信息
|
|
8
|
+
- agent 遇到歧义需要用户裁决
|
|
9
|
+
|
|
10
|
+
**问题**:OpenCode 内置 ACP 和 Harmony-ACP 原本都忽略了 `question.asked` SSE 事件,导致 agent 调用 UserQuestionTool 时 Deferred promise 永远不 resolve,agent 挂起。
|
|
11
|
+
|
|
12
|
+
**解决方案**:Harmony-ACP 拦截 `question.asked` SSE 事件,通过 ACP 扩展机制 `extMethod("questionAsked")` 转发给客户端,用户回答后通过 OpenCode SDK 回复,解除 agent 阻塞。
|
|
13
|
+
|
|
14
|
+
## 为什么用 extMethod 而不是 requestPermission
|
|
15
|
+
|
|
16
|
+
ACP 协议唯一的 agent→client request-response 机制是 `requestPermission`,但它只支持预定义选项(optionId),不支持自由文本。`extMethod` 的参数和返回值都是 `Record<string, unknown>`,天然支持任意结构。
|
|
17
|
+
|
|
18
|
+
| 方面 | requestPermission | extMethod("questionAsked") |
|
|
19
|
+
|------|-------------------|---------------------------|
|
|
20
|
+
| 返回值 | 只有 optionId | 自由结构,直接返回 answers 数组 |
|
|
21
|
+
| 自由文本 | 不支持 | 天然支持 |
|
|
22
|
+
| 多问题 | 需 hack 合并 | 一次性传递完整问题列表 |
|
|
23
|
+
| 语义 | 扭曲 permission 语义 | 干净的 question 语义 |
|
|
24
|
+
|
|
25
|
+
## 完整流程
|
|
26
|
+
|
|
27
|
+
### 数据流总览
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
OpenCode Harmony-ACP ACP Client
|
|
31
|
+
──────── ─────────── ──────────
|
|
32
|
+
|
|
33
|
+
agent 调用 UserQuestionTool
|
|
34
|
+
│
|
|
35
|
+
├── Question.Service.ask()
|
|
36
|
+
│ 创建 Deferred promise
|
|
37
|
+
│ 发布 "question.asked" SSE 事件
|
|
38
|
+
│ await Deferred(阻塞)
|
|
39
|
+
│
|
|
40
|
+
├── SSE: question.asked ───────► EventHandler.handleEvent()
|
|
41
|
+
│ │
|
|
42
|
+
│ ├── resolveSession(q.sessionID)
|
|
43
|
+
│ ├── ocEvent("question.asked", ...)
|
|
44
|
+
│ │
|
|
45
|
+
│ ├── connection.extMethod("questionAsked", {
|
|
46
|
+
│ │ sessionId,
|
|
47
|
+
│ │ questionId: "q_1",
|
|
48
|
+
│ │ questions: [{ question, header, options, ... }]
|
|
49
|
+
│ │ }) ─────────────────────────────────────►
|
|
50
|
+
│ │ │
|
|
51
|
+
│ │ │ 识别 questionAsked
|
|
52
|
+
│ │ │ 渲染问题 UI
|
|
53
|
+
│ │ │ 等待用户输入
|
|
54
|
+
│ │ ◄─────────────────────────────────────── ┤
|
|
55
|
+
│ │ { answers: [["React"], ["自定义文本"]] }
|
|
56
|
+
│ │
|
|
57
|
+
│ ├── sdk.question.reply({ requestID, answers })
|
|
58
|
+
│ │
|
|
59
|
+
◄── Deferred resolve,agent 继续 ──┘
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 阶段 1:Agent 发起提问
|
|
63
|
+
|
|
64
|
+
agent 调用 `UserQuestionTool`,OpenCode 内部流程:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
agent → UserQuestionTool.execute(params)
|
|
68
|
+
→ Question.Service.ask({
|
|
69
|
+
sessionID: "ses_root123",
|
|
70
|
+
questions: [{
|
|
71
|
+
question: "Which framework?",
|
|
72
|
+
header: "Framework",
|
|
73
|
+
options: [
|
|
74
|
+
{ label: "React", description: "React framework" },
|
|
75
|
+
{ label: "Vue", description: "Vue framework" }
|
|
76
|
+
],
|
|
77
|
+
multiple: false,
|
|
78
|
+
custom: true
|
|
79
|
+
}]
|
|
80
|
+
})
|
|
81
|
+
→ 生成 QuestionID (如 "q_1")
|
|
82
|
+
→ 创建 Deferred<Answer[], RejectedError>
|
|
83
|
+
→ GlobalBus 发布 "question.asked" 事件
|
|
84
|
+
→ await Deferred ← 阻塞在这里
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 阶段 2:Harmony-ACP 拦截并转发
|
|
88
|
+
|
|
89
|
+
`EventHandler.handleEvent()` 收到 SSE 事件,处理 `"question.asked"` case:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// src/event-handler.ts
|
|
93
|
+
|
|
94
|
+
case "question.asked": {
|
|
95
|
+
const q = event.properties // { id, sessionID, questions }
|
|
96
|
+
|
|
97
|
+
// 1. 解析 session 路由(子 session 也能提问)
|
|
98
|
+
const resolved = this.resolveSession(q.sessionID)
|
|
99
|
+
if (!resolved) return
|
|
100
|
+
|
|
101
|
+
// 2. 记录日志
|
|
102
|
+
ocEvent("question.asked", { id: q.id, questions: q.questions.length })
|
|
103
|
+
|
|
104
|
+
// 3. 序列化:同一 session 的多个 question 依次处理
|
|
105
|
+
const prev = this.questionQueues.get(q.sessionID) ?? Promise.resolve()
|
|
106
|
+
const next = prev.then(async () => {
|
|
107
|
+
|
|
108
|
+
// 4. 通过 extMethod 发给 ACP 客户端
|
|
109
|
+
const res = await this.connection
|
|
110
|
+
.extMethod("questionAsked", {
|
|
111
|
+
sessionId,
|
|
112
|
+
questionId: q.id,
|
|
113
|
+
questions: q.questions,
|
|
114
|
+
})
|
|
115
|
+
.catch(() => undefined)
|
|
116
|
+
|
|
117
|
+
// 5a. 客户端无响应 → reject
|
|
118
|
+
if (!res || !res.answers) {
|
|
119
|
+
await this.sdk.question.reject({ requestID: q.id, directory })
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5b. 客户端返回答案 → reply
|
|
124
|
+
await this.sdk.question.reply({
|
|
125
|
+
requestID: q.id,
|
|
126
|
+
directory,
|
|
127
|
+
answers: res.answers, // Array<Array<string>>
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// 6. 更新队列
|
|
132
|
+
this.questionQueues.set(q.sessionID, next)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 阶段 3:ACP 客户端处理
|
|
137
|
+
|
|
138
|
+
客户端实现 `extMethod` handler(以 vscode-acp 为例):
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// vscode-acp/src/core/AcpClientImpl.ts
|
|
142
|
+
|
|
143
|
+
async extMethod(method: string, params: Record<string, unknown>) {
|
|
144
|
+
if (method === "questionAsked") {
|
|
145
|
+
return this.questionHandler.handleQuestionAsked(params)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// vscode-acp/src/handlers/QuestionHandler.ts
|
|
152
|
+
|
|
153
|
+
async handleQuestionAsked(params) {
|
|
154
|
+
const { questions } = params
|
|
155
|
+
|
|
156
|
+
for (const q of questions) {
|
|
157
|
+
if (q.options.length > 0) {
|
|
158
|
+
// 有选项 → QuickPick(支持多选)
|
|
159
|
+
const selected = await vscode.window.showQuickPick(items, { canPickMany: q.multiple })
|
|
160
|
+
answers.push(selected.map(s => s.label))
|
|
161
|
+
} else {
|
|
162
|
+
// 自由文本 → InputBox
|
|
163
|
+
const answer = await vscode.window.showInputBox({ prompt: q.question })
|
|
164
|
+
answers.push([answer])
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { answers } // [["React"]] 或 [["自定义文本"]]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 阶段 4:回复 OpenCode 解除阻塞
|
|
173
|
+
|
|
174
|
+
Harmony-ACP 调用 `sdk.question.reply()`:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Harmony-ACP → OpenCode HTTP API: POST /question/q_1/reply
|
|
178
|
+
Body: { answers: [["React"]] }
|
|
179
|
+
|
|
180
|
+
OpenCode → Question.Service.reply()
|
|
181
|
+
→ Deferred.resolve([["React"]])
|
|
182
|
+
→ agent 收到回答,继续执行
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
或者如果用户取消,调用 `sdk.question.reject()`:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Harmony-ACP → OpenCode HTTP API: POST /question/q_1/reject
|
|
189
|
+
|
|
190
|
+
OpenCode → Question.Service.reject()
|
|
191
|
+
→ Deferred.reject(RejectedError)
|
|
192
|
+
→ agent 收到错误,处理取消逻辑
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 事件格式
|
|
196
|
+
|
|
197
|
+
### question.asked(SSE 事件,OpenCode → Harmony-ACP)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
{
|
|
201
|
+
type: "question.asked",
|
|
202
|
+
properties: {
|
|
203
|
+
id: "q_1", // QuestionID
|
|
204
|
+
sessionID: "ses_root123", // 哪个 session 在问
|
|
205
|
+
questions: [{
|
|
206
|
+
question: "Which framework do you want to use?", // 完整问题文本
|
|
207
|
+
header: "Framework", // 短标题(≤30 字符)
|
|
208
|
+
options: [ // 可选项
|
|
209
|
+
{ label: "React", description: "React framework" },
|
|
210
|
+
{ label: "Vue", description: "Vue framework" }
|
|
211
|
+
],
|
|
212
|
+
multiple: false, // 可多选
|
|
213
|
+
custom: true // 允许自由文本(默认 true)
|
|
214
|
+
}],
|
|
215
|
+
tool?: { // 关联的 tool call(可选)
|
|
216
|
+
messageID: "msg_abc",
|
|
217
|
+
callID: "call_xyz"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### extMethod("questionAsked")(Harmony-ACP → ACP Client)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// 请求
|
|
227
|
+
{
|
|
228
|
+
sessionId: "ses_root123",
|
|
229
|
+
questionId: "q_1",
|
|
230
|
+
questions: [{ question, header, options, multiple, custom }]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 响应(用户回答)
|
|
234
|
+
{ answers: [["React"]] }
|
|
235
|
+
|
|
236
|
+
// 响应(用户取消或无响应)
|
|
237
|
+
// extMethod 抛出错误或返回 undefined
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### sdk.question.reply(Harmony-ACP → OpenCode)
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// 回答
|
|
244
|
+
await sdk.question.reply({
|
|
245
|
+
requestID: "q_1",
|
|
246
|
+
directory: "/path/to/project",
|
|
247
|
+
answers: [["React"]] // QuestionAnswer = Array<string>
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// 拒绝
|
|
251
|
+
await sdk.question.reject({
|
|
252
|
+
requestID: "q_1",
|
|
253
|
+
directory: "/path/to/project"
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 序列化设计
|
|
258
|
+
|
|
259
|
+
同一 session 可能连续触发多个 question。`questionQueues` 确保它们按序处理:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
private questionQueues = new Map<string, Promise<void>>()
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
question 1 到达 → questionQueues["ses_1"] = Promise<处理 Q1>
|
|
267
|
+
│
|
|
268
|
+
├── Q1 处理中,客户端等待用户输入
|
|
269
|
+
│
|
|
270
|
+
question 2 到达 → questionQueues["ses_1"] = Promise<处理 Q1>.then(() => 处理 Q2)
|
|
271
|
+
│
|
|
272
|
+
├── Q1 完成 → 开始处理 Q2
|
|
273
|
+
│
|
|
274
|
+
└── Q2 完成 → questionQueues 删除 "ses_1"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
这与 permission 处理的 `permissionQueues` 使用相同模式,防止同一 session 的多个交互请求并发导致状态混乱。
|
|
278
|
+
|
|
279
|
+
## 涉及的文件
|
|
280
|
+
|
|
281
|
+
| 文件 | 改动 | 作用 |
|
|
282
|
+
|------|------|------|
|
|
283
|
+
| `src/event-handler.ts` | 新增 `question.asked` case + `questionQueues` | 拦截 SSE 事件、调用 extMethod、回复 OpenCode |
|
|
284
|
+
| `scripts/view-log.mjs` | 新增 `question.asked/reply/rejected` 格式化 | 日志查看器展示问题流程 |
|
|
285
|
+
| `vscode-acp/src/handlers/QuestionHandler.ts` | 新建 | VSCode 客户端问题 UI(QuickPick / InputBox) |
|
|
286
|
+
| `vscode-acp/src/core/AcpClientImpl.ts` | 新增 `extMethod()` | 客户端 extMethod 路由 |
|
|
287
|
+
| `vscode-acp/src/core/ConnectionManager.ts` | 创建 QuestionHandler | 注入 handler |
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# 子会话可见性(Subagent Visibility)实现说明
|
|
2
|
+
|
|
3
|
+
## 背景
|
|
4
|
+
|
|
5
|
+
OpenCode 的 agent 可以通过 Task 工具创建子 agent(subagent)来并行处理子任务。每个子 agent 有独立的 session,通过 SSE 事件流暴露出来。
|
|
6
|
+
|
|
7
|
+
**问题**:OpenCode 内置的 ACP agent 只注册通过 `newSession`/`loadSession` 显式创建的顶层 session。子 session 未注册在 SessionManager 中,导致其事件(消息、工具调用等)被静默丢弃,ACP 客户端完全看不到子 agent 的存在。
|
|
8
|
+
|
|
9
|
+
**Harmony-ACP 的解决方案**:"独立虚拟会话"策略 — 自动发现子 session,注册到 SessionManager,为其分配独立的 sessionId 通知客户端,使客户端能区分父/子消息。
|
|
10
|
+
|
|
11
|
+
## 核心数据结构
|
|
12
|
+
|
|
13
|
+
### SessionState(src/types.ts)
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
export interface SessionState {
|
|
17
|
+
id: string
|
|
18
|
+
cwd: string
|
|
19
|
+
parentID?: string // 子 session 有,顶层 session 没有
|
|
20
|
+
title?: string
|
|
21
|
+
mcpServers: McpServer[]
|
|
22
|
+
createdAt: Date
|
|
23
|
+
model?: { providerID: string; modelID: string }
|
|
24
|
+
variant?: string
|
|
25
|
+
modeId?: string
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### SessionManager 内部索引(src/session-manager.ts)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
class SessionManager {
|
|
33
|
+
private sessions = new Map<string, SessionState>() // sessionId → state
|
|
34
|
+
private children = new Map<string, Set<string>>() // parentId → childIds
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
两个 Map 维护完整的会话树:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
sessions:
|
|
42
|
+
"ses_root123" → { id: "ses_root123", parentID: undefined }
|
|
43
|
+
"ses_child456" → { id: "ses_child456", parentID: "ses_root123" }
|
|
44
|
+
"ses_child789" → { id: "ses_child789", parentID: "ses_root123" }
|
|
45
|
+
|
|
46
|
+
children:
|
|
47
|
+
"ses_root123" → Set { "ses_child456", "ses_child789" }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 完整流程
|
|
51
|
+
|
|
52
|
+
### 阶段 1:顶层 Session 创建
|
|
53
|
+
|
|
54
|
+
用户在 ACP 客户端发起 `session/new`:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
ACP Client Harmony-ACP OpenCode Server
|
|
58
|
+
───────── ──────────── ────────────────
|
|
59
|
+
session/new ──────────► Agent.newSession()
|
|
60
|
+
│
|
|
61
|
+
├── SessionManager.create()
|
|
62
|
+
│ └── sdk.session.create() ──► 创建 session
|
|
63
|
+
│ ◄── { id: "ses_root123" }
|
|
64
|
+
│
|
|
65
|
+
├── sessions.set("ses_root123", { ... })
|
|
66
|
+
│
|
|
67
|
+
└── response ─────────────────► { sessionId: "ses_root123" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
此时 `sessions` 中只有顶层 session,`children` 为空。
|
|
71
|
+
|
|
72
|
+
### 阶段 2:子 Session 自动发现
|
|
73
|
+
|
|
74
|
+
Agent 执行 Task 工具创建子 agent。OpenCode 通过 SSE 广播 `session.created` 事件:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
OpenCode Server ── SSE: session.created ──► EventHandler.handleEvent()
|
|
78
|
+
{ type: "session.created", │
|
|
79
|
+
properties: { │
|
|
80
|
+
info: { ├── 检查 parentID 存在 → 是子 session
|
|
81
|
+
id: "ses_child456", │
|
|
82
|
+
parentID: "ses_root123", ├── SessionManager.registerDiscovered()
|
|
83
|
+
title: "Explore project" │ ├── 创建 SessionState { parentID: "ses_root123" }
|
|
84
|
+
} │ ├── sessions.set("ses_child456", ...)
|
|
85
|
+
} │ └── children.get("ses_root123").add("ses_child456")
|
|
86
|
+
} │
|
|
87
|
+
├── announceChildSession()
|
|
88
|
+
│ └── sendToClient({
|
|
89
|
+
│ sessionId: "ses_child456", ← 子 session 自己的 ID
|
|
90
|
+
│ sessionUpdate: "session_info_update",
|
|
91
|
+
│ title: "Explore project",
|
|
92
|
+
│ _meta: {
|
|
93
|
+
│ parentSessionId: "ses_root123",
|
|
94
|
+
│ isSubagent: true
|
|
95
|
+
│ }
|
|
96
|
+
│ })
|
|
97
|
+
│
|
|
98
|
+
ACP Client ◄── session_info_update ────────────────┘
|
|
99
|
+
(sessionId = "ses_child456")
|
|
100
|
+
客户端看到一个新的虚拟 session,
|
|
101
|
+
通过 _meta.parentSessionId 关联到父 session
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
关键设计:
|
|
105
|
+
- 子 session 用**自己的 sessionId** 发送 `session_info_update`,不是用父 session 的
|
|
106
|
+
- `_meta.parentSessionId` 让客户端知道它是子 session
|
|
107
|
+
- `_meta.isSubagent = true` 标记身份
|
|
108
|
+
|
|
109
|
+
### 阶段 3:子 Session 事件路由
|
|
110
|
+
|
|
111
|
+
子 agent 产生消息、工具调用等事件时,SSE 事件携带子 session 的 sessionID。`resolveSession()` 负责路由:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
SSE 事件: message.part.delta { sessionID: "ses_child456", ... }
|
|
115
|
+
│
|
|
116
|
+
▼
|
|
117
|
+
resolveSession("ses_child456")
|
|
118
|
+
│
|
|
119
|
+
├── sessions.get("ses_child456") → 命中!返回 { sessionId: "ses_child456", cwd: "..." }
|
|
120
|
+
│
|
|
121
|
+
└── sendToClient({ sessionId: "ses_child456", update: { agent_message_chunk ... } })
|
|
122
|
+
│
|
|
123
|
+
▼
|
|
124
|
+
ACP Client 收到 sessionId="ses_child456" 的消息,
|
|
125
|
+
知道这是子 agent 的输出,可以渲染在对应位置
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 阶段 4:多层嵌套处理
|
|
129
|
+
|
|
130
|
+
子 agent 还可以创建孙 agent。`findRootSession()` 沿 parent 链向上递归查找:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
resolveSession("ses_grandchild")
|
|
134
|
+
│
|
|
135
|
+
├── sessions.get("ses_grandchild") → 命中!直接返回
|
|
136
|
+
│ (registerDiscovered 已经注册过了)
|
|
137
|
+
│
|
|
138
|
+
└── 返回 { sessionId: "ses_grandchild", cwd: "..." }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
如果中间某个节点未被注册(理论上不应发生),`findRootSession()` 会沿 parent 链向上查找:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
findRootSession("ses_grandchild")
|
|
145
|
+
→ sessions.get("ses_grandchild") → { parentID: "ses_child456" }
|
|
146
|
+
→ findRootSession("ses_child456")
|
|
147
|
+
→ sessions.get("ses_child456") → { parentID: "ses_root123" }
|
|
148
|
+
→ findRootSession("ses_root123")
|
|
149
|
+
→ sessions.get("ses_root123") → { parentID: undefined } → 返回 ses_root123
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 关键方法说明
|
|
153
|
+
|
|
154
|
+
| 方法 | 文件 | 作用 |
|
|
155
|
+
|------|------|------|
|
|
156
|
+
| `SessionManager.registerDiscovered()` | session-manager.ts:97 | 自动注册 SSE 发现的子 session,建立 parent-child 索引 |
|
|
157
|
+
| `SessionManager.findRootSession()` | session-manager.ts:123 | 递归向上查找顶层 session(沿 parent 链) |
|
|
158
|
+
| `SessionManager.getRelatedSessions()` | session-manager.ts:135 | 获取 session 子树所有 ID(含自身) |
|
|
159
|
+
| `EventHandler.resolveSession()` | event-handler.ts:169 | 路由事件:直接查找 → 向上查找 → 返回 sessionId 和 cwd |
|
|
160
|
+
| `EventHandler.announceChildSession()` | event-handler.ts:421 | 向 ACP 客户端发送 session_info_update 通知子 session |
|
|
161
|
+
|
|
162
|
+
## 与 OpenCode 内置 ACP 的对比
|
|
163
|
+
|
|
164
|
+
| 方面 | OpenCode 内置 ACP | Harmony-ACP |
|
|
165
|
+
|------|-------------------|-------------|
|
|
166
|
+
| 顶层 session 注册 | ✅ newSession/loadSession 时注册 | ✅ 同上 |
|
|
167
|
+
| 子 session 注册 | ❌ 不注册,事件丢弃 | ✅ SSE 事件自动发现并注册 |
|
|
168
|
+
| parent-child 索引 | ❌ 无 | ✅ `children` Map 维护 |
|
|
169
|
+
| 子 session 事件路由 | ❌ 无法路由 | ✅ `resolveSession()` 正确路由 |
|
|
170
|
+
| 客户端子 session 感知 | ❌ 客户端不知道子 session 存在 | ✅ 通过 `session_info_update` + `_meta` 通知 |
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# OpenCode TUI 能力 vs Harmony-ACP 对比分析
|
|
2
|
+
|
|
3
|
+
## 已实现的能力
|
|
4
|
+
|
|
5
|
+
| 能力 | TUI | Harmony-ACP | 说明 |
|
|
6
|
+
|------|-----|-------------|------|
|
|
7
|
+
| 会话创建/加载 | ✅ | ✅ | newSession, loadSession |
|
|
8
|
+
| 会话列表/切换 | ✅ | ✅ | listSessions |
|
|
9
|
+
| 会话 Fork | ✅ | ✅ | forkSession |
|
|
10
|
+
| 会话恢复 | ✅ | ✅ | resumeSession |
|
|
11
|
+
| Prompt(文本/图片) | ✅ | ✅ | 支持文本、图片、资源 |
|
|
12
|
+
| 取消执行 | ✅ | ✅ | cancel |
|
|
13
|
+
| 消息流式输出 | ✅ | ✅ | agent_message_chunk |
|
|
14
|
+
| 思考过程流式输出 | ✅ | ✅ | agent_thought_chunk |
|
|
15
|
+
| Tool Call 生命周期 | ✅ | ✅ | pending → running → completed/failed |
|
|
16
|
+
| 权限请求 | ✅ | ✅ | requestPermission |
|
|
17
|
+
| 问题回答 | ✅ | ✅ | extMethod("questionAsked") |
|
|
18
|
+
| 子会话可见性 | ✅ | ✅ | 独立虚拟 session + parent-child 关联 |
|
|
19
|
+
| 模式切换 | ✅ | ✅ | setSessionMode |
|
|
20
|
+
| 模型切换 | ✅ | ✅ | setSessionModel |
|
|
21
|
+
| Token/费用追踪 | ✅ | ✅ | usage_update |
|
|
22
|
+
| Diff 显示 | ✅ | ✅ | edit tool 带 diff content |
|
|
23
|
+
| 流量日志 | ❌ | ✅ | Harmony-ACP 独有能力,全量 ACP/OC 流量记录 |
|
|
24
|
+
|
|
25
|
+
## 未实现 / 缺失的能力
|
|
26
|
+
|
|
27
|
+
| 能力 | TUI 有 | Harmony-ACP | 影响 |
|
|
28
|
+
|------|--------|-------------|------|
|
|
29
|
+
| **Plan 更新** | ✅ | ❌ | plan_update 未实现,planning agent 输出不可见 |
|
|
30
|
+
| **Session 删除** | ✅ | ❌ | 无 deleteSession |
|
|
31
|
+
| **Session 重命名** | ✅ | ❌ | 无 rename |
|
|
32
|
+
| **Session 压缩** | ✅ | ❌ | compact 消息未转发 |
|
|
33
|
+
| **Undo/Redo** | ✅ | ❌ | 撤销/重做消息 |
|
|
34
|
+
| **Share/Export** | ✅ | ❌ | 分享/导出会话 |
|
|
35
|
+
| **Todo 更新** | ✅ | ❌ | todo.updated 事件未处理 |
|
|
36
|
+
| **认证** | ❌ | ❌ | 声明了 auth 但未实现 |
|
|
37
|
+
| **Modes 填充** | 部分 | ⚠️ | TODO: mode loading 未完成 |
|
|
38
|
+
| **Model Options** | 部分 | ⚠️ | TODO: 从 providers 填充 |
|
|
39
|
+
| **Context Size** | ✅ | ⚠️ | usage_update 中 size 硬编码为 0 |
|
|
40
|
+
| **LSP/MCP 状态** | ✅ | ❌ | 未转发 LSP、MCP、VCS 状态 |
|
|
41
|
+
| **VCS 分支** | ✅ | ❌ | vcs.branch.updated 未转发 |
|
|
42
|
+
| **诊断信息** | ✅ | ❌ | LSP errors/warnings 未转发 |
|
|
43
|
+
| **资源链接处理** | ✅ | ⚠️ | 非文本 resource_link 被跳过 |
|
|
44
|
+
|
|
45
|
+
## 按优先级排序
|
|
46
|
+
|
|
47
|
+
### 高优先级(影响基本使用体验)
|
|
48
|
+
|
|
49
|
+
1. **Plan 更新** — planning agent 的核心能力,缺失会导致 plan 模式下客户端看不到任何输出
|
|
50
|
+
2. **Session 删除** — 基础管理能力
|
|
51
|
+
3. **Modes 填充** — 当前 modes 返回 undefined
|
|
52
|
+
|
|
53
|
+
### 中优先级(提升可用性)
|
|
54
|
+
|
|
55
|
+
4. **Todo 更新** — task/todo 工具的可见性
|
|
56
|
+
5. **Session 压缩/Undo** — 长会话管理
|
|
57
|
+
6. **LSP/MCP 状态** — 编辑器集成体验
|
|
58
|
+
|
|
59
|
+
### 低优先级(锦上添花)
|
|
60
|
+
|
|
61
|
+
7. Share/Export、认证、VCS 分支、诊断信息
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harmonyos-arkts/opencode-acp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Enhanced ACP (Agent Client Protocol) server for OpenCode with subagent visibility",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"harmony-acp": "dist/index.cjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/index.cjs",
|
|
12
|
+
"dist/index.cjs.map",
|
|
13
|
+
"README.zh.md",
|
|
14
|
+
"docs/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"build": "node scripts/build.mjs",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"dev": "node --watch src/index.ts",
|
|
21
|
+
"test": "node scripts/test.mjs",
|
|
22
|
+
"test:integration": "node scripts/test-integration.mjs",
|
|
23
|
+
"logs": "node scripts/view-log.mjs",
|
|
24
|
+
"logs:filter": "node scripts/view-log.mjs --filter"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@agentclientprotocol/sdk": "^0.16.1",
|
|
28
|
+
"@opencode-ai/sdk": "latest"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.19.17",
|
|
32
|
+
"esbuild": "^0.25.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|