# 02 · 架构设计

> 中文版 · [**English**](02-architecture.md)

本文描述 `dsh-coding-remote-kit` 的内部架构，是 `README.md` 技术说明的来源，面向贡献者与维护者。

Verified 构建钉：`@deepseek-ai/dsh@0.1.1-rc.2`。安装闸门：`engines.dsh` `>=0.1.1-rc.2 <0.3.0`（覆盖 `0.2.x` 安装，如 `0.2.1-rc.2`）。升级 verified 构建钉需另开 ADR（`docs/01-mvp-scope.md`）。`0.1.2-alpha`、`0.1.5-rc.1` 与 `0.2.1-rc.2` 仍是未验证运行时候选。

## 1. 双平面

```text
Harness webServer（回环，通常 127.0.0.1:3080）
  └─ 管理面 /api/mobile-remote/*
       配对 offer、设备列表、吊销、隧道 / 会合中继开关
       OwnerRequestPolicy：loopback/SSH 或完整受信 HTTPS 反代证明

独立数据面（默认 127.0.0.1:6879，配对时可 widen 到 0.0.0.0）
  ├─ GET  /m, /m/*     手机静态页（no-store；CSP + `frame-ancestors 'none'`）
  ├─ POST /m/claim     配对 PIN → offer（有速率限制）
  └─ WS   /m/ws        E2EE 握手 + 白名单 RPC
```

本插件**不**反向代理 `dsh web`，也**不**抢宿主 `api-proxy` 的审批/提问 provider。会话观察与写操作走数据面的窄 RPC 白名单。

MVP 路线：**B — 语义窄 RPC + 双平面**（`docs/01-mvp-scope.md`）。路线 A（完整 Web 透传）已拒绝。路线 C（签名原生 App）延后。

## 2. 主机数据流

```text
设置页（src/client）
  └─ slots.register「移动远程」
       GET  /api/mobile-remote/status
       POST /api/mobile-remote/offers     → widen 或广告隧道 + 二维码 / PIN
       GET  /api/mobile-remote/devices
       POST /api/mobile-remote/revoke
       GET/POST /api/mobile-remote/tunnel
       GET/POST /api/mobile-remote/relay
       POST /api/mobile-remote/cloudflared

手机浏览器 /m（src/mobile）
  └─ location.hash fragment（配对 offer）或 POST /m/claim { code }
       └─ 手机侧 X25519 密钥
            └─ WebSocket /m/ws
                 e2ee_hello → transcript → session keys（secretbox）
                      └─ status.get → session.list / subscribe / respond / prompt

src/server
  ├─ DeviceRegistry     devices.json（只存 token 的 SHA-256）
  ├─ OfferRegistry      内存中的待配对 offer
  ├─ AuditLogger        audit.jsonl（method + id，无 payload）
  ├─ server-key.json    X25519 身份（0600）
  ├─ MobileDataPlane    HTTP + ws
  ├─ CloudflareQuickTunnel   只暴露数据面（永不 3080）
  ├─ RendezvousClient   出站 WSS 连自建 Worker（永不 3080）
  └─ UpstreamHub        apiProxy（0.1.1）/ sessionController（0.1.5+）会话 RPC
```

未认证的 WebSocket **只处理握手**。业务 RPC 在 `e2ee_auth` 之后才开始。

## 3. 模块职责

### `src/index.ts`

从 `src/server/index.ts` 再导出 Cordis 的 `name` / `inject` / `Config` / `apply`。

### `src/server/`

