# CCGUI 微信 ClawBot（iLink）对接方案

> 版本：1.0.25 实现完成（2026-06-17）。本文档为已落地实现的存档与维护参考。
> 协议（iLink）为社区逆向、非官方公开接口，字段可能随微信更新变动；本文所有字段均经**真机实测**确认。

---

## 一、概述与定位

通过微信官方 **ClawBot 插件**（底层 iLink 协议，域名 `ilinkai.weixin.qq.com`），把 CCGUI 各 Claude Code 会话的提醒推到微信，并支持从微信引用回复远程遥控会话。

**定位**：多会话异步通知中心 + 轻量遥控（模型 A）。不追求完整终端镜像（那是官方 Remote Control 的事）。

**模型 A 路由**：出站通知必带会话标识 `【#编号 · 项目名 · 标题】`；微信回复按 `#编号` 精确路由到对应会话，不带编号则兜底到"等待最久的待处理会话"。

**已验证能力**：扫码绑定、文字双向收发、会话/队列管理、查看输出、空闲立即发、图片接收（下载+解密+插入终端）。

---

## 二、总体架构与数据流

```
微信手机          ilinkai.weixin.qq.com           CCGUI 本机（Rust，无公网）
  │  你发文字            （官方中转）                    │
  │ ───────────────────────────► hold ◄── getupdates 长轮询 ─┤ wechat/mod.rs poller 线程
  │                                                          │  → emit wechat-inbound
  │                                                          │  → 前端按 #编号路由 → 写 PTY/入队
  │ ◄─── sendmessage（带 context_token）◄────────────────────┤ 出站：hooks→前端→wechat_send_notice
```

- 全程 **ureq 同步 HTTP + std::thread**（与 quota.rs 同栈，无 tokio）。
- **无需公网、不开入站端口**：扫码本地完成，收消息靠长轮询（只出站 HTTPS）。
- 安全模型与官方 Remote Control 一致（只过聊天消息，不传代码文件）。

---

## 三、iLink 协议真实字段（实测）

公共请求头（`wechat/api.rs::with_headers`）：

| Header | 值 |
|---|---|
| `AuthorizationType` | `ilink_bot_token`（固定，未登录也带）|
| `Authorization` | `Bearer <bot_token>`（登录后才带）|
| `X-WECHAT-UIN` | `base64(随机 u32 的十进制字符串)`，**每请求现算**（用 uuid 随机字节派生，免 rand 依赖）|
| `iLink-App-Id` | `bot` |
| `iLink-App-ClientVersion` | `67108867`（不严格校验）|

请求体里 `base_info.channel_version` 给 `"1.0.2"`。基址优先用登录返回的 `baseurl`/`redirect_host`（可能切 IDC）。

### 3.1 get_bot_qrcode — 取登录二维码
`GET /ilink/bot/get_bot_qrcode?bot_type=3`（不带 Authorization）

实测返回：
```json
{
  "qrcode": "f7be4686313bf7deb5af852c476e5633",
  "qrcode_img_content": "https://liteapp.weixin.qq.com/q/7GiQu1?qrcode=<qrcode>&bot_type=3",
  "ret": 0
}
```
- `qrcode`：32 位 hash，**轮询状态用的 token**（不是用来编码二维码的）。
- `qrcode_img_content`：**一个微信深链 URL（不是 base64 图片！）**——这是踩坑点。二维码要编码的就是这个 URL。
- **处理**：把 `qrcode_img_content`（URL）本地用 `qrcode` crate 编码成 PNG（`api::qr_to_data_url` / `qr_png_data_url`），转 `data:image/png;base64,...` 给前端 `<img>` 显示。兼容前缀 `data:`（直接用）/ `iVBOR`（base64 PNG，补前缀）。

### 3.2 get_qrcode_status — 轮询扫码状态（长轮询）
`GET /ilink/bot/get_qrcode_status?qrcode=<token>`（不带 Authorization）

- **是长轮询**（hold 住等扫码事件，实测 15s 都不返回）。客户端读超时须 ≥45s（本实现 60s）。
- 返回字段：`status`（`wait`/`scaned`/`confirmed`/`expired`/`scaned_but_redirect` 等）、`bot_token`（confirmed 时返回）、`baseurl`/`base_url`、`ilink_user_id`/`ilink_bot_id`、`redirect_host`、`nickname`/`nick`。
- 判定登录成功：`status` 含 `confirm` 或 `bot_token` 非空。
- `expired` → 重拉二维码（限 3 次）；`redirect_host` 非空 → 后续基址切到 `https://<redirect_host>`。

