Files
dsh_ai1net_server/交付物/手机回复WorkBuddy会话-架构与实施方案-20260928.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

269 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 手机回复 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 已实现(每插件令牌 + 单例长轮询闸) |