- `index.ts`：插件 `apply`。存储、服务端密钥、数据面监听、管理面路由、隧道与会合中继 disposer。
- `config.ts`：Zod：`enabled` / `bind` / `port` / `offerTtlMs` / fail-closed `ownerRequest`；旧 `trustedHosts` 不再授予访问权限。
- `context.ts`：宿主 `apiProxy` / `sessionController` / `webServer` 类型与能力诊断（两个会话服务任一可用即可，`healthy` 不再要求旧的 `apiProxy`）。
- `routes.ts`：每个 path 只 `webServer.register` 一次（DSH 按 path 去重、不认 HTTP method）。GET/POST 在 handler 内分支。
- `security.ts`：宿主 owner policy 优先；fallback 校验 loopback/SSH 或受信 HTTPS peer + Origin/Host + owner proof + Fetch Metadata + 独立 CSRF，并提供有界 JSON body。宿主策略异常或畸形时 fail closed。
- `dataplane.ts`：数据面端口上的独立 `node:http` + `ws`；静态 `/m`；`/m/claim`；`/m/ws`。
- `connection.ts`：`acceptMobileSocket` — `/m/ws` 与会合中继 accept 共用的 E2EE + RPC 会话。
- `e2ee.ts` / `crypto.ts`：服务端握手、token 查找、tweetnacl secretbox。
- `rpc.ts`：白名单分发；未知方法 → `forbidden`。
- `upstream.ts`：旧宿主 `apiProxy` 会话/审批/提问桥（保留给 `0.1.1-rc.2`）。
- `session-controller-upstream.ts`：DSH `0.1.5+` `sessionController` 后端——冷读 `list` / `page` 历史、每个订阅会话一个 `follow` 迭代器、`prompt` / `cancel` / `create`，以及 `api-session/*` 会话列表镜像。
- `interactions.ts`：共享的待处理审批/提问注册表——面向手机的 `rpcId` 卡片、重连重放，以及最后一个订阅者离开后的结算。
- `approval-bridge.ts`：以 `prepend` 注册 `approval/request` 与 `user-questions/request` 应答者，让手机与宿主既有应答链竞速，无人应答时按失败关闭处理。
- `registry.ts`：设备、内存 offer、JSONL 审计。
- `keys.ts` / `storage.ts`：`$DSH_HOME/storages/mobile-remote/`（目录 0700、文件 0600、原子写）。
- `net.ts`：二维码广告用的 LAN 候选地址。
- `backpressure.ts`：每连接出站队列上限。
- `tunnel.ts`：Cloudflare Quick Tunnel 子进程；持久化 `tunnel.json`；unload 时杀掉。
- `relay.ts`：出站会合客户端（`dshmr-relay/v1`）；持久化 `relay.json`；unload 时停止。禁止夹进 `cloudflared`。
- `cloudflared-install.ts`：可选官方二进制安装（禁止在 `apply()` 时跑）。

### `src/shared/`

无依赖的协议常量与编解码，Node 与手机页共用：`constants.ts`（RPC 白名单、帧长度、HKDF 标签）、`offer.ts`、`pair-code.ts`、`handshake.ts`、`frame.ts`、`hkdf.ts`、`transcript.ts`、`validation.ts`、`base64.ts`、`version.ts`、`relay.ts`（会合外层信封）。

### `src/client/`

classic-script 设置页（`window.__ModuleLoader__.load`）。二维码、8 位 PIN、设备列表、LAN / Quick Tunnel / 会合中继。注入 `@deepseek-ai/dsh-client-ui-settings` 与 `dsh-client-ui-slots`。

### `src/mobile/`

手机浏览器页，构建到 `lib/mobile/`。`main.ts` 读 fragment offer、保持一条 WebSocket、跑四步握手。`app.ts` 渲染会话列表 / transcript / 短回复 / 审批与提问卡片。`sw.js` 只缓存 `/m` 静态壳。

## 4. HTTP / WebSocket API

管理面（宿主 `webServer`，仅回环）：

```text
POST /api/mobile-remote/offers
GET  /api/mobile-remote/status
GET  /api/mobile-remote/devices          # 永不包含 tokenHash
POST /api/mobile-remote/revoke           # { deviceId }
GET  /api/mobile-remote/tunnel
POST /api/mobile-remote/tunnel           # { kind: "cloudflare-quick", action: "start"|"stop" }
GET  /api/mobile-remote/relay
POST /api/mobile-remote/relay            # { action: "start"|"stop", origin?, hostToken? }
POST /api/mobile-remote/cloudflared      # { action: "install" }
```

