# CCGUI 公网中继部署说明

一台境内 VPS + 一个域名，跑一个 Node.js 中继程序，即可让手机在外网（4G/异地 WiFi）连到你电脑上的 CCGUI。
**电脑侧零额外安装**：CCGUI 自己主动拨出到中继，本机不开端口、不装任何客户端。

## 架构

```
手机(外网, https://relay.你的域名/)
        │  wss
        ▼
  Caddy(自动 HTTPS, 443)  ──►  node server.js(127.0.0.1:8080, 中继撮合/转发)
        ▲
        │  wss（出站拨号，CCGUI 主动连，不开入站端口）
  CCGUI(电脑)
```

中继只按房间号把「桌面」和「手机」两端撮合、双向透明转发。**不同电脑用各自随机房间号，互不冲突**：
一台中继可同时服务任意多对「电脑↔手机」。

> 注意：本方案**不做端到端加密**，转发内容为明文，中继（VPS 运营者）可见终端内容。请部署在你自己信任的 VPS；
> 若要给他人使用，需让对方知悉此点。防闯入靠「房间号高熵 + 配对令牌」（见下）。

## 部署步骤（VPS，以 Debian/Ubuntu 为例）

1. 装 Node.js（18+）与依赖：
   ```bash
   cd /opt && git clone/scp 本 relay 目录到此，或直接上传 server.js package.json
   cd relay && npm install
   ```

2. 放页面：把 CCGUI 生成的 `mobile-test.html` 传到中继目录并改名 `mobile.html`
   （手机打开 `https://relay.你的域名/` 就是这个页面）。

3. 启动中继（建议用 pm2 或 systemd 常驻）：
   ```bash
   PORT=8080 node server.js
   # 或： pm2 start server.js --name ccgui-relay
   ```

4. 前置 Caddy 做自动 HTTPS（最省心，自动 Let's Encrypt 证书）。`/etc/caddy/Caddyfile`：
   ```
   relay.你的域名 {
       reverse_proxy 127.0.0.1:8080
   }
   ```
   然后 `systemctl reload caddy`。Caddy 会自动申请并续期证书，wss/https 都走 443。

5. **备案**：境内服务器用域名对外提供服务需 ICP 备案（域名/服务器商处办理）。

## 与 CCGUI 的对接（已内置，1.0.36+）

- CCGUI 设置 → 「公网中继（外网访问）」开启，填「中继地址 `wss://relay.你的域名/`」。
- CCGUI 自动拨出：连中继 `register {role:'host', room, token}`（房间号与令牌本机自动生成、持久化）。
- 设置页显示「手机配对」链接 `https://relay.你的域名/?room=xxxx&token=yyyy`，手机浏览器打开即自动连回本机。
- 手机页 `register {role:'guest', room, token}`；两端 `peer-online` 后，手机发 `hello{token}`，再走原有 hello/list/attach/data 协议。

## 安全说明

- **不做 E2E**：内容经中继为明文，VPS 运营者可见。请只在你信任的 VPS 上部署；给他人用需告知此点。
- 防闯入两道闸：① 房间号为高熵随机串，难猜中；② 配对令牌由桌面首个注册者设定，手机不带对令牌无法加入房间。
- 房间隔离：`rooms` 按 roomId 独立成对转发，不同电脑各自房间，互不干扰；一台中继可服务任意多对「电脑↔手机」。
- 远程写入不绕过 Claude Code 自身的权限确认（沿用桌面侧安全闸）。
- 建议：中继务必置于 Caddy 的 HTTPS 之后（wss），至少保证「手机↔中继」「电脑↔中继」两段链路在传输层加密，避免链路中途被窃听。

## 公网隧道（子域名反向代理，可选）

在手机镜像之外，中继还能把 **CCGUI 电脑本机的本地 HTTP 端口**以 `<前缀>.<你的域名>` 的形式映射到公网，
供临时发给客户预览（ngrok/frp 式反向隧道，用完即关）。电脑侧仍是主动拨出、不开入站端口。

隧道与手机镜像**同进程、互不干扰**：镜像走主域名（apex）的 WebSocket，隧道走各个子域名。

### 启用条件（两个环境变量，缺一即禁用）

- `BASE_DOMAIN`：本中继对外服务的主域名（如 `你的域名`）。**必须与 CCGUI 客户端设置里填的「中继域名」host 一致。** 未设置时隧道功能关闭，中继退化为纯镜像（兼容旧部署）。
- `TUNNEL_SECRET`：隧道**注册密钥**（运营级闸门）。CCGUI 客户端注册隧道时须携带同一串，不匹配即拒。
  - 作用：防止陌生人连上中继、借你的（已备案）域名匿名认领子域名挂违规内容。
  - 注意：这是**注册侧**闸门；**访问侧不做鉴权**——子域名一旦激活，任何人凭 URL 即可访问（这是刻意设计，预览完就在 CCGUI 里关掉映射即可）。

