Files
dsh_ai1net_server/交付物/手机App经覆盖网络操作WorkBuddy-架构定稿v3-20260929.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

17 KiB
Raw Permalink Blame History

手机 App 经 ai1net 覆盖网络操作 WorkBuddy · 架构定稿 v3(2026-09-29)

🔴 本件取代 v2(手机App经覆盖网络操作WorkBuddy-架构定稿v2-20260928.md 降为过程档案)。 取代原因:v2 的 §5.3 凭据注入、§5.2 端口判据 已被实测推翻/不足;且新增两件 v2 完全没写的会话侧硬约束。 更正来源:交付物/手机接入-目标与协同计划-20260929.md §三(C1–C6)+ 本工作区棒 0b 取证。


0 目标与验收(与协同计划 §一 一致,此处为唯一权威副本)

手机 App 通过 ai1net 覆盖网络(HTTPS 443)操作自己电脑上的 WorkBuddy 会话,能力做成 DSH 插件 (@dsh-local/ai1net 客户端半 + 平台半同一个包),并由账号核对保证「我的账号只能连我的客户端、访问我的 WorkBuddy」。

# 判据 怎么验
V1 手机(真机、非 USB)能看到桌面会话列表与最新消息 设备入口 https://<门户>/u/<uid>/desk/<hostId>/… 打开即见
V2 手机发出的消息进入桌面会话(任意同项目会话) 目标会话 replay 出现 user_message_chunk,文本逐字一致
V3 桌面正在用的会话不被夺走 活会话走 reply;非活会话走 ACP 借用并归还(§3.4)
V4 src/net/relay/** 零协议改动 该目录无新增 op
V5 权限面不扩大:不开任何对外端口、准入四道闸保持、默认关 端口清单 + device_web_enabled 默认值
V6 垫片是 DSH 客户端插件(并入 ai1net 包第 6 模块),⛔ 不是独立进程 包内模块清单 + dsh.profile.bundles
V7 🔴 绝不影响 WorkBuddy 本身的运行(2026-09-29 用户明令 · 见 §0.5) 见 §0.5「验收判据」

0.5 🔴 硬约束:绝不影响 WorkBuddy 本身的运行(2026-09-29 用户明令 · 最高优先级)

用户原话:「做这些功能都不能影响 workbuddy 本身的运行,要禁止这种锁死,技术实现上要严格遵循这个规则」 ⇒ 本约束优先于本件其它一切设计选择;与其它条款冲突时,按本约束裁剪。

0.5.1 总则(单向依赖 · fail-safe 方向)

垫片 / 桥 / 手机侧 对 WorkBuddy 只有"读动机"、没有"写动机"。 任一环节崩溃、被 kill、超时、卡住 ⇒ 必须表现为"这个功能不可用",⛔ 绝不允许表现为"WorkBuddy 异常"。 ⇒ 一切降级方向 = 关掉自己,不是"重试到成功"。

0.5.2 七条技术条款(每条都可检验 · 实现时必须逐条落地)

# 条款 为什么(本机实测教训) 检验判据
T1 进程隔离:⛔ 垫片/桥不得放进 **WorkBuddy 的"会话后台任务"**里跑;长跑一律走 ① 一次性定时自动化(跑完即退) 或 ② 独立进程(输出不接回任何会话) 会话后台任务每轮输出都会唤醒宿主会话 ⇒ 会话永不空闲 ⇒ 用户看到"卡死"(09-28 已立禁令,09-29 我违反 6 次复现) 起/停垫片 ≥10 次:WorkBuddy 主进程 pid 恒定、宿主会话能回 idle、UI 无卡顿
T2 不夺会话:⛔ 不得对正在运行 / 正被 writer 占用的会话做 session/load 或任何接管;reply 只在目标=当前活会话时用;ACP 借用先记原活会话,无论成败 finally 归还;遇 writer_occupied ⇒ 放弃,不重试、不抢 实测:session/load 会把桌面"当前对话"切走;被占用的会话 load 报 -32000 writer_occupied;我借用后没还回去,把用户会话指针带走了 借用流程跑 100 次:结束态 liveSessionId 与原值一致;期间桌面未被夺
T3 不占资源、不改它:⛔ 不绑可路由地址、⛔ 只绑一个回环口(20090)、⛔ 不开对外端口;⛔ 不改 WorkBuddy 的 settings.json / 令牌 / 插件 / 配置;⛔ 不 kill/restart 它的进程 端口/配置一改,影响面超出本功能 操作前后对 WorkBuddy 配置目录做哈希比对:零差异;端口清单无新增对外口
T4 限流友好:⛔ 绝不反复试凭据(网关失败限流 2/min + 12/h,只计失败 ⇒ 试错会把自己锁死一小时);⛔ 不轮询(客户端走 SSE/事件);页面静默同步若保留 ⇒ 仅读、间隔 ≥ 5 s、且仅在可见时;探活只用不计失败额度的路径 09-29 实测:x-access-token 未试出前连打多次 401 ≈ 逼近额度 连续 1 h / 50 次操作:无 429;无凭据失败计数增长
T5 超时与熔断:所有对 WorkBuddy 的调用带短超时(读 ≤ 5 s、写 ≤ 10 s、ACP 借用整体 ≤ 120 s);连续 3 次失败 ⇒ 降级为只读并停止写,⛔ 不自动重试写操作 无超时 = 把"等待"传导成"卡住" 拔网/关网关演练:调用在超时内返回,功能自报不可用,WorkBuddy 无感
T6 不挡宿主:⛔ 不得长期持有项目执行锁(handoff-guard 全局/域锁);⛔ 不得让任何"每轮输出"回流到会话 长跑常驻任务的 stdout 会反复唤醒宿主会话 锁检查 --check-exec:始终空闲(持有仅限单次短操作内)
T7 可静默、可回滚:任一环节能被直接停掉且无残留(⛔ 不留守护进程、⛔ 不留后台任务、⛔ 不留改过的 WorkBuddy 状态);停掉后 WorkBuddy 行为与装之前逐项一致 出问题时"关得掉"比"修得好"更重要 直接 kill 垫片 ⇒ WorkBuddy 与手机侧均无异常、无残留进程/端口/文件

0.5.3 验收判据(V7 · 必过)

连续 1 小时 / ≥50 次会话操作期间,以下各项同时成立:

  1. WorkBuddy 主进程 pid 恒定(未重启、未崩溃);
  2. 宿主会话能正常回 idle,且用户发消息的响应无退化;
  3. 桌面**"当前对话"未被改变**(借用必须归还);
  4. 无 429、无凭据失败计数增长;
  5. 无新增对外端口;WorkBuddy 配置目录哈希零差异;
  6. 演练一次"垫片直接被杀" ⇒ WorkBuddy 与手机侧均无异常。

0.5.4 ⛔ 明确禁止清单(违一条即违规,各线自查)

  1. ⛔ 给网关加 CORS 白名单 / 关闭网关鉴权(如 gateway.auth=none)/ 把口令写进文件或配置
  2. ⛔ 把长跑服务放进会话后台任务(含"反正跑完就停"的自我安慰)
  3. ⛔ 对宿主正在跑的会话做 load / 接管 / 抢占
  4. ⛔ 抢 writer_occupied 的会话(应放弃并如实报错)
  5. ⛔ 改 WorkBuddy 的配置 / 插件 / 令牌 / 进程(不 kill、不 restart)
  6. ⛔ 轮询 + 高频读写;⛔ 反复试凭据
  7. ⛔ 无超时地等待上游


1 🔴 v2 → v3 更正表(逐条含依据;各线按本表为准,⛔ 别照 v2 抄)

# v2 原文 v3 更正 依据
C1 §5.3「首跳用 ?password= 换 Cookie,之后全程复用 Cookie」;§1.1 以「凭据只在进程内存」论证垫片必须进程内 🔴 ?password= 在受保护路径上永远失效(requireAuth 构造请求对象时写死 query:{})。唯一稳定带法是 x-access-token: <明文>。口令来源 = process.env.CODEBBUDDY_GATEWAY_PASSWORD(网关源码 GatewayAuth.resolve:env → settings → 都没有才随机生成并写回) 源码 + 实测 200
C2 同上 §5.3 整段作废,改为「取口令 + x-access-token」。⚠️ 由此**§1.1 的"必须进程内"论据被削弱**:口令确实只在内存,但凡 WorkBuddy 的后代进程都能从 env 继承 ⇒ 垫片只需在 WorkBuddy 进程树内,不必在 WorkBuddy 进程内。若 DSH Host 不在该树内 ⇒ 需 WorkBuddy 侧薄组件投递口令(此点插件线须复核并给结论) 同上
C3 §5.1「Origin 归一:需要」 🔴 不够 —— 必须"剥掉" Origin(连同 Referer)。实测:无 Origin → 409(正常);带 Origin → 403 {"error":"Origin not allowed","hint":"Add this origin to CODEBUDDY_CODE_CORS_ORIGINS …"},一切写操作全废,而 GET 正常 ⇒ 表现为"能看不能发" 实测对照
C4 §5.2 三判据(进程名 / GET / 含 title / health 401) ⚠️ 三判据不足以选对实例:本机实测并存 3 个 WorkBuddy.exe(父链 pid 3176/22592/24260),各有网关端口,且共用同一口令 ⇒ 鉴权判据也不能区分。必须加判据④:父链(沿本进程父链上溯,选自己那条进程树里的宿主所监听的口);同档位多候选再按"活会话=列表里最近活跃那条"择一 实测(选中 57452,其 /live 与"最近会话"均指向本会话)
C5 (v2 无) 🔴 网关只读写"与自己同工作目录"的会话:跨项目会话 history/replay 一律 404 SESSION_NOT_FOUND(直连网关亦然,加 cwd 参数无用)。实测同目录 5 条可达 / 跨项目 19 条不可达 ⇒ 手机端会话列表必须按"可操作"过滤,工作目录取 GET /api/v1/info 的 cwd 实测
C6 §5.1 只说"上游改指 gateway" 🔴 "活会话"=桌面当前打开的那一个(sessionManager.sessionSubject.value),POST /sessions/{id}/reply 只认它,其余 409 SESSION_FOLLOW_NOT_LIVE。往任意会话发消息须走 ACP:session/load+session/prompt;且 `session/load new**必须带cwd+mcpServers**(缺则 -32602)。⚠️ **被 writer 占用的会话接管不了**:-32000 "already has an external writer" {reason:"writer_occupied"}`

2 目标架构(v3 · 仅列与 v2 有差异处,其余照 v2 §2/§3/§4)

2.1 垫片(M6 模块)职责(v3 版)

本机回环反代:占 127.0.0.1:20090、承接中继来连、注入本机服务凭据、转发到 WorkBuddy gateway(动态口)。四件事:

# 做什么 v3 要点
1 端口发现 四判据(v2 三条 + 父链),找不到 fail-closed
2 凭据注入 x-access-token: <口令>;口令优先取自 env,取不到则求 WorkBuddy 侧投递(⛔ 不落盘)
3 头改写 Host → 127.0.0.1:<gw口>;🔴 剥掉 Origin 与 Referer(C3)
4 去 webServer 依赖 照 v2 §4.1 归并口径不变

2.2 🔴 会话侧契约(v2 完全没有 · 手机端按此实现)

① 能操作哪些会话? 仅 workdir = GET /api/v1/info 的 cwd 的会话(其余 404)  ← C5
② 会话列表          GET /api/v1/sessions          (只返回①这批)
   跨项目浏览        GET /api/v1/sessions?cwd=*    (含跨项目;⚠️ 它们不可操作,须标灰)
③ 读历史            GET /api/v1/sessions/{id}/history   → {data:{name, requests:[{userInput, finalReply}], sessionId}}
   完整事件          GET /api/v1/sessions/{id}/replay    → {data:{events:[…]}}(实测 953+ 事件 / 2.6 MB,⚠️ 别整包拉)
④ 哪个是"当前会话"   GET /api/v1/sessions/live  → {sessionId, writerOccupied}
   ⚠️ sessionId 为 null = 桌面没打开任何会话;⛔ 列表里的 isCurrent **恒 false**,不能用
⑤ 发消息
   · 目标是 live 会话 ⇒ POST /api/v1/sessions/{id}/reply {"text":"…"}   ← 官方路径,不夺 writer
     (缺 text ⇒ 400;非 live ⇒ 409)
   · 目标不是 live   ⇒ ACP:connect → initialize → session/load{id,cwd,mcpServers:[]}
                        → session/prompt{sessionId,prompt:[{type:"text",text}]}
                        → (归还)session/load 回原 live
⑥ ACP 细节          必带 `Accept: application/json, text/event-stream`(否则 406);
                    响应走 **POST 回体的 SSE 流**(⛔ 不必另开长连);`DELETE /api/v1/acp` 断开
⑦ 回环豁免          `/api/v1/acp` 一族从 127.0.0.1 来**免凭据**(源码 `AcpSecurity.checkAuth`/`checkSessionToken` 均 `isLoopback → true`)

3 棒次与归属(与协同计划 §四 一致)

棒 归属线 做什么 前置
棒 0 / 0b ✅ 本工作区 端口发现 + 取证件 —
棒 0c ✅ 本工作区(本件) 6 条更正回写 → 架构 v3 + 同步工单 棒 0b ✅
🅐G-A ai1net-dsh-desktop 解 desktop profile 缺口 + 给挂载点结论 —
棒 1 ai1net_ui 垫片归并入 @dsh-local/ai1net 第 6 模块 + 按 §2.1 改造 🅐 可并行
棒 2 ai1net_ui(同包平台半) 装同一包 + 开关登记(准入零改动) 棒 1
棒 3 ✅ ai1net-dsh-anywhere Android MVP 已完
棒 4 ai1net-dsh-anywhere + 三方 对接单同步 + 端侧按 §2.2 调整 + 真机端到端 棒 1/2、棒 3

4 红线(v2 §8 + 本轮新增)

  1. ⛔ 不给网关加 CORS 白名单、不开任何对外端口 —— C3 的正解是剥掉 Origin,不是放宽网关
  2. ⛔ 不动 src/net/relay/**(V4)
  3. ⛔ 不改 DSH 那条已验通通道(两条通道并存,端口/设备行分开)
  4. ⛔ 口令不落盘、不进平台库、不进日志(现靠 env 自动取,最干净 —— 别退回"写文件")
  5. ⛔ 动 E:/github/dsh-client 前先入库(该仓 0 commit)
  6. ⛔ 子进程杀不掉上游就 fail-closed;⛔ 端口发现失败不许沿用上次值

6 🔴 G-C 处置(插件线 09-29 提出 · 本文定案)

问题(G-C):口令只在 WorkBuddy 进程树内可读(process.env.CODEBBUDDY_GATEWAY_PASSWORD 由其主进程下发、子进程继承)。 而垫片跑在 DSH Host 进程里 ⇒ 若 DSH Host 不在 WorkBuddy 进程树内,就读不到口令。

为什么不能靠"改 WorkBuddy"或"口令落盘"绕过:

  • 写 settings.json(如加 hook)去投递口令 ⇒ 违反 T3「⛔ 不改 WorkBuddy 的配置」;
  • 把口令写进文件/环境变量配置文件 ⇒ 违反 红线 4「口令不落盘」+ T3。 ⇒ 两条"省事的路"都被本件红线堵死,必须从"启动方式"上解。

定案(按优先级):

序 做法 判据 是否需改 WorkBuddy
①(默认) DSH 客户端从 WorkBuddy 内启动(在 WorkBuddy 内跑一条脚本/自动化把客户端拉起来)⇒ DSH Host 自然成为 WorkBuddy 后代,env 直接可读 垫片自检:DSH Host 父链含 WorkBuddy ⇒ 口令可得 ⛔ 不改(只是"谁拉起它"的差别)
② 若 V1 要求"客户端由系统自启/资源管理器启动" ⇒ 垫片读不到口令时必须 fail-closed:不启监听 + 返回 503 gateway-token-unavailable + 具名日志 负控:非后代启动 ⇒ 503、⛔ 不重试、⛔ 不退化成明文假定 ⛔ 不改
⛔ WorkBuddy 侧薄投递器(v2 §10.3 候选 1) —— 作废:需给 WorkBuddy 加组件/配置 ⇒ 违 T3

垫片自检必须暴露的三项(供 V7 验收,⛔ 不回显口令本身):口令来源名(env)/头名(x-access-token)/是否在场。


7 ⚠️ 关于 tmp/wb-phone/ 的定位(防后人误读)

它是「本机旁路原型 + 能力取证」,⛔ 不是产品形态,也不在本架构的实施路径上。

维度 本架构(上表) 旁路原型实际做法 判定
传输 ai1net 设备入口(443) USB adb reverse + 手机浏览器 localhost:18899 ❌ 不符
承载 DSH Host 插件(M6) 独立 Python 进程 bridge.py ❌ 不符
手机侧 Android App 手机浏览器页面(APK 未装) ⚠️ 原型

它的价值是把 §1 的 6 条更正经真机验出来(含"手机→桌面会话"的端到端 200)。产品实施仍按本件 §2 走。

5.1 遗留副作用(需知情)

  • 联调期间把活会话指针切走过(38499a2e → b419f820);用户会话 3a46cebb 因 writer 被占load 不回去 ⇒ 在桌面上点一下要用的会话即可归位。
  • 两个旧会话各多 1 条联调消息(38499a2e、b419f820)。
  • ⛔ 不要再把桥当"会话后台任务"起(见 §5.2)。

5.2 🔴 一条已被写进项目工具的禁令(本轮违反 6 次,代价=用户感到"卡")

把长跑服务放进会话的后台任务 ⇒ 它每轮输出都会唤醒宿主会话 ⇒ 会话永不空闲 ⇒ 用户看到"卡住 / 发消息不恢复"。 ✅ 正确用法(二选一):① 单轮 + 定时自动化叫(--once,跑完即退);② 独立进程(独立窗口/计划任务,输出不接回任何会话)。 📂 依据:tmp/supervise-inbox/watch.log 2026-09-28 23:17:54 的拒绝启动判决。