# 手机回复 WorkBuddy 会话 · 架构与实施方案(2026-09-28) > **目标(用户原话)**:让用户能**通过手机对 WorkBuddy 中对话进行回复**。 > **约束**:结合现有 ai1net 架构 —— **服务器节点(+插件)/客户端(+插件)**,复用既有能力,⛔ 不重造。 > **性质**:本文件 = **规划**(架构 + 详细方案)。落地按 §7 分阶段棒次执行,每棒独立验收。 > **证据分级**:`【已验】`有实测读数 | `【源码在】`代码在位未跑 | `【待实测】`机制在位、行为未验证 —— ⛔ 交付时不得把后两类写成已验。 --- ## 0 判定(先给结论) **一句话架构**:**手机在平台门户作答 ⇒ 平台 IM 内核 ⇒ 覆盖网络反隧道 ⇒ 设备侧桥 ⇒ WorkBuddy 钩子把答案交给模型**。 三个关键结论: 1. **能做成,且不需要给 WorkBuddy 打补丁、不需要开任何公网入站口。** WorkBuddy 的钩子契约支持 `permissionDecision: deny` + `permissionDecisionReason`(模型据此重写/继续)与 `additionalContext`(注入上下文)——【已验】本机已装的钩子正在用这两个字段,WorkBuddy 自身 CLI 内分别命中 96/70/42 处。 2. **主要工作量在「客户端侧桥」,不在服务器侧。** 平台侧 IM 房间/消息/**面板(带可点按钮)**/插件桥端点**全部已存在**【源码在】;手机侧只需一个新页面。 3. **有一条硬约束必须先拍板**:若该房间启用 IM 的 E2EE 档位,**设备侧桥(非人端)读不到明文** —— 这正是 IM 线当初记下的三代价之一「agent 代答须重做」。见 §3 D5。 --- ## 1 现状资产盘点 ### 1.1 直接复用(⛔ 不重造) | 资产 | 载体 | 证据 | |---|---|---| | 覆盖网络中继 + 设备只拨出 | `src/net/relay/**`;设备节点 `overlay-node-daemon.ps1` | 【已验】真机端到端已跑通(设备声明端口 → 中继端点 → 平台解析 → 打回垫片) | | 配对仪式(二维码) | `src/web/routes/overlay-pair.ts` + `web/pair.html` | 【已验】六态、28 条判据全绿;生产 `pair.html`/`overlay-pair.js` 已部署 | | 设备台账与准入 | `overlay_devices` + device-web 四道闸 | 【已验】入口 200 + WS 101;无会话 401、他人设备 404 | | 用户级开关模式 | `users.device_web_enabled`(默认 0)+ `DSHS_DEVICE_WEB` 运维急停 | 【已验】S5 已上线,26/26 判据 | | IM 房间/成员/消息 | `src/im/**` + `/api/im/rooms/**` | 【源码在】消息收发/成员/在线态齐全 | | **IM 面板 + 动作(可点按钮)** | `/api/im/rooms/:id/panels[/:panelId/actions]` | 【源码在】`interactive = 有 onAction`;写入口唯一,`actorId` 取会话不取 body | | **IM 插件桥(双向长轮询)** | `/api/im/plugins/{register,out/message,out/frame,bridge,bridge/pull,bridge/result}` | 【源码在】M5 已实现插件侧客户端(含每插件令牌 + 实例级单例长轮询闸) | | **agent 触发与上下文内核** | `src/im/agent-bridge.ts`(370 行) | 【源码在】形态 A 能力机器人 / 形态 B **组员代理**(`autoReply`+`agentRef`)、四条硬约束 | | 问答数据模型 | 决策库(`options` / `ai_recommendation` / `user_*`) | 【已验】权威 PG `dsl-pg`,已落 135 行;SQLite 兜底 | | 钩子桥基础设施 | `settings.json.hooks` + `decision_bridge.py` | 【已验】四事件已挂、实测生效 | ### 1.2 需要新增(少量) | 新增 | 归属 | 说明 | |---|---|---| | **M6「移动会话面」模块** | 服务器节点 · ai1net 插件 | 手机端问答台账 + 开关 + 面板注册(表前缀 `p_ai1net_mobile_`) | | **移动会话页** `web/mobile.html` | 服务器节点 · 平台 | 复用 `pair.html` 的登录态与版式;手机唯一入口 | | **设备侧桥** `wb-mobile-bridge` | 客户端 · 设备 | 本地回环口 + 平台长轮询 + 下行队列(**本方案主体工作量**) | | **WorkBuddy 钩子分支** | 客户端 · WorkBuddy | 在既有钩子桥上加"手机代答"分支(默认关) | ### 1.3 明确不做 ⛔ 不新造第二套取址/中继协议 | ⛔ 不给 relay 加协议 op | ⛔ 不把设备升格为 worker | ⛔ 不引入 AA 式云中转 | ⛔ 不改官方 dsh/WorkBuddy 主程序。 --- ## 2 架构 ### 2.1 三层拓扑 ``` ① 手机浏览器 https://ai1net.com/mobile.html ← 平台登录态 Cookie(复用) │ HTTPS 443 ▼ ② 服务器节点(+插件) 47.77.182.89 ├ 门户 / 账号 / 会话 (既有) ├ 设备台账 overlay_devices (既有) ├ IM 内核:房间·成员·消息·面板动作 (既有) ├ 配对端点 overlay-pair (既有) ├ 覆盖网络中继 relay(443 兜底) (既有) └ ai1net 插件:M5(插件桥)+ M6(移动会话面,新) ▲ wss 反隧道(设备只拨出,⛔ 无公网入站口) │ ③ 客户端(+插件)= 用户自己的电脑 ├ 覆盖网络设备节点守护 (既有:overlay-node-daemon.ps1) ├ 设备侧桥 wb-mobile-bridge (新:本地回环 + 平台长轮询) └ WorkBuddy + ai1net 插件 (钩子分支:手机代答,默认关) ``` ### 2.2 两条通路(本方案的全部数据流) **通路 A · 下行(会话 → 手机):让我在手机上看得见"它在等我"** ``` WorkBuddy 触发钩子(SessionStart / PreToolUse^AskUserQuestion$ / SessionEnd) → 钩子把事件投给本机回环的 设备侧桥(毫秒级,⛔ 不走网络) → 桥经 IM 插件面 /api/im/plugins/out/message 上报(每插件令牌) → 平台落到房间消息 + M6 台账(p_ai1net_mobile_questions) → 手机在 mobile.html 看到「会话 X 在等你答复」+ 选项按钮 ``` **通路 B · 上行(手机 → 会话):把答案交回模型** ``` 手机点选项 / 打字 → POST /api/im/rooms/:id/panels/:panelId/actions(成员校验) → M6 面板 onAction 落台账 → 经 IM 插件桥下发(/bridge/result 或 out/frame) → 设备侧桥收下并放进【本地答案队列】(带一次性 nonce) → 钩子在下一个取用点把它交给模型: · PreToolUse(阻断式)⇒ permissionDecision:deny + permissionDecisionReason · UserPromptSubmit(队列式)⇒ additionalContext 注入 → 会话继续;结果再经通路 A 回手机 ``` 🔴 **一句话判据**:通路 A 让手机**看见**,通路 B 让手机**说话**;两者都只经**既有面**,⛔ 不新增对外服务。 --- ## 3 关键设计决策(含取舍 · 可推翻) | # | 决策 | 选了什么 | 理由 / 代价 | |---|---|---|---| | **D1** | 承载 | **走 IM 房间承载**,⛔ 不做"手机反代 WorkBuddy 界面" | WorkBuddy 是 Electron 外壳、**无对外 web 面**(承不承载得起反代:不能)⇒ 手机看的是**平台渲染的会话视图**,不是 WorkBuddy 的 UI。代价:视图要做一份,⛔ 不追求"逐像素同桌面" | | **D2** | 设备侧载体 | **独立守护进程(扩展设备节点)为主**;MCP app 作可选第二形态 | 守护不依赖 WorkBuddy 进程存活、钩子只需本地回环调用(毫秒级,远低于钩子超时);与既有 device shim 同构。代价:多一个要装的东西 | | **D3** | 触发形态 | **形态 B 组员代理**(`autoReply`+`agentRef`),⛔ 不用形态 A bot | 代理**不占成员位**、借用户本人身份回写 ⇒ 与"用户的会话"语义一致,且复用 agent-bridge 既有四条硬约束(禁自激/预算/静默期/排队) | | **D4** | 答案送达路径 | **两段式**:实时窗口内用阻断式;超时转队列式 | 钩子有超时上限【待实测具体值】⇒ 不能无限阻塞等待。超时后钩子仍返回 `deny` + reason「已推送到手机,请先暂停该动作」⇒ **会话不被卡死**(这是关键:⛔ 不能为了手机把桌面会话挂住) | | **D5** | **E2EE 档位** | **该房间不启用 E2EE**(走平台可见档) | 设备侧桥是非人端,加密房**读不到明文**;IM 线原决策已记此代价「agent 代答须重做」。代价:该房消息对平台可见 ⇒ 若不可接受,则须走"设备侧持钥+棘轮持久化"(另立一棒,成本显著更高) | | **D6** | 默认状态 | **默认关**(`users.mobile_reply_enabled = 0`),用户自己开 | 沿用 S5 已验证的开关模式;关闭时手机入口整体不存在 ⇒ **可见面零扩大** | | **D7** | 与桌面弹窗的关系 | **默认不拦弹窗**;仅在开"手机代答"且命中窗口时才接管 | 阻断式会 `deny` 掉原生弹窗(用户回桌面看不到)⇒ 必须由开关显式授权,⛔ 不静默抢答 | --- ## 4 服务器节点(+插件)侧 ### 4.1 平台进程(47 / 106) | 项 | 内容 | 状态 | |---|---|---| | IM 内核 | 房间 / 成员 / 消息 / **面板动作** | 复用,⛔ 零改动 | | 设备台账 + 四道闸 | `overlay_devices` + `deviceWebDecision` | 复用,⛔ 零改动 | | 配对 | `overlay-pair` 五端点 | 复用;新增一对「答题码」语义(见 4.3) | | 新增静态页 | `web/mobile.html`(移动会话页) | **新**(复用 `pair.html` 登录态/版式) | | 新增用户字段 | `users.mobile_reply_enabled`(默认 0,v19 迁移) | **新**(照 `device_web_enabled` 的 D 档流程) | ### 4.2 ai1net 插件 · 新增 M6「移动会话面」 - **一个包一个库**:仍 `dshs_pl_ai1net`;表名前缀 **`p_ai1net_mobile_`**(模块分段,⛔ 不与 M1–M5 混)。 - **精确路径路由**(⛔ 全精确,无前缀/通配;沿用两端交集纪律): | 方法 | 路径 | 干什么 | |---|---|---| | GET | `/api/ai1net/mobile/pending` | 本设备待答问题列表(只回当前用户本人的设备) | | POST | `/api/ai1net/mobile/answer` | 提交答案(**只收一次**,带 nonce) | | GET | `/api/ai1net/mobile/state` | 开关 + 桥在线态 + 队列深度(给手机页首屏) | - **卡片**:M6 出 1 张卡(`plugins.item` 槽位),显示「手机代答」开关 + 桥在线 + 今日问答数。 - **面板注册**:M6 向 IM 注册一个交互面板(`onAction`),手机上的「选项按钮」即此面板的 action。 ### 4.3 答案的一次性语义(⛔ 防重放) 沿用 `overlay-pair` 的码语义:每个待答问题带 **一次性 nonce**(128 bit CSPRNG),答案写入即烧码;重复提交 ⇒ `409`;过期 ⇒ `410`。**判据**:同 nonce 二次提交必拒。 --- ## 5 客户端(+插件)侧 ### 5.1 设备侧桥 `wb-mobile-bridge`(本方案主体) | 职责 | 设计 | 判据 | |---|---|---| | 本地服务口 | **只绑 `127.0.0.1`**(沿用 device shim 纪律);⛔ 不暴露整机 | `ss -lntH` 只见回环;外部连不上 | | 平台长轮询 | 走 M5 既有插件桥(`/api/im/plugins/bridge` 长轮询 + `/pull` + `/result`);**每插件令牌 fail-closed** | 取不到令牌 ⇒ 不发任何请求 | | 答案队列 | 本地落盘队列(带 nonce + 过期时间),供钩子按序取用 | 队列可回读、可对账;过期项具名丢弃 | | 下行上报 | 会话事件(含待答题干/选项)→ 平台 IM | 平台侧可见即判通 | | 生命周期 | 随设备节点守护起停(复用 `-Action install/uninstall`) | 停守护 ⇒ 平台立刻"离线"(具名状态,⛔ 不静默) | ### 5.2 WorkBuddy 钩子分支(在既有钩子桥上增,⛔ 不改其原有行为) | 事件 | 原行为(保留) | 新增分支 | |---|---|---| | `SessionStart` | 注入提问规范 | + 向桥注册本会话(拿到会话键) | | `PreToolUse ^AskUserQuestion$` | 提问规范门 + 落库 | + **若开关开**:上报待答 → 短窗等待 → 命中则 `deny` + `permissionDecisionReason="用户已在手机上作答:<选项>(<理由>)—— 这是答复,不是拒绝"` | | `UserPromptSubmit` | 逐轮识别 + 回收决策 | + 若队列有**未取用**答案 ⇒ `additionalContext` 注入 | | `SessionEnd` | 未答收口 | + 桥侧会话下线 | 🔴 **两条硬纪律**(沿用既有钩子桥的教训): 1. **fail-open** —— 桥不可达/超时/异常 ⇒ **按原逻辑放行**,⛔ 绝不因为手机把桌面会话弄坏; 2. **留痕先于一切** —— 每次取用/放弃都写审计流水,⛔ 否则"没调用"与"静默失败"无法区分(该桥已因此踩过坑)。 --- ## 6 数据面(插件库 `dshs_pl_ai1net`) | 表 | 关键列 | 用途 | |---|---|---| | `p_ai1net_mobile_questions` | `question_id` · `device_id` · `session_key` · `stem` · `options_json` · `state` · `nonce` · `expires_at` | 待答问题 | | `p_ai1net_mobile_answers` | `question_id` · `actor_user_id` · `choice` · `text` · `via` · `answered_at` | 作答(`actor_user_id` 恒取会话) | | `p_ai1net_mobile_outbox` | `seq` · `device_id` · `kind` · `payload_json` · `state` | 下行待投递队列 | - ⛔ **包内零数据库动作**(不 import 驱动、不拼 SQL)—— 沿用 `DB-03 §二` 红线;建库在平台侧,**先建库再投放**。 - 列级 `maxBytes` 唯一把关人仍是包内 `data-limits.js`;**改声明必回读两处**。 --- ## 7 分阶段实施(每棒做完即停,判据可机器验) | 棒 | 目标 | 验收判据(可机器验) | |---|---|---| | **S1** | 设备侧桥骨架:本地回环口 + IM 插件注册 + 长轮询 | 注册 `200`;长轮询有包;`ss` 只见 `127.0.0.1`;取不到令牌 ⇒ 具名拒绝 | | **S2** | **下行通路**:钩子上报 → 手机可见 | 手机页出现该会话待答项;题干/选项与桌面弹窗一致(逐字比对) | | **S3** | **上行·作答**:手机作答 → 模型收到 | 会话继续且模型回复/动作体现**用户所选**;同 nonce 二次提交 `409`;过期 `410` | | **S4** | **上行·续聊**:手机自由发言 | 注入生效(`additionalContext` 或 automation 起新会话),且**不打断**正在跑的任务 | | **S5** | 开关 + 台账 + 手机页收口 | 开关关 ⇒ 入口与阻断**整体不存在**(可见面零扩大);开关开 ⇒ 全链 200/101 | | **S6** | 真机 + 回归 | 真手机端到端一次;**既有线零回归**(配对 28 条 · S5 26 条 · 既有入口无变化) | 🔴 每棒的"完成"= **交付门禁**通过(⛔ 改完/build 过/已 commit 都不算交付)。 --- ## 8 安全与权限(R5:本方案只收窄,不扩大) | 面 | 处置 | |---|---| | 设备侧暴露 | **只绑回环**;⛔ 不新开公网入站口(复用 443);端口窗口沿用 `20000..20999` | | 平台准入 | 复用四道闸(平台开关 / 登录用户本人 / 台账 active+desktop / relay 有在线口) | | 身份键 | `actorId` **恒取会话**,⛔ 不取 body 自报值(沿用既有纪律) | | 凭据 | 长期设备凭据仍**只落设备侧**;手机**从不持有**设备凭据(沿用配对线的 G5 结论) | | 一次性 | 每个答案带 nonce,写即烧(§4.3) | | 默认状态 | 全链**默认关**;开启 = 用户显式动作(D6) | | 内容可见性 | 🔴 本节最需注意:走平台可见档(D5)⇒ 会话正文对该房间平台侧可见。**这是本方案唯一的"面扩大",必须由用户拍板** | --- ## 9 回滚(每项都有独立回滚点) | 变更 | 回滚 | |---|---| | 钩子新增分支 | 摘钩子(既有 `install_hooks --rollback` 幂等)或设 `DSH_DECISION_OFF=1` 秒级急停 | | 用户开关 | `users.mobile_reply_enabled` 置 `0` ⇒ 入口与阻断立即消失 | | 设备侧桥 | 设备节点守护 `-Action uninstall`;桥进程停止 ⇒ 平台显示离线 | | 平台静态页/路由 | 删 `mobile.html` + 撤回路由(生产 `lib-bak-*` 兜底) | | 插件 M6 | 模块级回滚(表分段 ⇒ 一次迁移牵动不了 M1–M5) | --- ## 10 未验证项与风险(⛔ 不得当成已定) 1. **钩子超时的实际上限**【待实测】—— 决定前置窗口 T1 能开多长;若上限很短 ⇒ 直接以 D4 的队列式为主。 2. **`deny` 语义对模型的真实影响**【待实测】—— 模型可能把 deny 读成"工具被拒"而非"用户答复"。缓解:`reason` 里写明"这是答复不是拒绝",并在 S3 用真机验收;⛔ 不靠猜。 3. **阻断式会吞掉桌面弹窗**(D7)—— 产品取舍,已用开关收敛;⚠️ 若用户同时在桌面点,存在双答竞速 ⇒ 需"先到先得 + 另一方收到具名失效"。 4. **MCP app 作第二载体的进程生命周期**【未公开】—— 采用与否以实测为准。 5. **WorkBuddy 升级可能改钩子契约** —— 红线 R1:⛔ 不自动升级;契约变更须走独立"测试→评估→修复"。 6. **决策库遗留缺陷会污染问答对**(该库自报:同题重复识别、答复错配 2 条)—— 若 M6 复用其口径,须先排除,⛔ 否则答案可信度受损。 7. **权威库不自起**(`dsl-pg` 实测处于 Exited 状态)—— 若复用决策库,须纳入探活。 --- ## 11 边界(⛔ 不重造清单,交付时逐条自查) 1. ⛔ 不新造取址 / 中继协议 / 第二套配对 / 第二套 IM 内核(全部已在)。 2. ⛔ 不让设备成为"被外部直接连入"的一方(设备只拨出;D8 用户已拍板"先不开端口")。 3. ⛔ 不把设备升格为平台 worker(沿用既有决定:设备形态 =「带端口的终端」)。 4. ⛔ 不在服务器上 build(`/opt/dshs/src` 陈旧,会盖坏产物)。 5. ⛔ 不引入 AA 云中转(用户已定 B 方案:用自己的覆盖网络)。 6. ⛔ 不跨线并行改 `aliyun-dsh-server` / `ai1net-dsh-desktop`(各线在活动)。 --- ## 附录:本次取证读数(可复跑) | # | 取证点 | 读数 | |---|---|---| | 1 | `strings app.asar \| grep hookSpecificOutput/permissionDecision/additionalContext` | **96 / 70 / 42** —— 契约字段存在 | | 2 | 本机已装钩子 `decision_bridge.py` | 用 `permissionDecision:deny`+`permissionDecisionReason` 打回残缺提问;用 `additionalContext` 注入常驻规矩 ⇒ **契约在跑** | | 3 | `settings.json` hooks | 四事件(SessionStart / PreToolUse `^AskUserQuestion$` / UserPromptSubmit / SessionEnd) | | 4 | `src/web/routes/im.ts` | 房间/成员/消息/**panels**/**panels actions**/插件桥(register·out·bridge·pull·result)/gateway/presence/stats 全在位 | | 5 | `src/im/agent-bridge.ts` | 370 行;形态 A bot / 形态 B 组员代理;四条硬约束(禁自激·预算·静默期·房间级限速排队) | | 6 | `overlay-pair` + `pair.html` | 六态、28 条判据全绿、生产已部署 | | 7 | `users.device_web_enabled` + S5 | 用户级开关模式已验证(默认 0) | | 8 | `E:/dsh-worker-dev/bin/overlay-node-daemon.ps1` | 设备节点守护在位(含 `-Action install/uninstall/status`) | | 9 | `dsh-plugin-ai1net` M5 | 插件侧 SDK 已实现(每插件令牌 + 单例长轮询闸) |