### 3.3 getupdates — 长轮询收消息
`POST /ilink/bot/getupdates`（带 Authorization）
```json
{ "get_updates_buf": "<游标，首次空串>", "base_info": { "channel_version": "1.0.2" } }
```
- 服务端最长 hold ~35s（`longpolling_timeout_ms: 35000`）。**客户端读超时须 ≥45s（本实现 60s）**，否则每次都超时。
- 响应 `get_updates_buf`（新游标）+ `msgs[]`。**游标必须覆盖并随 token 持久化续传**，否则重复收消息（唯一去重手段）。
- 每条消息：`from_user_id`（`xxx@im.wechat`，回复时即出站 `to_user_id`）、`context_token`（回复必带）、`message_type`（**只处理 ==1** 用户消息）、`item_list[]`。
  - 文本 item：`type:1`，文本在 `text_item.text`。
  - 图片 item：`type:2`，见 §3.5。

### 3.4 sendmessage — 主动发/回复
`POST /ilink/bot/sendmessage`（带 Authorization）
```json
{
  "msg": {
    "from_user_id": "",
    "to_user_id": "<入站消息的 from_user_id>",
    "client_id": "ccgui-<uuid>",
    "message_type": 2, "message_state": 2,
    "context_token": "<来自入站消息，必填>",
    "item_list": [{ "type": 1, "text_item": { "text": "..." } }]
  },
  "base_info": { "channel_version": "1.0.2" }
}
```
- `context_token` 必填，从该用户最近一条入站消息取（进程内 `user_id -> context_token` 缓存）。
- 返回 `ret`/`errcode`，非 0 即失败。长文按 ~3500 字符分片多次发。
- **不能凭空主动推陌生人**：必须用户先给 bot 发过消息（才有 context_token）。**进程重启后 context_token 在内存丢失，需用户再发一条激活**。

### 3.5 图片消息结构与解密（实测验证）
图片 item（`item_list[].type == 2`）：
```json
{
  "type": 2,
  "image_item": {
    "aeskey": "61bd3af30e4f40fd721052418e3a18ec",      // 32位hex = 16字节 AES-128 密钥
    "hd_size": 1217,                                     // 明文字节数（解密后应吻合）
    "media": {
      "aes_key": "<base64(上面的hex字符串)>",            // 同一把钥匙的另一种编码
      "encrypt_query_param": "...",
      "full_url": "https://novac2c.cdn.weixin.qq.com/c2c/download?encrypted_query_param=...&taskid=..."
    }
  }
}
```
**解密配方（已实测）**：
1. `GET full_url` 下载密文（无需特殊头）。
2. key = **hex 解码 `aeskey`**（16 字节）。
3. **AES-128-ECB + PKCS7** 解密 → 明文即原图。
4. 验证：解密后长度应 == `hd_size`；按魔数判后缀（PNG `89 50 4E 47` / JPEG `FF D8 FF` / GIF `GIF8` / WebP `RIFF`）。
- 实现：`api::download_image`（ureq 下载 + `aes`/`ecb`/`cipher` crate 解密）。落盘 `%TEMP%/ccgui/wx-<ms>.<ext>`，前端把带引号路径插入会话输入框（同本地拖拽图片）。

---

## 四、后端模块（src-tauri/src/wechat/）

- **store.rs**：凭据 `Creds{token, base_url, account_id, nick, cursor, operator}` 落盘 `%APPDATA%/ccgui/wechat.json`（与只读 `.credentials.json` 分开；bot_token 是遥控密钥，单独文件便于解绑）。
- **api.rs**：协议层（四端点 + 二维码 PNG 生成 + 图片下载解密）。集中所有协议细节，失效时单点适配。
- **mod.rs**：
  - `WechatState`（Tauri 托管）：creds、`ctx_tokens` map、login 状态、`generation`（代际，启停/解绑递增，旧线程检测到变化即退出）、`quota_alerted`。
  - 三个 std 线程：扫码登录轮询、长轮询收消息（emit `wechat-inbound`）、额度告警（每 5 分钟）。
  - 命令（网络型标 `#[tauri::command(async)]`）：`wechat_status` / `wechat_start_login`（返回二维码 data URL）/ `wechat_unbind` / `wechat_send_notice`。
- **lib.rs**：`update_settings` 响应 `wechat_enabled` 翻转启停（仿 `index_enabled`）；启动时若已开启+有凭据则自启；`last_assistant` 命令（取会话最近回复，供"查看输出"）。

---

## 五、前端集成（src/）

