# WeChat

[简体中文](./README.zh_CN.md)

OpenClaw's WeChat channel plugin, supporting login authorization via QR code scanning.

## Prerequisites

[OpenClaw](https://docs.openclaw.ai/install) must be installed (the `openclaw` CLI needs to be available).

## Quick Install

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

## Manual Installation

If the quick install doesn't work, follow these steps manually:

### 1. Install the plugin

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

### 2. Enable the plugin

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

### 3. QR code login

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

A QR code will appear in the terminal. Scan it with your phone and confirm the authorization. Once confirmed, the login credentials will be saved locally automatically — no further action is needed.

### 4. Restart the gateway

```bash
openclaw gateway restart
```

## Adding More WeChat Accounts

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

Each QR code login creates a new account entry, supporting multiple WeChat accounts online simultaneously.

## Multi-Account Context Isolation

By default, all channels share the same AI conversation context. To isolate conversation context for each WeChat account:

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

This gives each "WeChat account + message sender" combination its own independent AI memory, preventing context cross-talk between accounts.

## Backend API Protocol

This plugin communicates with the backend gateway via HTTP JSON API. Developers integrating with their own backend need to implement the following interfaces.

All endpoints use `POST` with JSON request and response bodies. Common request headers:

| Header              | Description                             |
| ------------------- | --------------------------------------- |
| `Content-Type`      | `application/json`                      |
| `AuthorizationType` | Fixed value `ilink_bot_token`           |
| `Authorization`     | `Bearer <token>` (obtained after login) |
| `X-WECHAT-UIN`      | Base64-encoded random uint32            |

### Endpoint List

| Endpoint     | Path           | Description                              |
| ------------ | -------------- | ---------------------------------------- |
| getUpdates   | `getupdates`   | Long-poll for new messages               |
| sendMessage  | `sendmessage`  | Send a message (text/image/video/file)   |
| getUploadUrl | `getuploadurl` | Get CDN upload pre-signed URL            |
| getConfig    | `getconfig`    | Get account config (typing ticket, etc.) |
| sendTyping   | `sendtyping`   | Send/cancel typing status indicator      |

### getUpdates

Long-polling endpoint. The server responds when new messages arrive or on timeout.

**Request body:**

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

| Field             | Type     | Description                                                                |
| ----------------- | -------- | -------------------------------------------------------------------------- |
| `get_updates_buf` | `string` | Sync cursor from the previous response; empty string for the first request |

**Response body:**

```json
{
  "ret": 0,
  "msgs": [...],
  "get_updates_buf": "<new cursor>",
  "longpolling_timeout_ms": 35000
}
```

| Field                    | Type              | Description                                                  |
| ------------------------ | ----------------- | ------------------------------------------------------------ |
| `ret`                    | `number`          | Return code, `0` = success                                   |
| `errcode`                | `number?`         | Error code (e.g., `-14` = session timeout)                   |
| `errmsg`                 | `string?`         | Error description                                            |
| `msgs`                   | `WeixinMessage[]` | Message list (structure below)                               |
| `get_updates_buf`        | `string`          | New sync cursor to pass in the next request                  |
| `longpolling_timeout_ms` | `number?`         | Server-suggested long-poll timeout for the next request (ms) |

### sendMessage

Send a message to a user.

**Request body:**

```json
{
  "msg": {
    "to_user_id": "<target user ID>",
    "context_token": "<conversation context token>",
    "item_list": [
      {
        "type": 1,
        "text_item": { "text": "Hello" }
      }
    ]
  }
}
```

### getUploadUrl

Get CDN upload pre-signed parameters. Call this endpoint before uploading a file to obtain `upload_param` and `thumb_upload_param`.

**Request body:**

```json
{
  "filekey": "<file identifier>",
  "media_type": 1,
  "to_user_id": "<target user ID>",
  "rawsize": 12345,
  "rawfilemd5": "<plaintext MD5>",
  "filesize": 12352,
  "thumb_rawsize": 1024,
  "thumb_rawfilemd5": "<thumbnail plaintext MD5>",
  "thumb_filesize": 1040
}
```

| Field              | Type      | Description                                          |
| ------------------ | --------- | ---------------------------------------------------- |
| `media_type`       | `number`  | `1` = IMAGE, `2` = VIDEO, `3` = FILE                 |
| `rawsize`          | `number`  | Original file plaintext size                         |
| `rawfilemd5`       | `string`  | Original file plaintext MD5                          |
| `filesize`         | `number`  | Ciphertext size after AES-128-ECB encryption         |
| `thumb_rawsize`    | `number?` | Thumbnail plaintext size (required for IMAGE/VIDEO)  |
| `thumb_rawfilemd5` | `string?` | Thumbnail plaintext MD5 (required for IMAGE/VIDEO)   |
| `thumb_filesize`   | `number?` | Thumbnail ciphertext size (required for IMAGE/VIDEO) |

**Response body:**

```json
{
  "upload_param": "<original image upload encrypted parameters>",
  "thumb_upload_param": "<thumbnail upload encrypted parameters>"
}
```

### getConfig

Get account configuration, including the typing ticket.

**Request body:**

```json
{
  "ilink_user_id": "<user ID>",
  "context_token": "<optional, conversation context token>"
}
```

**Response body:**

```json
{
  "ret": 0,
  "typing_ticket": "<base64-encoded typing ticket>"
}
```

### sendTyping

Send or cancel the typing status indicator.

**Request body:**

```json
{
  "ilink_user_id": "<user ID>",
  "typing_ticket": "<obtained from getConfig>",
  "status": 1
}
```

| Field    | Type     | Description                       |
| -------- | -------- | --------------------------------- |
| `status` | `number` | `1` = typing, `2` = cancel typing |

### Message Structure

#### WeixinMessage

| Field            | Type             | Description                                                   |
| ---------------- | ---------------- | ------------------------------------------------------------- |
| `seq`            | `number?`        | Message sequence number                                       |
| `message_id`     | `number?`        | Unique message ID                                             |
| `from_user_id`   | `string?`        | Sender ID                                                     |
| `to_user_id`     | `string?`        | Receiver ID                                                   |
| `create_time_ms` | `number?`        | Creation timestamp (ms)                                       |
| `session_id`     | `string?`        | Session ID                                                    |
| `message_type`   | `number?`        | `1` = USER, `2` = BOT                                         |
| `message_state`  | `number?`        | `0` = NEW, `1` = GENERATING, `2` = FINISH                     |
| `item_list`      | `MessageItem[]?` | Message content list                                          |
| `context_token`  | `string?`        | Conversation context token, must be passed back when replying |

#### MessageItem

| Field        | Type                | Description                                         |
| ------------ | ------------------- | --------------------------------------------------- |
| `type`       | `number`            | `1` TEXT, `2` IMAGE, `3` VOICE, `4` FILE, `5` VIDEO |
| `text_item`  | `{ text: string }?` | Text content                                        |
| `image_item` | `ImageItem?`        | Image (with CDN reference and AES key)              |
| `voice_item` | `VoiceItem?`        | Voice (SILK encoded)                                |
| `file_item`  | `FileItem?`         | File attachment                                     |
| `video_item` | `VideoItem?`        | Video                                               |
| `ref_msg`    | `RefMessage?`       | Referenced message                                  |

#### CDN Media Reference (CDNMedia)

All media types (image/voice/file/video) are transferred via CDN using AES-128-ECB encryption:

| Field                 | Type      | Description                                  |
| --------------------- | --------- | -------------------------------------------- |
| `encrypt_query_param` | `string?` | Encrypted parameters for CDN download/upload |
| `aes_key`             | `string?` | Base64-encoded AES-128 key                   |

### CDN Upload Flow

1. Calculate the file's plaintext size, MD5, and ciphertext size after AES-128-ECB encryption
2. If a thumbnail is needed (image/video), calculate the thumbnail's plaintext and ciphertext parameters as well
3. Call `getUploadUrl` to get `upload_param` (and `thumb_upload_param`)
4. Encrypt the file content with AES-128-ECB and PUT upload to the CDN URL
5. Encrypt and upload the thumbnail in the same way
6. Use the returned `encrypt_query_param` to construct a `CDNMedia` reference, include it in the `MessageItem`, and send

> For complete type definitions, see [`src/api/types.ts`](src/api/types.ts). For API call implementations, see [`src/api/api.ts`](src/api/api.ts).
