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

193 lines
17 KiB
Markdown
Raw Permalink 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.
# 手机 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 的拒绝启动判决。