Files
dsh_ai1net_server/交付物/手机回复WorkBuddy会话-架构与实施方案-20260928.md
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

19 KiB
Raw Permalink Blame History

手机回复 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 已实现(每插件令牌 + 单例长轮询闸)