cs-bots 0.7.5 → 0.8.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.
@@ -0,0 +1,280 @@
1
+ # CONTRACT-升级09 · Boss 体验 / 任务可管理 / 升级接续
2
+
3
+ > 版本目标:**0.8.0**(有 UI 破坏性变更:删按钮、改文案口径)。
4
+ > 本契约是三个人并行改码的**唯一真相**。接口形状、字段名、文案口径以本文为准;
5
+ > 实现细节自由,契约不许各自发明。
6
+ >
7
+ > 三条铁律(全版本适用,违反即退回):
8
+ > 1. **零第三方运行时依赖**(CI `test/ci-zero-dep.sh` 会拦)。
9
+ > 2. **不改既有 38 个工具契约的语义**(只许纯增量加新工具/新可选参数)。
10
+ > 3. **绝不静默失败**:任何「本该发生却没发生」都要有可见理由(返回值 + 落盘/UI)。
11
+
12
+ ---
13
+
14
+ ## 0. 术语口径(需求1)
15
+
16
+ Boss 在看界面时,人只有一个称呼:**Boss**。
17
+
18
+ - `HUMAN_LABEL = 'Boss'`,`HUMAN_SELF_LABEL = 'Boss(你)'` —— 已有,不改。
19
+ - 禁止第三同义词:`用户`、`你`(作为「用户」义)、`人`(作为「用户」义)、`派活方`、`人工`、`操作者`。
20
+ - 例外(允许保留):`其他 Bot`、`智能体`、`管理员`(房间超管)、`执行者`、`负责人`。
21
+ - `(ui)` / `by: 'ui'` 这类**机器标识**可以保留在技术字段里,但**面向 Boss 的展示文案**要写 `Boss`。
22
+ 例:补充说明历史里的 `(ui)` → `(Boss)`。
23
+ - 判定方法:`grep` 出来的每一处在 UI 文案里(`h(...)` 的文本参数)都得改;
24
+ 注释、日志、README/CHANGELOG 不强制(注释里保留历史原话可以,那是证据)。
25
+
26
+ ---
27
+
28
+ ## 1. 任务栏(需求2)
29
+
30
+ - 顶层子页签**保留两个**:`任务栏`(原 `Boss的任务`)、`智能体任务`。
31
+ - 左侧 `TABS` 里 `workspace.subs` 的 `board` 项 `label` 改为 **`任务栏`**。
32
+ - **删掉**页内的切换按钮对:`看全部Boss的任务` / `只看需要我管的`(client.js L2853-2854 一带)。
33
+ - **删掉** `attentionOnly` state 与 `q.set('attention','1')`。
34
+ - `任务栏` 页**默认全量**:请求 `GET /tasks` **不带** `attention`、**不带** `source`。
35
+ ⇒ 显示全部任务(Boss 建的 + 智能体建/派发的 + Boss 介入过的)。
36
+ - `智能体任务` 页(`AiTasksTab`)仍然 `source=ai`。
37
+ - 页头 `desc` 改成一句人话:`全部任务都在这里:Boss 建的、智能体派发的、以及已经完成的。`(不再提「切回去」)。
38
+
39
+ ---
40
+
41
+ ## 2. 阶段产出物(需求3)
42
+
43
+ Boss 的原话:「点击去看产出物,看不到过程产物」。⇒ 任务详情里必须能按阶段看到每阶段交了什么东西,
44
+ **不需要去翻对话**。
45
+
46
+ ### 2.1 存储(board 侧新增,C 负责)
47
+
48
+ `task-meta.jsonl` 新增一种行(与 `longrun` 同一份文件、同一套读写):
49
+
50
+ ```jsonc
51
+ { "kind": "phase", "taskId": "t-36", "index": 0, "state": "doing|done|blocked",
52
+ "artifacts": ["/path/a.md", "https://..."], "note": "一句话说明", "by": "bot-xxx|ui", "at": 1794... }
53
+ ```
54
+
55
+ - 同一 `index` 有多行时,**最后一行胜**(append-only,不覆盖写)。
56
+ - `artifacts` 为空数组是合法的(表示「这一阶段没产出物」),要能在 UI 上说清。
57
+
58
+ ### 2.2 读取端点(api 侧,B 负责)
59
+
60
+ `GET /tasks/phases?id=t-xx` →
61
+
62
+ ```jsonc
63
+ { "ok": true, "taskId": "t-36",
64
+ "phases": [
65
+ { "index": 0, "title": "范围与验收标准确认", "state": "done",
66
+ "artifacts": ["docs/a.md"], "note": "确认完毕", "by": "bot-x", "at": 1794... }
67
+ ],
68
+ "currentIndex": 1, // 第一个 state!=='done' 的下标;全 done ⇒ phases.length
69
+ "totalArtifacts": 3,
70
+ "isLongrun": true,
71
+ "note": "非长任务时 phases 为空数组,isLongrun=false" }
72
+ ```
73
+
74
+ - 阶段清单来自 `readLongrunMeta()`;没有 longrun 记录 ⇒ `phases: []`、`isLongrun: false`,**不是错误**。
75
+ - 非长任务但已有 `task.artifacts` 的,`GET /tasks/detail` 已有的 `artifacts` 字段不动(向后兼容)。
76
+
77
+ ### 2.3 写入端点(api 侧,B 负责)
78
+
79
+ `POST /tasks/phase`(body:`{ id, index, state, artifacts?, note?, by? }`)
80
+
81
+ - `index` 必须落在 `0..phases.length-1`,越界 400 且说明合法范围。
82
+ - `state` 缺省 `done`;`artifacts` 归一为字符串数组(去空、去重、最多 50 项、每项 ≤ 2000 字)。
83
+ - 必须写一条 `kind:'phase'` 到 task-meta.jsonl;写不进去 ⇒ 503(不许静默丢证据)。
84
+ - 返回 `{ ok, phase, phases }`(回填整份最新视图,省一次读)。
85
+
86
+ ### 2.4 工具面(tools.js,C 负责)
87
+
88
+ 新工具 `bot_task_phase`:让执行者**边做边交**阶段产出物。
89
+
90
+ ```
91
+ 参数:id(必填), expectedRev(可选), index(必填), state(enum doing|done|blocked, 默认 done),
92
+ artifacts(string[]), note(string)
93
+ ```
94
+ - 不改变既有工具的语义;只新增。
95
+ - 工具描述里明确一句:**「每完成一个阶段就调用一次,带上这一阶段交出的文件路径/链接」**。
96
+
97
+ ### 2.5 UI(client.js,A 负责)
98
+
99
+ 任务详情新增区块 `阶段产出物`(放在「产出物证据」**之前**),数据来自 `/tasks/phases`:
100
+
101
+ - 每阶段一行:`① 阶段名` + 状态徽标(`未开始` / `进行中` / `已完成` / `卡住`)+ 该阶段产出物列表。
102
+ - 产出物复用现有「产出物证据」那套渲染:路径可预览、URL 可打开。
103
+ - 空态:`执行者还没汇报这一阶段。`(长任务才有此区块;非长任务显示 Empty + 一句「这不是长任务,没有分阶段产物;总产出物见下方」)。
104
+
105
+ ---
106
+
107
+ ## 3. 建团队自动建房间(需求4)
108
+
109
+ `POST /teams` 建团队成功后,**同一个请求内**自动建一个房间并把团队负责人/成员拉进去。
110
+
111
+ - 房间名:`` `${team.name} · 议事厅` ``。
112
+ - 成员:`[leadId, ...team.members]`;`adminId = leadId`(负责人管这个房间)。
113
+ **注意**:`adminId` 必须是房间成员之一,否则 room.js 会拒。
114
+ - 房间失败**不阻断建团队**(团队已建好),但必须回 `room.reason` 让 Boss 看见。
115
+ - 团队记录新增字段 `roomId`(字符串),通过 `teams.js` 新方法 `attachRoom({ id, roomId, by })` 落盘
116
+ (新事件 action `attachRoom`,CAS 用 `expectedRev`;幂等:同 roomId 重复调用直接返回既有)。
117
+ - `POST /teams/add` 成功后,若团队有 `roomId` ⇒ 把新成员也加进房间(失败只 warn 到返回值,不阻断)。
118
+ - `POST /teams` 的返回体新增 `room?: { id, name }`、`roomError?: string`。
119
+ - UI(A 负责):团队卡片显示 `房间:<名>` + `去房间` 按钮(走 `goTab('team','rooms')` 并选中该房间)。
120
+
121
+ ---
122
+
123
+ ## 4. Boss 直接动手(需求5)
124
+
125
+ Boss 勾选要能直接做的:**改优先级/截止/复杂度/是否付费、改目标与边界、换负责人/收回/重新派发、
126
+ 阻塞/解阻塞/归档、编辑长任务阶段清单**。(不新增「改进度」入口——已有,见 §6。)
127
+
128
+ ### 4.1 走既有端点(A 直接用,B 只需保证可用)
129
+
130
+ | Boss 动作 | 端点 | body |
131
+ |---|---|---|
132
+ | 改优先级/截止/复杂度/付费 | `POST /tasks/update` | `{ id, expectedRev, action:'edit', priority?, dueAt?, expectedComplexity?, requiresPaid? }` |
133
+ | 改目标/边界 | `POST /tasks/update` | `{ id, expectedRev, action:'edit', subject?, description?, writeScopes? }` |
134
+ | 收回任务 | `POST /tasks/update` | `{ id, expectedRev, action:'release' }` |
135
+ | 换负责人 / 重新派发 | `POST /tasks/reassign` | `{ id, ownerId, expectedRev, wake? }` |
136
+ | 阻塞 / 解阻塞 / 归档 | `POST /tasks/update` | `action:'block'`(+`reason`) / `'unblock'` / `'delete'` |
137
+
138
+ - `expectedComplexity: null` / `requiresPaid: null` 表示**清除**(board 已有该语义,别改)。
139
+
140
+ ### 4.2 编辑阶段清单(新端点,B 负责)
141
+
142
+ `POST /tasks/phases`(body:`{ id, phases: string[], note?, expectedRev? }`)
143
+
144
+ - 复用 `normalizePhases()`(上限 12 项 / 单项 200 字 / 不许空白)。
145
+ - 追加一条 `kind:'longrun'` 记录(与 `/tasks/evolve mode:longrun` 同形),`note` **可空**,
146
+ 空则自动写 `Boss 调整阶段清单`。
147
+ - 明确一条:**已经汇报过的阶段产物不丢**(`kind:'phase'` 行不动);新的 phases 变短时,
148
+ `index >= phases.length` 的旧产物行保留但不再展示(读取端按 index 过滤)。
149
+
150
+ ### 4.3 UI(A 负责)
151
+
152
+ - 「可以做什么」区块按 Boss 视角重写,把上面 5 类动作做成**内联表单/弹窗**,不跳页。
153
+ - 每个写操作都要 toast 出真实结果(成功/失败理由),不许「点了没反应」。
154
+ - 文案不许出现「不由你操作」这种把 Boss 排除在外的句子——现在那行要改。
155
+
156
+ ---
157
+
158
+ ## 5. 派活失败必须可见且可救(需求6)
159
+
160
+ Boss 报的故障原文:`调研专家《调研专家测试001》已多次指派,subagent 持续失败,无产出。`
161
+
162
+ 已掌握的硬证据(B 必须复核并写进诊断报告):
163
+
164
+ - `~/.dsh/cs-bots/wakes.jsonl` 里 12 条 `result:'failed'`,`detail:'agent-not-live'`。
165
+ - `t-36` 任务上有 `delivery: { error:'agent-not-live', at:1794294124827 }`。
166
+ - roster 里 `myfree调研员`(`bot-87b3…`)**preset 为 None** ⇒ 走宿主默认预设,工具面不保证。
167
+ - 一次派活的完整链路:`prepare → claim → ensureLive → assertDeliverable → baseline → notify → armDeliveryWatch`。
168
+
169
+ ### 5.1 必须做的事(B 负责 api.js)
170
+
171
+ 1. **诊断脚本/端点**:`GET /tasks/diagnose?id=t-xx` → 一次性给出这个人能不能干活:
172
+ ```jsonc
173
+ { ok:true, taskId, ownerId, bot:{ name, hasSession, preset, presetResolved, hasModelRoute },
174
+ live:{ ok, reason }, deliverable:{ ok, reason },
175
+ wakes:[{ at, result, reason }],
176
+ verdict:'ok'|'blocked', advice:'一句人话 + 下一步点哪个按钮' }
177
+ ```
178
+ 判定逻辑不许猜:每项都从现有服务读(roster / kernel.sessionMeta / dispatcher.list / board)。
179
+ 2. **失败原因中文化**:`agent-not-live` / `undeliverable:*` / `followup-failed` 等
180
+ 统一映射成一句人话 + 一句下一步(如「去 Bot 档案确认能跑起来,再点重试唤醒」)。
181
+ 3. **杜绝「静默卡住」**:`armDeliveryWatch` 超时未兑现的落点必须能在任务详情看到(已有 `delivery` 字段,UI 要展示,A 负责)。
182
+ 4. 修完要能**自证**:写一个真实回归测试 `test/api-diagnose.test.mjs`,用假 services 覆盖
183
+ 「会话不在线 / 无模型路由 / 无工具面」三种,断言 `verdict==='blocked'` 且 `advice` 非空。
184
+
185
+ > 需求6 的验收口径(Boss 已明确):**不阻塞阶段 1 核心确认**;阶段 2 可按已有源码梳理直接实施;
186
+ > 调研可后续重发。⇒ 本条的 DoD 是「失败可见 + 有可执行下一步 + 不再重复空派」,不是「保证 subagent 一定成功」。
187
+
188
+ ---
189
+
190
+ ## 6. 进度:队长填、Boss 可改(需求7)
191
+
192
+ - 保持 `progress` 0–100。**队长(Lead/Bot)按 estimate 自估自填**;Boss 随时可改。
193
+ - board 侧(C)新增两个**只读派生**字段(不参与 CAS 白名单写入):无。
194
+ 改为:`progressBy`(string,最后一次写进度的 `byId`)+ `progressAt`(number)。
195
+ 在 `edit`/`set` 写 `progress` 时**自动**同写这两个字段(不需要调用方传)。
196
+ - UI(A):`更新进度` 区块标题旁显示来源——`当前 40%(队长估算)` / `当前 40%(Boss 设定)`;
197
+ 并把 `estimate`(预估工作量)与 `progress` 分两行显示,别混成一个数。
198
+ - 进度输入框**保留**,且保存后必须刷新出新的 `rev`(CAS)。
199
+
200
+ ---
201
+
202
+ ## 7. 租约不出现、随时发消息就激活(需求8)
203
+
204
+ Boss 原话:「每次点击太麻烦…不用给我展示租约,我希望随时发送信息就可以激活,不需要点击」。
205
+
206
+ ### 7.1 UI(A 负责)
207
+
208
+ - **删除整个「高级:租约」Section**(client.js L1128-1134):不显示 `当前占用到 …`、不显示「延长 5 分钟」。
209
+ - 任务详情里任何 `租约` / `延长` / `占用到` 字样一律不再出现。
210
+ 唯一例外:错误提示里必须解释冲突时可以说「这个任务正被另一个 Bot 占着」(不说租约、不给点延长)。
211
+
212
+ ### 7.2 服务端:发消息即自动续租/激活(B 负责 api.js)
213
+
214
+ - `POST /tasks/note`(Boss 发消息)成功后,在**同一次请求内**自动把任务「激活」:
215
+ 1. 有 `ownerId` 且在 `in_progress` ⇒ 自动 `heartbeat`(续租)。失败忽略(不影响消息已落盘)。
216
+ 2. `status === 'pending'` 且 Boss 指定/已有执行者 ⇒ 自动 `claim` 后唤醒。
217
+ 3. `status === 'blocked'` ⇒ **不自动重开**(那会掩盖「等 Boss 拍板」),但返回里明说
218
+ `需要先解除阻塞:点「解除阻塞」`。
219
+ - `POST /tasks/wake`(重试唤醒)同样自动 heartbeat。
220
+ - 返回值补 `activated: { claimed?: boolean, heartbeat?: boolean, reason?: string }`(失败也要有理由)。
221
+ - 租约本身**保留**(它是防抢机制),只是:① 不给 Boss 看;② Boss 的任何动作都自动续上。
222
+ - 顺带:`defaultLeaseMs` 的设置项 help 文案(settings.js 由 A 改 UI 文案部分;settings.js 是 A 的文件?
223
+ → `src/settings.js` 归 **A**,见 §9 文件分区)改成 Boss 口径。
224
+
225
+ ---
226
+
227
+ ## 8. 升级后原有任务接续(需求9)
228
+
229
+ 硬要求:**升级不丢任何既有任务、证据、阶段、历史**。
230
+
231
+ ### 8.1 兼容读(全员)
232
+
233
+ - `board.jsonl` / `board.snap.json` / `task-meta.jsonl` / `task-notes.jsonl` / `wakes.jsonl` 的
234
+ **既有行格式一律不改**;新字段一律「读时给默认值」。
235
+ - `progress`、`bossTouched`、`roomId`、`phase` 行在旧数据里不存在 ⇒ 视为 `0` / `false` / 无 / 无。
236
+
237
+ ### 8.2 升级预检与迁移(B 负责)
238
+
239
+ - `GET /upgrade/preflight` → 只读体检:
240
+ ```jsonc
241
+ { ok:true, stateDir, counts:{tasks, longrun, phaseRows, notes, wakes},
242
+ willDo:[ '为 N 个任务补 bossTouched', ... ], risks:[...], backupHint:'...' }
243
+ ```
244
+ - `POST /upgrade/migrate` → 幂等迁移(写前先 `backupState()`):
245
+ 1. 旧任务补 `bossTouched`(凡 `createdBy`/`mandate.sender`/历史 `byId` 是 Boss ⇒ true)。
246
+ 2. 有 `longrun` 但**没有任何 `phase` 行**的任务 ⇒ 按 `task.artifacts` 生成一条
247
+ `{ kind:'phase', index: phases.length-1, state:'done' }`,让「已完成的长任务」也有阶段产物可看。
248
+ 这是**唯一的「推断写入」**,必须在 `note` 里写明 `升级迁移:按总产出物回填`。
249
+ 3. 已存在的 `phase` 行不动。
250
+ - 迁移失败 ⇒ 非 0 结果 + 明确理由,绝不半途而废地写一半(先备份,失败可从备份恢复)。
251
+
252
+ ### 8.3 部署与回滚(Lead 负责)
253
+
254
+ - 交付物:`docs/升级08-接续与回滚.md`(怎么装、怎么验、怎么回滚到 0.7.5)。
255
+ - 部署脚本沿用 `install.sh`;升级前 `backupState()` 目录已存在 ⇒ 直接复用。
256
+
257
+ ---
258
+
259
+ ## 9. 文件分区(并行改码,**越界即冲突**)
260
+
261
+ | 人 | 独占写入 | 需求 |
262
+ |---|---|---|
263
+ | **A(前端)** | `src/client.js`、`src/settings.js`、`test/client-*.test.mjs` | 1, 2, 3(UI), 4(UI), 5(UI), 6(UI 展示), 7(UI), 8(UI) |
264
+ | **B(服务端)** | `src/api.js`、`src/teams.js`、`src/room.js`、`test/api-*.test.mjs`、`test/teams*.test.mjs`、`test/room*.test.mjs` | 3(数据), 4, 5(端点), 6, 8(服务端), 9 |
265
+ | **C(任务板/工具)** | `src/board.js`、`src/tools.js`、`test/board*.test.mjs`、`test/tools.test.mjs` | 3(存储), 5(字段), 7(progressBy), 9(兼容读), 新工具 |
266
+ | **Lead** | `package.json`、`CHANGELOG.md`、`docs/**`、`test/run-all.sh` | 版本号、变更记录、验收、部署 |
267
+
268
+ 不许碰别人的文件。需要别人配合 ⇒ 在任务板**补一条任务/留言**,不要自己动手。
269
+
270
+ ---
271
+
272
+ ## 10. 总验收(Lead 执行,任何一条不过即退回)
273
+
274
+ 1. `bash test/run-all.sh` 退出码 **0**,且用例总数 ≥ 1178 + 新增。
275
+ 2. `bash test/ci-zero-dep.sh` 退出码 0(无第三方依赖)。
276
+ 3. `grep -n "只看需要我管的\|看全部Boss的任务\|高级:租约\|延长 5 分钟" src/client.js` **无输出**。
277
+ 4. `grep -n "需要你管的\|不由你操作\|别人别抢" src/client.js` **无输出**。
278
+ 5. `node -e` 静态装载 `src/client.js` 通过(`test/client.test.mjs` 的 A 层必过)。
279
+ 6. 需求 1–9 每条都有:① 代码落点 ② 对应测试 ③ 人话证据(命令 + 真实输出)。
280
+ 7. 版本号 0.8.0 + CHANGELOG 写**为什么**,不只写做了什么。
@@ -0,0 +1,298 @@
1
+ # 升级08 · 接续与回滚(改造09 → v0.8.0)
2
+
3
+ > 面向:**装插件的人**。本文只讲三件事:怎么装、怎么验、怎么退。
4
+ > 实现细节见 `docs/CONTRACT-升级09-Boss体验.md` §8 与 `src/api.js` 的 `upgradeMigrate`。
5
+
6
+ ---
7
+
8
+ ## 0. 这次升级的承诺(Boss 需求 9)
9
+
10
+ > **升级不丢任何既有任务、证据、阶段、历史。**
11
+
12
+ 这不是口号,是下面这几条机制撑起来的:
13
+
14
+ | 风险 | 机制 | 位置 |
15
+ |---|---|---|
16
+ | 旧数据缺新字段 → 报错 | 一律**读时给默认值**,绝不抛 | `src/board.js` `normalizeLegacyTask()` |
17
+ | 迁移把 `rev`/`id` 改掉 → 前端与后端对不上 | 迁移**只往事件里补一个字段**,其余一字不动;**绝不重写整个 board 文件** | `src/api.js` `upgradeMigrate` |
18
+ | 迁移写一半就崩 → 数据半残 | **先备份**,备份失败则一个字节都不动 | `src/store.js` `backupState()` |
19
+ | 迁移失败 → 无法回退 | 备份目录 + `restoreState()` | 本文 §3 |
20
+ | 迁移跑两次 → 重复改动 | 幂等:版本已到位 ⇒ 不重写;phase 行按「是否已有」判定 | `src/store.js` `migrate()` |
21
+
22
+ ### 为什么不能「重写整个 board 文件」
23
+
24
+ `board.jsonl` 是 **CAS 幂等键与 `rev` 语义的真相源**。整体覆盖重写的话,
25
+ 只要漏一个字段,任务的 `rev` 就会和前端手里的对不上 ——
26
+ Boss 需求 9「任务原样存活」当场破功,而且**症状是隐性的**(页面能打开,
27
+ 只是任务状态悄悄回退)。所以这里必须逐条合并。
28
+
29
+ ---
30
+
31
+ ## 1. 装(沿用 `install.sh`)
32
+
33
+ ```bash
34
+ cd /home/ubuntu/output2026/butlers/dev/cs-bots
35
+
36
+ # ① 打包(profile 是通过 file: tgz 引用的,见 §1.1)
37
+ npm pack # → cs-bots-0.8.0.tgz
38
+
39
+ # ② 安装 + 自检(一条命令)
40
+ bash install.sh
41
+
42
+ # 或指定通道/profile
43
+ bash install.sh --channel git --ref v0.8.0
44
+ bash install.sh --profile web
45
+ ```
46
+
47
+ ### 1.1 退出码(**必须看懂**)
48
+
49
+ | 退出码 | 含义 | 怎么办 |
50
+ |---|---|---|
51
+ | `0` | 装好**且自检通过** | 继续 §2 |
52
+ | `1` | 安装失败 | 看输出;多半是网络/npm 通道问题 |
53
+ | `2` | **装上去但自检不通过** —— 最危险 | 见下方「为什么危险」 |
54
+
55
+ **为什么 `2` 最危险**:DSH 遇到 `link:` 断链 / bundle 没进
56
+ `dsh.profile.bundles` / 版本错位时,**只打一行 `skipping profile bundle` 就静默跳过** ——
57
+ 页面照常 HTTP 200,功能全失效。「页面能打开」≠「插件装好了」。
58
+ 所以 `install.sh` 用 `--dump-config` 验证**装配树里真有我们的行**。
59
+
60
+ ### 1.2 只自检(不动任何东西)
61
+
62
+ ```bash
63
+ bash install.sh --check-only
64
+ dsh --profile web --dump-config | grep -n 'cs-bots' # 必须能搜到
65
+ ```
66
+
67
+ ### 1.3 profile 引用方式
68
+
69
+ ```
70
+ /home/ubuntu/.dsh/profiles/web/package.json
71
+ "cs-bots": "file:/home/ubuntu/output2026/butlers/dev/cs-bots/cs-bots-0.7.2.tgz"
72
+ ```
73
+
74
+ > 升级时这个 tgz 文件名会随版本变(`0.7.2` → `0.8.0`)。
75
+ > `install.sh` 负责改引用;手工装的话记得同步这行。
76
+ > bundle 列表里必须有 `cs-bots`(第 25 行附近)。
77
+
78
+ ---
79
+
80
+ ## 2. 验(三步,**顺序不能乱**)
81
+
82
+ ### 第 1 步:预检 —— 只读,一个字节都不写
83
+
84
+ ```bash
85
+ curl -s http://127.0.0.1:PORT/cs-bots/api/upgrade/preflight
86
+ ```
87
+
88
+ ```jsonc
89
+ {
90
+ "ok": true,
91
+ "stateDir": "/home/ubuntu/.dsh/cs-bots",
92
+ "schemaVersion": 0, // 当前 board 事件流 schema(未迁移过是 0)
93
+ "targetVersion": 2, // 本次要到的版本
94
+ "counts": { "tasks": 36, "longrun": 12, "phaseRows": 8, "notes": 200, "wakes": 48,
95
+ "boardEvents": 541, "bossTouched": 0 },
96
+ "legacy": { /* 旧字段体检:哪些任务缺哪些新字段 */ },
97
+ "willDo": [ "为 3 个任务补 bossTouched",
98
+ "为 1 个已完成长任务按总产出物回填阶段产物",
99
+ "把 board 事件流 schema 版本记到 2(现有任务 rev/id/字段原样保留)" ],
100
+ "risks": [ "无阻塞风险(迁移前仍会自动整目录备份)" ],
101
+ "backupHint": "迁移前会自动整目录备份到 …bak-<时间>-upgrade09;要手动回滚就 restoreState(...)"
102
+ }
103
+ ```
104
+
105
+ **怎么确认它真的没写**(别只信返回值):
106
+
107
+ ```bash
108
+ stat -c '%s %Y' /home/ubuntu/.dsh/cs-bots/board.jsonl /home/ubuntu/.dsh/cs-bots/task-meta.jsonl
109
+ # → 记下来;跑完 preflight 再 stat 一次,数字必须一模一样
110
+ ```
111
+
112
+ ### 第 2 步:迁移 —— 幂等、先备份
113
+
114
+ ```bash
115
+ curl -s -X POST http://127.0.0.1:PORT/cs-bots/api/upgrade/migrate
116
+ ```
117
+
118
+ ```jsonc
119
+ {
120
+ "ok": true,
121
+ "backupPath": "/home/ubuntu/.dsh/cs-bots.bak-2026-11-10T22-30-00-123Z-upgrade09",
122
+ "bossTouched": { "updated": 3, "skipped": 33 },
123
+ "phases": { "backfilled": 1, "tasks": [ "t-35" ] },
124
+ "schema": { "ok": true, "migrated": true, "version": 2 },
125
+ "replayed": null
126
+ }
127
+ ```
128
+
129
+ **迁移做三件事**(详见契约 §8.2):
130
+
131
+ 1. 旧任务补 `bossTouched` —— 凡 `createdBy` / `mandate.sender` / 历史 `byId`
132
+ 是 Boss 的 ⇒ `true`。**只改这一个字段。**
133
+ 2. 「已完成 + 有 longrun + 没有任何 phase 行」的任务 ⇒ 按 `task.artifacts`
134
+ 生成一条末阶段产物 `{ kind:'phase', index: phases.length-1, state:'done' }`,
135
+ 让已完成的长任务也有阶段产物可看。
136
+ **这是唯一的「推断写入」**,`note` 里写明 `升级迁移:按总产出物回填`。
137
+ 3. **已存在的 phase 行不动**。
138
+
139
+ **幂等验证**(重要):
140
+
141
+ ```bash
142
+ # 跑两次,第二次必须 updated=0 / backfilled=0
143
+ curl -s -X POST .../upgrade/migrate | grep -o '"updated": [0-9]*'
144
+ curl -s -X POST .../upgrade/migrate | grep -o '"backfilled": [0-9]*'
145
+ # → 第二次都应为 0
146
+ ```
147
+
148
+ ### 第 3 步:验收
149
+
150
+ ```bash
151
+ # ① 任务原样存活:数量与 id 列表不变
152
+ curl -s .../tasks | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
153
+ const j=JSON.parse(d); console.log("tasks:", j.tasks?.length ?? j.length, "ids:", [...(j.tasks??[]).map(t=>t.id)].join(","))})'
154
+
155
+ # ② 全量套件(在源码目录跑,别在 profile 里跑)
156
+ cd /home/ubuntu/output2026/butlers/dev/cs-bots && bash test/run-all.sh
157
+ # 退出码 0,且用例数 ≥ 1191
158
+
159
+ # ③ 零依赖闸
160
+ bash test/ci-zero-dep.sh # 退出码 0
161
+
162
+ # ④ Boss 口径 grep(必须无输出)
163
+ grep -n "只看需要我管的\|看全部Boss的任务\|高级:租约\|延长 5 分钟\|需要你管的\|不由你操作\|别人别抢" src/client.js
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 3. 退(两种粒度)
169
+
170
+ ### 3.1 完整回滚:整目录恢复
171
+
172
+ `backupState` 的备份是**整目录快照**,所以回滚就是「把目录换回去」:
173
+
174
+ ```bash
175
+ # ① 先确认备份目录存在且非空
176
+ ls -la /home/ubuntu/.dsh/cs-bots.bak-*/upgrade09
177
+
178
+ # ② 用 store.js 的恢复原语(confirm:true 是硬要求)
179
+ node --input-type=module -e '
180
+ import { restoreState } from "/home/ubuntu/output2026/butlers/dev/cs-bots/src/store.js"
181
+ const r = restoreState("/home/ubuntu/.dsh/cs-bots", "/home/ubuntu/.dsh/cs-bots.bak-XXXXXX-upgrade09", { confirm: true })
182
+ console.log(r) // { ok:true, path: "/home/ubuntu/.dsh/cs-bots" }
183
+ '
184
+
185
+ # ③ 再回退插件本体
186
+ cd /home/ubuntu/output2026/butlers/dev/cs-bots && git checkout v0.7.5 -- .
187
+ bash install.sh
188
+ ```
189
+
190
+ > `restoreState` 会把**当前目录**改名为 `.pre-restore-<时间戳>` 再覆盖,
191
+ > 所以回滚本身也是可再回退的(`...bak-*` 与 `...pre-restore-*` 都在)。
192
+
193
+ ### 3.2 只回滚数据、不动插件
194
+
195
+ 迁移只补 `bossTouched` 和 phase 行,两者都是**加法**。
196
+ 如果只想撤销这两处:
197
+
198
+ ```bash
199
+ # 备份里 task-meta.jsonl 的 phase 行是迁移前的;board.jsonl 的 bossTouched 同理
200
+ diff <(grep '"kind":"phase"' /home/ubuntu/.dsh/cs-bots.bak-*/upgrade09/task-meta.jsonl | sort) \
201
+ <(grep '"kind":"phase"' /home/ubuntu/.dsh/cs-bots/task-meta.jsonl | sort)
202
+ # → 空输出 = 没有新增;非空 = 删掉新增的行即可
203
+ ```
204
+
205
+ ### 3.3 迁移失败怎么办
206
+
207
+ **迁移失败不会留下半残数据**:备份失败 ⇒ 直接中止、一个字节都不动
208
+ (返回 `UPGRADE_BACKUP_FAILED`);中间出错 ⇒ 备份目录还在,按 §3.1 恢复。
209
+
210
+ ```jsonc
211
+ // 失败时的真实返回
212
+ { "ok": false, "reason": "迁移中止:备份失败(…)——未写入任何改动", "code": "UPGRADE_BACKUP_FAILED" }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 4. 常见坑
218
+
219
+ | 现象 | 原因 | 处理 |
220
+ |---|---|---|
221
+ | 页面 200 但插件功能全没 | bundle 没进 profile(DSH 静默跳过) | `bash install.sh --check-only`,看退出码是不是 2 |
222
+ | 迁移后任务状态回退 | 有人在迁移时重写了 board 文件 | 见 §0「为什么不能重写整个 board 文件」;用 §3.1 回滚 |
223
+ | 迁移跑了两次,phase 行多了 | 没走 `/upgrade/migrate` 而是手工写 | 契约要求幂等,用端点跑;真出错用 §3.2 |
224
+ | `preflight` 说 `willDo` 有东西但不想动 | 正常 —— 那正是「升级接续」要做的事 | 先 §2 第 1 步核对 `risks`,再决定是否迁移 |
225
+ | 装完页面没变化 | 需要重启 DSH | `systemctl restart dsh-web.service`(**会中断在途会话**,先确认) |
226
+
227
+ ---
228
+
229
+ ## 5. 上线演练(**已实测,可复现**)
230
+
231
+ 上面 §1–§3 是操作手册。这一节是**已经在真实数据副本上跑过的结果**——
232
+ 不是「应该能行」,是「跑过了长这样」。
233
+
234
+ ### 5.1 包本身是否可用
235
+
236
+ ```bash
237
+ npm pack # → cs-bots-0.8.0.tgz (598 kB, 57 files)
238
+ mkdir -p /tmp/pkg-verify && cd /tmp/pkg-verify && tar xzf …/cs-bots-0.8.0.tgz
239
+ cp -r <repo>/test package/ && cd package && bash test/run-all.sh
240
+ # → ✅ 全部通过(exit 0) 1242 / 1242
241
+ ```
242
+
243
+ **为什么这一步不能省**:源码目录能跑通,不等于**打出来的包**能跑通
244
+ (`files` 字段漏一个目录、`.npmignore` 误伤、`exports` 没指到新文件 —— 都会让包静默残缺)。
245
+ 在包里跑一遍全量套件,是唯一能证明「这个 tgz 是完整的」的办法。
246
+
247
+ ### 5.2 拿真实状态副本演练
248
+
249
+ 数据来自当时的 `~/.dsh/cs-bots/` 副本(144 条 board 事件、2 个任务)。
250
+
251
+ **预检是只读的**(前后指纹对比,证明一个字节都没写):
252
+
253
+ ```
254
+ board.jsonl ✅ 未动 {"size":110720, "mtimeMs":1794326787403.48}
255
+ task-meta.jsonl ✅ 未动 {"size":4503, "mtimeMs":1794326787407.48}
256
+ ```
257
+
258
+ 预检结果:
259
+
260
+ ```jsonc
261
+ counts: {"tasks":2,"longrun":2,"phaseRows":0,"notes":13,"wakes":48,"boardEvents":144,"bossTouched":0}
262
+ willDo: ["为 2 个任务补 bossTouched","把 board 事件流 schema 版本记到 2(现有任务 rev/id/字段原样保留)"]
263
+ risks: ["无阻塞风险(迁移前仍会自动整目录备份)"]
264
+ ```
265
+
266
+ **迁移后任务原样存活**(逐字段比 `id` / `rev` / `status` / `subject`):
267
+
268
+ ```
269
+ 迁移前: [{"id":"t-35","rev":5, "status":"pending","subject":"Myfree开发改造"},
270
+ {"id":"t-36","rev":15,"status":"pending","subject":"测试-插件优化"}]
271
+ 迁移后: [{"id":"t-35","rev":5, "status":"pending","subject":"Myfree开发改造"},
272
+ {"id":"t-36","rev":15,"status":"pending","subject":"测试-插件优化"}]
273
+ ✅ 完全一致
274
+ ```
275
+
276
+ > **`rev` 没有漂**,这是「升级不破坏 CAS 语义」最硬的证据。
277
+ > 如果实现走了「重写整个 board 文件」的做法,`rev` 这里几乎必然对不上。
278
+
279
+ **幂等**(第二次跑):
280
+
281
+ ```
282
+ bossTouched.updated: 0 phases.backfilled: 0
283
+ 两次备份不同: true 第三次任务状态: ✅ 仍一致
284
+ ```
285
+
286
+ ### 5.3 由此得到的两条结论
287
+
288
+ 1. **包可用**:解包 + 全量套件 1242/1242,说明 `files` 没漏、`exports` 没断。
289
+ 2. **接续成立**:真实数据上迁移后 `rev`/`id`/`status`/`subject` 零漂移,且可重复执行。
290
+
291
+ ---
292
+
293
+ ## 6. 一句话总结
294
+
295
+ > **装用 `install.sh`(退出码 0 才算装好)→ 验用 preflight/migrate 三步 →
296
+ > 退用 `restoreState`。**
297
+ > 全程**先备份、逐条合并、绝不重写 board 文件**,
298
+ > 所以 Boss 的需求 9「任务原样存活」是有机制保证的,不是口头承诺。
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "cs-bots",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
- "description": "多智能体协作:任务板、房间、分层记忆、Bot 注册。",
5
+ "description": "Boss 体验 + 任务管控 + 升级可回滚。",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js",