# 「活会话」是什么 · 怎么往任意会话发消息 > 2026-09-29 · 工作区 `E:/ProgramData/AIProject/ai1net-dsh-server` · 结论全部来自**源码实读 + 真机实测** --- ## 一、「活会话」到底是什么 **它不是"正在运行的会话",而是「桌面当前打开的那一个会话」。** 源码依据(WorkBuddy 网关 `resolveLiveSessionFollow()` / `replySession()`): ```js // 活会话 = 网关进程内存里的这个 let L = this.sessionManager.sessionSubject.value; if (!L) return { sessionId: null, writerOccupied: false }; ``` - `GET /api/v1/sessions/live` 返回的 `sessionId` 就是它;为 `null` = **桌面当前没打开任何会话**。 - `writerOccupied` = 这个会话是否**正被某个客户端持有写权**(ACP 连接 / 桌面正在跑)。 **为什么 `reply` 只认它?** 上游给 `reply` 的定位就是"**向当前活会话投递回复(不占用 ACP writer)**" —— 即"手机**跟随**桌面正在聊的那一个",而不是"手机遥控任意会话"。所以: | 目标 | `POST /sessions/{id}/reply` | |---|---| | 就是当前活会话 | ✅ 200 `{"delivered":true}` | | 别的会话 | ❌ **409 `SESSION_FOLLOW_NOT_LIVE`** | --- ## 二、怎么"往任意会话发消息"(已实现并实测通过) 用 **ACP 借用**:把一个会话切成活会话,发完再还回去。 ``` connect → initialize → session/load {sessionId, cwd, mcpServers: []} ← 把目标切成活会话 → session/prompt {sessionId, prompt:[{type:"text",text}]} → (可选)session/load 回原活会话 ← 归还 ``` **实测结果**(对**非活会话** `b419f820-…` 发一条): | 步骤 | 结果 | |---|---| | `session/load` | 200,回放 404 事件,且 `GET /sessions/live` **切到了目标** ✅ | | `session/prompt` | agent 真回复(`agent_message_chunk` 348 个)✅ | | 目标会话 `replay` | 出现 **`user_message_chunk`**,文本逐字一致 ✅ | ### 🔴 三条硬约束(⛔ 别踩) 1. **`session/load` / `session/new` 必须带 `cwd` 与 `mcpServers`** —— 缺则 `-32602 Invalid params`。 `cwd` 取自 `GET /api/v1/info`(⚠️ 作用域列表 `/api/v1/sessions` **不返回 `cwd`**)。 2. **目标会话若已被外部 writer 占用 ⇒ 无法借用**: `-32000 "Persistent Session already has an external writer" {reason:"writer_occupied"}` ⇒ **正被桌面打开/正在跑的会话,外部接管不了**;**空闲会话可以**。 这正是「当前对话的主权在桌面」的体现。 3. **只有与网关口同工作目录的会话可达**。跨项目会话连 `history`/`replay` 都 **404 `SESSION_NOT_FOUND`**。 (本机实测:同目录 5 条可达;跨项目 19 条不可达。) ### 副作用(必须知道) `session/load` 会把**桌面的"当前对话"切到目标会话**。所以实现里默认**借用完归还**; 但归还也可能失败(原活会话若被 writer 占用)—— 这时**在桌面上点一下你要用的会话即可归位**。 --- ## 三、现在手机上的行为 | 你点的那条会话 | 手机发消息时 | |---|---| | 带绿色「活会话」标 | 走官方 `reply` —— **不切换桌面**,立即投递 | | 其他(同项目) | **借用**方式:短暂切桌面当前会话 → 发送 → 归还 | | 标灰「不在当前项目」 | ⛔ 不可发(网关读不到它) | 页面「发送」已统一改走桥的 **`POST /__bridge/send {sessionId, text}`**, 它在内部自动选路(活会话→`reply`;否则→ACP 借用),**默认异步**(立刻返回,agent 回复稍后同步)。 --- ## 四、⛔ 已知的运维限制(本轮踩到的) **沙箱会杀掉跑太久的前台命令,并连带杀掉桥。** 实测 >~45~100 秒的前台命令会被终止,桥(后台任务)随之死掉 ⇒ 手机突然打不开。 ⇒ 对策:命令切短步、长任务走异步;**桥需要"现场重开"**(我这边一条命令即可重启,口令自动取自进程环境)。 ⚠️ 你自己开的 cmd 窗口**拿不到口令**(口令在 WorkBuddy 进程环境里),所以现在只能由我从 WorkBuddy 里拉起桥。 长期方案 = 把桥做成 **WorkBuddy 侧常驻组件**(即 棒 1)。