# 🤖 OpenClaw 企业微信插件（支持直接加到微信）

> ⭐ 如果觉得有用，请点击右上角的 **Star** 支持一下！

### 🔍 项目概述

**openclaw-wechat** 是一个 [OpenClaw](https://openclaw.ai)（原 ClawdBot/Moltbot）的 <img src="docs/images/wecom-icon.png" width="16" height="16"> **企业微信渠道插件**，让你的 AI 智能体通过企业微信自建应用与用户对话。接入企业微信后，<img src="docs/images/wechat-icon.png" width="16" height="16"> **个人微信用户也可以直接对话**——只需在企业微信管理后台「我的企业 → 微信插件」中扫码关联即可。

> 🍴 本项目 fork 自 [dingxiang-me/OpenClaw-Wechat](https://github.com/dingxiang-me/OpenClaw-Wechat)（v0.1.0，作者：勾勾的数字生命），并进行了大量功能扩展以兼容新版 OpenClaw。

### 🧠 v0.3.7 — OpenClaw 2026.3+ 兼容修复

**修复了 OpenClaw 2026.3+ 中企业微信“能发不能收”的典型故障**，核心兼容点包括：
- ✅ 补齐 `status` 运行态信息，避免 health-monitor 持续误判渠道已停止
- ✅ `gateway.startAccount` 改为常驻直到 `abortSignal`，不再反复 auto-restart
- ✅ `registerHttpRoute()` 显式增加 `auth: "plugin"` 和 `match: "exact"`
- ✅ `/wecom/callback` 不再被 Control UI 的 SPA fallback 覆盖，健康检查稳定返回 `wecom webhook ok`
- ✅ 实战验证：错误绑定 `AgentId` 也会导致“能发不能收”，现在 README 已加入对齐检查

### 🧠 v0.3.6 — 对话记忆系统（与官方 Telegram Channel 实现一致）

**解决了企业微信渠道"失忆"问题**：之前每条消息都是独立对话，AI 无法记住上下文。

现在使用 OpenClaw SDK 的 `recordPendingHistoryEntry` + `buildPendingHistoryContextFromMap` 实现对话历史管理，**与官方 Telegram/Discord 渠道完全一致**：
- 📝 自动记录用户消息和 AI 回复到内存历史
- 🔄 每次对话自动携带最近 20 条历史上下文
- 🗑️ `/clear` 命令同时清除 session 和内存历史
- 📊 `/status` 显示当前历史消息数量

### ✨ 与上游的主要区别

| 特性 | 上游 (OpenClaw-Wechat v0.1.0) | 本 Fork (v0.3.7) |
|------|------|------|
| 🎯 平台兼容 | ClawdBot | OpenClaw（同时保留 ClawdBot 兼容） |
| 📄 插件描述文件 | `clawdbot.plugin.json` | `openclaw.plugin.json` + `clawdbot.plugin.json` |
| ⚙️ 配置文件 | `~/.clawdbot/clawdbot.json` | `~/.openclaw/openclaw.json` |
| 📨 消息类型 | 文本、图片、语音 | 文本、图片、语音、**视频**、**文件**、**链接** |
| 📤 发送类型 | 仅文本 | 文本、**图片**、**视频**、**文件**（自动类型识别） |
| 🎙️ 语音识别 | 仅企业微信自带 | 企业微信自带 + **远程 Qwen3-ASR** / 本地 FunASR（自动降级） |
| 🧠 对话历史 | 无 | **SDK 级对话记忆（与官方 Telegram 一致）** |
| 🖥️ Chat UI | 无 | **消息同步到 Transcript + 实时广播** |
| 🌐 代理支持 | 无 | **WECOM_PROXY 环境变量** |
| 📝 消息分段 | 按字符 | **按字节（UTF-8），二分查找分割** |

### 📋 功能特性

#### 🔌 核心功能
- [x] ✅ 支持个人微信对话（通过企业微信桥接）
- [x] ✅ 接收/发送企业微信消息
- [x] ✅ 自动调用 AI 代理处理消息
- [x] ✅ 消息签名验证（SHA1）和 AES-256-CBC 加解密
- [x] ✅ Webhook URL 验证
- [x] ✅ OpenClaw 2026.3+ 渠道生命周期兼容
- [x] ✅ access_token 自动缓存和刷新

#### 🎬 媒体功能
- [x] 🖼️ 图片消息收发 + AI Vision 识别
- [x] 🎙️ 语音消息转文字（企业微信自带 + 远程 Qwen3-ASR + 本地 FunASR 降级）
- [x] 📹 视频消息接收、下载、发送
- [x] 📎 文件消息接收（支持 .txt/.md/.json/.pdf 等自动读取）
- [x] 🔗 链接分享消息接收

#### 🎨 用户体验
- [x] 📝 命令系统（`/help`、`/status`、`/clear`）
- [x] 🔄 Markdown → 纯文本自动转换（企业微信不支持 Markdown 渲染）
- [x] ✂️ 长消息自动分段（2048 字节限制，按 UTF-8 字节精确分割）
- [x] 🧠 对话历史记忆（SDK 级，与官方 Telegram 一致）
- [x] 🛡️ API 限流保护（10 并发，100ms 间隔）
- [x] ⏳ 处理中提示（"收到您的消息，正在处理中..."）

#### 🚀 高级功能
- [x] 👥 多账户支持（`WECOM_<ACCOUNT>_*` 格式）
- [x] 🔒 Token 并发安全（Promise 锁）
- [x] 🖥️ Chat UI 集成（Transcript 写入 + Gateway 实时广播）
- [x] 🌐 HTTP 代理支持（`WECOM_PROXY`）
- [x] 🩺 渠道状态探测（status/probe，兼容新版 OpenClaw health monitor）

### 🆕 OpenClaw 2026.3+ 兼容说明

OpenClaw 2026.3+ 对渠道插件的要求比旧版本更严格。企业微信插件如果只保留旧版写法，常见症状是：

- 能主动发送消息
- `/wecom/callback` 返回的是 Control UI HTML，而不是 `wecom webhook ok`
- `health-monitor` 日志里反复出现 `restarting (reason: stopped)`
- 企业微信后台显示消息发送成功，但 OpenClaw 没有任何 `wecom inbound` 日志

本插件从 `v0.3.7` 起已补齐以下兼容点：

- `status`：提供运行态摘要、账户快照、探测结果
- `gateway.startAccount`：保持常驻直到 `abortSignal`
- `api.registerHttpRoute()`：显式使用 `auth: "plugin"` 和 `match: "exact"`

如果你升级到 OpenClaw `2026.3.x` 或更高版本，建议至少验证这三项：

```bash
openclaw channels status --probe
curl https://你的域名/wecom/callback
openclaw logs -f | grep wecom
```

期望结果：

- 渠道状态显示 `running`
- callback 返回 `wecom webhook ok`
- 收到消息时能看到 `wecom inbound:`、`session registered`、`sent AI reply`

### 📊 支持的消息类型

| 类型 | 接收 | 发送 | 说明 |
|:----:|:----:|:----:|------|
| 📝 文本 | ✅ | ✅ | 完全支持，超长消息自动按字节分段 |
| 🖼️ 图片 | ✅ | ✅ | 支持 AI Vision 识别，下载后保存到临时文件 |
| 🎙️ 语音 | ✅ | ❌ | 企业微信自带识别 + 远程 Qwen3-ASR / 本地 FunASR 降级（AMR→WAV→文本） |
| 📹 视频 | ✅ | ✅ | 自动下载保存，支持发送视频消息 |
| 📎 文件 | ✅ | ✅ | 自动下载，可读类型自动交给 AI 分析 |

### 📦 前置要求

- [OpenClaw](https://openclaw.ai) 已安装并正常运行（`openclaw doctor` 通过）
- Node.js 环境（npm 可用）
- 企业微信管理员权限
- 公网可访问的服务器或隧道（用于接收企业微信回调）
- （可选）FFmpeg + 语音转文字后端（二选一）：
  - **远程 Qwen3-ASR**（推荐）：需要 GPU 服务器运行 `asr_server.py`，详见 [`asr_api_README.md`](asr_api_README.md)
  - **本地 FunASR**：Python 3 + [FunASR](https://github.com/modelscope/FunASR) + PyTorch（支持 CUDA / Apple MPS / CPU）

### 🛠️ 安装

#### 方式一：CLI 安装

```bash
openclaw plugin install --path /path/to/openclaw-wechat
```

#### 方式二：手动安装

1. 克隆本仓库：

```bash
git clone https://github.com/Xueheng-Li/openclaw-wechat.git
cd openclaw-wechat
npm install
```

2. 在 OpenClaw 配置文件 `~/.openclaw/openclaw.json` 中注册插件：

```json
{
  "plugins": {
    "load": {
      "paths": [
        "/path/to/openclaw-wechat"
      ]
    },
    "entries": {
      "wecom": {
        "enabled": true
      }
    }
  }
}
```

> 💡 **注意**：插件 ID 为 `wecom`（与渠道名一致）。

### ⚙️ 配置（详细步骤）

#### 第一步：创建企业微信自建应用 🏢

1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)
2. 进入 **应用管理** → **自建** → **创建应用**
3. 填写应用名称、Logo、可见范围等信息
4. 创建完成后，记录：
   - **AgentId**：应用的 AgentId
   - **Secret**：应用的 Secret

#### 第二步：获取企业信息 🆔

1. 在管理后台首页，点击 **我的企业**
2. 记录 **企业ID (CorpId)**

#### 第三步：配置接收消息 📨

1. 进入你创建的应用 → **接收消息** → **设置API接收**
2. 填写：
   - **URL**：`https://你的域名/wecom/callback`
   - **Token**：自定义一个 Token（随机字符串）
   - **EncodingAESKey**：点击随机生成
3. ⚠️ **先不要保存！** 需要先完成后续步骤启动 OpenClaw 服务

> ⚠️ **务必记住这组配置与具体应用绑定**：`AgentId`、`Secret`、`Token`、`EncodingAESKey` 是针对某一个企业微信自建应用的，不能和另一套应用参数混用。混用时常见现象是“OpenClaw 可以发消息，但收不到你从企业微信发回来的消息”。

#### 第四步：配置环境变量 🔑

在 `~/.openclaw/openclaw.json` 中添加环境变量：

```json
{
  "env": {
    "vars": {
      "WECOM_CORP_ID": "你的企业ID",
      "WECOM_CORP_SECRET": "你的应用Secret",
      "WECOM_AGENT_ID": "你的应用AgentId",
      "WECOM_CALLBACK_TOKEN": "你设置的Token",
      "WECOM_CALLBACK_AES_KEY": "你生成的EncodingAESKey",
      "WECOM_WEBHOOK_PATH": "/wecom/callback",
      "WECOM_PROXY": ""
    }
  }
}
```

##### 多账户配置

支持配置多个企业微信账户，使用 `WECOM_<ACCOUNT>_*` 格式：

```json
{
  "env": {
    "vars": {
      "WECOM_CORP_ID": "默认账户企业ID",
      "WECOM_CORP_SECRET": "默认账户Secret",
      "WECOM_AGENT_ID": "默认账户AgentId",
      "WECOM_CALLBACK_TOKEN": "默认账户Token",
      "WECOM_CALLBACK_AES_KEY": "默认账户AESKey",

      "WECOM_SALES_CORP_ID": "销售账户企业ID",
      "WECOM_SALES_CORP_SECRET": "销售账户Secret",
      "WECOM_SALES_AGENT_ID": "销售账户AgentId",
      "WECOM_SALES_CALLBACK_TOKEN": "销售账户Token",
      "WECOM_SALES_CALLBACK_AES_KEY": "销售账户AESKey"
    }
  }
}
```

#### 第五步：配置公网访问 🔗

企业微信需要能够访问你的回调 URL。推荐使用 Cloudflare Tunnel：

```bash
# 安装 cloudflared
brew install cloudflared   # macOS
# 或 apt install cloudflared  # Linux

# 创建隧道
cloudflared tunnel create openclaw

# 配置隧道路由
cloudflared tunnel route dns openclaw 你的域名

# 启动隧道（将流量转发到本地 Gateway 端口）
cloudflared tunnel --url http://localhost:18789 run openclaw
```

其他方案：SSH 隧道、Tailscale、Nginx 反向代理 + 端口转发等。

#### 第六步：配置企业可信 IP 🛡️

企业微信要求调用 API（发送消息、获取 token 等）的服务器 IP 在白名单中。

1. 查询你服务器的**出口公网 IP**：

```bash
curl -s https://ifconfig.me
```

> ⚠️ 注意：这里需要的是你服务器**发出请求时的 IP**（出口 IP），不是 Cloudflare Tunnel 的 IP。Cloudflare Tunnel 只处理入站流量，服务器调用企业微信 API 时仍然走自己的公网出口。

2. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)
3. 进入 **应用管理** → 选择你创建的自建应用
4. 滚动到页面底部，找到 **企业可信 IP**，点击 **配置**：

   ![企业可信 IP 位置](docs/images/ip-whitelist.png)

5. 添加上一步查到的 IP 地址

> ⚠️ **IP 白名单是按应用隔离的**：每个自建应用（AgentId）有独立的「企业可信 IP」列表。为应用 A 添加的 IP **不会**自动对应用 B 生效。创建新应用后，必须单独为该应用添加可信 IP，否则会报 `60020` 错误。
>
> 💡 如果调用 API 报错 `60020`（not allow to access from your ip），检查日志中提示的 IP 并添加到**当前应用**的白名单即可。

**动态 IP 场景（家用宽带 / 无固定公网 IP 的服务器）**：

如果你的服务器没有固定公网 IP，推荐通过一台有固定 IP 的 VPS 做正向代理：

1. 在 VPS 上安装代理（如 tinyproxy）：
   ```bash
   sudo apt-get install -y tinyproxy
   ```
2. 编辑 `/etc/tinyproxy/tinyproxy.conf`，设置监听地址和访问控制（建议只允许内网访问）
3. 在插件配置中设置 `WECOM_PROXY` 环境变量指向代理：
   ```jsonc
   // ~/.openclaw/openclaw.json
   {
     "env": {
       "vars": {
         "WECOM_PROXY": "http://你的VPS内网IP:8888"
       }
     }
   }
   ```
4. 将 VPS 的**公网 IP** 添加到企业可信 IP 白名单

> 插件内置了 `wecomFetch()` 函数，会自动通过 `WECOM_PROXY` 代理所有发往 `qyapi.weixin.qq.com` 的请求。如果使用 ZeroTier / Tailscale 等虚拟内网连接 VPS，代理地址填内网 IP 即可。

#### 第七步：启动并验证 🚀

1. 重启 OpenClaw Gateway：

```bash
openclaw gateway restart
```

2. 检查插件是否加载：

```bash
openclaw plugin list
```

3. 验证 Webhook 可达：

```bash
curl https://你的域名/wecom/callback
# 应返回 "wecom webhook ok"
```

4. 回到企业微信管理后台，点击**保存**回调配置
5. 如果验证通过，配置完成！🎉

#### 第七步补充：核对 AgentId 与回调参数是否是同一套应用

这一步非常关键，尤其是你创建过多个自建应用时。

必须逐项一致：

- 企业微信后台显示的 `AgentId`
- OpenClaw 配置里的 `WECOM_AGENT_ID` / `channels.wecom.agentId`
- 当前应用对应的 `Secret`
- 当前应用对应的 `Token`
- 当前应用对应的 `EncodingAESKey`

推荐这样核对：

```bash
openclaw config get channels.wecom
curl https://你的域名/wecom/callback
```

如果你遇到下面这种情况，优先怀疑绑错了应用：

- OpenClaw 主动发消息给你能成功
- 你从企业微信回消息后，日志里没有新的 `wecom inbound`
- callback 健康检查正常，但回调始终不进插件

一个真实案例是：本机误绑到 `agentId=1000002`，而企业微信后台实际配置的是 `agentId=1000003`。切回正确应用后，入站和回复立即恢复。

#### 第八步：关联个人微信 📱（可选）

如果希望**个人微信**也能直接与 AI 对话，需在企业微信管理后台开启微信插件：

1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)
2. 进入 **我的企业** → **微信插件**
3. 用个人微信扫描页面上的二维码，关联到企业
4. 关联后，个人微信中会出现企业的应用入口，直接发消息即可

<p align="center">
  <img src="docs/images/wecom2wechat.jpg" alt="企业微信管理后台 → 我的企业 → 微信插件" width="600">
  <br>
  <em>在企业微信管理后台「我的企业 → 微信插件」中，用个人微信扫码即可关联</em>
</p>

### 🎙️ 语音转文字（stt.py）

`stt.py` 支持两种语音识别模式，自动选择最优方案：

| 模式 | 模型 | 运行位置 | 配置 |
|------|------|---------|------|
| 🌐 远程 ASR（推荐） | [Qwen3-ASR-0.6B](https://modelscope.cn/models/Qwen/Qwen3-ASR-0.6B) | GPU 服务器 | 设置 `WECOM_STT_URL` |
| 💻 本地 ASR（降级） | [FunASR SenseVoice-Small](https://modelscope.cn/models/iic/SenseVoiceSmall) | 本地 CPU/MPS/CUDA | 设置 `WECOM_STT_PYTHON` |

**优先级：** 企业微信自带识别 → 远程 Qwen3-ASR → 本地 FunASR

**工作流程：**
1. 收到语音消息 → 下载 AMR 音频文件
2. 使用 FFmpeg 转换为 WAV（16kHz 单声道）
3. 调用 `stt.py`：优先 POST 到远程 ASR 服务，失败时降级到本地 FunASR
4. 将识别结果作为文本消息发送给 AI 代理

**方式一：远程 ASR（推荐）**

在 `openclaw.json` 中配置远程 ASR 服务地址：

```jsonc
"env": {
  "vars": {
    "WECOM_STT_URL": "http://your-gpu-server:8990/transcribe"
  }
}
```

远程 ASR 服务部署详情参见 [`asr_api_README.md`](asr_api_README.md)。快速搭建：

```bash
# GPU 服务器上
python3 -m venv ~/qwen-asr-env && source ~/qwen-asr-env/bin/activate
pip install qwen-asr fastapi uvicorn python-multipart

# 下载模型（首次需联网）
python3 -c "from modelscope import snapshot_download; snapshot_download('Qwen/Qwen3-ASR-0.6B')"

# 启动服务
TRANSFORMERS_OFFLINE=1 python3 asr_server.py
# 监听 0.0.0.0:8990，POST /transcribe 接收音频，GET /health 健康检查
```

生产部署建议使用 systemd 服务（`Restart=always`），详见 [`asr_api_README.md`](asr_api_README.md)。

**性能参考（RTX 4090D）：** 模型加载 ~3.7s，显存占用 ~3.7GB，短语音 ~2-4s 推理，支持 52 种语言 + 22 种中国方言。

**方式二：本地 FunASR（无 GPU 服务器时）**

```bash
# FFmpeg（音频格式转换）
brew install ffmpeg        # macOS
# 或 apt install ffmpeg    # Linux

# Python 依赖
pip install funasr modelscope torch torchaudio torchcodec
```

> 🍎 **Apple Silicon (M1/M2/M3/M4) 支持：** `stt.py` 会自动检测并使用 MPS (Metal Performance Shaders) 加速推理。首次运行时模型会从 ModelScope 自动下载（约 1GB）。
>
> ⚠️ **macOS launchd 部署注意：** 如果 OpenClaw 以 launchd 服务运行，默认 `python3` 可能找不到 ML 依赖。需在 plist 中设置环境变量 `WECOM_STT_PYTHON` 指向正确的 Python 路径（如 conda 环境的 python3）。

**独立使用：**

```bash
# 远程模式
WECOM_STT_URL=http://gpu-server:8990/transcribe python3 stt.py /path/to/audio.wav

# 本地模式
python3 stt.py /path/to/audio.wav
```

> 💡 如果企业微信已开启语音识别（Recognition 字段），会优先使用企业微信的结果；仅在无 Recognition 字段时才会调用本地 STT。

> 🤖 **AI Agent 自动部署：** 详细的多环境（CUDA / MPS / CPU）安装部署指南见 [`docs/stt-deploy-guide.md`](docs/stt-deploy-guide.md)，可供 Claude Code 等 AI 编程助手直接参照执行自动化安装。

### 📝 使用

配置完成后，在企业微信或个人微信中找到你的应用，直接发送消息即可：

1. 📱 在企业微信中找到你创建的应用
2. 💬 发送文字、图片、语音、视频、文件消息
3. 🤖 AI 会自动回复

**<img src="docs/images/wechat-icon.png" width="16" height="16"> 个人微信接入：** 在企业微信管理后台「我的企业  → 微信插件」中，用个人微信扫码关联即可。

#### 命令系统

| 命令 | 说明 |
|------|------|
| `/help` | 📋 显示帮助信息 |
| `/status` | 📊 查看系统状态（含账户信息） |
| `/clear` | 🗑️ 清除会话历史，开始新对话 |

### 🔧 环境变量参考

| 变量名 | 必填 | 默认值 | 说明 |
|--------|:----:|--------|------|
| `WECOM_CORP_ID` | ✅ | — | 企业微信企业 ID |
| `WECOM_CORP_SECRET` | ✅ | — | 自建应用的 Secret |
| `WECOM_AGENT_ID` | ✅ | — | 自建应用的 AgentId |
| `WECOM_CALLBACK_TOKEN` | ✅ | — | 回调配置的 Token |
| `WECOM_CALLBACK_AES_KEY` | ✅ | — | 回调配置的 EncodingAESKey（43 字符 Base64） |
| `WECOM_WEBHOOK_PATH` | ❌ | `/wecom/callback` | Webhook 路径 |
| `WECOM_PROXY` | ❌ | — | 出站 WeCom API 的 HTTP 代理地址（如 `http://10.x.x.x:8888`） |

### 🔍 故障排查

#### ❌ 回调验证失败

1. 检查 URL 是否可公网访问：
```bash
curl https://你的域名/wecom/callback
# 应返回 "wecom webhook ok"
```

2. 检查环境变量是否正确配置（Token 和 AESKey 必须与企业微信后台一致）

3. 查看 OpenClaw 日志：
```bash
openclaw logs -f | grep wecom
```

#### ❌ 消息没有回复

1. 检查日志中是否有 `wecom inbound` 记录
2. 确认 AI 模型配置正确（检查 `agents.defaults.model`）
3. 检查是否有错误日志

如果完全没有 `wecom inbound` 记录，再重点排查这 4 项是否来自**同一个自建应用**：

1. `AgentId`
2. `Secret`
3. `Token`
4. `EncodingAESKey`

很多“能发不能收”不是模型问题，而是本机绑定到了错误的企业微信应用。

#### ❌ OpenClaw 2026.3+ 升级后，`/wecom/callback` 返回 Control UI 页面

这是新版 Gateway 兼容问题的典型症状。请确认：

1. 插件版本至少为 `0.3.7`
2. `openclaw channels status --probe` 中 WeCom 账户状态为 `running`
3. `curl https://你的域名/wecom/callback` 返回 `wecom webhook ok`
4. 日志中不再出现持续的 `health-monitor: restarting (reason: stopped)`

如果第 3 项返回的是 HTML 页面，通常说明 webhook 路由没有正确命中插件处理器。

#### ❌ access_token 获取失败

1. 确认 `WECOM_CORP_ID` 和 `WECOM_CORP_SECRET` 正确
2. 检查应用的可见范围是否包含测试用户
3. 确认服务器能访问 `qyapi.weixin.qq.com`（如有代理需设置 `WECOM_PROXY`）

#### ❌ 报错 `60020` (not allow to access from your ip)

企业微信 API 拒绝了你服务器的出口 IP。解决方法：
1. 查看错误日志中提示的 IP 地址
2. 将该 IP 添加到企业微信管理后台 → 应用管理 → 企业可信 IP
3. 如果服务器是动态 IP，参考第六步配置代理方案

#### ❌ 报错 "Outbound not configured"

OpenClaw 要求插件同时提供 `sendText` **和** `sendMedia` 两个出站方法。如果缺少任一方法，`createPluginHandler()` 会返回 null，导致此错误。确认插件版本包含完整的出站配置。

#### ❌ 语音识别失败

1. 确认已安装 FFmpeg：`ffmpeg -version`
2. 如果使用远程 ASR：检查 `WECOM_STT_URL` 是否可达（`curl $WECOM_STT_URL`），检查 GPU 服务器上 `qwen3-asr.service` 是否运行
3. 如果使用本地 FunASR：确认已安装 Python 依赖 `python3 -c "from funasr import AutoModel"`
4. 首次本地运行会从 ModelScope 下载模型（约 1GB），需要网络连接
5. 本地 `stt.py` 会自动检测设备：CUDA GPU → Apple MPS → CPU（按优先级依次降级）

#### ❌ 语音消息发送了但 AI 没收到内容

`RawBody` 为空字符串 `""` 时会短路 `??` 运算符的回退链，导致 AI 收到空消息。确认插件版本中 `RawBody` 设置为 `content || messageText || ""`（而非 `content || ""`）。

#### ❌ macOS 上 STT 找不到 Python / 模型加载失败

macOS 通过 launchd 启动 OpenClaw 时，PATH 不包含 conda 环境。解决方法：

1. 在 `openclaw.json` 的 `env.vars` 中设置 `WECOM_STT_PYTHON` 指向 conda 环境中的 Python：
   ```jsonc
   {
     "env": {
       "vars": {
         "WECOM_STT_PYTHON": "/path/to/anaconda3/envs/sci/bin/python3"
       }
     }
   }
   ```
2. 如遇 Apple Silicon MPS 不支持某些操作，设置 `PYTORCH_ENABLE_MPS_FALLBACK=1`
3. 较新版本 torchaudio 需要额外安装 `torchcodec`：`pip install torchcodec`

#### ❌ AI 无法"看到"用户发送的图片

图片会保存到本地磁盘并通过工具指令告知 AI 读取，而非以 base64 多模态方式传入。AI 需要主动调用 Read 工具才能看到图片内容，这取决于模型是否正确使用了工具。

#### ❌ sendMedia 发送文件失败 / 文件被静默拦截

OpenClaw 核心层通过 `mediaLocalRoots` 限制可发送的本地文件路径，仅允许以下目录：
- `tmpdir`（系统临时目录）
- `~/.openclaw/media`
- `~/.openclaw/agents`
- `~/.openclaw/workspace`
- `~/.openclaw/sandboxes`

目录外的文件会被 `assertLocalMediaAllowed()` 静默拦截。解决方法：先将文件复制到 `~/.openclaw/workspace/` 再发送。

#### ❌ Node.js `fetch()` 不走代理

Node.js 原生 `fetch()` **不支持** `HTTPS_PROXY` 环境变量。插件使用 `undici.ProxyAgent` 配合 `dispatcher` 参数实现代理，仅需设置 `WECOM_PROXY` 环境变量即可，无需额外配置系统代理。

### 🏗️ 架构

```
┌──────────────┐         ┌──────────────────┐         ┌───────────────┐
│  企业微信     │ ──XML──▶│ OpenClaw Gateway │ ──────▶ │  AI Agent     │
│  / 个人微信   │         │  (port 18789)    │         │  (LLM)        │
│              │ ◀──API──│                  │ ◀────── │               │
└──────────────┘         └──────┬───────────┘         └───────────────┘
                                │
                    ┌───────────┼───────────┐
                    ▼           ▼           ▼
              ┌──────────┐ ┌────────┐ ┌──────────┐
              │ 消息加解密 │ │ STT    │ │ Chat UI  │
              │ AES-256  │ │ FunASR │ │ Broadcast│
              └──────────┘ └────────┘ └──────────┘
```

**消息流程：**

1. 📩 用户在企业微信/个人微信发送消息
2. 🔒 企业微信服务器将加密的 XML 回调发送到你的 Webhook URL
3. 🔓 插件验证签名，解密消息（AES-256-CBC）
4. ⚡ 立即返回 HTTP 200（企业微信要求 5 秒内响应）
5. 🔄 异步处理：根据消息类型分发处理
   - 📝 文本 → 直接交给 AI
   - 🖼️ 图片 → 下载保存 → AI Vision 分析
   - 🎙️ 语音 → 下载 AMR → FFmpeg 转 WAV → 远程 Qwen3-ASR / 本地 FunASR → 文本交给 AI
   - 📹 视频/📎 文件 → 下载保存 → 通知 AI
   - 🔗 链接 → 提取元信息 → 交给 AI
6. 🤖 AI 代理生成回复
7. 📤 回复经 Markdown 转换后，自动分段发送回企业微信
8. 🖥️ 同时写入 Transcript + 广播到 Chat UI

### 📁 项目结构

```
openclaw-wechat/
├── index.js                 # 入口文件（重导出）
├── src/
│   └── index.js             # 插件主逻辑（1400+ 行）
├── stt.py                   # 🎙️ 语音识别（远程 Qwen3-ASR + 本地 FunASR 降级）
├── asr_server.py            # 🖥️ 远程 ASR 服务（FastAPI + Qwen3-ASR，部署到 GPU 服务器）
├── openclaw.plugin.json     # OpenClaw 插件描述文件（新格式）
├── clawdbot.plugin.json     # ClawdBot 插件描述文件（兼容旧版）
├── package.json             # npm 包配置 (v0.3.7)
├── .env.example             # 环境变量示例
├── skills/
│   └── wecom-notify/        # 📨 Claude Code WeCom 通知技能
│       ├── SKILL.md
│       └── scripts/
│           └── send_wecom.py
├── docs/
│   └── channels/
│       └── wecom.md         # 渠道文档
├── CHANGELOG.md             # 版本变更日志
└── LICENSE                  # MIT 许可证
```

### 📨 Claude Code WeCom 通知技能

本仓库还包含一个独立的 **Claude Code 技能**（`wecom-notify`），可以在 Claude Code 中直接发送企业微信消息。这是一个**独立工具**，不依赖 OpenClaw 插件，直接调用企业微信 API。

#### 安装技能

将 `skills/wecom-notify/` 目录复制到 `~/.claude/skills/` 即可：

```bash
cp -r skills/wecom-notify ~/.claude/skills/
```

#### 使用方式

在 Claude Code 中可以直接使用 `/wecom-notify` 命令，或让 AI 自动调用：

```bash
# 发送文本消息
python3 skills/wecom-notify/scripts/send_wecom.py "你好，这是一条测试消息"

# 指定接收人
python3 skills/wecom-notify/scripts/send_wecom.py "消息内容" --to UserName

# 发送图片
python3 skills/wecom-notify/scripts/send_wecom.py --image /path/to/photo.png

# 发送文件
python3 skills/wecom-notify/scripts/send_wecom.py --file /path/to/report.pdf
```

#### 特点

- 🔧 **零依赖**：仅使用 Python 标准库（`urllib.request`、`json`），无需 `pip install`
- 📄 自动从 `~/.openclaw/openclaw.json` 读取 WeCom 配置（复用 OpenClaw 的环境变量）
- 📝 支持文本（2048 字节限制）、图片（jpg/png/gif，≤2MB）、文件（任意格式，≤20MB）
- 🌐 支持 `WECOM_PROXY` 代理

### 📜 版本历史

查看 [CHANGELOG.md](./CHANGELOG.md) 了解完整版本历史。

---

## 🔗 相关链接

- 🌐 [OpenClaw 官方网站](https://openclaw.ai)
- 📖 [企业微信开发文档](https://developer.work.weixin.qq.com/document/)
- 🔐 [企业微信消息加解密](https://developer.work.weixin.qq.com/document/path/90968)
- 🍴 [上游项目：dingxiang-me/OpenClaw-Wechat](https://github.com/dingxiang-me/OpenClaw-Wechat)
- 🎙️ [FunASR SenseVoice](https://modelscope.cn/models/iic/SenseVoiceSmall)

## 📄 许可证

[MIT License](./LICENSE)

## 🙏 致谢

- 🍴 原始项目：[dingxiang-me/OpenClaw-Wechat](https://github.com/dingxiang-me/OpenClaw-Wechat)，作者 **勾勾的数字生命** ([@dingxiang-me](https://github.com/dingxiang-me))
- 🤖 [OpenClaw](https://openclaw.ai)，由 Peter Steinberger 和 OpenClaw 社区开发
- 🎙️ [FunASR SenseVoice](https://github.com/modelscope/FunASR)，由阿里巴巴达摩院开发

## 🤝 贡献

欢迎提交 Issue 和 Pull Request！