systemd 注入示例（`/etc/systemd/system/ccgui-relay.service` 的 `[Service]` 段）：

```
Environment=PORT=8080
Environment=BASE_DOMAIN=你的域名
Environment=TUNNEL_SECRET=换成一串足够长的随机码
```

改完 `systemctl daemon-reload && systemctl restart ccgui-relay`（会瞬断现有镜像/隧道连接，选低峰）。

### DNS 与防火墙（一次性）

1. 加一条**泛解析**记录：`*.你的域名  A  ->  本 VPS 公网 IP`（一次性，无需任何 API）。apex 的 A 记录保持不变。
2. 放行入站 **80 与 443**（腾讯云安全组 + ufw 都要）。on-demand 证书走 HTTP-01/TLS-ALPN，80 不通会签发失败。

### Caddy（按需逐子域名签发证书）

因为没有 DNS API，无法申请泛域名单证书，改用 Caddy 的 **on-demand TLS**：每个子域名首次被访问时现场签一张证书。
参考 `Caddyfile.example`（把「你的域名」替换后与现有 `/etc/caddy/Caddyfile` 合并）：

- 全局块加 `on_demand_tls { ask http://127.0.0.1:8080/internal/tls-check }`——Caddy 签发前先问中继「该子域名是否有活隧道」，只有活隧道才放行，防扫子域名刷爆证书额度。
- apex 站点块**保留** `/ccgui.html` 与 `/downloads/*` 的 `file_server`，其余 `reverse_proxy 127.0.0.1:8080`。
- 新增 `*.你的域名` 站点块：`tls { on_demand }` + `reverse_proxy 127.0.0.1:8080 { header_up Host {host}; flush_interval -1 }`。

改完 `caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy`。

> Let's Encrypt 限额：新证书按**注册域**（如 `你的域名` 的顶级注册域）计，**每周 50 张**。频繁更换新前缀预览会吃额度；**复用同一前缀名即复用旧证书**（约 60 天自动续期）。Caddy 2.x 的 on-demand 已无 `rate_limit` 选项，防刷靠中继的 `ask` 端点（仅活隧道放行）+ 客户端克制换前缀。

### 与 CCGUI 的对接

- CCGUI 设置：开启「公网中继」并填好中继地址后，新增的「隧道管理」页可用；在设置里填「隧道注册密钥」（= 中继的 `TUNNEL_SECRET`）。
- 在隧道管理页新增映射：填「子域名前缀 + 本地端口」，开启后公网地址即 `https://<前缀>.<你的域名>/`。
- 桌面按前缀在**本地映射表**里查端口后才连 `127.0.0.1:<端口>`，绝不使用中继下发的端口（防 localhost 端口越权）。

### 协议帧（桌面 ↔ 中继，供实现/排障参考）

- 控制帧（JSON 文本，桌面→中继）：`{type:'tunnel-register', secret, room, token, tunnels:[{sub, enabled}]}`（全量快照，重发即增删前缀）。
- 控制帧（中继→桌面）：`{type:'tunnel-registered', active:[...], conflicts:[...]}`；`{type:'error', message}`（密钥不符等，随后断开）。
- 数据帧（二进制）：`[type:1][streamId:u32 大端][payload]`。
  - 中继→桌面：`REQ_HEAD 0x01`(JSON{sub,method,url,headers,remoteIp}) / `REQ_BODY 0x02` / `REQ_END 0x03` / `REQ_ABORT 0x04` / `U_OPEN 0x20`(升级连接开始) / `PAUSE 0x30` / `RESUME 0x31`。
  - 桌面→中继：`RES_HEAD 0x10`(JSON{status,statusText,headers}，已去 hop-by-hop、已解 chunked) / `RES_BODY 0x11` / `RES_END 0x12` / `RES_ABORT 0x13`。
  - 双向：`U_DATA 0x22`(升级连接原始字节) / `U_CLOSE 0x23`。
- `/internal/tls-check?domain=` 仅对 loopback Host 应答（Caddy 从 127.0.0.1 发起），活前缀 200、否则 403。

本机可用 `node _smoke.js` 跑一遍隧道链路回归（两前缀不串流 / chunked 重组 / WebSocket 穿透 / tls-check 闸门 / 注册密钥）。