写请求 JSON 有界。响应只有状态、offer 元数据、二维码文本和非秘密过期时间——从不返回 `deviceToken` 或服务端私钥。

数据面（默认端口 6879）：

```text
GET  /m  → 302 /m/
GET  /m/*                 手机静态资源，no-store
POST /m/claim             { code } → { offer }   # 8 位 PIN；每 IP 每分钟最多 8 次失败
WS   /m/ws                E2EE + RPC
```

RPC 方法与推送信封见 `docs/03-protocol.md`。

## 5. 存储

`$DSH_HOME/storages/mobile-remote/`（`$DSH_HOME` 默认 `~/.dsh`）：

| 文件 | 作用 |
|---|---|
| `server-key.json` | X25519 身份；0600；首次启动生成 |
| `devices.json` | 已配对设备；**只存 deviceToken 的 SHA-256** |
| `audit.jsonl` | `rpc_write` / offer / 吊销 / 隧道事件；无 payload |
| `tunnel.json` | Quick Tunnel 持久化，崩溃后可回收残留子进程 |
| `relay.json` | 会合 origin / hostId / hostToken（0600）；GET 永不返回 token |

## 6. 配对与 E2EE

1. 桌面创建配对 offer（endpoint、页面 URL、服务端公钥、TTL）。
2. 手机打开 `/m#<offer>`（二维码）或把 8 位 PIN POST 到 `/m/claim`。
3. 手机生成自己的 X25519 密钥并连接 `/m/ws`。
4. 四步握手（`dshmr-e2ee/v1`）钉死桌面公钥，经 HKDF 派生会话密钥，再用 device token 做 `e2ee_auth`。
5. 之后的帧走 tweetnacl secretbox。连续 5 次解密失败关闭连接。

密码学库替换**延期**（本里程碑继续用 tweetnacl，直至 E2EE 版本升级）：见 [`docs/research/adr-tweetnacl-vs-libsodium-webcrypto.md`](research/adr-tweetnacl-vs-libsodium-webcrypto.md)。除设置页吊销 / 空闲自动吊销外的 deviceToken 轮换**本版不做**：见 [`docs/research/adr-device-key-rotation.md`](research/adr-device-key-rotation.md)。

v0 诚实边界：裸 LAN 上 **`/m` 的首次 HTTP 下发**可被 MITM。页面被替换后，E2EE 只保护「恶意脚本与服务器之间」的信道。推荐 Tailscale / WireGuard；可选 Cloudflare Quick Tunnel 在边缘终结 TLS，且**不得**把 3080 送进隧道。可选自建会合中继（M5）用 HTTPS 提供 `/m` 并拼接出站 WebSocket，也**不得**看见 3080。细节见 `docs/04-threat-model.md`、`docs/05-cloud-relay.md`。

## 7. 构建产物

| 产物 | 作用 |
|---|---|
| `lib/server/index.js` | 打包后的 Cordis 入口（`packages: "external"` — 不要把 `tweetnacl` 打进 ESM） |
| `lib/client.js` | 设置页 classic-script |
| `lib/mobile/` | 手机页 + service worker |
| `lib/**` 转译树 | 仅供单元测试 import |

`package.json` `exports` 必须包含 `"."`、`"./client"` 和 `"./package.json"`（DSH 用 `require.resolve("<pkg>/package.json")` 扫描客户端模块）。

## 8. 兼容性

- Cordis 插件 id：`mobile-remote`。
- 配置默认：`enabled: true`，`bind: "127.0.0.1"`，`port: 6879`。
- 配对广告 LAN 候选且没有公网隧道 / 会合中继时，数据面会 widen 到 `0.0.0.0`。
- 线协议版本：`MOBILE_PROTOCOL_VERSION = 1`（`src/shared/constants.ts`）。

手机先完成审批或计划选择时，结束本次转发的子生命周期并撤回桌面卡片；手机断线超过现有宽限期时只退出手机分支，保留宿主链等待。历史读取在订阅快照确认后进行，并与加载期间收到的消息合并。