- **types.ts**：`TerminalTab.wxLabel`（会话编号）；`Settings` 加 5 个微信字段；`WechatStatus` / `WechatInbound`（含 `imagePath`）。
- **ipc.ts**：`wechatStatus/wechatStartLogin/wechatUnbind/wechatSendNotice/lastAssistant` + `onWechatInbound`。
- **App.tsx**：
  - `wxSeqRef` 单调分配 `wxLabel`；`configRef` 避免监听器闭包陈旧。
  - `openTab` 初始状态 **`'idle'`**（关键修复，见 §七）。
  - `onSessionStatus` 出站推送（attention/done/额度，复用 attention 过滤）；done 摘要改用 `lastAssistant`（落盘后取，见 §七）。
  - `onWechatInbound`：图片→暂存(`wxPendingImgRef`)与下一条文字原子提交；文本→粘性当前会话路由(`wxCurrentRef`) + 队列管理 + 帮助/会话/项目/查看输出 + 屏幕/方向键，空闲直发、忙碌入队。
- **SettingsPage.tsx**：微信分组（开关、扫码二维码、1.5s 轮询绑定状态、解绑、遥控/推送开关）。

---

## 六、微信遥控指令文法（onWechatInbound）

每条指令作用于某会话。`#编号` 来自通知标题。**无编号消息默认发给"当前会话"（粘性）**，见下方寻址机制。

| 发送 | 作用 |
|---|---|
| `帮助` / `help` / `指令` / `命令` / `菜单` | 回指令速查（全局）|
| `项目` / `projects` | 列所有项目 `P编号 名称（运行中N）`（全局）|
| `P1 会话` / `项目名 会话` | 列该项目：运行中（`#编号`）+ 历史最近 5 条（`H编号`）|
| `H2 恢复` / `H2 恢复 plan` / `自动编辑` / `绕过` | 恢复启动历史会话，可指定权限模式 |
| `P1 新会话`（可加 `plan`/`自动编辑`/`绕过`）| 在该项目起新会话 |
| `会话` / `列表` / `sessions` / `ls` | 列出所有运行中会话（全局）|
| `#N`（单独发）| **切换当前会话为 #N**；之后无编号消息/图片都发它（15 分钟有效）|
| 普通文本（无编号）| 发给**当前会话**：忙则入队、空闲立即发；附回执 |
| `y`/`是`/`允许`、`n`/`否`/`拒绝`、`1`~`2位数字` | 按键+回车，答权限框/选菜单项（不入队）|
| `回车`/`确定`/`确认` | 发回车确认高亮项 |
| `上`/`下`/`左`/`右`（或 up/down/...）| 发方向键移动菜单高亮（不加回车）|
| `#N 屏幕` / `看屏` | 抓该会话终端底部 20 行（看清选项/菜单）|
| `#N 输出` / `看` / `回复` | 取该会话最近一条**完整**回复（长则分条发）|
| `队列` / `?` ; `撤回` ; `删 K` ; `清空` | 队列管理 |
| `停止` / `中断` / `stop` | 发 Esc 中断会话 |
| 发图片 | 下载解密后**暂存**给当前会话（不进输入框）；下一条文字会前置图片路径**合并成一条原子提交**（空闲立即发、忙碌整条入队）。`#N 发图`=单独发、`#N 清图`=取消；暂存超 `WX_IMG_TTL`(10min) 过期 |

**寻址机制（粘性当前会话）**：
- `#编号` = 运行中会话的 `wxLabel`；`P编号` = `config.projects` 顺序（稳定）；`H编号` = 列项目会话时对历史会话的临时映射（`wxHistRef`，每次列表刷新）。
- **当前会话（`wxCurrentRef`）**：任何 `#N` 寻址（含单独发 `#N`）或命中当前会话都会**设/刷新**它，带时间戳，超 **15 分钟（`WX_CURRENT_TTL`）** 无操作即失效。
- 无编号消息路由：先判按键类（y/n/数字/方向/回车）——**恰好一个会话待权限**时优先答它（不改当前会话）；否则 → 当前会话（有效则刷新）→ 等待最久的待处理 → 聚焦 → 最近。失效/无目标则提示带编号。
- `P/H 会话`、`新会话` 仅当能解析到真实项目时才接管，否则按普通文本处理（避免劫持正常提示）。
- 选项/菜单类交互（结构化优先 + 屏幕兜底）：iLink 出站**不支持按钮/卡片**（已查证：协议文档/corespeed SDK/x1ah/yarrow 四源一致，只 5 种 item），故选项走文本。
  - **方向二（结构化）**：attention 时（延迟 600ms）调 `last_tool_use` 读 transcript 最近一个待处理 tool_use——`AskUserQuestion` → 解析问题+选项渲染干净编号列表（`formatToolPrompt`，存 `wxPromptRef`）；其它工具 → 渲染"Claude 要执行【工具】：摘要 + 1/2/3"（`permissionSummary`）。回数字即选：权限框数字/`y`/`n` 直发；AskUserQuestion 单题单选把"回 N"翻译成 `下×(N-1)+回车`、多选翻译成"逐个 下到位+空格、末尾回车"（`sendKeys`，间隔 90ms）。
  - **方向三（屏幕兜底）**：识别不了的非工具菜单（`/model` 选择器等）退回 `readTerminalScreen` 底部 20 行；`#N 屏幕` 按需看。
  - **手动键**：`tokenizeKeys` 支持一条消息连发多个按键词（`下下回车`/`下 下 回车`/`上回车`），翻译成 `\x1b[A/B/C/D`、`\r`、空格。
  - 限制：AskUserQuestion 的"数字→按键"翻译依赖 TUI 导航模型（默认高亮在首项、Space 切多选、Enter 确认），**多问题/多选的自动翻译需真机验证**，未验证前多问题用手动键兜底。
