- 变更规模:新增 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/ 知识文件,按口径入库)
17 KiB
手机 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 次会话操作期间,以下各项同时成立:
- WorkBuddy 主进程 pid 恒定(未重启、未崩溃);
- 宿主会话能正常回 idle,且用户发消息的响应无退化;
- 桌面**"当前对话"未被改变**(借用必须归还);
- 无 429、无凭据失败计数增长;
- 无新增对外端口;WorkBuddy 配置目录哈希零差异;
- 演练一次"垫片直接被杀" ⇒ WorkBuddy 与手机侧均无异常。
0.5.4 ⛔ 明确禁止清单(违一条即违规,各线自查)
- ⛔ 给网关加 CORS 白名单 / 关闭网关鉴权(如
gateway.auth=none)/ 把口令写进文件或配置 - ⛔ 把长跑服务放进会话后台任务(含"反正跑完就停"的自我安慰)
- ⛔ 对宿主正在跑的会话做
load/ 接管 / 抢占 - ⛔ 抢
writer_occupied的会话(应放弃并如实报错) - ⛔ 改 WorkBuddy 的配置 / 插件 / 令牌 / 进程(不 kill、不 restart)
- ⛔ 轮询 + 高频读写;⛔ 反复试凭据
- ⛔ 无超时地等待上游
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 + 本轮新增)
- ⛔ 不给网关加 CORS 白名单、不开任何对外端口 —— C3 的正解是剥掉 Origin,不是放宽网关
- ⛔ 不动
src/net/relay/**(V4) - ⛔ 不改 DSH 那条已验通通道(两条通道并存,端口/设备行分开)
- ⛔ 口令不落盘、不进平台库、不进日志(现靠 env 自动取,最干净 —— 别退回"写文件")
- ⛔ 动
E:/github/dsh-client前先入库(该仓 0 commit) - ⛔ 子进程杀不掉上游就 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 加组件/配置 ⇒ 违 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 的拒绝启动判决。