cs-bots 0.8.0 → 0.9.0
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 +186 -0
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +5 -5
- package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +3 -3
- package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +3 -3
- package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +2 -2
- package/package.json +2 -2
- package/src/api.js +100 -1
- package/src/board.js +61 -0
- package/src/client.js +194 -14
- package/src/dispatcher.js +70 -11
- package/src/index.js +8 -1
- package/src/kernel.js +15 -0
- package/src/room.js +160 -9
- package/src/roster.js +54 -0
- package/src/teams.js +353 -4
- package/src/tools.js +48 -2
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,192 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## [0.9.0] - 2026-11-11
|
|
17
|
+
|
|
18
|
+
> 第 1 批 = 定向唤醒 / kind 字段 / attachRoom(止血)
|
|
19
|
+
> 第 2 批 = `bot_team_route` 跨团队路由 / 环路防护 / 指标埋点 / 文档校验(补欠账)
|
|
20
|
+
|
|
21
|
+
### Added · 第 2 批:跨团队桥本体 `bot_team_route`
|
|
22
|
+
|
|
23
|
+
**这是「大团队 = 跨团队桥」的第一块实体。** 此前 42 个工具里**没有任何一个**
|
|
24
|
+
能让 Bot 表达「我要找另一个团队」—— 桥是断的。
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
bot_team_route({ fromTeam?, toTeam?, toBot?, taskId, text, hop? })
|
|
28
|
+
// 成功: { ok:true, decision:'delivered', to:{kind:'team',id,leadId,name}, hop, wake }
|
|
29
|
+
// 被拒: { ok:false, decision:'refused', reason:'ROUTE_HOP_EXCEEDED'|... }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
三条设计约束:
|
|
33
|
+
1. **只唤醒对方 `leadId`**,不唤醒全部成员(测试断言 `delivered.length === 1`)
|
|
34
|
+
2. **先落 route 事件再投递**(intent-first)——「记了没投」可重试;「投了没记」会绕过环路统计
|
|
35
|
+
3. 失败一律返回可读 `refused + reason`,**绝不静默**
|
|
36
|
+
|
|
37
|
+
**为什么 `toBot` 在 cs-bots 团队里找,不去 DSH 原生 `agentTeam` 找**:
|
|
38
|
+
实测 `grep -n "agentTeam|TeamId" src/teams.js` → 除新代码外**零命中**。
|
|
39
|
+
`teams.jsonl` 存 `{id, name, leadId, type, kind, members}`,而原生 team 是 `teamId = root.id`——
|
|
40
|
+
**两套人模型无 id 交集**。混用恰恰是 Boss 这次要区分的那个错误。
|
|
41
|
+
|
|
42
|
+
### Added · 环路防护(两道闸)
|
|
43
|
+
|
|
44
|
+
1. `hop > 3` ⇒ `ROUTE_HOP_EXCEEDED`(实测 hop 0–3 放行,hop 4 拒绝)
|
|
45
|
+
2. **复用 dispatcher 的 `dedupKey`**(没另造一套):键 = `route:<from>:<to>:<taskId>`,
|
|
46
|
+
**刻意不含 hop** —— 这样 A→B(hop0) 与 A→B(hop2) 是同一个键,
|
|
47
|
+
dispatcher 的 `IdempotencySet` 判 `dedup-hit`,第 3 跳自然被拦。
|
|
48
|
+
|
|
49
|
+
> 为什么不把 hop 放进键:放进去就"每一跳都是新消息",去重永远命不中,环路防护形同虚设。
|
|
50
|
+
|
|
51
|
+
### Fixed · route 审计记录重启后全丢(真 bug)
|
|
52
|
+
|
|
53
|
+
`hydrate()` 里 `if (!ev || typeof ev.id !== 'string')` 把 route 事件当残缺数据跳过 ——
|
|
54
|
+
**但 route 事件本来就没有 `id`**(它不改团队实体,只是审计记录)。
|
|
55
|
+
|
|
56
|
+
后果:`listRoutes` 重启后返回 0 条,而**环路防护和成本核算都依赖这份历史** —— 等于失效。
|
|
57
|
+
修法:回放时对 `action === 'route'` 单独放行。
|
|
58
|
+
|
|
59
|
+
### Added · 指标埋点补全(欠账2)
|
|
60
|
+
|
|
61
|
+
**之前 256 条 metrics 全是 `turn_end`** —— 因为只有 `index.js` 在回合结束自动记,
|
|
62
|
+
认领/完成/验收这些**真正该测的动作从没被记过**。PDCA 页面因此看不到验收通过率。
|
|
63
|
+
|
|
64
|
+
修法:在 `commit()` 埋点(**所有状态变更的唯一出口**,埋这里才不会漏):
|
|
65
|
+
| board 动作 | 指标 kind |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `claim` | `task_claim` |
|
|
68
|
+
| `complete` | `task_complete` |
|
|
69
|
+
| `block` | `task_blocked` |
|
|
70
|
+
| `accept(pass)` | `accept_pass` |
|
|
71
|
+
| `accept(reject)` | `accept_reject` |
|
|
72
|
+
|
|
73
|
+
**装配顺序坑**:`index.js` 里 board 在 L130 装配、metrics 在 L276 装配。
|
|
74
|
+
若只接受实例,board 装配时 metrics 还是 undefined ⇒ 埋点永久静默失效。
|
|
75
|
+
⇒ 改成接受**取值函数** `metrics: () => services.metrics`,规避顺序耦合。
|
|
76
|
+
|
|
77
|
+
**纪律**:埋点全包在 `try{}catch{}` 里 —— 指标是旁路,**绝不能**让业务动作失败。
|
|
78
|
+
|
|
79
|
+
### Added · 文档一致性校验(欠账3)
|
|
80
|
+
|
|
81
|
+
`test/docs-consistency.test.mjs`(5 项,随 `run-all.sh` 自动跑):
|
|
82
|
+
① 主文档版本声明 == `package.json` version
|
|
83
|
+
② 文档里的「NNN 项测试」不得低于基线
|
|
84
|
+
③ 文档里的工具数 == `TOOL_NAMES.length`
|
|
85
|
+
④ 历史快照必须保留「勿引用/已过期」标记
|
|
86
|
+
⑤ `reports/` 交付报告必须存在
|
|
87
|
+
|
|
88
|
+
**它上线当天就抓出真失真**:
|
|
89
|
+
```
|
|
90
|
+
docs/02-03-04 声明 v0.7.3,实际 0.8.0
|
|
91
|
+
5 处写 1108 项测试,实际 1263
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**刻意不做的事**:
|
|
95
|
+
- 不改历史叙述(`01-架构文档.md` 里「0.7.3 已部署重启」是当时事实,改写就是伪造历史)
|
|
96
|
+
- 不校验 CHANGELOG(它本来就按版本分段记历史)
|
|
97
|
+
- 测试数用**下限**而非精确相等(否则每加一个用例就红一次,最后会被人 disable —— 比没有还糟)
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## [Unreleased] - bots04 第 1 批「止血」
|
|
102
|
+
|
|
103
|
+
大团队语义澄清后的第一批落地。**核心是止血**:修掉一个已经在烧 token 的实测缺陷。
|
|
104
|
+
|
|
105
|
+
### Fixed · 唤醒风暴(实测已在烧钱)
|
|
106
|
+
|
|
107
|
+
**改造前实测**(`~/.dsh/cs-bots/wakes.jsonl`):
|
|
108
|
+
```
|
|
109
|
+
room-msg:m-1:ui → failed ← 居然去唤醒 Boss 自己
|
|
110
|
+
room-msg:m-1:bot-6b65… → failed
|
|
111
|
+
room-msg:m-1:bot-df1c… → failed
|
|
112
|
+
```
|
|
113
|
+
一条房间消息 = 3 次唤醒尝试,**3 次全失败**。
|
|
114
|
+
|
|
115
|
+
**两个根因**:
|
|
116
|
+
1. `relay` 的 `targets` 缺省 = 「除发送者外全部成员」⇒ 一条消息 N 个成员 = N 次模型调用;
|
|
117
|
+
2. 人类身份(`ui`/`user`/`person`)被当成普通成员 ⇒ **去唤醒人**,这没有任何意义。
|
|
118
|
+
|
|
119
|
+
**修法**(`src/room.js`):
|
|
120
|
+
- `DEFAULT_SETTINGS` 新增 `mentionOnly: true` —— **缺省安静**,要广播必须显式打开
|
|
121
|
+
- 新增 `HUMAN_IDS` + `isHumanId()`,**任何模式**下 human id 都不被唤醒
|
|
122
|
+
- `relay()` 重写为四档 `targetSource`:
|
|
123
|
+
| 档 | 行为 |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `explicit` | 显式 targets 原样;混入 human 剔除 + warn |
|
|
126
|
+
| `mention` | @ 到谁只唤醒谁 |
|
|
127
|
+
| `mention-only-no-target` | **没 @ ⇒ `relayed:0` 且不抛错**(止血核心) |
|
|
128
|
+
| `broadcast` | `mentionOnly:false` 才回退全员,且仍排除 human |
|
|
129
|
+
|
|
130
|
+
**为什么缺省是 `true` 而不是 `false`**:安全的默认值必须落在「少打扰」这一侧。
|
|
131
|
+
要广播是少数场景,让它多打一次字,比让每条消息都烧 N 次调用更划算。
|
|
132
|
+
|
|
133
|
+
### Added · 团队归属标识 `kind`
|
|
134
|
+
|
|
135
|
+
`teams.jsonl` 新增 `kind: 'human' | 'agent'`:
|
|
136
|
+
- `by ∈ {ui, user, person}` ⇒ **强制 `'human'`**
|
|
137
|
+
- 其他 ⇒ **强制 `'agent'`**
|
|
138
|
+
- 显式传入与推导矛盾 ⇒ **抛 `TEAM_KIND_MISMATCH`**
|
|
139
|
+
|
|
140
|
+
**为什么必须服务层强制推导、不信任入参**:`createdBy` 是可冒用的字符串。
|
|
141
|
+
Bot 传 `by:'ui'` 就能自称「Boss 建的团队」—— 那两个方向的伪造都得拦:
|
|
142
|
+
Bot 谎称 Boss 建(骗信任)、Boss 谎称 Bot 建(掩盖归属)。
|
|
143
|
+
|
|
144
|
+
**与 `type(big|small)` 的关系**:`type` 保留但**降级为规模提示**,不再承载「谁建的」语义。
|
|
145
|
+
历史数据缺 `kind` ⇒ 前端按 `createdBy` 兜底,不显示 undefined。
|
|
146
|
+
|
|
147
|
+
UI 徽标:**`👤 Boss建设`** / **`🤖 智能体建设`**。
|
|
148
|
+
> 文案纪律:**不用「人建」**(Boss:「不太好听」)。连注释里也不许出现。
|
|
149
|
+
|
|
150
|
+
### Fixed · 桥断了:`bot_teams_add` 不建房间
|
|
151
|
+
|
|
152
|
+
`attachRoom` 只在 API 层(`src/api.js:1555`)有,**工具层没有** ⇒
|
|
153
|
+
智能体用 `bot_teams_add` 加人后团队与房间脱钩,**跨团队消息发不出去**。
|
|
154
|
+
这是「大团队这座桥是断的」的直接原因。
|
|
155
|
+
|
|
156
|
+
**修法**(`src/tools.js`):加成员后若 `team.roomId` 存在则自动 `room.join`;
|
|
157
|
+
失败**不阻断**团队编辑,返回 `{ok:true, team, roomError}` 留痕。
|
|
158
|
+
|
|
159
|
+
**过程教训**:初版在工具 `run` 里调 `warn(...)`,但 `warn` 只是 `installTools` 的**形参**、
|
|
160
|
+
不在工具闭包作用域内 ⇒ `ReferenceError: warn is not defined` 导致工具整体失败。
|
|
161
|
+
工具闭包里只能拿到 `ctx`,拿不到安装期的形参。
|
|
162
|
+
|
|
163
|
+
### Added · 测试(+22,基线 1241 → 1263)
|
|
164
|
+
|
|
165
|
+
| 文件 | 新增 |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `test/room.test.mjs` | +6(mention 定向、无 @ 静默、human 排除、显式 targets 剔除、settings 白名单、回放持久) |
|
|
168
|
+
| `test/teams.test.mjs` | +5(Boss⇒human、Bot⇒agent、矛盾抛错、事件流持久、与 type 并存) |
|
|
169
|
+
| `test/tools.test.mjs` | +3(自动入房、无房间不报错、join 失败不阻断) |
|
|
170
|
+
| `test/client-t38.test.mjs` | 新增 7 项(徽标文案、不置灰、@ 提示) |
|
|
171
|
+
|
|
172
|
+
**变异验证**(确认断言不是摆设):
|
|
173
|
+
| 变异 | 结果 |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `mentionOnly: true` → `false` | 5 用例转红 |
|
|
176
|
+
| `derivedKind` 硬编码 `'agent'` | 4 用例转红 |
|
|
177
|
+
| 关掉 auto-attach 分支 | 2 用例转红 |
|
|
178
|
+
|
|
179
|
+
### 认知纠正(记录在案,避免后人重踩)
|
|
180
|
+
|
|
181
|
+
旧调研 `design/大团队-可行性调研-v1.md`(11-06)把大团队定位成「组织层 / 聚合视图」,
|
|
182
|
+
**这个定位错了**。Boss 2026-11-11 正式澄清:大团队是**跨团队沟通的桥梁**(路由层)。
|
|
183
|
+
|
|
184
|
+
> **小团队是「谁干活」,大团队是「谁跟谁说话」。**
|
|
185
|
+
|
|
186
|
+
另外调研里「小团队:Lead 自动 spawn,人不进去」也**不准确** ——
|
|
187
|
+
官方 `dsh-experimental-client-ui-agent-team/README.zh.md` L34/L91/L184 证实:
|
|
188
|
+
用户**能**点进 active teammate 会话发言,走 addressed-subagent 路径。
|
|
189
|
+
正确表述是「人可以进,只是不常进」。
|
|
190
|
+
|
|
191
|
+
### 已知限制
|
|
192
|
+
|
|
193
|
+
- 「Boss 发言同步给 DSH 原生团队 leader」**本批未做**。3 条硬限制:
|
|
194
|
+
① 官方无「Bot→Lead 发 peer 消息」公开工具;
|
|
195
|
+
② teammate 投递需 Lead-to-child edge 授权,sibling 消息不能伪装成 Lead;
|
|
196
|
+
③ cs-bots 当前无法读取 `agentTeam` 投影(只在 `compat.js:150` 探测服务存在)。
|
|
197
|
+
⇒ 需单独立项 2–3 天,硬做只能走「伪装成 Lead」,违反官方不变量。
|
|
198
|
+
- 跨团队定向请求(`bot_team_route`)未实现,属第 2 批。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
16
202
|
## [0.8.0] - 2026-11-10
|
|
17
203
|
|
|
18
204
|
改造09「Boss 体验 + 任务可管理性 + 升级接续」。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cs-bots 架构总览(改造07 · 分层架构 + 改造08 · 人机分治)
|
|
2
2
|
|
|
3
|
-
> 版本:v0.
|
|
3
|
+
> 版本:v0.9.0(改造07 分层 + 改造08 需求1–5 + 改造09 Boss 体验/任务可管理性/升级接续)
|
|
4
4
|
> 日期:2026-11-10
|
|
5
5
|
> 状态:改造09 进行中 —— 套件规模 1190 项,截至 2026-11-10 有 6 项失败待修(详见 `docs/CONTRACT-升级09-Boss体验.md` §10)。改完前**不要**把本文当作「全绿」依据。
|
|
6
6
|
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
│ │
|
|
51
51
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
52
52
|
│ │ AI 智能体调用接口(机器友好、可审计) │ │
|
|
53
|
-
│ │
|
|
53
|
+
│ │ 43 个 bot_* 工具 + HTTP API │ │
|
|
54
54
|
│ └──────────────────────────────────────────────────────┘ │
|
|
55
55
|
└─────────────────────────────────────────────────────────────┘
|
|
56
56
|
```
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
| 后端运行时 | Node.js ≥22.5,ESM,零第三方运行时依赖 |
|
|
142
142
|
| HTTP API | 宿主 `webServer.register`(前缀路由 `/cs-bots/api`) |
|
|
143
143
|
| 前端 | React 18,宿主模块加载器 `window.__ModuleLoader__.load` |
|
|
144
|
-
| 模型工具 | 宿主 `tools.register`,
|
|
144
|
+
| 模型工具 | 宿主 `tools.register`,43 个 `bot_*` 工具 |
|
|
145
145
|
| 持久化 | JSONL 事件流 + 原子快照,SQLite 不引入 |
|
|
146
146
|
| 安全 | 会话鉴权围栏 + 同源校验 + 受保护区 + CSRF 防护 |
|
|
147
147
|
|
|
@@ -212,7 +212,7 @@ src/subscription.js 任务订阅 + 房间扇出(改造07 新增)
|
|
|
212
212
|
src/routing/ 复杂度路由
|
|
213
213
|
src/store.js 存储原语(jsonl + 快照 + 幂等集 + 原子写)
|
|
214
214
|
src/kernel.js 宿主桥(唯一可引用宿主包的文件)
|
|
215
|
-
src/tools.js
|
|
215
|
+
src/tools.js 43 个模型工具
|
|
216
216
|
src/index.js 装配入口
|
|
217
217
|
```
|
|
218
218
|
|
|
@@ -227,7 +227,7 @@ src/index.js 装配入口
|
|
|
227
227
|
| 房间→任务 | 无连接 | Subscription 扇出 |
|
|
228
228
|
| Bot 开工 | 无准备步骤 | prepare command |
|
|
229
229
|
| 底层可见性 | 无(黑盒) | 穿透面板(7 区块) |
|
|
230
|
-
| 工具数 | 38 个 | 41 个 |
|
|
230
|
+
| 工具数 | 38 个 | 41 个 | **43 个**(改造09 新增 `bot_task_phase`,bots04 新增 `bot_team_route`) |
|
|
231
231
|
| 测试数 | 949 项 | 1036 项 |
|
|
232
232
|
|
|
233
233
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cs-bots 改造07 + 改造08(需求1–5)需求规格书
|
|
2
2
|
|
|
3
|
-
> 版本:v0.
|
|
3
|
+
> 版本:v0.9.0(改造07 分层 + 改造08 需求1–5 + 改造09 + bots04 大团队)
|
|
4
4
|
> 日期:2026-11-09
|
|
5
5
|
> 状态:已实现
|
|
6
6
|
|
|
@@ -132,7 +132,7 @@ cs-bots v0.6.x 有 10 个扁平页签,所有功能平铺在一起,人和 AI
|
|
|
132
132
|
| 编号 | 需求 | 验收 |
|
|
133
133
|
|---|---|---|
|
|
134
134
|
| NFR-1 | 零第三方运行时依赖 | `bash test/ci-zero-dep.sh` 通过 |
|
|
135
|
-
| NFR-2 | 全部既有测试通过 |
|
|
135
|
+
| NFR-2 | 全部既有测试通过 | 1263 项测试 0 失败 |
|
|
136
136
|
| NFR-3 | 零 `window.prompt/confirm/alert` | CI 检查 5 通过 |
|
|
137
137
|
| NFR-4 | 所有失败可读 | 每个错误有 code + 人话 |
|
|
138
138
|
| NFR-5 | 移动端可用 | 页签横向滚动、穿透面板全屏抽屉 |
|
|
@@ -269,7 +269,7 @@ cs-bots v0.6.x 有 10 个扁平页签,所有功能平铺在一起,人和 AI
|
|
|
269
269
|
```
|
|
270
270
|
test/kernel-conversation.test.mjs 8 项 ✅
|
|
271
271
|
test/client-anchor.test.mjs 39 项 ✅
|
|
272
|
-
全套 node --test test/*.test.mjs →
|
|
272
|
+
全套 node --test test/*.test.mjs → 1263 / 1263 pass, 0 fail
|
|
273
273
|
bash test/run-all.sh → ✅ 全部通过(exit 0)
|
|
274
274
|
dsh-plugins.sh precheck → OK cs-bots -> src/client.js
|
|
275
275
|
dsh web 重启 → 启动图端到端校验 PASS
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# cs-bots 改造07 + 改造08(需求1–5)详细设计文档
|
|
2
2
|
|
|
3
|
-
> 版本:v0.
|
|
3
|
+
> 版本:v0.9.0(改造07 分层 + 改造08 需求1–5 + 改造09 + bots04 大团队)
|
|
4
4
|
> 日期:2026-11-09
|
|
5
|
-
> 状态:已实现,
|
|
5
|
+
> 状态:已实现,1263 项测试全通过(`bash test/run-all.sh` exit 0)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -552,7 +552,7 @@ locate: current.id === 'team' && activeView.id === 'rooms' ? locate : undefined
|
|
|
552
552
|
```
|
|
553
553
|
test/kernel-conversation.test.mjs 8 项 ✅
|
|
554
554
|
test/client-anchor.test.mjs 39 项 ✅
|
|
555
|
-
全套 node --test test/*.test.mjs →
|
|
555
|
+
全套 node --test test/*.test.mjs → 1263 / 1263 pass, 0 fail
|
|
556
556
|
bash test/run-all.sh → ✅ 全部通过(exit 0)
|
|
557
557
|
dsh-plugins.sh precheck → OK cs-bots -> src/client.js
|
|
558
558
|
dsh web 重启 → 启动图端到端校验 PASS
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cs-bots 改造07 + 改造08(需求1–5)用户操作手册
|
|
2
2
|
|
|
3
|
-
> 版本:v0.
|
|
3
|
+
> 版本:v0.9.0(改造07 分层 + 改造08 需求1–5 + 改造09 + bots04 大团队)
|
|
4
4
|
> 日期:2026-11-09
|
|
5
5
|
> 适用对象:使用 cs-bots 协作平台的人(不是 AI)
|
|
6
6
|
|
|
@@ -554,7 +554,7 @@ A:能。页签横向滚动,穿透面板是全屏抽屉。
|
|
|
554
554
|
```
|
|
555
555
|
test/kernel-conversation.test.mjs 8 项 ✅
|
|
556
556
|
test/client-anchor.test.mjs 39 项 ✅
|
|
557
|
-
全套 node --test test/*.test.mjs →
|
|
557
|
+
全套 node --test test/*.test.mjs → 1263 / 1263 pass, 0 fail
|
|
558
558
|
bash test/run-all.sh → ✅ 全部通过(exit 0)
|
|
559
559
|
dsh-plugins.sh precheck → OK cs-bots -> src/client.js
|
|
560
560
|
dsh web 重启 → 启动图端到端校验 PASS
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -2675,7 +2675,30 @@ export function installApi(ctx, { warn } = {}) {
|
|
|
2675
2675
|
}
|
|
2676
2676
|
}
|
|
2677
2677
|
|
|
2678
|
-
|
|
2678
|
+
// ── t-39 第2批 C:发言同步给 DSH 原生 team 的 Lead(Boss 体验 item 5)──
|
|
2679
|
+
//
|
|
2680
|
+
// Boss 在 cs-bots 议事厅说话时,原生 team 的 Lead 感知不到 —— 两套团队模型
|
|
2681
|
+
// 是分离的。这一步把消息也投递给绑定的原生 Lead(root session 路径,合法)。
|
|
2682
|
+
//
|
|
2683
|
+
// 诚实原则:同步失败**不报** ok:false(房间消息本身成功了),而是
|
|
2684
|
+
// `leadSync.synced:false` + `reason`,页面据此显示「同步失败:<原因>」。
|
|
2685
|
+
// 绝不装成功 —— 那比不显示更糟。
|
|
2686
|
+
let leadSync
|
|
2687
|
+
try {
|
|
2688
|
+
leadSync = await withService(services, 'teams', (t) =>
|
|
2689
|
+
t.syncToNativeLead({
|
|
2690
|
+
roomId: message.roomId,
|
|
2691
|
+
messageId: message.id,
|
|
2692
|
+
text: message.text,
|
|
2693
|
+
by: body.from ?? 'ui',
|
|
2694
|
+
dispatcher: services.dispatcher,
|
|
2695
|
+
}))
|
|
2696
|
+
} catch (error) {
|
|
2697
|
+
// 同步是附加动作:异常也必须变成可见的 synced:false,而不是 500
|
|
2698
|
+
leadSync = { ok: true, synced: false, reason: 'SYNC_ERROR', detail: String(error?.message ?? error) }
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
sendJson(response, 200, { ok: true, message, wakes: toWake.length, revives, fanout, leadSync })
|
|
2679
2702
|
return
|
|
2680
2703
|
}
|
|
2681
2704
|
|
|
@@ -5000,6 +5023,82 @@ export function installApi(ctx, { warn } = {}) {
|
|
|
5000
5023
|
sendJson(response, 200, { ok: true, team: out })
|
|
5001
5024
|
return
|
|
5002
5025
|
}
|
|
5026
|
+
// ── 跨团队路由(t-39 第2批 A · HTTP 出口)───────────────────────────
|
|
5027
|
+
//
|
|
5028
|
+
// 为什么必须有这一条:页面上的「📤 发协作请求」按钮(client.js)就是打这个
|
|
5029
|
+
// 路径的。只有 MCP 工具 `bot_team_route` 而没有 HTTP 出口 ⇒ Boss 点按钮 404。
|
|
5030
|
+
//
|
|
5031
|
+
// 复用 `teams.deliverRoute()` —— 与工具层**同一套序列编排**(决定 → 投递 →
|
|
5032
|
+
// 留痕)。绝不在这里重写一遍,否则两条路的行为会漂。
|
|
5033
|
+
if (request.method === 'POST' && path === '/teams/route') {
|
|
5034
|
+
const body = (await readBody(request)) ?? {}
|
|
5035
|
+
const out = await withService(services, 'teams', (t) =>
|
|
5036
|
+
t.deliverRoute({
|
|
5037
|
+
...body,
|
|
5038
|
+
by: body.by ?? OWNER,
|
|
5039
|
+
// dispatcher 从装配层取出后注入:TeamsService 不反向依赖 dispatcher
|
|
5040
|
+
dispatcher: services.dispatcher,
|
|
5041
|
+
})).catch(markTeamError)
|
|
5042
|
+
// refused 是**正常的业务结论**,不是服务器错误 ⇒ 200 带 decision:'refused'。
|
|
5043
|
+
// 用 4xx 会让前端把它当异常处理,丢掉 reason 的可读性(DecisionKind 语义)。
|
|
5044
|
+
sendJson(response, 200, out)
|
|
5045
|
+
return
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
// ── 团队唤醒成本统计(t-39 第2批 D · HTTP 出口)─────────────────────
|
|
5049
|
+
//
|
|
5050
|
+
// 页面团队卡片要显示「今日唤醒 N 次」。数据源是 dispatcher 的 wakes.jsonl
|
|
5051
|
+
// 按 teamId 聚合(`byTeam`)。不传 id ⇒ 返回全部团队的横向对比。
|
|
5052
|
+
//
|
|
5053
|
+
// `today` = 今日 0 点(本地时区)之后;`total` = 该团队历史累计。
|
|
5054
|
+
if (request.method === 'GET' && path === '/teams/wake-stats') {
|
|
5055
|
+
const id = qp(url, 'id')
|
|
5056
|
+
const dispatcher = services.dispatcher
|
|
5057
|
+
if (!dispatcher || typeof dispatcher.byTeam !== 'function') {
|
|
5058
|
+
sendJson(response, 503, { ok: false, reason: 'DISPATCHER_UNAVAILABLE' })
|
|
5059
|
+
return
|
|
5060
|
+
}
|
|
5061
|
+
const since = qn(url, 'since')
|
|
5062
|
+
const nowMs = Date.now()
|
|
5063
|
+
const midnight = since ?? new Date(nowMs).setHours(0, 0, 0, 0)
|
|
5064
|
+
const todayAgg = dispatcher.byTeam({ since: midnight })
|
|
5065
|
+
const allAgg = dispatcher.byTeam({})
|
|
5066
|
+
|
|
5067
|
+
const pick = (agg) => {
|
|
5068
|
+
if (!id) {
|
|
5069
|
+
return {
|
|
5070
|
+
teams: agg.teams,
|
|
5071
|
+
unassigned: agg.unassigned,
|
|
5072
|
+
total: agg.total,
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
const hit = agg.teams.find((t) => t.teamId === id)
|
|
5076
|
+
return hit ?? { teamId: id, count: 0, sent: 0, skipped: 0, failed: 0, pending: 0, targets: 0, targetsList: [] }
|
|
5077
|
+
}
|
|
5078
|
+
const today = pick(todayAgg)
|
|
5079
|
+
const all = pick(allAgg)
|
|
5080
|
+
if (id) {
|
|
5081
|
+
sendJson(response, 200, {
|
|
5082
|
+
ok: true,
|
|
5083
|
+
id,
|
|
5084
|
+
today: today.count ?? 0,
|
|
5085
|
+
total: all.count ?? 0,
|
|
5086
|
+
byResult: {
|
|
5087
|
+
sent: today.sent ?? 0, skipped: today.skipped ?? 0,
|
|
5088
|
+
failed: today.failed ?? 0, pending: today.pending ?? 0,
|
|
5089
|
+
},
|
|
5090
|
+
byResultAllTime: {
|
|
5091
|
+
sent: all.sent ?? 0, skipped: all.skipped ?? 0,
|
|
5092
|
+
failed: all.failed ?? 0, pending: all.pending ?? 0,
|
|
5093
|
+
},
|
|
5094
|
+
since: midnight,
|
|
5095
|
+
})
|
|
5096
|
+
return
|
|
5097
|
+
}
|
|
5098
|
+
sendJson(response, 200, { ok: true, since: midnight, today, all })
|
|
5099
|
+
return
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5003
5102
|
// 管理策略(2026-11-09「管理员权限边界从页面控制」):CAS 改团队的 adminPolicy。
|
|
5004
5103
|
if (request.method === 'POST' && path === '/teams/policy') {
|
|
5005
5104
|
const body = (await readBody(request)) ?? {}
|
package/src/board.js
CHANGED
|
@@ -87,6 +87,22 @@ const STATUSES = ['pending', 'in_progress', 'completed', 'blocked', 'deleted']
|
|
|
87
87
|
const ACCEPT_RESULTS = ['pass', 'reject']
|
|
88
88
|
/** 租约缺省 TTL(ms)。 */
|
|
89
89
|
const DEFAULT_LEASE_MS = 300_000
|
|
90
|
+
/**
|
|
91
|
+
* board 动作 → `src/metrics.js` 的 METRIC_KINDS 映射(bots04 欠账2)。
|
|
92
|
+
*
|
|
93
|
+
* 为什么需要这张表:board 的动作名(`claim`/`complete`/`accept`…)与 metrics 的
|
|
94
|
+
* 指标种类(`task_claim`/`task_complete`/`accept_pass`…)**不是同名**,而且不是一对一
|
|
95
|
+
* (`accept` 要按 result 拆成 pass/reject)。硬编码在 commit 里会变成一长串 if。
|
|
96
|
+
*
|
|
97
|
+
* 没有映射的动作(`create`/`edit`/`release`/`reopen`/`delete`)故意不记:
|
|
98
|
+
* metrics 的 METRIC_KINDS 里没有对应种类,**新增种类要同步改 metrics.js 与它的测试**,
|
|
99
|
+
* 不能在这里偷偷塞一个不认识的 kind(metrics.record 会抛错的)。
|
|
100
|
+
*/
|
|
101
|
+
const ACTION_TO_METRIC = Object.freeze({
|
|
102
|
+
claim: 'task_claim',
|
|
103
|
+
complete: 'task_complete',
|
|
104
|
+
block: 'task_blocked',
|
|
105
|
+
})
|
|
90
106
|
/** 复杂度档位(契约 §6 冻结;路由侧 `src/routing/complexity.js` 用同一套字面量)。 */
|
|
91
107
|
const COMPLEXITY_TIERS = ['T0', 'T1', 'T2', 'T3']
|
|
92
108
|
/**
|
|
@@ -620,6 +636,31 @@ export function createBoard(ctx, config = {}) {
|
|
|
620
636
|
: (Number.isFinite(config.wipLimitPerBot) && config.wipLimitPerBot >= 1 ? config.wipLimitPerBot : 2)
|
|
621
637
|
const requireTriageForReady = settings.requireTriageForReady === true || config.requireTriageForReady === true
|
|
622
638
|
|
|
639
|
+
/**
|
|
640
|
+
* 指标旁路(bots04 欠账2)。
|
|
641
|
+
*
|
|
642
|
+
* 为什么是「注入」而不是从 ctx 取:服务注册表在 `src/registry.js` 用的是
|
|
643
|
+
* `WeakMap<ctx, services>`,且 `index.js` 里 **board 先于 metrics 装配的顺序不保证**。
|
|
644
|
+
* 用注入(`config.metrics`)能拿到**装配完成后的实例**,也避免 board→registry 的循环依赖。
|
|
645
|
+
*
|
|
646
|
+
* 取不到就不记(返回 false),绝不抛 —— 指标缺失是可接受的退化,
|
|
647
|
+
* 业务动作失败是不可接受的。
|
|
648
|
+
*/
|
|
649
|
+
// ⚠️ 为什么支持函数:`index.js` 里 **board 先于 metrics 装配**(board 在 L130,
|
|
650
|
+
// metrics 在 L276)。若只接受实例,board 装配时 metrics 还是 undefined,
|
|
651
|
+
// 埋点会永久静默失效 —— 那正是「256 条全是 turn_end」这类 bug 的成因。
|
|
652
|
+
// 传函数 ⇒ 每次记录时现取,装配顺序就不再影响正确性。
|
|
653
|
+
const metricsSink = config && config.metrics !== undefined ? config.metrics : null
|
|
654
|
+
function recordMetric(kind, taskId, by) {
|
|
655
|
+
let sink = metricsSink
|
|
656
|
+
if (typeof sink === 'function') {
|
|
657
|
+
try { sink = sink() } catch { sink = null }
|
|
658
|
+
}
|
|
659
|
+
if (!sink || typeof sink.record !== 'function') return false
|
|
660
|
+
sink.record({ kind, taskId: taskId ?? null, by: by ?? null })
|
|
661
|
+
return true
|
|
662
|
+
}
|
|
663
|
+
|
|
623
664
|
const jsonlPath = statePath(stateDir, 'board.jsonl')
|
|
624
665
|
const snapPath = statePath(stateDir, 'board.snap.json')
|
|
625
666
|
|
|
@@ -696,6 +737,20 @@ export function createBoard(ctx, config = {}) {
|
|
|
696
737
|
}
|
|
697
738
|
tasks.set(taskId, clone(after))
|
|
698
739
|
events.push(event)
|
|
740
|
+
// ── 指标埋点(bots04 欠账2)─────────────────────────────────────────
|
|
741
|
+
// 为什么埋在 commit 而不是各个动作里:commit 是**所有**状态变更的唯一出口,
|
|
742
|
+
// 埋在这里才不会漏(create/update/claim/complete/accept 全过它)。
|
|
743
|
+
//
|
|
744
|
+
// 为什么必须**服务端**埋:此前 256 条 metrics 全是 `turn_end` —— 因为
|
|
745
|
+
// 只有 `index.js` 在回合结束自动记,其余 11 种 kind 只靠「谁主动调
|
|
746
|
+
// bot_metrics_record」才记,而没有任何业务代码主动调。PDCA 页面因此只看得见
|
|
747
|
+
// 「回合结束」一种指标,看不到验收通过率、任务认领率。
|
|
748
|
+
//
|
|
749
|
+
// 纪律:埋点失败**绝不能**影响业务动作 —— 主流程已经落盘成功,指标是旁路。
|
|
750
|
+
try {
|
|
751
|
+
const metricsAction = ACTION_TO_METRIC[action]
|
|
752
|
+
if (metricsAction) recordMetric(metricsAction, taskId, byId)
|
|
753
|
+
} catch { /* 指标是旁路,绝不影响主流程 */ }
|
|
699
754
|
if (after.sourceSeq) {
|
|
700
755
|
// 幂等键在这里登记(而不是在 create 开头),保证「校验/落盘失败不污染键」
|
|
701
756
|
idem.check(after.sourceSeq)
|
|
@@ -1863,6 +1918,12 @@ export function createBoard(ctx, config = {}) {
|
|
|
1863
1918
|
delete next.latestReport // 旧的收尾结论一并作废(重做后会重新产生)
|
|
1864
1919
|
delete next.accepted // 驳回即取消「已验收」,与 acceptance.result='reject' 一致
|
|
1865
1920
|
}
|
|
1921
|
+
// 验收埋点(bots04 欠账2):accept 的指标要按 result 拆成 pass/reject,
|
|
1922
|
+
// 不能用 ACTION_TO_METRIC(那里是动作名→kind 的一对一映射)。
|
|
1923
|
+
// 放在 commit **之前**:commit 成功后已经落盘,埋点是旁路,先记不影响主结果。
|
|
1924
|
+
try {
|
|
1925
|
+
recordMetric(result === 'pass' ? 'accept_pass' : 'accept_reject', taskId, actor)
|
|
1926
|
+
} catch { /* 指标是旁路,绝不影响主流程 */ }
|
|
1866
1927
|
return commit(taskId, 'accept', actor, at, before, next)
|
|
1867
1928
|
}
|
|
1868
1929
|
|