- `权限模式` 词映射：`plan/计划`→plan，`自动编辑/接受编辑`→acceptEdits，`绕过/危险`→bypassPermissions。

安全：仅当「微信对接」+「允许微信遥控」都开启才接受入站；远程写入不绕过 Claude Code 自身权限确认（仍会弹框→再推回微信答 y/n）。**远程启动会话**=远程拉起一个能跑命令的 Claude Code 进程，比发文字风险高；其中 `绕过`(bypassPermissions) 会让该会话无人值守地自动执行——已按用户决议**开放**给微信，使用者需自行掂量风险。远程启动会切 PC 端视图到该会话（`openTab` 行为）。

---

## 七、关键修复与踩坑记录（重要）

1. **二维码裂图**：`qrcode_img_content` 是深链 URL 不是 base64 图——必须本地编码成 QR PNG（不要当 base64 拼 data URL）。
2. **登录状态长轮询**：`get_qrcode_status` 会 hold；读超时须 ≥45s；登录轮询线程遇偶发超时不要直接退出（连续 5 次才放弃）。
3. **空闲会话消息卡队列**：`openTab` 初始状态原为 `'working'`，而刚打开的会话不触发 Stop，永远停在假"忙碌"→微信消息入队后等不到回合结束。**改初始为 `'idle'`**，真正忙碌由 `UserPromptSubmit` hook 置 working。
4. **完成摘要只推前半段**：hook 的 `last_assistant_info`（120 字）在 Stop 那刻读 transcript，末段文本块可能尚未落盘 → 只抓到前面的块。**改为回合结束等 600ms 后用 `lastAssistant`（128KB 尾部、≤500 字）重取**。
5. **进程重启后无法主动推送**：context_token 仅在内存，重启丢失，需用户再发一条消息激活（协议限制，UI 已提示）。
6. **队列发送依赖 status 不可靠 → 改"就绪泵"**：原先靠 `status==='idle'` 决定立即发、靠 `Stop` hook 驱动队列自动发，有两个坑——① 刚 Stop 后 TUI 未回到输入态就立即写，回车被吞，文字卡在输入框（"已发送"假象）；② 按 ESC 中断**不触发 Stop**，status 卡 `working`，入队的指令永远等不到回合结束。**改为统一"就绪泵"**（`lastDataRef` 记 PTY 最后输出时间 + `readyToSend` + 每 600ms 的 `setInterval`）：会话 PTY 静默够久才发队首（idle>0.8s 含 settle；working 但静默>2.5s 视为已停/ESC，也发；attention 不发先答权限）。微信入站文字/图片一律入队、由泵逐回合推进；移除了 Stop 专门发送。副作用利好：不装 hooks 也能靠纯静默信号工作（openTab 初始 idle）。

---

## 八、配置项（store.rs Settings，camelCase 同步到 types.ts）

| 字段 | 默认 | 说明 |
|---|---|---|
| `wechat_enabled` | false | 总开关（翻转启停长轮询）|
| `wechat_remote_input` | true | 允许入站遥控（关则只收通知）|
| `wechat_push_attention` | true | 推送：等待权限确认 |
| `wechat_push_done` | true | 推送：回合完成摘要 |
| `wechat_push_quota` | true | 推送：额度告警 |

---

## 九、风险与维护提示

- iLink 非官方接口，字段/版本可能变；维护时优先核对 `api.rs` 的字段解析与本文 §三的实测样本。
- 单 bot_token 绑单微信；仅私聊；ClawBot 插件灰度放量、需较新微信版本。
- 调试抓取：含非文本 item 的入站消息原始 JSON 落盘 `%TEMP%/ccgui/wechat-debug.jsonl`，逆向新媒体类型时看它。
- 依赖：新增 `qrcode`（二维码）、`aes`/`ecb`/`cipher`（图片解密）。

---

## 十、构建与版本

- 版本号三处同步：`package.json` / `src-tauri/tauri.conf.json` / `src-tauri/Cargo.toml`。
- 打包：`npm run tauri build` → `src-tauri/target/release/bundle/nsis/CCGUI_<版本>_x64-setup.exe`。
- 校验：前端 `tsc --noEmit`，后端 `cargo check`。
- 当前 1.0.25。
