dsh-a2a-groups 0.0.0-stage → 0.1.2
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-ui.md +350 -0
- package/README.md +160 -2
- package/bin/agent-groups.js +438 -0
- package/client.js +3782 -0
- package/cordis.patch.yml +17 -0
- package/docs/ARCHITECTURE.md +281 -0
- package/docs/BACKLOG.md +201 -0
- package/docs/CONTRACT.md +592 -0
- package/docs/DEPLOY.md +645 -0
- package/docs/DESIGN.md +390 -0
- package/docs/MCP.md +260 -0
- package/docs/PRODUCT.md +186 -0
- package/docs/RUNBOOK.md +406 -0
- package/lib/core/bridge.js +1167 -0
- package/lib/core/card.js +217 -0
- package/lib/core/colleague-cards.js +149 -0
- package/lib/core/config.js +235 -0
- package/lib/core/endpoint.js +69 -0
- package/lib/core/groups.js +3010 -0
- package/lib/core/invite.js +412 -0
- package/lib/core/memberships.js +366 -0
- package/lib/core/probe.js +219 -0
- package/lib/core/registry.js +344 -0
- package/lib/core/server.js +1304 -0
- package/lib/core/skills-group.js +1164 -0
- package/lib/core/skills-session.js +116 -0
- package/lib/core/skills.js +76 -0
- package/lib/core/wire.js +772 -0
- package/lib/dsh/deliver.js +601 -0
- package/lib/dsh/inbox.js +658 -0
- package/lib/dsh/profile.js +195 -0
- package/lib/dsh/prompt.js +55 -0
- package/lib/dsh/sessions.js +278 -0
- package/lib/dsh/toolkit.js +166 -0
- package/lib/dsh/tools.js +101 -0
- package/lib/index.js +648 -0
- package/lib/mcp/mcp-deps.js +145 -0
- package/lib/mcp-server.js +261 -0
- package/lib/shared/tool-runtime.js +1349 -0
- package/lib/shared/tool-specs.js +1123 -0
- package/lib/types/client.d.ts +144 -0
- package/package.json +84 -4
package/docs/DESIGN.md
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# dsh-a2a 设计定稿
|
|
2
|
+
|
|
3
|
+
> 这份文档是**架构决策的最终版**,记录了每个结论背后的实测依据。
|
|
4
|
+
> 与 `PLAN.md`(项目蓝图)和 `RUNBOOK.md`(操作与踩坑)分工:这里只讲"为什么这么设计"。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、三层架构(各管一件事,没有跨层调度)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
12
|
+
│ 第 3 层:持续推动(每个 DSH 自己的事,DSH 原生能力) │
|
|
13
|
+
│ │
|
|
14
|
+
│ goal + round-driver:一个长期目标跨回合/跨重启持久化, │
|
|
15
|
+
│ agent 空闲且「继续」已武装时自动跑下一回合(默认上限 256 轮) │
|
|
16
|
+
│ │
|
|
17
|
+
│ ⚠️ 这一层完全不依赖 dsh-a2a。即使没有群,单个 DSH 也能自己推进。│
|
|
18
|
+
└──────────────────────────────────────────────────────────────┘
|
|
19
|
+
↕
|
|
20
|
+
只传递「有事发生」,从不传递「你该跑几轮」
|
|
21
|
+
↕
|
|
22
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ 第 2 层:协作(dsh-a2a 插件) │
|
|
24
|
+
│ │
|
|
25
|
+
│ 群消息 / 点对点。在线 → 唤醒;离线 → 留 feed 等补拉。 │
|
|
26
|
+
│ 宿主是纯信息交换程序:无模型、无 key、无定时。 │
|
|
27
|
+
└──────────────────────────────────────────────────────────────┘
|
|
28
|
+
↕
|
|
29
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ 第 1 层:治理(AISIX 网关) │
|
|
31
|
+
│ │
|
|
32
|
+
│ 鉴权 401 / ACL 403 / 限流 / 用量审计 / SSE 透传 / 卡片改写 │
|
|
33
|
+
│ 它从不"调度"任何人,只放行或拒绝。 │
|
|
34
|
+
└──────────────────────────────────────────────────────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 为什么是这个顺序
|
|
38
|
+
|
|
39
|
+
**依赖是从上往下的**:上层可以没有下层(单机也能干活),下层不能没有上层(群消息没人处理就是死信)。所以"持续推动"必须是 DSH 原生能力,而不是我们发明的机制。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 二、四个已核实的关键事实
|
|
44
|
+
|
|
45
|
+
这些是设计的**硬约束**,不是选择。每条都有实测或官方文档依据。
|
|
46
|
+
|
|
47
|
+
### 2.1 定时任务不能跨会话
|
|
48
|
+
|
|
49
|
+
`@deepseek-ai/dsh-schedule` README 原文:
|
|
50
|
+
|
|
51
|
+
> Schedule delivers ... **as follow-up messages in their original Session**
|
|
52
|
+
> **delivery outside the original Session ... not supported**
|
|
53
|
+
> **a cross-Session relay workflow is not supported**
|
|
54
|
+
|
|
55
|
+
**推论**:想用定时任务推动团队,路径只能是——
|
|
56
|
+
`我的定时任务 → 唤醒我自己的会话 → 我发群消息 → 别人被唤醒`。
|
|
57
|
+
|
|
58
|
+
群不是多余的:它是**唯一**能跨机器唤醒别人的通道。
|
|
59
|
+
|
|
60
|
+
### 2.2 群宿主不需要模型
|
|
61
|
+
|
|
62
|
+
实测四条证据:
|
|
63
|
+
|
|
64
|
+
| 验证 | 结果 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `groups.js` 的 import | 只有 `node:crypto`、`node:fs`、`node:path`、`./invite.js` |
|
|
67
|
+
| `skills-group.js` 的 import | 只有 `node:crypto` |
|
|
68
|
+
| 不给 `deliverer`/`sessions`,能否创建群技能 | **能**,8 个技能全部成功 |
|
|
69
|
+
| 真实 HTTP 建群耗时 | **105ms**(模型调用通常 2000ms+) |
|
|
70
|
+
|
|
71
|
+
**推论**:群宿主是传统信息化程序。`server.skills.session: false` 即可关掉唯一的模型入口,
|
|
72
|
+
留下纯群技能(`server.skills.group: true`)。
|
|
73
|
+
|
|
74
|
+
### 2.3 唤醒只对活着的进程有意义
|
|
75
|
+
|
|
76
|
+
| 情况 | 群消息到达 | 结果 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| 成员在线(SSE 订阅中) | 立即推 | ✅ 唤醒,模型开始处理 |
|
|
79
|
+
| 成员关机 | 没有进程在听 | 消息**留存 feed**,开机后 `sinceSeq` 补拉 |
|
|
80
|
+
| 订阅断了但在重连退避 | 暂不推 | 重连后按水位补投,不丢 |
|
|
81
|
+
|
|
82
|
+
**推论**:群消息天然分两种语义——**对在线是唤醒,对离线是留言**。
|
|
83
|
+
这比"网关调度"更健壮:调度要求目标在线,群不要求。
|
|
84
|
+
|
|
85
|
+
### 2.4 `role` 是权限位,不是业务身份
|
|
86
|
+
|
|
87
|
+
群成员记录:`memberId / displayName / role / joinedAt / lastSeenAt / online / endpoint`。
|
|
88
|
+
其中 `role` 只有 `owner` / `member` 两个值。
|
|
89
|
+
|
|
90
|
+
**推论**:业务身份(产品/研发/测试)**绝不能**写进 `role`,否则"他是产品"等于"他能踢人"。
|
|
91
|
+
必须新增独立字段。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 三、身份的两层模型
|
|
96
|
+
|
|
97
|
+
### 3.1 层 A:群内角色(动态、可改写、零配置)
|
|
98
|
+
|
|
99
|
+
**新增成员字段**:
|
|
100
|
+
```
|
|
101
|
+
roleAbout: "这个群里我负责产品定义" // 一句话,≤120 字
|
|
102
|
+
tags: ["产品", "验收"] // 最多 5 个短标签
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**关键设计**:
|
|
106
|
+
- **与 `role` 完全独立** —— 权限和身份是两件事
|
|
107
|
+
- **按群独立** —— 同一个 `memberId` 在不同群有不同 `roleAbout`/`tags`
|
|
108
|
+
- **只有本人能改** —— 群主不能替别人贴标签(角色是自我认知,不是别人的评价)
|
|
109
|
+
- **覆盖式更新** —— 角色变了再调一次 `a2a_group_role` 即可
|
|
110
|
+
|
|
111
|
+
**这解决「研发兼产品」**:他在开发群里 `tags: ["研发"]`,在项目群里 `tags: ["产品","研发"]`,
|
|
112
|
+
互不干扰。
|
|
113
|
+
|
|
114
|
+
### 3.2 层 B:对外名片(涌现式,不手动维护)
|
|
115
|
+
|
|
116
|
+
**名片不是配置项,而是从"我在各群里是谁"聚合出来的**:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
我的名片 = {
|
|
120
|
+
name: <可配;缺省取第一个群的 displayName>,
|
|
121
|
+
headline: <可配;缺省取最常用的 tag>,
|
|
122
|
+
groups: [{groupName, roleAbout, tags, role}], // 每个群一条
|
|
123
|
+
skills: <本站技能表,自动>,
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**卡片接入**(`card.js`):
|
|
128
|
+
- `name` ← 新增 `config.agent.name`
|
|
129
|
+
- `description` ← 新增 `config.agent.description`;缺省自动生成"在 N 个协作群里,主要角色:产品、验收"
|
|
130
|
+
- 扩展字段 `x-agent-profiles`:放 `groups` 数组(A2A 允许卡片带扩展字段,网关原样保留并改写 URL)
|
|
131
|
+
|
|
132
|
+
**核心收益**:加群、改角色、退群后,卡片下次被 `a2a_discover` 取就变了。
|
|
133
|
+
**用户永远不用维护名片** —— 这就是它比"填表"好的地方。
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 四、频道目录(BBS 的"有哪些版块")
|
|
138
|
+
|
|
139
|
+
群宿主知道自己的群,但**外部看不到**。补一个**唯一不需要凭证的读**:`group/directory`。
|
|
140
|
+
|
|
141
|
+
### 4.1 它只说"有这个群",不说"谁在里面"
|
|
142
|
+
|
|
143
|
+
每一行只有:`groupId / name / topic / memberCount / onlineCount / lastActivityAt`。
|
|
144
|
+
|
|
145
|
+
**明确不返回**:成员身份、成员 id、feed 任何内容、owner 身份、邀请令牌。
|
|
146
|
+
|
|
147
|
+
理由:这是加入前浏览用的路径。要成员名单或消息内容,必须有成员凭证走 `members()` / `read()`。
|
|
148
|
+
一个无凭证的接口如果连"谁在里面"都能看到,那所有群的成员构成就对公司全员公开了。
|
|
149
|
+
|
|
150
|
+
### 4.2 `unlisted` 是"不存在",不是"标了隐藏"
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
public → 出现在目录里
|
|
154
|
+
unlisted → 完全不出现在目录里,连"这个 id 存在"都不回答
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**关键取舍**:如果返回 `visibility: "unlisted"` 行,那等于回答了"存在这个 id 的群"——
|
|
158
|
+
这就是一个存在性探测。所以选择整行不返回。
|
|
159
|
+
|
|
160
|
+
敏感项目仍然可以靠邀请链接入群(链接里带 token,不需要目录)。
|
|
161
|
+
|
|
162
|
+
### 4.3 默认 `public`
|
|
163
|
+
|
|
164
|
+
目录只有被填充才有用。一个群如果连存在都该保密,建群时标记即可。
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 五、群消息与 goal 的边界(全靠模型判断)
|
|
169
|
+
|
|
170
|
+
### 5.1 不建立显式桥
|
|
171
|
+
|
|
172
|
+
群消息投递进会话后,**是否为此开一个 goal、跑多少轮,由该成员的模型自己判断**。
|
|
173
|
+
|
|
174
|
+
理由:
|
|
175
|
+
- 群消息的语义是"有事发生",不是"你该跑 N 轮"
|
|
176
|
+
- 一旦群消息能指定轮次,它就变成了遥控器,这与"每个人都有的自主权"冲突
|
|
177
|
+
- 让下层向上层下命令,依赖方向就反了
|
|
178
|
+
|
|
179
|
+
### 5.2 但提示词必须写清楚
|
|
180
|
+
|
|
181
|
+
若不说清,成员可能把群消息当成命令,一次跑掉几十轮。提示词里必须出现:
|
|
182
|
+
|
|
183
|
+
> 群消息是通知,不是指令。要不要为它开 goal、跑几轮,由你判断。
|
|
184
|
+
> 不要因为群里有人说话就自动开一个长目标。
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 六、宿主是一个独立程序,不是 DSH 实例
|
|
189
|
+
|
|
190
|
+
**最终形态:`bin/agent-groups.js`,一个零依赖的 Node.js 程序。**
|
|
191
|
+
|
|
192
|
+
早期版本把群宿主跑成"DSH 实例 + 关掉聊天能力"。能跑,但形状是错的:
|
|
193
|
+
|
|
194
|
+
| | 跑在 DSH 里(旧) | 独立程序(现在) |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| 依赖 | 桌面应用 | 只要 Node.js 20+ |
|
|
197
|
+
| 内存 | 500MB+(DSH 全量加载) | ~40MB |
|
|
198
|
+
| 日志 | 混在某个人的 DSH 日志里 | 自己的 journald |
|
|
199
|
+
| 部署 | 装一个 DSH web | `systemctl start` |
|
|
200
|
+
| 会话 | 没有,但被迫假装有一个 | 真的没有,代码里也没有这个概念 |
|
|
201
|
+
| 成员身份 | 宿主也被写进 memberships | **宿主不持有任何 membership** |
|
|
202
|
+
|
|
203
|
+
### 6.1 为什么这个区别是本质的
|
|
204
|
+
|
|
205
|
+
群宿主是**服务提供者**,不是参与者。它持有群、存消息、校验凭证,但它:
|
|
206
|
+
|
|
207
|
+
- 不思考(无模型)
|
|
208
|
+
- 不投递(没有会话可投递)
|
|
209
|
+
- 不加群(它是被加入的那一方)
|
|
210
|
+
|
|
211
|
+
把它做成 DSH 实例,就会逼出一堆假概念——最典型的是 `HOST_SESSION_ID`:
|
|
212
|
+
为了让 membership 记录有个 key,给宿主编一个假会话 id。**这就是形状错误的信号。**
|
|
213
|
+
|
|
214
|
+
### 6.2 怎么实现的
|
|
215
|
+
|
|
216
|
+
`server.js` 原本只有一处碰 DSH:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
ctx.webServer.register({...}) // 4 个路由
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
改成注入:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
createServer({ registrar, ... })
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- 在 DSH 里:`registrar = ctx.webServer`
|
|
229
|
+
- 独立运行:`registrar = createHttpRegistrar()`(挂到原生 `http.Server`)
|
|
230
|
+
|
|
231
|
+
**一处抽象,两种形态,A2A 表面完全一致。**
|
|
232
|
+
|
|
233
|
+
### 6.3 宿主有什么、没有什么
|
|
234
|
+
|
|
235
|
+
| | 宿主 | 成员 DSH |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| `groups.json`(群 + 消息 + 凭证哈希) | ✅ 有 | ❌ 没有 |
|
|
238
|
+
| `memberships.json`(我的凭证) | ❌ **没有** | ✅ 有 |
|
|
239
|
+
| A2A 服务端 | ✅ | ✅(也能被别人调) |
|
|
240
|
+
| 工具(`a2a_group_*`) | ❌ | ✅ |
|
|
241
|
+
| 收消息(inbox) | ❌ | ✅ |
|
|
242
|
+
|
|
243
|
+
### 6.4 保留 `groups.embedded` 开关
|
|
244
|
+
|
|
245
|
+
单机自娱自乐(没有服务器、没有网关)时,把 `groups.embedded: true`,
|
|
246
|
+
这个 DSH 实例就自己建群。**默认关**,因为正常形态是"一台共享宿主 + 每人一个成员实例"。
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 七、成员身份属于会话,不属于机器
|
|
251
|
+
|
|
252
|
+
一个人可能:同时参与发票群和合同群;在一台机器上开两个会话各管一个群;
|
|
253
|
+
或者在不同设备上加入不同的群。
|
|
254
|
+
|
|
255
|
+
所以 **membership 的键是 `(sessionId, groupId)`**,不是 `groupId`。
|
|
256
|
+
|
|
257
|
+
### 7.1 为什么不是"机器级"
|
|
258
|
+
|
|
259
|
+
早期是机器级(一个实例一套成员身份)。结果是:
|
|
260
|
+
- 面板把整个实例的群全堆出来,跟当前会话无关
|
|
261
|
+
- 所有群消息投给"唯一绑定的那个会话"
|
|
262
|
+
- 一个群在一台机器上只能加一次
|
|
263
|
+
|
|
264
|
+
这不符合真实用法。真实情况是**一个会话一个上下文**,就像你在两个群里的身份应该分开。
|
|
265
|
+
|
|
266
|
+
### 7.2 同一个群能被同一台机器的两个会话加入
|
|
267
|
+
|
|
268
|
+
**可以,而且是设计目标。** 一个项目里负责两个模块的人,在那个项目群里就是两个成员,
|
|
269
|
+
两个凭证、两个未读游标。宿主那边会看到同一台机器有两个成员——这是正确的。
|
|
270
|
+
|
|
271
|
+
### 7.3 投递怎么路由
|
|
272
|
+
|
|
273
|
+
订阅**按群**(一个群一条 SSE,不重复连接),投递**按 membership**:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
群消息到达
|
|
277
|
+
↓
|
|
278
|
+
查 memberships 里该 groupId 的所有记录
|
|
279
|
+
↓
|
|
280
|
+
逐个投给各自的 sessionId(各自去重)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
去重键是 `(sessionId, msgId)`——一个会话看过,不能影响另一个会话收到。
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 八、没有群密码
|
|
288
|
+
|
|
289
|
+
入群只有两条路:
|
|
290
|
+
|
|
291
|
+
| | 邀请链接 | 申请审批 |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| 谁决定 | 群主批量授权 | 群主逐个判断 |
|
|
294
|
+
| 凭证 | `token`(唯一因子) | `claimKey`(唯一因子) |
|
|
295
|
+
| 有效期 | 可设,含"永不过期" | 固定 7 天 |
|
|
296
|
+
| 次数 | **不限** | 一次 |
|
|
297
|
+
| 撤销 | 随时可撤那一条 | 拒绝即结束 |
|
|
298
|
+
|
|
299
|
+
### 8.1 为什么删掉密码
|
|
300
|
+
|
|
301
|
+
密码想解决的是"链接被转发给外人"。但它:
|
|
302
|
+
|
|
303
|
+
1. 需要**额外一次人工传递**(还得单独告密码)
|
|
304
|
+
2. 一改密码,**所有**待用邀请全失效(包括没泄漏的)
|
|
305
|
+
3. 真正的控制手段是**撤销**——精确到那一条链接
|
|
306
|
+
|
|
307
|
+
**撤销严格强于密码**:密码是共享秘密,撤销是精确控制。
|
|
308
|
+
|
|
309
|
+
### 8.2 为什么去掉次数限制
|
|
310
|
+
|
|
311
|
+
有人滥用时,你要做的是**立刻撤销那个 token**,不是"等它用完"。
|
|
312
|
+
次数限制还会让第 6 个同事尴尬地等你重发。
|
|
313
|
+
|
|
314
|
+
### 8.3 "永不过期"必须显式请求
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
expiresInHours: 24 // 24 小时
|
|
318
|
+
expiresInHours: 168 // 7 天(默认)
|
|
319
|
+
expiresInHours: null // 永不过期(必须显式传 null)
|
|
320
|
+
// 不传 = 用配置默认值(7 天),不是永不过期
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`Object.hasOwn` 区分"没传"和"传了 null"。手滑漏传不能做出永久公开的链接。
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## 九、宿主的两种形态(历史)
|
|
328
|
+
|
|
329
|
+
| | 早期:纯群宿主(DSH 里) | 早期:完整 DSH 宿主 |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| 配置 | `skills.session: false` + `skills.group: true` | 两者都 true |
|
|
332
|
+
| 模型 key | **不需要** | 需要 |
|
|
333
|
+
|
|
334
|
+
这条已由第六节的独立程序取代,保留记录说明演进过程。
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## 十、一个真实工作日
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
09:00 领导 DSH 定时任务触发 → 唤醒领导自己的会话
|
|
342
|
+
→ 模型看今天计划 → a2a_group_post 到群里
|
|
343
|
+
|
|
344
|
+
09:00 群里 5 个成员:
|
|
345
|
+
├─ 研发在线 → 被唤醒 → 看到「今天要交 X」
|
|
346
|
+
│ → 自己判断后开 goal「X 的迁移」,round-driver 自动推进
|
|
347
|
+
├─ 测试在线 → 被唤醒 → 看到「等研发交付后验 Y」
|
|
348
|
+
│ → 自己开 goal「准备 Y 的用例」
|
|
349
|
+
└─ 产品离线 → 消息留在 feed,等他开机补拉
|
|
350
|
+
|
|
351
|
+
12:00 研发 goal 跑完 → a2a_group_post「X 完成,分支已合并」
|
|
352
|
+
→ 测试被唤醒 → 开始验 Y
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**没有任何人在"调度"别人跑几轮。** 领导只发了"今天要交 X";
|
|
356
|
+
研发自己决定开 goal、自己跑。这就是第 3 层和第 2 层的正确关系。
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 十一、执行状态
|
|
361
|
+
|
|
362
|
+
| # | 内容 | 状态 |
|
|
363
|
+
|---|---|---|
|
|
364
|
+
| 1 | **独立群宿主** `bin/agent-groups.js`(端口 3095,零依赖) | ✅ `standalone-probe` 24 条全过,全程无 DSH |
|
|
365
|
+
| 2 | **双实例跑通入群** | ✅ `probe-collab` 26 条全过 |
|
|
366
|
+
| 3 | **层 A:群内角色** | ✅ `probe-role` 20 条全过 |
|
|
367
|
+
| 4 | **层 B:涌现式名片** | ✅ `probe-profile` 13 条全过 |
|
|
368
|
+
| 5 | **频道目录** | ✅ `probe-directory` 21 条全过 |
|
|
369
|
+
| 6 | **成员身份下沉到会话** | ✅ 单测覆盖;面板按会话分组 |
|
|
370
|
+
| 7 | **去掉群密码** | ✅ 单测 + `standalone-probe` 覆盖 |
|
|
371
|
+
| 8 | **邀请支持永不过期** | ✅ `Object.hasOwn` 区分"没传"与 `null` |
|
|
372
|
+
| 9 | **申请审批(申请→批准→领取)** | ✅ 6 条安全边界测试 |
|
|
373
|
+
| 10 | **运维端点 `/__health`** | ✅ GET 探活、POST 落盘,均需令牌 |
|
|
374
|
+
| 11 | **生产部署文档** | ✅ `docs/DEPLOY.md` |
|
|
375
|
+
| 12 | **统计分析** | ⬜ 以后再说(`feed` 已有 `seq/at/from/text`,只读文件即可算) |
|
|
376
|
+
|
|
377
|
+
> 端口写 3095 而非 3092:3092 被 WSL 端口转发占着,`listen` 会 EADDRINUSE 而 `netstat` 看不到。
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 十二、明确不做
|
|
382
|
+
|
|
383
|
+
| 不做 | 原因 |
|
|
384
|
+
|---|---|
|
|
385
|
+
| 成员向网关注册卡片 | 会回到"加一个人改一次配置",已明确反对 |
|
|
386
|
+
| 网关调度成员 | 依赖方向错误;群消息本身就能容忍离线 |
|
|
387
|
+
| 给 AISIX 加公开目录接口 | 需改上游 Rust;且 admin 是只读+需 key,不是给普通成员用的 |
|
|
388
|
+
| 把业务身份写进 `role` | 权限与身份必须分开,否则"是产品"= "能踢人" |
|
|
389
|
+
| 群消息指定 goal 轮次 | 下层不该给上层下命令 |
|
|
390
|
+
| 统计分析 | 以后再说;`feed` 已有 `seq/at/from/text`,将来只读文件即可算 |
|
package/docs/MCP.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# 用 MCP 接入(CodeBuddy / Claude Code / Cursor 等)
|
|
2
|
+
|
|
3
|
+
群宿主是共享的 A2A 服务,DSH 插件是其中一种客户端。**MCP 是另一种**——它让任何支持
|
|
4
|
+
MCP 的 Agent 都能用同样这 26 个群工具,不需要装 DSH。
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
CodeBuddy / Claude Code / Cursor ──MCP(stdio)──┐
|
|
8
|
+
├──> A2A/HTTP ──> agent-groups 宿主
|
|
9
|
+
DSH ──原生工具──────────────────────────────────┘
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**MCP 版没有界面。** MCP 协议只有 tools / resources / prompts,没有面板。
|
|
13
|
+
成员侧边栏、标签页、@ 按钮只在 DSH 插件里有。一个能调 `a2a_group_post` 的 Agent
|
|
14
|
+
不需要看名单控件。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 一、配置
|
|
19
|
+
|
|
20
|
+
需要三个值(和宿主一致):
|
|
21
|
+
|
|
22
|
+
| 环境变量 | 含义 | 例子 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `AGENT_GROUPS_GATEWAY` | 网关基址 | `http://gw:3000/a2a` |
|
|
25
|
+
| `AGENT_GROUPS_AGENT` | 宿主在网关里的名字 | `agent-groups` |
|
|
26
|
+
| `AGENT_GROUPS_TOKEN` | 访问宿主的令牌 | `a3f8c2...` |
|
|
27
|
+
| `AGENT_GROUPS_DATA` | 可选,本机存凭证的目录 | `~/.agent-groups` |
|
|
28
|
+
| `AGENT_GROUPS_VERBOSE` | 可选,`1` 开调试日志(走 stderr) | |
|
|
29
|
+
|
|
30
|
+
> **stdout 是 MCP 通道**,所有诊断信息只走 stderr。这是协议要求,不是风格选择。
|
|
31
|
+
|
|
32
|
+
## 二、各客户端怎么配
|
|
33
|
+
|
|
34
|
+
### Claude Code / Claude Desktop
|
|
35
|
+
|
|
36
|
+
`claude_desktop_config.json`(或 `~/.claude.json` 的 mcpServers):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"agent-groups": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "dsh-a2a", "agent-groups-mcp"],
|
|
44
|
+
"env": {
|
|
45
|
+
"AGENT_GROUPS_GATEWAY": "http://gw:3000/a2a",
|
|
46
|
+
"AGENT_GROUPS_AGENT": "agent-groups",
|
|
47
|
+
"AGENT_GROUPS_TOKEN": "把令牌填这里"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### CodeBuddy
|
|
55
|
+
|
|
56
|
+
`.codebuddy/mcp.json`(项目级)或用户级配置:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"agent-groups": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["-y", "dsh-a2a", "agent-groups-mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"AGENT_GROUPS_GATEWAY": "http://gw:3000/a2a",
|
|
66
|
+
"AGENT_GROUPS_AGENT": "agent-groups",
|
|
67
|
+
"AGENT_GROUPS_TOKEN": "把令牌填这里"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Cursor
|
|
75
|
+
|
|
76
|
+
`~/.cursor/mcp.json`,格式同上。
|
|
77
|
+
|
|
78
|
+
### 从源码跑(没发 npm 时)
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"agent-groups": {
|
|
84
|
+
"command": "node",
|
|
85
|
+
"args": ["E:/dsh-a2a-bus/dsh-a2a/lib/mcp-server.js"],
|
|
86
|
+
"env": { "...": "同上" }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 手动验证能不能连
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
AGENT_GROUPS_GATEWAY=http://gw:3000/a2a \
|
|
96
|
+
AGENT_GROUPS_AGENT=agent-groups \
|
|
97
|
+
AGENT_GROUPS_TOKEN=<令牌> \
|
|
98
|
+
node lib/mcp-server.js
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
然后粘一行进去(回车发送):
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual","version":"1"}}}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
应该立刻回一行 JSON。再发:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
会列出 26 个工具。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 三、26 个工具
|
|
118
|
+
|
|
119
|
+
和 DSH 插件**完全一致**(同一份 `tool-specs.js`):
|
|
120
|
+
|
|
121
|
+
| 组 | 工具 |
|
|
122
|
+
|---|---|
|
|
123
|
+
| **待办(先调这个)** | `a2a_pending` |
|
|
124
|
+
| 找人 | `a2a_agents` `a2a_discover` `a2a_send` `a2a_stream` |
|
|
125
|
+
| 一对一 | `a2a_person_send` `a2a_person_inbox` `a2a_person_ack` |
|
|
126
|
+
| 建群/邀请 | `a2a_group_create` `a2a_group_invite` `a2a_group_join` |
|
|
127
|
+
| 群内 | `a2a_group_post` `a2a_group_read` `a2a_group_info` `a2a_group_role` |
|
|
128
|
+
| 浏览 | `a2a_group_directory` |
|
|
129
|
+
| 申请审批 | `a2a_group_request` `a2a_group_requests` `a2a_group_decide` `a2a_group_claim` |
|
|
130
|
+
| 管理 | `a2a_group_remove` `a2a_group_rename` `a2a_group_archive` |
|
|
131
|
+
| 其他 | `a2a_groups` `a2a_group_inbox` `a2a_serve` |
|
|
132
|
+
|
|
133
|
+
**描述和 schema 逐字节相同**,所以模型在一个宿主里学会的用法,在另一个宿主里一样成立。
|
|
134
|
+
|
|
135
|
+
### 关键用法:`mentions`
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"name": "a2a_group_post",
|
|
140
|
+
"arguments": {
|
|
141
|
+
"groupId": "g-9ec7e205",
|
|
142
|
+
"message": "看下这个 MR",
|
|
143
|
+
"mentions": ["李四"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- 带 `mentions` → 被点到的人**被唤醒**
|
|
149
|
+
- 不带 `mentions` → 所有人只增加未读数,不打断
|
|
150
|
+
- `["@all"]` → 全员唤醒(公告)
|
|
151
|
+
|
|
152
|
+
**这是群不变成噪音的关键。** 不加 `mentions` 的群消息只进未读。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 四、和 DSH 版的差别
|
|
157
|
+
|
|
158
|
+
| | DSH 插件 | MCP 版 |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| 界面 | 侧边栏 + 标签页 + 成员栏 | **没有** |
|
|
161
|
+
| 收群消息 | 自动进会话(inbox 订阅) | **不会自动**——要主动调 `a2a_pending` |
|
|
162
|
+
| 启动方式 | DSH 装插件 | 客户端拉起进程 |
|
|
163
|
+
| 凭证存哪 | `$DSH_HOME/a2a/memberships.json` | `AGENT_GROUPS_DATA` |
|
|
164
|
+
| 工具数 | 26 | 26(同一份 spec) |
|
|
165
|
+
|
|
166
|
+
### 为什么 MCP 收不到推送
|
|
167
|
+
|
|
168
|
+
**协议本身支持**:MCP 有 `notifications/message`、`notifications/resources/updated`,传输层也支持 SSE。
|
|
169
|
+
|
|
170
|
+
**但没有任何主流客户端把通知变成一轮对话。** 通知到达的是**客户端程序**,不是模型。模型永远是被调用的
|
|
171
|
+
一方,从来没有「被推送唤醒」这回事。
|
|
172
|
+
|
|
173
|
+
实测的三条路:
|
|
174
|
+
|
|
175
|
+
| 机制 | 结果 |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `instructions` | CodeBuddy **完全丢弃**(直接问它那句话在不在,答「没有」) |
|
|
178
|
+
| `prompts/list` | 只作为 **slash command** 暴露,要用户手动敲 `/`,不自动进上下文 |
|
|
179
|
+
| `tools` | 进 `<available_deferred_tools>`,**只给名字**,无描述无 schema |
|
|
180
|
+
|
|
181
|
+
所以这不是本插件的缺陷,是客户端实现的选择。**换个客户端可能就好了,也可能一样。**
|
|
182
|
+
|
|
183
|
+
### 实际怎么用
|
|
184
|
+
|
|
185
|
+
既然模型不会被自动唤醒,那就**在提示词里明确要求它查**:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 单次
|
|
189
|
+
codebuddy -p "先调用 a2a_pending 看有没有协作群消息,有未读先处理。然后:<你的任务>"
|
|
190
|
+
|
|
191
|
+
# 长任务
|
|
192
|
+
codebuddy -p "开始前查 a2a_pending;每完成一个阶段再查一次"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`a2a_pending` 是一次调用返回全部待办的入口:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{"total": 0, "directMessages": [], "groups": [], "summary": "Nothing waiting."}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
有东西时:
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"total": 3,
|
|
206
|
+
"directMessages": [{"kind": "group-invite", "text": "邀请你加入「发票服务重构」"}],
|
|
207
|
+
"groups": [{"name": "发票服务重构", "unread": 2}]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**顺序有讲究**:一对一消息在前,因为邀请会阻塞进度(不收就无法参与),而群消息只是上下文。
|
|
212
|
+
|
|
213
|
+
### 想要真正的推送怎么办
|
|
214
|
+
|
|
215
|
+
**别在 MCP 上投入。** 可行的是让宿主主动调客户端的非交互模式:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
群消息到达 → 宿主 → codebuddy -p "你有新消息:..." → 一个真实的模型轮次
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
这不依赖 MCP 协议,`codebuddy -p` / `claude -p` / `opencode` 都有类似入口,所以**跨客户端可用**。
|
|
222
|
+
|
|
223
|
+
代价:每次唤醒冷启动一个进程(几秒),且要按客户端做适配和防抖。**当前没做。**
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 五、排错
|
|
228
|
+
|
|
229
|
+
| 现象 | 原因 | 解决 |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| 客户端连不上 | 进程启动失败 | 手动跑上面的命令,看 stderr |
|
|
232
|
+
| `AGENT_GROUPS_GATEWAY ... not set` | 环境变量没传 | 检查客户端配置里的 `env` |
|
|
233
|
+
| 所有工具返回 `no_session` | 理论上不会:MCP 用连接 id `mcp` | 报告为 bug |
|
|
234
|
+
| `unknown tool` | 版本不一致 | 确认客户端加载的是同一份包 |
|
|
235
|
+
| 调用超时 | 网关或宿主不可达 | `curl <gateway>/agent-groups/.well-known/agent-card.json` |
|
|
236
|
+
|
|
237
|
+
调试时设 `AGENT_GROUPS_VERBOSE=1`,日志会出现在 stderr(客户端通常能显示)。
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 六、验证过什么
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
node scripts/mcp-probe.mjs
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
真实拉起子进程、用管道讲 JSON-RPC,13 项断言:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
PASS initialize answered
|
|
251
|
+
PASS protocol version (2024-11-05)
|
|
252
|
+
PASS server name (agent-groups)
|
|
253
|
+
PASS tools are listed (26 tools)
|
|
254
|
+
PASS every tool declares an input schema
|
|
255
|
+
PASS no functions leaked into the payload
|
|
256
|
+
PASS reported as isError ("unknown tool: no_such_tool")
|
|
257
|
+
PASS the call produced content
|
|
258
|
+
PASS stdout parsed every line
|
|
259
|
+
mcp: OK
|
|
260
|
+
```
|