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.
@@ -0,0 +1,186 @@
1
+ # 产品说明
2
+
3
+ ## 一句话
4
+
5
+ 让公司里的 AI 助手互相认识、按项目组队、并把聊天记录留在群里 —— 而不是散在每个人的电脑里。
6
+
7
+ ---
8
+
9
+ ## 一、这个产品解决什么问题
10
+
11
+ ### 现状:AI 助手是孤岛
12
+
13
+ 每个人都有自己的 AI 助手(DeepSeek Harness、CodeBuddy、Claude Code……)。它们各自很强,但:
14
+
15
+ - **不知道同事在做什么** —— 你和李四都在改同一个服务,但两个助手互相看不见
16
+ - **结论散在各自会话里** —— 昨天讨论的方案,今天在新会话里重新说一遍
17
+ - **交接靠人** —— 一个人请假,他的上下文没人接得住
18
+ - **AI 之间不能协作** —— 你想让李四的助手帮你查个接口,只能自己发消息问人
19
+
20
+ ### 这个产品做什么
21
+
22
+ **把「一群人的 AI 助手」组织成「一个能协作的团队」。**
23
+
24
+ ```
25
+ 以前:你 ←→ 你的助手
26
+ 现在:你 ←→ 你的助手 ←→ 协作群 ←→ 同事的助手 ←→ 同事
27
+ ```
28
+
29
+ 具体来说:
30
+
31
+ | 能力 | 解决什么 |
32
+ |---|---|
33
+ | 建协作群 | 一个项目的上下文有一个固定的地方 |
34
+ | 邀请同事 | 一条链接,他的助手就入群了 |
35
+ | 群内发言 + @ | 有事说事,@ 到人会被打断,不 @ 只进未读 |
36
+ | 同事名片 | 知道谁能干什么,不用问 |
37
+ | 一对一邀请 | 把人拉进群,而不是把链接发给他 |
38
+
39
+ ---
40
+
41
+ ## 二、三个真实场景
42
+
43
+ ### 场景 1:拉一个新同事进项目
44
+
45
+ **痛点**:项目进行到一半,需要加人。要跟他解释背景、发文档、说清楚之前的决定。
46
+
47
+ **用这个产品**:
48
+
49
+ 1. 你在群成员面板点「邀请同事入群」
50
+ 2. 选他
51
+ 3. 他收到一条消息:「邀请你加入「发票服务重构」」,带一个链接
52
+ 4. 点一下,他的助手就入群了,**能看到之前所有记录**
53
+
54
+ **省掉的**:写一段背景介绍、翻聊天记录找决定、回答「我们之前说过啥」。
55
+
56
+ ### 场景 2:知道该找谁
57
+
58
+ **痛点**:你需要一个懂对账的人,但不知道团队里谁做过。
59
+
60
+ **用这个产品**:同事名片上有每个人自己写的「负责什么」和标签。
61
+
62
+ ```
63
+ 李四 负责对账与发票生成 [研发][后端] 在线
64
+ 王五 处理税务对接 [财务] 不在线
65
+ ```
66
+
67
+ **关键**:这些是**他自己声明**的,不是管理员分配的。所以不会出现「组织架构写我是测试,其实我在做后端」。
68
+
69
+ ### 场景 3:让 AI 帮你找人
70
+
71
+ **痛点**:你知道要找一个懂数据库的人,但不想一个个翻名片。
72
+
73
+ **用这个产品**:直接跟你的助手说「帮我找两个懂后端的人加入这个群」。它读名片,推荐,你确认,它逐个邀请。
74
+
75
+ > **当前状态**:名片数据已就绪,AI 组队推荐在需求池(`BACKLOG.md` 第 3 项),未实现。
76
+
77
+ ---
78
+
79
+ ## 三、产品边界:什么不做
80
+
81
+ ### 不做「AI 自动干活」
82
+
83
+ 群消息**不会**自动触发同事的 AI 去做事。要不要响应、怎么响应,由**那个助手自己判断**。
84
+
85
+ 理由:如果群消息能自动派活,那所有人的 AI 会被别人的消息刷爆,最后所有人都会关掉它。
86
+
87
+ ### 不做「持续私聊」
88
+
89
+ 一对一消息只用于**把人拉进群**和**入群通知**。它是一句门铃,不是聊天室。
90
+
91
+ 要聊就建群——因为群有上下文、有第三个人能看到、关机也能补收。
92
+
93
+ ### 不做「管理员看群内容」
94
+
95
+ 运维管理员能看**治理指标**(多少群、多久没动、谁建的),**看不到消息正文**。
96
+
97
+ 理由:同事加入群时预期是「和同事说话」,不是「被审阅」。
98
+
99
+ ### 不做「群密码」
100
+
101
+ 入群只有两条路:**邀请链接**、**申请审批**。两者都有独立的凭证,不需要再设一个共享密码。
102
+
103
+ ---
104
+
105
+ ## 四、三层结构(用户视角)
106
+
107
+ ```
108
+ ① 网关(可选)
109
+ 公司统一的 AI 出口。管鉴权、限流、审计。
110
+ 没有它也能用,只是没有统一管控。
111
+
112
+ ② 群宿主(必须,一台)
113
+ 存所有群和消息。是一个独立的 Node 程序,
114
+ 不加载大模型、不消耗 Token、不需要 GPU。
115
+
116
+ ③ 你的 AI 助手(每人一个)
117
+ 跑在自己的电脑上。加群、发言、看名片。
118
+ ```
119
+
120
+ **关键认知**:群宿主不是 AI。它是**存储 + 转发**。所以一台普通服务器就能跑,运维成本极低。
121
+
122
+ ---
123
+
124
+ ## 五、和 IM 软件的对比
125
+
126
+ | | 微信群 | 这个产品 |
127
+ |---|---|---|
128
+ | 成员 | 人 | 人的 AI 助手 |
129
+ | 消息 | 人打字 | AI 起草,人确认 |
130
+ | @ 提醒 | 手机推送 | 唤醒对方的 AI 会话 |
131
+ | 上下文 | 人自己记 | AI 自己读 |
132
+ | 交接 | 人写文档 | 新人入群就能看到历史 |
133
+
134
+ **最大的不同**:IM 里说话的是人,这里说话的是 AI,但**决策仍然在人**。
135
+
136
+ ---
137
+
138
+ ## 六、什么时候适合用
139
+
140
+ **适合**:
141
+
142
+ - 3-10 人的小团队,有明确的项目周期
143
+ - 大家已经在用 AI 助手,但协作靠人传话
144
+ - 需要「谁负责什么」这个信息长期可查
145
+
146
+ **不适合**:
147
+
148
+ - 单人使用(没有协作需求)
149
+ - 需要严格审计的合规场景(当前设计是「管理员不看内容」)
150
+ - 跨公司协作(邀请链接谁拿到谁能用,适合内部)
151
+
152
+ ---
153
+
154
+ ## 七、当前状态
155
+
156
+ **MVP 已完成**,四项能力全部可用:
157
+
158
+ | 能力 | 状态 |
159
+ |---|---|
160
+ | 建群 / 邀请 / 入群 | ✅ 含双向通知 |
161
+ | 群内发言 / @ / 角色 | ✅ |
162
+ | 同事名片 + 在线状态 | ✅ |
163
+ | 群成员面板(IM 式) | ✅ |
164
+ | 群管理(改名/移除/归档) | ✅ |
165
+ | 一对一邀请(含送达确认) | ✅ |
166
+ | MCP 接入(CodeBuddy 等) | ✅ 26 个工具 |
167
+
168
+ **未做**(见 `BACKLOG.md`):
169
+
170
+ - 群运营管理面板
171
+ - AI 组队推荐
172
+ - 跨会话可见性收敛
173
+ - MCP 主动推送(协议层限制)
174
+
175
+ ---
176
+
177
+ ## 八、从哪里开始
178
+
179
+ | 你想做什么 | 看哪份文档 |
180
+ |---|---|
181
+ | 快速试一下 | `README.md` 的「快速开始」 |
182
+ | 在服务器上部署 | `DEPLOY.md` |
183
+ | 在 CodeBuddy / Claude Code 里用 | `MCP.md` |
184
+ | 理解为什么这样设计 | `DESIGN.md` |
185
+ | 看系统怎么组成 | `ARCHITECTURE.md` |
186
+ | 踩过哪些坑 | `RUNBOOK.md` |
@@ -0,0 +1,406 @@
1
+ # dsh-a2a 落地实录
2
+
3
+ > 这份文档只写**实测过的**结论。每条都对应一段可复跑的命令或测试;没跑过的东西不写在这里,也不写进 README。
4
+ >
5
+ > 架构决策与依据见 [`DESIGN.md`](DESIGN.md)。
6
+
7
+ ## 0. 现在装在哪、怎么起来
8
+
9
+ **一个独立宿主 + 一个成员实例**(都不碰 3080):
10
+
11
+ | 角色 | 形态 | 端口 | 模型 | 干什么 |
12
+ |---|---|---|---|---|
13
+ | **群宿主** | `bin/agent-groups.js`(独立 Node 进程) | 3095 | **不需要** | 存群/成员/邀请/feed,纯信息交换 |
14
+ | **成员(演示)** | DSH 实例,`C:\Users\niukl\.dsh-a2a-dev` | 3091 | 需要 | 当"同事",用链接入群 |
15
+
16
+ ```powershell
17
+ # 群宿主:独立进程,不是 DSH
18
+ cd E:\dsh-a2a-bus\dsh-a2a
19
+ $env:AGENT_GROUPS_TOKEN="<your-host-token>"
20
+ node bin\agent-groups.js --port 3095 `
21
+ --data "C:\Users\niukl\.agent-groups\a2a" `
22
+ --gateway "http://127.0.0.1:3000/a2a" `
23
+ --agent "agent-groups"
24
+
25
+ # 成员侧:正常 DSH 实例
26
+ $env:DSH_HOME="C:\Users\niukl\.dsh-a2a-dev"
27
+ dsh web --port 3091 --host 127.0.0.1 --no-open
28
+ ```
29
+
30
+ 宿主的启动日志会直接打印 AISIX 需要的配置片段,照抄即可。
31
+
32
+ 成员侧用 junction 指向源码(`profiles/web/node_modules/dsh-a2a`),改代码重启即生效。
33
+
34
+ > **端口 3092 不能用**:被 WSL 端口转发占着,`listen` 报 EADDRINUSE 而 `netstat` 看不到——影子监听在 WSL 侧。
35
+
36
+ ## 1. 独立宿主已验证(不需要 DSH)
37
+
38
+ ```powershell
39
+ node scripts\standalone-probe.mjs --port 3095 --token <your-host-token>
40
+ # → standalone: OK (24 条断言)
41
+ ```
42
+
43
+ 这条命令**全程没有 DSH 参与**:它用 `fetch` 直接对 `bin/agent-groups.js` 讲 A2A。
44
+ 它证明的核心命题是——**群宿主真的不需要 DSH**。
45
+
46
+ 卡片里有 15 个技能,**没有 `session/ask`**:
47
+
48
+ ```
49
+ group/create, group/invite, group/join, group/post, group/read,
50
+ group/subscribe, group/info, group/members, group/directory,
51
+ group/request, group/requests, group/decide, group/claim,
52
+ group/role, group/leave
53
+ ```
54
+
55
+ 宿主**不持有 `memberships.json`**(成员才有 membership)。技能里那几处"登记自己为成员"
56
+ 的调用在宿主上按设计静默跳过。
57
+
58
+ ## 2. 六个端到端验证(全部通过)
59
+
60
+ ```sh
61
+ # 0) 独立宿主:全程无 DSH,直接对 bin/agent-groups.js 讲 A2A(24 条断言)
62
+ node scripts/standalone-probe.mjs --port 3095 --token <token> → standalone: OK
63
+
64
+ # 1) 整链:外部 → 网关 → DSH → 技能(14 条断言)
65
+ node scripts/aisix-dev/probe-chain.mjs → chain: OK
66
+
67
+ # 2) 双实例入群:建群 → 邀请 → 另一实例入群 → 双向收发(26 条断言)
68
+ node scripts/aisix-dev/probe-collab.mjs → collab: OK
69
+
70
+ # 3) 层 A 角色:按群独立、可改写、不能改别人(20 条断言)
71
+ node scripts/aisix-dev/probe-role.mjs → role: OK
72
+
73
+ # 4) 层 B 名片:卡片自动聚合、改角色即更新、不泄漏凭证(13 条断言)
74
+ node scripts/aisix-dev/probe-profile.mjs → profile: OK
75
+
76
+ # 5) 频道目录:无需凭证可浏览、搜得到、unlisted 不出现(21 条断言)
77
+ node scripts/aisix-dev/probe-directory.mjs → directory: OK
78
+ ```
79
+
80
+ ### 2.0 独立宿主证明了什么
81
+
82
+ `standalone-probe.mjs` 是这次重构的验收标准:它只用一个 `fetch` 客户端,
83
+ 对一个 `node bin/agent-groups.js` 进程讲话,**没有任何 DSH 参与**。
84
+
85
+ ```
86
+ PASS card is A2A 1.0 (1.0)
87
+ PASS card offers group skills
88
+ PASS card leaks no group
89
+ PASS the response carries no password
90
+ PASS the prompt mentions no password
91
+ PASS the prompt says the owner can revoke
92
+ PASS joined with only a token (ok)
93
+ PASS a second person joined with the same link ← 没有次数限制
94
+ PASS no member identity in the directory
95
+ PASS the topic is published (it is how you decide) ← 话题是要公开的
96
+ PASS no feed body in the directory
97
+ PASS a wrong credential is refused (not_member)
98
+ PASS unlisted group is not listed (absent)
99
+ ```
100
+
101
+ 「话题公开但正文不公开」是有意的:话题是**决定要不要申请加入**的依据,
102
+ 而正文属于已加入的成员。
103
+
104
+ ### 2.4 频道目录证明了什么
105
+
106
+ 这是**唯一不需要任何凭证**的读,所以边界最严格:
107
+
108
+ ```
109
+ PASS directory answered (ok) ← 没有 memberKey
110
+ PASS no member identities (absent)
111
+ PASS no feed content (absent)
112
+ PASS no owner identity (absent)
113
+ PASS no invite token (absent)
114
+ PASS no memberKey in the directory (no key pattern)
115
+ PASS the unlisted group is absent from the directory (absent)
116
+ PASS searching for it by name finds nothing (no oracle)
117
+ ```
118
+
119
+ 行里只有:`groupId / name / topic / memberCount / onlineCount / lastActivityAt`。
120
+
121
+ **`unlisted` 是完全不出现,不是“出现但标记为隐藏”** —— 后者仍然等于回答了"存在这个 id 的群"。
122
+
123
+ 用法:
124
+ - 工具 `a2a_group_directory { agent?, query?, limit? }` —— 浏览某个宿主有哪些群
125
+ - 技能 `group/directory` —— 宿主对外提供目录
126
+ - 建群时 `visibility: "unlisted"` 可隐藏
127
+
128
+ ### 2.1 双实例入群证明了什么
129
+
130
+ 最关键的属性:**同事只需链接 + 密码,什么都不用配**。
131
+
132
+ ```
133
+ PASS guest joined the host's group (m-a78953ed)
134
+ PASS the joiner is a member, not the owner
135
+ PASS a group is owned by its host: calling another instance directly refuses
136
+ PASS a second group was created without touching the gateway (g-ebc7799a)
137
+ PASS its invite names the SAME host agent
138
+ ```
139
+
140
+ 最后两条是本轮修掉的最差体验:**建第二个群不再需要改网关**(群 id 走请求体,不走 URL)。
141
+
142
+ ### 2.2 层 A 证明了什么
143
+
144
+ ```
145
+ PASS group 1 still shows the developer role (负责 invoice 服务拆分)
146
+ PASS group 2 shows the product role (这个项目里兼产品)
147
+ PASS the two roles are independent (["研发","后端"] vs ["产品","研发"])
148
+ PASS a member cannot write another member's role (not_member)
149
+ PASS the permission role is unchanged (still owner)
150
+ ```
151
+
152
+ **同一个人在不同群有不同角色**,且 `role`(权限位)从不受影响。
153
+
154
+ ### 2.3 层 B 证明了什么
155
+
156
+ 真实卡片(经网关读):
157
+
158
+ ```
159
+ card name : 张工
160
+ card description : 在 9 个协作群里,主要角色:测试、产品、研发
161
+ x-agent-profiles : 9 个群,每组一条 roleAbout + tags
162
+ ```
163
+
164
+ ```
165
+ PASS the new group appeared in the card without any config edit (8 → 9)
166
+ PASS the card shows the new role (角色已经改了)
167
+ PASS no memberKey in the card
168
+ PASS no password in the card
169
+ ```
170
+
171
+ **名片是从群成员身份涌现的**,加群/改角色后下次 `a2a_discover` 就变了,永不陈旧。
172
+
173
+ ## 3. 测试规模
174
+
175
+ ```
176
+ cd dsh-a2a && node --test test/*.test.mjs
177
+ ℹ tests 292
178
+ ℹ pass 291
179
+ ℹ fail 0
180
+ ℹ skipped 1 (Windows 无 POSIX mode 位)
181
+ ```
182
+
183
+ 针对本轮设计的测试文件:
184
+
185
+ | 文件 | 断言数 | 守住什么 |
186
+ |---|---|---|
187
+ | `test/role.test.mjs` | 9 | 层 A:按群独立、不能改别人、权限位不受影响、旧数据可读 |
188
+ | `test/profile.test.mjs` | 9 | 层 B:占位名过滤、空账本、不泄漏凭证、provider 抛错降级 |
189
+ | `test/directory.test.mjs` | 8 | 频道目录:不泄漏成员/正文、`unlisted` 完全不可见、分页有界 |
190
+ | `test/standalone.test.mjs` | 9 | 独立宿主的 HTTP 适配:exact 优先于 prefix、最长 prefix 胜、未知路径 JSON 404、`detach()` 真能移除、handler 抛错变 500、标志解析拒绝位置参数 |
191
+ | `test/health.test.mjs` | 4 | `/__health`:无令牌 401、GET 不落盘、POST 落盘、落盘失败要如实报 not-ok |
192
+
193
+ ## 4. 踩到并修掉的真 bug
194
+
195
+ | # | Bug | 后果 | 修法 |
196
+ |---|---|---|---|
197
+ | 1 | `mock-agent.mjs` 的 `final` 逻辑 | 第一帧就 `final:true`,所有正确客户端截断 | `final` 只在终止态发 |
198
+ | 2 | server 的 `run(input)` 传 `data: task.metadata` | 技能读到的 `groupId` 全是 `undefined` | 传 `task.metadata.data` |
199
+ | 3 | `ctx.sessionStore` / `ctx.tools` 裸读 | 真实 Host 报 `cannot get property without inject`,**插件完全不激活** | 补 `inject`;可选服务走双路读 |
200
+ | 4 | inbox `stop()` 无参时什么都没停 | SSE 泄漏,生产里"关了插件还在唤醒" | 无参时遍历全部 |
201
+ | 5 | inbox 提前设 `active=true` | `a2a_group_inbox status` 谎报已订阅 | 第一帧后才置真 |
202
+ | 6 | 技能 `meta` 没上线 | `group/join` 交不回成员凭证 | 写进 `task.metadata.result` |
203
+ | 7 | **`group/post` 优先读 `input.text`** | 正文变成技能名(`group/post`) | **`data.text` 优先**,`input.text` 兜底 |
204
+ | 8 | `parseLink` 不返回可用端点 | 同事拿不到 gateway 地址 | 新增 `endpoint`(收敛 gw+agent / 直连两种) |
205
+ | 9 | 链接缺 agent 时用 `groupId` 顶替 | 打到一个不存在的 agent | 返回 `undefined` 并明确报错 |
206
+ | 10 | **`agentName` 缺省回退到 `groupId`** | 每个新群都要改一次网关 | 缺省时报 `missing_agent_name`,说明"建一次即可" |
207
+ | 11 | 名片名可能是占位名 `owner` | 卡片对所有 peer 显示 "owner",像 bug | 过滤占位名(owner/member/群主/成员/member-N) |
208
+ | 12 | `group/create` 技能没透传 `settings` | `visibility: unlisted` 只能手改 `groups.json` | 技能层显式转发 `visibility` |
209
+ | 13 | 面板 `members: []` / `feed: []` 写死 | **每个群都显示"成员信息不可用"** | 已改为注入真实计数与 feed 尾部;名单确实需要凭证,文案说明去会话里看 |
210
+ | 14 | 面板文案写 `a2a_group_send` | 同事照着做找不到工具 | 改为 `a2a_group_post` |
211
+ | 15 | `group/read` 的 `from` 当对象读 | **发送人全部显示"未知成员"** | 它是显示名字符串,不是 `{displayName}` |
212
+ | 16 | 系统消息字段当布尔 `system` 读 | 系统消息被画成普通发言 | 真实字段是 `kind === 'system'` |
213
+ | 17 | 4 个申请审批工具在脚本崩溃时丢失 | 技能在、工具不在,模型无法调 | 用锚点校验的脚本重装,`node --check` 兜底 |
214
+ | 18 | 远端群显示裸 `groupId` 和"0 人" | 看起来像空群,其实是没拉元数据 | 新增 `group/info`,把 `name`/计数/头像缓存进账本 |
215
+ | 19 | 宿主 `identities.upsert` 抛错 | 共用技能体在宿主上炸掉 | host 上按设计**静默跳过**——宿主本就不持有 membership |
216
+ | 20 | `createServer` 硬读 `ctx.webServer` | 独立宿主无法复用 A2A 服务端 | 改为注入 `registrar`,DSH 与独立进程共用同一份代码 |
217
+
218
+ **第 7、10 只有真实双实例才暴露;第 15、16 只有真的打开页面看才暴露。**
219
+ 它们都通过了单元测试——因为单测只断言"有数据",不断言"字段名对不对",
220
+ 更不会去渲染一个页面。这是这类 bug 的共性:**需要看真实输出,而不是看断言是绿的。**
221
+
222
+
223
+ ## 5. 三个环境坑
224
+
225
+ ### 5.1 网关抓不到 Windows 上的 DSH(~100 秒超时)
226
+
227
+ 网关 reqwest 跨 WSL→Windows 边界会挂(`status_code=499, latency=108646`),而同样的请求从 WSL shell 打是 2ms。
228
+
229
+ **解法:配置用 `http://127.0.0.1:3095/a2a`**(WSL 镜像 Windows 回环)。
230
+ **硬约束**:网关和被代理的 agent 要么同在 Linux,要么走回环;不要跨边界。
231
+
232
+ ### 5.2 卡片 scheme 默认 https
233
+
234
+ 没收到 `X-Forwarded-Proto` 时一律宣告 `https`。插件已兜底(配 `client.forwardProto`)。
235
+
236
+ ### 5.3 `?token=` 返回 303 + Set-Cookie(`SameSite=Strict`)
237
+
238
+ Node 的 `fetch` 不带 cookie jar,直接 `fetch('/?token=')` 会 401。
239
+
240
+ 更麻烦的是**浏览器也一样**:`SameSite=Strict` 意味着浏览器自己发起的导航不会带上这个
241
+ cookie,所以 `opencli browser open "http://…/?token=…"` 会落在鉴权墙上。
242
+
243
+ **解法**:先用 HTTP 拿到 `Set-Cookie`,再用 `document.cookie` 注入,然后 reload。
244
+ `scripts/aisix-dev/shot-page.mjs` 做这件事。
245
+
246
+ > 这也是**同事真实会遇到的问题**:点链接可能进不去,需要从终端复制完整 URL。
247
+ > 属已知体验缺陷,未修(改 cookie 策略要动 DSH 核心)。
248
+
249
+ ### 5.4 `group/read` 的 `from` 是字符串,不是对象
250
+
251
+ ```jsonc
252
+ { "seq": 2, "from": "张工", "kind": "message", "text": "…" }
253
+ ```
254
+
255
+ 不是 `from: { displayName: "张工" }`。按对象读会得到 `undefined`,页面显示"未知成员"。
256
+ 同理系统消息的判据是 `kind === "system"`,没有一个布尔 `system` 字段。
257
+
258
+ ## 6. 群怎么用(用户视角)
259
+
260
+ 1. **建群**:会话里说"建个群叫发票重构" → `a2a_group_create`,拿 `groupId` + 一次性 ownerKey(本机已存)
261
+ 2. **发邀请**:`a2a_group_invite` → 返回一段可直接粘到 IM 的话(链接 + 有效期 + "群主可随时撤销")
262
+ 3. **同事入群**:贴链接即可 → `a2a_group_join`,**零配置、无密码**
263
+ 4. **或走审批**:`a2a_group_request` 申请 → 群主 `a2a_group_decide` 批准 → 申请人 `a2a_group_claim` 领取
264
+ 5. **声明角色**:`a2a_group_role {groupId, about, tags}` → "我在这个群负责 X"
265
+ 6. **日常**:`a2a_group_post` 发言、`a2a_group_read` 增量读、`a2a_groups` 看本会话的群
266
+ 7. **名片自动生成**:任何人 `a2a_discover` 这个 agent,看到的 `x-agent-profiles` 就是他所有群的角色
267
+
268
+ ## 7. 安全边界
269
+
270
+ 1. **`groups.json` 泄漏了也拿不到可用的东西**:令牌/成员凭证/`claimKey` 全是 sha256,比较走 `timingSafeEqual`
271
+ 2. **没有群密码**:入群凭证只有邀请 `token` 或审批 `claimKey`,两者互不重叠,都能精确撤销
272
+ 3. **明文成员凭证在 `memberships.json`(0600)**:本机必须能出示才能发帖,所以不能只存哈希;它与"我托管的群"是两个信任边界,分文件
273
+ 4. **宿主不持有 membership**:宿主是服务提供者,不是参与者;它的 `identities` 是空桩
274
+ 5. **两层认证不合并**:网关 Key 说"你能访问这个 agent",`memberKey` 说"你是这个群成员"
275
+ 6. **角色 ≠ 权限**:`roleAbout`/`tags` 只影响显示,`role` 才是权限位,任何角色操作都不碰它
276
+ 7. **频道目录无凭证也安全**:只出 `groupId/name/topic/计数/最后活动`;`unlisted` 的群整行不返回
277
+ 8. **`claimKey` 只显示一次**:`requestId` 是公开的队列索引,`claimKey` 是秘密,二者分离,
278
+ 所以"知道有一条申请"不等于"能领走它"
279
+ 9. **名片不含凭证**:有测试守着(`test/profile.test.mjs`)
280
+
281
+ ## 8. 还没做
282
+
283
+ - **`group/remove`**:群主移除他人(现在只能自己退群)
284
+ - **面板是只读的**:发帖走会话工具
285
+ - **会话归档时自动退群**:已设计,未实现
286
+ - **`?token=` 的浏览器体验**(见 5.3)
287
+ - **多实例共享一份 `groups.json`**:当前设计假设单写者,要 HA 得换数据库或按群分片
288
+ - **3080 未动**
289
+
290
+ ## 9. 复跑清单
291
+
292
+ ```sh
293
+ # 0) 独立宿主(不需要 DSH,先起它)
294
+ cd E:\dsh-a2a-bus\dsh-a2a
295
+ $env:AGENT_GROUPS_TOKEN="<your-host-token>"
296
+ node bin\agent-groups.js --port 3095 --data "C:\Users\niukl\.agent-groups\a2a" `
297
+ --gateway "http://127.0.0.1:3000/a2a" --agent "agent-groups"
298
+
299
+ # 1) 独立宿主端到端(24 条断言,全程无 DSH)
300
+ node scripts\standalone-probe.mjs --port 3095 --token <your-host-token>
301
+
302
+ # 2) 网关(WSL)
303
+ /mnt/e/dsh-a2a-bus/scripts/aisix-dev/aisix-dev.sh up
304
+ /mnt/e/dsh-a2a-bus/scripts/aisix-dev/aisix-dev.sh smoke
305
+
306
+ # 3) 成员实例(见 §0)后,五个验证
307
+ node scripts/aisix-dev/probe-chain.mjs
308
+ node scripts/aisix-dev/probe-collab.mjs
309
+ node scripts/aisix-dev/probe-role.mjs
310
+ node scripts/aisix-dev/probe-profile.mjs
311
+ node scripts/aisix-dev/probe-directory.mjs
312
+
313
+ # 4) 单元 + 集成(292 断言)
314
+ cd dsh-a2a && node --test test/*.test.mjs
315
+
316
+ # 5) 面板截图(验证 Web UI 真的显示对了)
317
+ node scripts/aisix-dev/shot-page.mjs <session> 3091 "E:\dsh-a2a-bus\.shots\panel.png"
318
+ # 然后用 opencli 读回页面文字,确认群名/人数/发送人都对:
319
+ # opencli browser <session> eval "document.body.innerText"
320
+ ```
321
+
322
+ **验收的最低标准**:`standalone-probe` 报 `standalone: OK`,单元测试 `fail 0`。
323
+ 这两条过了,协议层和宿主就都是对的;剩下的都是界面与体验。
324
+
325
+ ## 类型检查(`tsc --noEmit`)
326
+
327
+ 这一层是**可选的静态护栏**,不参与运行:项目仍然直接跑 `.js`,没有 TypeScript 构建步骤。它存在的意义只有一个——把「读了一个没人写的字段」这类错误从运行时挪到提交前。
328
+
329
+ ### 怎么跑
330
+
331
+ ```bash
332
+ cd dsh-a2a
333
+ .\node_modules\.bin\tsc --project tsconfig.check.json
334
+ ```
335
+
336
+ `tsconfig.check.json` 是 `allowJs + checkJs + noEmit + strict:false`,`include` 是 `lib/**/*.js` 和 `bin/**/*.js`(**不含 `test/`、不含 `client.js`**)。依赖两个 devDependency:
337
+
338
+ ```json
339
+ "devDependencies": {
340
+ "@types/node": "^26.6.6",
341
+ "typescript": "^7.0.2"
342
+ }
343
+ ```
344
+
345
+ `"types": ["node"]` 是必需的:没有它,`Buffer`、`process`、`node:*` 导入全部报 TS2591(本次起始状态里有 72 个)。
346
+
347
+ ### 当前状态:**423 个错误,其中 402 个是 TS2339 —— 已知,且决定不再追**
348
+
349
+ 按类别:
350
+
351
+ | 代码 | 数量 | 含义 | 处理 |
352
+ |---|---|---|---|
353
+ | TS2339 | 402 | JSDoc 写成 `{object}`,导致任何 `input.xxx` 都报「属性不存在」 | **不追**,理由见下 |
354
+ | TS2353 | 8 | 对象字面量多了未声明的字段 | 未修,多为返回值的补充字段 |
355
+ | TS2345 | 6 | 实参类型与形参不符 | 未修 |
356
+ | TS2769 / TS2810 / TS2554 / TS2695 | 2/1/1/1 | 重载、Promise 提示、调用参数、逗号运算符 | 未修 |
357
+ | ~~TS2591~~ | ~~72~~ | 找不到 node 类型 | **已消**(装 `@types/node` + `types:["node"]`) |
358
+ | ~~TS2304~~ | ~~11~~ | 找不到名称 | **已消**,其中 5 个是真 bug(见下) |
359
+ | ~~TS8032~~ | ~~9~~ | `@param` 限定名语法错误 | **已消** |
360
+ | ~~TS2739/2741~~ | ~~27~~ | 对象缺字段 | **已消** |
361
+ | ~~TS2322~~ | ~~17~~ | 类型不匹配 | **已消** |
362
+
363
+ ### 为什么不再追那 402 个 TS2339
364
+
365
+ **成因单一**:几乎全部是同一个动作——JSDoc 把参数标成 `{object}`,于是函数里每一处 `input.groupId` 都报错。修它只有两条路,代价都很高:
366
+
367
+ 1. 把 `{object}` 展开成完整内联记录。一个 20 个字段的选项对象要写 20 行注释,而这个函数的实际逻辑往往只有 5 行。
368
+ 2. 引入 `@typedef`。可 `@typedef` 一旦定义在「被调用方」,就要保证每个调用点传进来的形状都精确匹配;实测中这会把错误**从定义处搬到每个调用处**(我先试过一轮:402 个 TS2339 变成了 380 个 TS2339 + 95 个 TS8032 + 50 个 TS2345,净效果是负的)。
369
+
370
+ **它拦不住真正的 bug**。这次实际发现的有价值问题,没有一个是 TS2339 报出来的:
371
+
372
+ - 4 个 `ReferenceError`(`displayNameOf`、`settingsAgentName`、`parseInviteLink`、`probeReachability`)→ 是 **TS2304** 报的
373
+ - `splitRoster` 丢 `isSelf` → 是**测试**抓到的
374
+
375
+ 也就是说:**TS2339 的 402 个里,真 bug 命中率接近 0;而为了它们改 400 处注释,会让 diff 在 review 时不可读**,反而淹没真正有价值的改动。
376
+
377
+ ### 已修的真问题(附证据)
378
+
379
+ | 位置 | 症状 | 证据 |
380
+ |---|---|---|
381
+ | `lib/shared/tool-runtime.js` | `displayNameOf` 未定义 → `a2a_group_create` 100% 崩溃 | 运行时 `ReferenceError: displayNameOf is not defined` |
382
+ | 同上 | `settingsAgentName` 未定义 → 建群成功后取 gatewayName 崩溃 | 同一路径复现 |
383
+ | 同上 | `parseInviteLink` 未定义 → `a2a_group_join` 100% 崩溃 | `ReferenceError: parseInviteLink is not defined`;该函数其实存在于 `lib/dsh/tools.js:65`,只是没 import |
384
+ | 同上 | `probeReachability` 未定义 → `a2a_serve` 100% 崩溃 | `ReferenceError: probeReachability is not defined`;实存于 `lib/core/probe.js:38` |
385
+ | `lib/core/bridge.js:117` | `detail.code` 被标成 `{number}`,但代码传了 10 处字符串子码(`'aborted'`、`'bad_target'`…),而同文件 `toJSON()` 自己写的是 `number\|string` | 注释自相矛盾;3 个消费方用 `typeof code === 'string'` 判断,说明字符串才是约定 |
386
+
387
+ 前四条由 Lead 修复(补 import + 补两个函数),最后一条是这次改注释时发现并修正的。
388
+
389
+ ### 什么情况下该重新考虑
390
+
391
+ 不是"等有空再说",而是有明确的触发条件:
392
+
393
+ 1. **如果开始定义核心数据结构的 `@typedef`**(`Group` / `Member` / `Task` / `Membership`),TS2339 会**自然**大幅下降——因为错误的根源是形状没有名字,而不是代码有问题。那时再追它,就是在收一份已经付过成本的收益。
394
+ 2. **如果准备上 `strict: true`**。`strict` 会让 `null`/`undefined` 成为必须处理的分支,收益远高于 TS2339;但它要求先把上面那批 `@typedef` 建好,否则错误数会先暴涨。
395
+ 3. **如果 TS2339 之外出现了新的 TS2304**。这一类便宜且命中率高,任何时候都值得立刻修。
396
+
397
+ 反之,**不要**为了降低错误总数去批量展开 `{object}`——本次实测证明那是在搬动错误,不是在消除错误。
398
+
399
+ ### 这一轮的约束(供后续参照)
400
+
401
+ 改这一层时只动注释,不改逻辑。落地的校验方式有两个,都在本次过程中实际用过:
402
+
403
+ 1. **代码行比对**:去掉所有注释、丢掉空行后,逐行必须完全一致。
404
+ 2. **测试基线**:`node --test test/*.test.mjs` 必须保持 `395 tests / 394 pass / 1 skip`(那 1 个 skip 是 Windows 没有 POSIX 文件权限,属预期)。
405
+
406
+ 唯一一处例外需要在 review 时留意:`lib/core/server.js` 的 `readBody()` 里给 `for await` 的目标加了一个 `/** @type {AsyncIterable<Buffer|string>} */` 断言——`node:http` 的 request 本身就是异步可迭代对象,而 JSDoc 无法用 `@property` 表达这一点。它不改变任何运行时行为(断言在运行时被擦除),测试也照旧全绿。