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

319 lines
26 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.
# 手机 App 经 ai1net 覆盖网络操作 WorkBuddy · 架构定稿 v2(2026-09-28)
> ## 🔴 状态块(2026-09-29 加):**本件已被取代,降为过程档案**
> ⇒ **权威件 = `交付物/手机App经覆盖网络操作WorkBuddy-架构定稿v3-20260929.md`**(各线按 v3,⛔ 别照本篇抄)。
> 取代原因(均为源码/实测):本件 **§5.3「首跳 `?password=` 换 Cookie」整段作废**(`?password=` 在受保护路径永远失效,正解=`x-access-token`);**§5.2 三判据不足**(本机并存 3 个 `WorkBuddy.exe` 且共用口令 ⇒ 必须加"父链"判据);**§5.1「Origin 归一」不足**(必须**剥掉** Origin,否则写操作一律 403);并新增 v2 完全没写的**会话侧硬约束**(会话按工作目录限定、"活会话"语义、往任意会话发要走 ACP)。
> ⛔ 正文按机制**不动**(保留取证原样)。
> **用户口径(原话)**:「通过 **dsh 平台的覆盖网络**去实现,功能做到**插件**中:**本地 dsh 客户端插件** + **dsh 平台插件**配合,让**手机 app 通过 ai1net 网络**使用」
> +「**平台插件和客户端插件是一个程序**,作用除了覆盖网络通讯,还有个作用是**账号核对**,这样才能**我的账号连我的客户端、访问我的 workbuddy**」
> **性质**:**v2 定稿**(取代 v1)。落地按 §6 棒次执行。
> **v1 已被本版更正的**:v1 把"客户端插件"误读为 **WorkBuddy 插件**,据此得出"垫片必须改成独立进程"。**该结论撤销** —— 见 §1。
---
## 0 判定(三句话)
1. 🔴 **一处重要更正**:你说的「客户端插件」= **DSH 客户端(桌面)上的插件**。DSH **有完整插件宿主** ⇒ 垫片**保持既有 DSH Host 插件形态**,v1 的"改独立进程"**撤销**。
2. ✅ **账号核对不用新做 —— 平台侧已完整具备**(§3)。你要的"我的账号只能连我的客户端、访问我的 WorkBuddy",正是既有那一套:**网络名与 hostId 从账号推导 + 台账属于本人 + 四道闸**。
3. ✅ **"一个程序两角色"也是既有模式** —— ai1net 插件本就是「同一包、客户端侧走 `dsh.profile.bundles`/平台侧走 `link:`」(§4)。
⇒ **净新增只有两处**(v1 说三处,现减为一处撤销 + 两处保留):垫片**上游**改指 WorkBuddy gateway + **凭据注入**改方式。
---
## 1 更正说明(v1 → v2)
| 项 | v1 的结论 | v2 更正后 | 依据 |
|---|---|---|---|
| 「客户端插件」指谁 | 误读为 **WorkBuddy 插件** | **DSH 客户端插件**(用户原话「本地 dsh 客户端插件」) | 用户口径 |
| 垫片承载形态 | 改为**独立进程**(因"WorkBuddy 无插件宿主") | **维持 DSH Host 插件**(⛔ 不改造) | DSH 有完整插件宿主;`device-shim` 已是该形态且已验通 |
| 改动数量 | 三处 | **两处**(上游 + 凭据) | 承载形态那条撤销 |
⚠️ **但 v1 那条观察仍然成立且重要**:WorkBuddy 侧**确实没有插件宿主**(只有 skill / mcp-app)⇒ 所以**插件装在 DSH 客户端侧**、WorkBuddy 只当**本地被访问对象**(它自带 gateway 已够)。这正是你的设计意图,比我 v1 的方案更干净。
---
### 1.1 术语:什么是「垫片」(shim)
> 这一节是后补的 —— 投递方自己曾把「客户端插件」误读为 WorkBuddy 插件(见上表),根因就是**术语没先讲清**。⇒ 后续棒请先读本节。
**一句话**:夹在两个**对不上**的东西之间、让它们能接上的**薄中间层**(工程借用词,与硬件无关)。
在本方案里 = 一个跑在电脑上的小程序,**只做"转接"、不做业务**:
| # | 卡在哪 | 垫片怎么办 |
|---|---|---|
| ① | 中继**只肯连回环地址**(代码写死 `'127.0.0.1'`,原文「永不接受可路由地址」),且**只认它从平台拿到的那个端口**,端口还须落在允许区间(生产实测 `19000..21999`) | 垫片**占一个区间内的固定口、真监听**(现用 `127.0.0.1:20090`) |
| ② | 目标服务的口**不归我们管、还会变**(DSH Web 19387 动态分配;WorkBuddy gateway 实测 `50753 → 59914`) | 垫片**对外固定,对内自己去找**(本方案需新增"动态发现",见 §5.2) |
| ③ | 目标服务**要它自己的凭据**,且凭据**不落盘**(DSH 的 token 是进程内随机值,只出现在启动链接里) | 垫片**在进程内拿到并注入**;并改写 `Host` / `Origin` 过对方的栅栏,否则一律 401/403 |
**「薄」的含义**:⛔ 不处理业务 · ⛔ 不存数据 · ⛔ 不做权限判断 —— 只**转发 + 改几个请求头**。越薄,故障面越小。
**为什么不能省**(两条都被实测否掉):
- 把目标服务的口**直接**声明给中继 ⇒ ❌ 不行(口不归我们管;且凭据对不上)。
- 用**外部脚本**代替垫片 ⇒ ❌ 不行(凭据**只在进程内存里**,外部拿不到)。
⇒ 这也是它**必须是"进程内插件"而非独立脚本**的根本原因 —— 而「进程内」在 DSH 侧成立(DSH 有插件宿主),在 WorkBuddy 侧不成立(只有 skill / mcp-app)。
🔑 **所以本方案的落点就是**:垫片**留在 DSH 客户端插件**里,它去访问的**目标**换成 WorkBuddy gateway。
## 2 目标架构(v2)
```
手机 App(Android 客户端)
│ HTTPS 443(平台登录态)
▼
① ai1net 平台(47)—— 【既有,零改动】
├ 账号核对(下方 §3 全链) ← 决定"我的账号 ↔ 我的客户端"
├ 设备入口 /u/<uid>/desk/<hostId>/* ← device-web.ts(202 行,五道闸 + 整站前缀转发)
├ 配对 overlay-pair(六态 · 28 条判据绿)
└ 覆盖网络中继(wss 走 443,⛔ 不开端口)
▲ 设备只拨出
│
② 用户电脑
├ 【客户端插件 · 与平台插件同一程序】
│ ├ 覆盖网络通讯:复用平台仓自己的客户端入口(`lib/net/relay/main.js --client`)
│ ├ 本地监听:只绑 127.0.0.1:20090(DSH Host 侧插件形态,⛔ 不改)
│ └ 凭据注入:改用 WorkBuddy gateway 令牌(🔴 改动 2)
│ · 上游改指 127.0.0.1:<gateway 动态口>(🔴 改动 1)
│ · Host 改写 → 过 gateway 的 hostValidation
│ └ 持有设备凭据(账号核对链路的落点)
▼
③ WorkBuddy gateway(官方自带)→ ④ WorkBuddy 会话(桌面那份)
```
**★ "一个程序"怎么成立**:客户端侧与平台侧是**同一个包**,只是装配位置不同(§4)。客户端侧多一层"本机承接"职责(监听 + 注入),平台侧多一层"账号核对 + 准入"职责。
---
## 3 🔴 账号核对:平台侧已完整具备(⛔ 不用新做)
你要的「**我的账号连我的客户端、访问我的 WorkBuddy**」,逐环对应如下 —— **每一环都已存在**:
| # | 环节 | 实现 | 位置 |
|---|---|---|---|
| 1 | **登录态** | 门户会话 ⇒ `request.user.id` | `src/web/routes/auth.ts`(698 行) |
| 2 | **网络名由账号推导** | `network = tenantNetworkOf(userId)` | `src/net/relay/device-grant.ts` |
| 3 | **hostId 由账号推导** | `hostId = deviceHostIdOf(userId, nodeKey)` | 同上 |
| 4 | **凭据签发绑账号** | `issueDeviceGrant({db, **userId**, nodeKey})`,落**四处**(返回值/registry/台账/relay 密钥表) | `device-grant.ts#issueDeviceGrant`(**唯一实现**) |
| 5 | **准入闸 · 属于本人** | 台账 `overlay_devices` **属于本人** 且 `status='active'` 且 `kind='desktop'` | `device-web.ts:14` 第③闸 |
| 6 | **用户级开关** | `getDeviceWebEnabled(userId)` —— **默认关** | `device-web.ts:92` |
| 7 | **停发即失效** | 台账置 `revoked` ⇒ 再签被拒("⛔ 不能续一次签就复活") | `instance-credential.ts:256` |
| 8 | **配对方向有利** | 由**已登录的手机**批准 ⇒ 归属由**手机的会话**确定;`approve` 请求体**只有 code**,`userId` 恒取 `request.user.id` ⇒ **"替别人签发"结构上不可能** | `overlay-pair.ts` |
🔑 **一句话**:**网络名与 hostId 都是从 `userId` 推导出来的** —— 这在结构上就保证了"**我的账号只能连到我的那台客户端**"(原文注释:「提权在结构上不可能」)。
⇒ **结论:账号核对是既有能力。** 你把它列入插件的作用是对的(它确实是这条链路的核心作用之一),但**它不需要新写** —— 新写的只是"客户端侧承接 WorkBuddy"那一段。
---
## 4 "一个程序两角色"= 既有模式(原文照录)
既有 ai1net 插件的 README 原文:
> **两端怎么装它(同一套机制)**
> ① **客户端(桌面壳)**:profile 的 `package.json` 里 `dsh.profile.bundles` 含 `@dsh-local/ai1net`;
> ② **平台侧(实例)**:同一个键 + `dependencies` 里一条 `link:` 指向共享只读包库
>  (平台侧协议见 `src/supervisor/plugin-assembly.ts`,⛔ 本包不重造分发机制)。
⇒ 你要的"一个程序"**已经有现成装配协议**(`link:` + `plugin-assembly.ts`)。本方案照它走,⛔ 不新造分发机制。
---
### 4.1 🔴 归属定案:垫片 = ai1net 包的**一块功能**(2026-09-28 用户拍板)
**用户口径**:垫片就是 ai1net 插件中的一个功能。
**核实结论**:**方向对,但现状不是** —— 垫片现在是一个**独立插件包**(`@dsh-client/device-shim`,自带 `cordis.patch.yml` + `package.json` 里的 `dsh.bundle.patch`,与 ai1net 包**各装各的**)。
⇒ **定案:归并** —— 把垫片的 host 半并入 `@dsh-local/ai1net`,作为**第 6 块模块**,保留其 `apply()` 与 `forwardHeaders()`;独立包 `@dsh-client/device-shim` 退役。装载仍走 `dsh.profile.bundles`(§4),⛔ 不新造分发机制。
**归并要顺带解决三件(①② 是硬阻碍,均已取证)**:
| # | 事项 | 实测 / 依据 |
|---|---|---|
| ① | 规格书 §五 A1 只列 **M1–M5 五块**(多租户 / 模型管理 / 技能插件管理 / **覆盖网络界面** / IM 插件侧),**没有"设备接入"** | 规格书 §五 A1 原文;`dsh.data.yaml` 模块段只有 `tenant/model/plugin/net/im` ⇒ 归并**必须带一条规格修订**(加模块 + 表段,建议 `net_dev_*`;无持久化需求则⛔ 不建表) |
| ② | 🔴 **`inject` 不兼容** | 垫片现为 `inject = ['connection','webServer']`(靠 `webServer.port` 拿本机口);而 ai1net 包 E1 明文「`inject = ['connection']` **必写**」,且分步实施方案 §1.3 / B1 已记「**桌面端没有 `webServer`**」⇒ 归并**必须去掉 `webServer` 依赖**(端口改自身配置 + 动态发现 —— 与 §5.2 是同一处改动,正好合并) |
| ③ | ✅ **不碰那条红线** | 规格书 M4 行写「「取身份」(入网凭据)⇒ **引导层,⛔ 不进包**」。核实:入网凭据 = `nodeKey / grant / grantSig`(`DSHS_OVERLAY_NODE_KEY_FILE` / `..._GRANT_FILE`),由**节点守护 + 平台**在拿;垫片持的是**本机服务的会话凭据**(`dsh-auth-*` 签名 Cookie)⇒ **两类凭据、不是一回事**,归并**不触红线** |
**代价(如实登记)**:归并要动**两处别人的成果** —— 插件线的 `S1` 冻结正文(加模块)与客户端线的 `device-shim`(迁出);且 `E:/github/dsh-client` **0 commit** ⇒ 迁之前**必须先入库**(§8 第 5 条)。
**收益**:一个包、一个 pluginId、一个库、一套装载机制(与 §4 完全一致);两条通道(DSH / WorkBuddy)共用**同一个模块**,以后改一处即可。
---
## 5 净新增:只有两处
### 5.1 【改动 1】垫片上游:DSH Web(19387)→ WorkBuddy gateway(动态口)
| 项 | 现状(已验通) | 本方案 |
|---|---|---|
| 上游地址 | `127.0.0.1:<webServer.port>` | `127.0.0.1:<gateway 口>` |
| 端口来源 | DSH 注入 `webServer` | **动态发现**(下方 5.2) |
| Host 改写 | → `127.0.0.1:<port>` | **同样需要**(gateway 有 `hostValidation`) |
| Origin 归一 | 需要(DSH 有 Host/Origin 栅栏) | **同样需要** |
✅ 既有 `forwardHeaders(rawHeaders, authority, cookie)` **逻辑可原样复用**,只换 authority 的值。
### 5.2 【必须新增】动态发现 gateway 端口
🔴 实测:**gateway 端口每次重启都变**(`50753 → 59914`)⇒ 写死必炸。
**发现判据**(三项同时成立):
1. 监听者是 **`WorkBuddy.exe`**(或其后代);
2. `GET /` 返回 **200** 且正文含 `CodeBuddy Gateway` / `CodeBuddy Remote Control`;
3. `GET /api/v1/health` 返回 **401**(路由在、需鉴权)。
⇒ 找不到 **fail-closed**(不启 + 具名报错),⛔ 不许沿用上次端口。
### 5.3 【改动 2】凭据注入:DSH 签名 Cookie → WorkBuddy gateway 令牌
- DSH 版:Cookie **authority 绑定**(名字 = `dsh-auth-`+sha256(authority))⇒ 必须改写 Host。
- WorkBuddy 版:鉴权是 **`?password=<令牌>`** 或 Cookie,**不绑 authority**。
- 🔴 **约束**:鉴权中间件有**防爆破限流 `2 次/分钟` + `12 次/小时`** ⇒ **绝不能每请求带 query 密码**。
- **选定做法**:垫片**首跳**用 `?password=` 换 **Cookie**,之后**全程复用 Cookie**(Cookie 不在该限流口径内)。
---
## 6 分工与棒次
| 棒 | 归属 | 做什么 | 判据 |
|---|---|---|---|
| **棒 0** | 本会话 | ① 端口发现脚本(§5.2 三判据)② **U1 只读实测**:`/jobs` 是否含桌面正在聊的会话 ③ 记录令牌取得路径 | 发现稳定;U1 定论 |
| **棒 1** | **ai1net 包 · 客户端半**(§4.1 归并后) | 上游改指 gateway;凭据换 Cookie 复用;Host/Origin 改写沿用既有;**去掉 `webServer` 依赖** | `curl 127.0.0.1:20090/api/v1/jobs` 经垫片返回 JSON;连续 50 次**无 429** |
| **棒 2** | **ai1net 包 · 平台半**(同一包,装法见 §4) | ⚠️ 见 §7 G-B —— **准入判定零改动**;本棒实际是**装同一包 + 开关登记**(走既有 `link:` 协议) | 经设备入口访问垫片通;两条通道(DSH / WorkBuddy)互不影响 |
| **棒 3** | Android 客户端 | 按对接单 MVP(列表/历史/SSE 尾随/发送/重连) | 对接单 §7 |
| **棒 4** | 联调 | 真机端到端 + 断网重连 + 中继零协议改动 | 判据:`src/net/relay/**` 无新增 op |
---
## 7 两个缺口(新发现 · 需处理)
| # | 缺口 | 说明 |
|---|---|---|
| **G-A** | 🔴 **DSH 桌面客户端的 profile 已随 `E:\ProgramDSH` 消失** | 现 `C:/Users/Administrator/.dsh/profiles/` 只剩 `rescue` / `web`,**无 `desktop`**;而垫片要挂在 `desktop` profile 上 ⇒ **客户端插件目前无处可挂**。须先恢复 `desktop` profile(或确认新的挂载点)。⚠️ **§4.1 归并后此缺口照旧**(换的是包名,不是挂载点)。 |
| **G-B** | ⚠️ **"平台插件"这一角色按核实是零改动** | `device-web.ts` 用通用反代(前缀之后原样转发),**准入四道闸与后端是谁无关** ⇒ **准入判定**零改动、⛔ 不需要为新功能改代码。✅ **按你的口径对齐**:平台侧那一「配合」角色由**同一个 ai1net 包的 host 半**在实例内承担(装法见 §4);本阶段它只需「开关 + 对账」,MVP 用平台侧既有 `device_web_enabled` 即可、**零界面**。若日后要「我的设备」自助界面 ⇒ 那是 **M4(覆盖网络界面)的延伸**,属插件线后续棒、**不属本方案**。 |
---
## 8 红线与风险
| # | 项 | 处置 |
|---|---|---|
| 1 | gateway 端口会变 | §5.2 动态发现 + fail-closed |
| 2 | 限流 2/min | 换 Cookie 后复用;客户端 ⛔ 不轮询 |
| 3 | 权限面 | gateway 能**执行任务、改会话** ⇒ 准入四道闸保持 + 默认关;⛔ 不新增共享密钥、⛔ 令牌不落平台库 |
| 4 | 两条通道并存 | 端口/设备行分开,⛔ 不动 DSH 那条已验通的 |
| 5 | `E:/github/dsh-client` **0 commit** | 动之前**先入库**(回滚唯一保障) |
| 6 | R5 只收窄 | 本方案**不开任何对外端口**、不新增暴露面 ⇒ ✅ 合规 |
---
## 9 归属定案(原「待确认」项已由用户拍板)
> 🟢 **2026-09-28 用户拍板:垫片就是 ai1net 插件中的一个功能。** ⇒ 「放哪个仓」不再是开放问题:**⛔ 不新建独立包、⛔ 不跨线共用**,改为**归并进 `@dsh-local/ai1net`**(§4.1)。原先三个候选(加配置项 / 新建同源包 / 另起进程)**全部作废**。
**随之而来的三件必办(无一项需要再拍板)**:
| # | 办什么 | 归属 |
|---|---|---|
| 1 | **规格修订**:规格书 §五 A1 加第 6 块模块(建议「设备接入」,表段 `net_dev_*`),并在 M4 那行「取身份不进包」后补一句边界定义(**入网凭据 ≠ 本机服务凭据**) | 插件线(S1 正文;本会话只出条款草案) |
| 2 | **垫片改造**:上游改指 WorkBuddy gateway + 凭据换 Cookie 复用 + **动态发现端口** + **去掉 `webServer` 依赖** | 插件线(迁入后)/客户端线(迁出前须先入库) |
| 3 | **恢复挂载点**:`desktop` profile(§7 G-A) | 客户端线 |
### 9.1 规格修订条款草案(可直接并入规格书 §五 A · 交插件线)
**A1 表增一行**:
| **M6** | **设备接入**(=原「垫片」) | 本机回环反代:占**区间内固定口**、承接中继来连、把**本机服务凭据**注入转发请求(并改写 `Host` / `Origin`);**上游目标可配置**(DSH Web / WorkBuddy gateway) | 「取身份」(入网凭据 `nodeKey/grant/grantSig`)⇒ **引导层,⛔ 不进包**(同 M4 口径) | 1 |
**A1 表下补一句边界定义**(防后人误读):
> M4 / M6 的「取身份不进包」管的是**入网凭据**(平台签发、由节点守护持有);M6 持有并注入的是**本机服务的会话凭据**(本机私产)。**两类凭据不同源、不同生命周期** —— ⛔ 不得因"都是凭据"就把 M6 判为越界。
**表名分段**:M6 **默认不建表**(纯转发、无状态)。若日后确需(如设备-目标映射),在既有 `net_*` 段下**加子段** ⇒ `p_ai1net_net_dev_*`,⛔ **不新开第 6 个模块段**(保持 `tenant / model / plugin / net / im` 五段不变)。
**⚠️ 一条必须显式授权的例外(否则与 A2 打架)**:A2 写「凡想到加服务端路由 / 加路径前缀 / 加一张页面 ⇒ **当场作废**」,而 M6 的动作是**在 `127.0.0.1` 上绑一个端口并监听** —— 这既不是路由、也不是页面,属 **E1–E3 之外的 Node 能力**。⇒ 规格须为 M6 **单列一条授权**:
> 「M6 得在 `127.0.0.1` 绑**一个**端口(默认 `20090`;须落在中继允许区间内、排除中继自身口),⛔ 不得绑可路由地址、⛔ 不得开多口。」
**这一条不写清,M6 就是违规插件。**
**须同步加注的两处既有文字**:① A2 的 E1 行「`inject = ['connection']` 必写」→ 加注「**M6 同此,⛔ 不得 inject `webServer`**(桌面端无此服务)」;② 分步实施方案 §1.3 / B1 的「桌面端没有 `webServer`」→ 加注「这也是 **M6 归并的硬前提**」。
---
## 附:v2 本次取证读数
| # | 取证点 | 读数 |
|---|---|---|
| 1 | 账号核对链路 | `auth.ts` **698 行** · `overlay-pair.ts` **578 行** · `overlay-device.ts` **99 行** · `tenants.ts` **362 行** |
| 2 | 凭据签发唯一实现 | `device-grant.ts#issueDeviceGrant`(落**四处**);`instance-credential.ts` 明写"⛔ 不复制那段判据" |
| 3 | 归属由账号推导 | `network = tenantNetworkOf(userId)`;`hostId = deviceHostIdOf(userId, nodeKey)` ⇒ 原文注「提权在结构上不可能」 |
| 4 | 四道闸第③条 | 「台账 `overlay_devices`:**设备属于本人** 且 `status='active'` 且 `kind='desktop'`」 |
| 5 | 停发语义 | 台账 `revoked` ⇒ 再签被拒(`instance-credential.ts:256`) |
| 6 | 一程序两角色 | ai1net 插件 README §「两端怎么装它(同一套机制)」:客户端 `dsh.profile.bundles` / 平台 `link:` |
| 7 | DSH 客户端 | `E:/github/dsh-desktop-0.1.7rc2` EXIST;但 `~/.dsh/profiles/` **只有 `rescue`/`web`,无 `desktop`** ⇒ 缺口 G-A |
| 8 | 既有 daemon 复用方式 | `overlay-node-daemon.ps1` 头注:「reuses the platform repo's OWN client entry, read-only: `node <Repo>/lib/net/relay/main.js --client`」+「Zero new protocol code」 |
---
## 10 棒 0 实测读数(2026-09-28 20:5x · 本会话交付)
> §6 把棒 0 定为「本会话」的活:① 端口发现脚本 ② U1 只读实测 ③ 记录令牌取得路径。**三项均已交付**。
> 交付件:`交付物/棒0-网关端口发现-20260928.mjs`(+ 逻辑夹具在 `tmp/wb-phone/_棒0逻辑夹具.mjs`)。
> 🔴 **棒 1–4 不属本会话**(见 §6 归属列),本会话只出件 + 派活。
### 10.1 ① 端口发现脚本(交付 · 可复跑)
| 项 | 读数 |
|---|---|
| 正向 | `node 棒0-网关端口发现-20260928.mjs` ⇒ **端口 52954 · pid 47964(WorkBuddy.exe) · `GET /` 200 含 «CodeBuddy Gateway» · `/api/v1/health` 401 · exit=0** |
| 纯逻辑夹具 | **PASS=10 / FAIL=0**(netstat 解析 5 条 + 判据① 5 条,含"父链成环不死循环") |
| fail-closed 负例 | ① netstat 空 ⇒ `no-loopback-listener` ② 去掉 52954 ⇒ `fingerprint-mismatch` ③ 名字表空 ⇒ `not-workbuddy-process`(**判据① 不放宽**)—— 三条都 exit=3 |
**两条设计要点(都是实测逼出来的)**:
- 🔴 **判据① 单靠进程名不够** —— 回环上**另有 3 个 `WorkBuddy.exe` 监听口**(`18488` · `59889` · `59890`)⇒ 必须三判据同时成立,否则会误认。
- 🔴 **受限环境逃生口**:实测在 WorkBuddy 的 AI 沙箱里 **Node spawn `netstat`/`tasklist` 恒 `EBUSY`**(而同一沙箱 **Python 正常**)⇒ 脚本支持 `--netstat-file/--tasklist-file/--parents-file` 喂入外层抓好的文本。⛔ 下棒若在受限环境跑,别把 `EBUSY` 误判成"机器上没有 gateway"。
### 10.2 ② U1 定论(🔴 这条修正对接单 §3.1)
| 端点 | 读数 | 结论 |
|---|---|---|
| `GET /api/v1/jobs` | 200 → `{data:{jobs:[]}}` —— **空** | 🔴 **桌面正在聊的会话不在 jobs 里** |
| `GET /api/v1/jobs/events` | 200(SSE,无即时应答体) | 同上,无实例可订 |
| `GET /api/v1/sessions?cwd=*` | 200 → **16 条**,字段 `id`/`cwd`/`name` | ✅ 含活会话 |
| `GET /api/v1/sessions/live` | 200 → `{sessionId, writerOccupied:true}` = **桌面当前会话** | ✅ 「操作现有会话」成立 |
| `GET /api/v1/sessions/{id}/history` | 200 → `{sessionId,name,requests:[{userInput,finalReply}]}` · **requests=3**,与本会话 3 次发言**逐数吻合** | ✅ 历史覆盖到最新一轮 |
| `GET /api/v1/sessions/{id}/replay` | 200 | ✅ 在 |
⇒ 🔴 **对接单 §3.1 的「主推一套」(jobs 系列)在本机上操作不了现有会话**;**必须走它列的"备选一套"(sessions 系列)**。棒 3(Android 客户端)请按 **sessions 一套**实现,⛔ 别按 jobs 写。
(附带更正对接单 §2-②:那里写的 `127.0.0.1:59914` 已失效,端口实测为 `52954`。)
### 10.3 ③ 令牌取得路径 —— 并由此暴露 v2 §5.3 的一个前提不成立
**取得路径(实测)**:令牌 = **`CODEBUDDY_GATEWAY_PASSWORD`**,由 `workbuddy-server` **进程内惰性生成**(32B base64url、模块单例),只经 `gatewaySecretEnv()` 注入**它 spawn 的子进程 env**。
| 事实(实测) | 读数 |
|---|---|
| 作用域 | **User = 空 · Machine = 空 · 仅 Process** ⇒ ⛔ 不是持久变量,WorkBuddy 每次重启即换 |
| 落盘 | ⛔ **刻意不落盘**(源码注释:该 secret 存在就是为了"同机其他进程无法读取"——`/api/v1/*` 曾是未鉴权本地 RCE) |
| `settings.json` | ⛔ **无 `gateway` 键** ⇒ 用户敲的 `/gateway token` **实测未落盘**(该子命令语义=*regenerate*,且 env 优先级**高于** `settings.gateway.password`) |
| 谁能读到 | 只有 **WorkBuddy 进程树内**的进程(实测:本会话的 Bash 工具继承到了;旧 bridge.py 从资源管理器启动则**读不到**,恒 401) |
🔴 **v2 §5.3 需修订**:§5.3 说「垫片**在进程内**拿到并注入」—— 这句话在 **DSH 路线**成立(DSH 令牌就在 DSH Host 进程里,而垫片正跑在那),但**在本路线的目标上不成立**:垫片跑在 **DSH Host 进程**内,而 WorkBuddy 令牌在 **WorkBuddy 进程树**内 —— **两者不是同一进程树 ⇒ 垫片拿不到令牌**。
⇒ **候选解法(⛔ 留给定归属的那一棒,本会话不代做、不定)**:
1. **把"取令牌"那一半挪进 WorkBuddy 进程树**(WorkBuddy 的 skill / hook / 它 spawn 的子进程都能继承该 env;本会话已实测继承成立)。**优点**:不落盘、不重开漏洞、令牌永远新鲜。**缺点**:垫片从一个"进程内插件"裂成两半,多一条本机 IPC。
2. **由 WorkBuddy 侧主动把令牌交给垫片**(需 WorkBuddy 侧存在承载点,⛔ 不改官方客户端 —— R2)。**优点**:形态最干净。**缺点**:目前无此承载点,须先确认扩展面。
3. **落盘**(写 `token.txt`)。**优点**:改动最小。**缺点**:🔴 **等于把厂商刚修掉的本地 RCE 面重新打开**,且每次重启要重写 ⇒ ⛔ 不建议。
### 10.4 派活(§6 棒次的落地指向 · ⛔ 本会话不代做)
| 棒 | 归属线 | 派活内容 | 卡点 |
|---|---|---|---|
| 棒 1 | **插件线 / 客户端线**(垫片改造) | 上游改指 gateway + 凭据换 Cookie 复用 + 动态发现(**直接复用 10.1 的脚本**)+ 去掉 `webServer` 依赖 | 🔴 **先解 §10.3 的进程树问题**,否则凭据注入无处取;且 **G-A(`desktop` profile 已随 `E:\ProgramDSH` 消失)仍在** —— 实测 `~/.dsh/profiles/` 只有 `rescue`/`web` |
| 棒 2 | 插件线(平台半) | ⚠️ §7 G-B:准入**零改动**,本棒=装同一包 + 开关登记 | 依赖棒 1 |
| 棒 3 | **anywhere 线(Android)** | 按对接单 MVP 五件事实现 | 🔴 **改用 sessions 一套**(见 §10.2) |
| 棒 4 | 联调 | 真机端到端 + 断网重连 | 依赖 1–3 |
📌 **一处环境事实(供各棒判读)**:**DSH 桌面客户端当前没在跑** —— `19387`(DSH Web)与 `20090`(垫片)**均无监听**,且无 electron 进程。⇒ 两条通道里,DSH 那条的**承载进程不在**,这也是 §10.3 那个进程树问题必须先定的原因。