# 微信

[English](./README.md)

OpenClaw 的微信渠道插件，支持通过扫码完成登录授权。

## 前提条件

已安装 [OpenClaw](https://docs.openclaw.ai/install)（需要 `openclaw` CLI 可用）。

## 一键安装

```bash
npx -y @tencent-weixin/openclaw-weixin-cli install
```

## 手动安装

如果一键安装不适用，可以按以下步骤手动操作：

### 1. 安装插件

```bash
openclaw plugins install "@tencent-weixin/openclaw-weixin"
```

### 2. 启用插件

```bash
openclaw config set plugins.entries.openclaw-weixin.enabled true
```

### 3. 扫码登录

```bash
openclaw channels login --channel openclaw-weixin
```

终端会显示一个二维码，用手机扫码并在手机上确认授权。确认后，登录凭证会自动保存到本地，无需额外操作。

### 4. 重启 gateway

```bash
openclaw gateway restart
```

## 添加更多微信账号

```bash
openclaw channels login --channel openclaw-weixin
```

每次扫码登录都会创建一个新的账号条目，支持多个微信号同时在线。

## 多账号上下文隔离

默认情况下，所有渠道的 AI 会话共享同一个上下文。如果希望每个微信账号的对话上下文相互隔离：

```bash
openclaw config set agents.mode per-channel-per-peer
```

这样每个「微信账号 + 发消息用户」组合都会拥有独立的 AI 记忆，账号之间不会串台。

## 后端 API 协议

本插件通过 HTTP JSON API 与后端网关通信。二次开发者若需对接自有后端，需实现以下接口。

所有接口均为 `POST`，请求和响应均为 JSON。通用请求头：

| Header              | 说明                           |
| ------------------- | ------------------------------ |
| `Content-Type`      | `application/json`             |
| `AuthorizationType` | 固定值 `ilink_bot_token`       |
| `Authorization`     | `Bearer <token>`（登录后获取） |
| `X-WECHAT-UIN`      | 随机 uint32 的 base64 编码     |

### 接口列表

| 接口         | 路径           | 说明                             |
| ------------ | -------------- | -------------------------------- |
| getUpdates   | `getupdates`   | 长轮询获取新消息                 |
| sendMessage  | `sendmessage`  | 发送消息（文本/图片/视频/文件）  |
| getUploadUrl | `getuploadurl` | 获取 CDN 上传预签名 URL          |
| getConfig    | `getconfig`    | 获取账号配置（typing ticket 等） |
| sendTyping   | `sendtyping`   | 发送/取消输入状态指示            |

### getUpdates

长轮询接口。服务端在有新消息或超时后返回。

**请求体：**

```json
{
  "get_updates_buf": ""
}
```

| 字段              | 类型     | 说明                                       |
| ----------------- | -------- | ------------------------------------------ |
| `get_updates_buf` | `string` | 上次响应返回的同步游标，首次请求传空字符串 |

**响应体：**

```json
{
  "ret": 0,
  "msgs": [...],
  "get_updates_buf": "<新游标>",
  "longpolling_timeout_ms": 35000
}
```

| 字段                     | 类型              | 说明                             |
| ------------------------ | ----------------- | -------------------------------- |
| `ret`                    | `number`          | 返回码，`0` = 成功               |
| `errcode`                | `number?`         | 错误码（如 `-14` = 会话超时）    |
| `errmsg`                 | `string?`         | 错误描述                         |
| `msgs`                   | `WeixinMessage[]` | 消息列表（结构见下方）           |
| `get_updates_buf`        | `string`          | 新的同步游标，下次请求时回传     |
| `longpolling_timeout_ms` | `number?`         | 服务端建议的下次长轮询超时（ms） |

### sendMessage

发送一条消息给用户。

**请求体：**

```json
{
  "msg": {
    "to_user_id": "<目标用户 ID>",
    "context_token": "<会话上下文令牌>",
    "item_list": [
      {
        "type": 1,
        "text_item": { "text": "你好" }
      }
    ]
  }
}
```

### getUploadUrl

获取 CDN 上传预签名参数。上传文件前需先调用此接口获取 `upload_param` 和 `thumb_upload_param`。

**请求体：**

```json
{
  "filekey": "<文件标识>",
  "media_type": 1,
  "to_user_id": "<目标用户 ID>",
  "rawsize": 12345,
  "rawfilemd5": "<明文 MD5>",
  "filesize": 12352,
  "thumb_rawsize": 1024,
  "thumb_rawfilemd5": "<缩略图明文 MD5>",
  "thumb_filesize": 1040
}
```

| 字段               | 类型      | 说明                                 |
| ------------------ | --------- | ------------------------------------ |
| `media_type`       | `number`  | `1` = IMAGE, `2` = VIDEO, `3` = FILE |
| `rawsize`          | `number`  | 原文件明文大小                       |
| `rawfilemd5`       | `string`  | 原文件明文 MD5                       |
| `filesize`         | `number`  | AES-128-ECB 加密后的密文大小         |
| `thumb_rawsize`    | `number?` | 缩略图明文大小（IMAGE/VIDEO 时必填） |
| `thumb_rawfilemd5` | `string?` | 缩略图明文 MD5（IMAGE/VIDEO 时必填） |
| `thumb_filesize`   | `number?` | 缩略图密文大小（IMAGE/VIDEO 时必填） |

**响应体：**

```json
{
  "upload_param": "<原图上传加密参数>",
  "thumb_upload_param": "<缩略图上传加密参数>"
}
```

### getConfig

获取账号配置，包括 typing ticket。

**请求体：**

```json
{
  "ilink_user_id": "<用户 ID>",
  "context_token": "<可选，会话上下文令牌>"
}
```

**响应体：**

```json
{
  "ret": 0,
  "typing_ticket": "<base64 编码的 typing ticket>"
}
```

### sendTyping

发送或取消输入状态指示。

**请求体：**

```json
{
  "ilink_user_id": "<用户 ID>",
  "typing_ticket": "<从 getConfig 获取>",
  "status": 1
}
```

| 字段     | 类型     | 说明                           |
| -------- | -------- | ------------------------------ |
| `status` | `number` | `1` = 正在输入，`2` = 取消输入 |

### 消息结构

#### WeixinMessage

| 字段             | 类型             | 说明                                      |
| ---------------- | ---------------- | ----------------------------------------- |
| `seq`            | `number?`        | 消息序列号                                |
| `message_id`     | `number?`        | 消息唯一 ID                               |
| `from_user_id`   | `string?`        | 发送者 ID                                 |
| `to_user_id`     | `string?`        | 接收者 ID                                 |
| `create_time_ms` | `number?`        | 创建时间戳（ms）                          |
| `session_id`     | `string?`        | 会话 ID                                   |
| `message_type`   | `number?`        | `1` = USER, `2` = BOT                     |
| `message_state`  | `number?`        | `0` = NEW, `1` = GENERATING, `2` = FINISH |
| `item_list`      | `MessageItem[]?` | 消息内容列表                              |
| `context_token`  | `string?`        | 会话上下文令牌，回复时需回传              |

#### MessageItem

| 字段         | 类型                | 说明                                                |
| ------------ | ------------------- | --------------------------------------------------- |
| `type`       | `number`            | `1` TEXT, `2` IMAGE, `3` VOICE, `4` FILE, `5` VIDEO |
| `text_item`  | `{ text: string }?` | 文本内容                                            |
| `image_item` | `ImageItem?`        | 图片（含 CDN 引用和 AES 密钥）                      |
| `voice_item` | `VoiceItem?`        | 语音（SILK 编码）                                   |
| `file_item`  | `FileItem?`         | 文件附件                                            |
| `video_item` | `VideoItem?`        | 视频                                                |
| `ref_msg`    | `RefMessage?`       | 引用消息                                            |

#### CDN 媒体引用 (CDNMedia)

所有媒体类型（图片/语音/文件/视频）通过 CDN 传输，使用 AES-128-ECB 加密：

| 字段                  | 类型      | 说明                       |
| --------------------- | --------- | -------------------------- |
| `encrypt_query_param` | `string?` | CDN 下载/上传的加密参数    |
| `aes_key`             | `string?` | base64 编码的 AES-128 密钥 |

### CDN 上传流程

1. 计算文件明文大小、MD5，以及 AES-128-ECB 加密后的密文大小
2. 如需缩略图（图片/视频），同样计算缩略图的明文和密文参数
3. 调用 `getUploadUrl` 获取 `upload_param`（和 `thumb_upload_param`）
4. 使用 AES-128-ECB 加密文件内容，PUT 上传到 CDN URL
5. 缩略图同理加密并上传
6. 使用返回的 `encrypt_query_param` 构造 `CDNMedia` 引用，放入 `MessageItem` 发送

> 完整的类型定义见 [`src/api/types.ts`](src/api/types.ts)，API 调用实现见 [`src/api/api.ts`](src/api/api.ts)。
