1. 为什么要在 OpenClaw 里做一个微信自动发消息 Skill
OpenClaw Skill 是一套把「本地能力」封装成 AI 可调用工具的机制,简单说就是让 AI 助手从「只会聊天」变成「能动手干活」。微信自动发送消息这个场景特别典型:你每天可能要重复给同事发日报提醒、给客户发进度同步、给群里发固定格式通知,这些动作机械、重复、容易忘,但偏偏又必须做。把它做成 Skill 之后,你只需要对 OpenClaw 说一句「给小明发消息:今天下午三点开会」,剩下的窗口切换、联系人搜索、内容粘贴、回车发送全部自动完成。
这个 Skill 适合三类人:一是经常需要批量触达微信联系人的运营和销售;二是想把日常重复操作交给 AI 的开发者;三是正在学习 OpenClaw Skill 开发、想找一个完整可跟做案例的同学。它不需要你懂微信协议,也不需要任何第三方登录手段,纯粹基于本地已登录的微信桌面端做 UI 自动化,风险可控、链路透明。
技术上有几个真实的坑必须提前说清楚。微信桌面端在新版本里大量使用 UWP 容器(WeChatAppEx),传统的FindWindow拿不到标准窗口句柄,SendKeys直接发中文还会因为输入法干扰变成乱码。我试过用 VBS 的SendKeys模拟 Win 键组合,结果完全不生效,因为 VBS 的 SendKeys 语法根本不支持 Win 键。最后稳定跑通的方案是:用keybd_event模拟 Win+数字切到任务栏固定位置的微信,用win32clipboard剪贴板粘贴绕过输入法,再用回车完成发送。整套逻辑用 Python + pywin32 + ctypes 实现,封装成 OpenClaw Skill 后由模型决定什么时候调用。
而模型调用的那一环,我用 TaoToken 统一 Key 接入,一个 Key 就能走通对话模型通道,不用在多个平台之间来回切换配置。下面从环境准备开始,一步步把这条链路搭起来。
2. TaoToken 统一 Key 与 OpenClaw 环境前置准备
在写 Skill 之前,先把两件事准备好:模型调用通道和本地 Python 环境。很多人卡在第一步不是因为技术难,而是因为 Key 和 Base URL 配错,导致 Skill 里调用模型时一直 401。
TaoToken 的定位是统一模型接入通道,你可以在它的控制台创建一个 API Key,然后所有兼容 OpenAI 协议的客户端都能用同一个 Key 和同一个 Base URL 去请求不同模型。对 OpenClaw Skill 来说,这意味着你的 Skill 里如果需要模型做意图解析、联系人名称归一化、消息内容润色,都可以直接复用这套通道,不用为每个模型单独维护一套鉴权。
先拿 Key。打开控制台页面:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite登录后在 API Keys 页面创建一个新 Key,复制出来形如sk-xxxxxxxx。这个 Key 只显示一次,建议先存到本地环境变量里,不要硬编码进脚本。
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewriteBase URL 统一用:
https://taotoken.net/api注意这个地址后面不加 UTM 参数,它是给程序调用的接口地址。模型 ID 按你实际要用的填,比如claude-sonnet-4-5或gpt-4o这类,具体以控制台模型列表为准。
本地环境这边,你需要 Python 3.9 以上,然后装两个包:
pip install pywin32 requestspywin32提供剪贴板操作,requests用来调 TaoToken 的接口。装完之后验证一下:
python -c "import win32clipboard; print('clipboard ok')"如果这条命令报ImportError,说明 pywin32 没装好,重装一次并确认 Python 版本匹配。Windows 上有时需要以管理员身份运行一次python Scripts/pywin32_postinstall.py -install来注册 DLL。
环境变量建议这样设,PowerShell 里:
$env:TAOTOKEN_API_KEY="sk-你的key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"这样 Skill 脚本里用os.environ.get("TAOTOKEN_API_KEY")读取,既安全又方便切换。前置准备做完,接下来进入 Skill 目录结构和可复制配置。
3. OpenClaw Skill 目录结构与可复制配置
OpenClaw Skill 的本质是一个带SKILL.md的文件夹,SKILL.md里的description字段决定模型在什么场景下触发这个 Skill。目录结构建议这样组织:
wechat-messenger/ ├── SKILL.md ├── README.md ├── scripts/ │ ├── wechat_send_win9.py │ ├── wechat_send.ps1 │ └── model_client.py └── config/ └── settings.jsonSKILL.md是核心,它的 frontmatter 必须写清楚触发条件。下面这份可以直接复制,注意description要覆盖用户可能说的各种说法:
--- name: wechat-messenger description: "WeChat auto-messaging skill. Send messages to WeChat contacts via local Windows client. Triggers when user asks to: send WeChat message, message someone on WeChat, chat with a contact, 给某人发微信, 微信发消息. Supports single and batch messages. Uses clipboard paste to bypass IME issues." --- # WeChat Messenger ## Prerequisites - WeChat Windows client running and logged in - WeChat pinned to taskbar position 9 - Python with pywin32: `pip install pywin32` ## Usage Call `scripts/wechat_send.ps1 -Contact "name" -Message "content"`config/settings.json放模型调用配置,这样 Skill 里调 TaoToken 时不用改代码:
{ "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model_id": "claude-sonnet-4-5", "timeout": 30, "retry": 2 }model_client.py负责读这份配置并调用模型,用于把用户口语化的指令解析成结构化的联系人和消息内容:
import os import json import requests CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "settings.json") def load_config(): with open(CONFIG_PATH, "r", encoding="utf-8") as f: return json.load(f) def parse_intent(user_text): cfg = load_config() api_key = os.environ.get(cfg["api_key_env"]) if not api_key: raise RuntimeError("TAOTOKEN_API_KEY not set") headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "model": cfg["model_id"], "messages": [ {"role": "system", "content": "Extract contact and message from user input. Return JSON: {\"contact\": \"\", \"message\": \"\"}"}, {"role": "user", "content": user_text} ] } resp = requests.post( f"{cfg['base_url']}/v1/chat/completions", headers=headers, json=payload, timeout=cfg["timeout"] ) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return json.loads(content)这里三件套要记牢:Base URL 是https://taotoken.net/api,Key 从环境变量读,Model ID 写在 settings.json 里。三者缺一,调用就会失败。配置写好后,下一节进入核心的窗口定位与消息注入代码。
4. pywin32 窗口定位与消息注入完整实现
微信桌面端不能用传统FindWindow定位,所以这里走任务栏快捷键方案:把微信固定在任务栏第 9 个位置,用Win+9切过去。这个方案的好处是不依赖窗口标题,也不受微信版本更新影响,只要位置固定就能用。
核心脚本wechat_send_win9.py完整如下,可以直接复制:
import ctypes import time import sys import win32clipboard VK_LWIN = 0x5B VK_CONTROL = 0x11 VK_V = 0x56 VK_F = 0x46 VK_ENTER = 0x0D VK_9 = 0x39 user32 = ctypes.windll.user32 def key_down(vk): user32.keybd_event(vk, 0, 0, 0) def key_up(vk): user32.keybd_event(vk, 0, 2, 0) def press_combo(modifier, key): key_down(modifier) time.sleep(0.05) key_down(key) time.sleep(0.05) key_up(key) key_up(modifier) def set_clipboard(text): win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(text, win32clipboard.CF_UNICODETEXT) win32clipboard.CloseClipboard() def send_wechat(contact, message): # 1. Win+9 切到微信 key_down(VK_LWIN) time.sleep(0.05) key_down(VK_9) time.sleep(0.05) key_up(VK_9) key_up(VK_LWIN) time.sleep(0.8) # 2. Ctrl+F 打开搜索 press_combo(VK_CONTROL, VK_F) time.sleep(0.8) # 3. 粘贴联系人姓名 set_clipboard(contact) press_combo(VK_CONTROL, VK_V) time.sleep(1.5) # 4. 回车进入聊天 key_down(VK_ENTER) time.sleep(0.05) key_up(VK_ENTER) time.sleep(0.8) # 5. 粘贴消息内容 set_clipboard(message) press_combo(VK_CONTROL, VK_V) time.sleep(0.5) # 6. 回车发送 key_down(VK_ENTER) time.sleep(0.05) key_up(VK_ENTER) print(f"Message sent to {contact}") if __name__ == "__main__": if len(sys.argv) < 3: print("Usage: python wechat_send_win9.py <contact> <message>") sys.exit(1) send_wechat(sys.argv[1], sys.argv[2])几个关键点解释一下。keybd_event的第三个参数 0 表示按下,2 表示抬起,这是 Windows API 的约定。set_clipboard里用CF_UNICODETEXT是为了保证中文不乱码,这一点比默认的SetClipboardText更稳。每一步之间的time.sleep不是随便写的,微信 UI 响应有延迟,切窗口后至少等 0.8 秒,搜索后等 1.5 秒让结果列表渲染出来,否则回车可能进错聊天。
PowerShell 封装脚本wechat_send.ps1负责把参数透传给 Python:
param( [Parameter(Mandatory=$true)][string]$Contact, [Parameter(Mandatory=$true)][string]$Message ) $pyScript = Join-Path $PSScriptRoot "wechat_send_win9.py" $python = "python" & $python $pyScript $Contact $Message如果你用 conda 环境,把$python换成绝对路径,比如D:\anaconda3\python.exe。这样 OpenClaw 调用 Skill 时只需要传联系人和消息两个参数,内部细节全部封装好。
5. 验证请求与常见报错排查
写完代码先别急着接 OpenClaw,单独跑一遍验证链路。先测模型调用是否通:
python -c "from scripts.model_client import parse_intent; print(parse_intent('给小明发消息:今天下午三点开会'))"正常应该返回类似{'contact': '小明', 'message': '今天下午三点开会'}。如果这里报错,对照下面几个真实错误排查。
报错一:401 Unauthorized
requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://taotoken.net/api/v1/chat/completions原因通常是 Key 没读到或者写错了。检查TAOTOKEN_API_KEY环境变量是否在当前终端生效,PowerShell 里用echo $env:TAOTOKEN_API_KEY确认。注意 Key 不要带引号,也不要有多余空格。
报错二:local proxy failed
requests.exceptions.ProxyError: HTTPSConnectionPool ... local proxy failed这是本地代理配置干扰了请求。检查系统环境变量里有没有HTTP_PROXY/HTTPS_PROXY,如果有就临时清掉:
Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue报错三:reading choices 时 KeyError
KeyError: 'choices'说明返回体结构不对,多半是 Base URL 写成了带/v1的完整路径又拼了一次,或者模型 ID 不存在。确认 Base URL 是https://taotoken.net/api,代码里拼/v1/chat/completions,模型 ID 从控制台复制。
报错四:微信没反应或发错人
如果脚本跑完没报错但消息没发出去,先确认微信是否固定在任务栏第 9 位。手动按Win+9看能不能切到微信,不能的话调整固定位置或改脚本里的VK_9。如果发错人,多半是搜索后等待时间不够,把time.sleep(1.5)加到 2 秒再试。
报错五:中文乱码
如果消息内容变成问号或方块,检查set_clipboard是否用了CF_UNICODETEXT。用SendKeys直接发中文在搜狗五笔等输入法下必然乱码,剪贴板方案是唯一稳定的绕过方式。
排查完这些,再跑一次完整发送:
.\scripts\wechat_send.ps1 -Contact "文件传输助手" -Message "测试消息"用文件传输助手测试最安全,不会打扰别人。看到消息出现在聊天窗口,说明整条链路通了。
6. 接入 OpenClaw 与长期使用建议
Skill 本地跑通后,把它放到 OpenClaw 的 skills 目录,重启 Gateway 就能被识别。之后你在 OpenClaw 里说「给小明发消息:今天下午三点开会」,模型会先调parse_intent解析出联系人和内容,再调wechat_send.ps1完成发送。
如果你打算长期用这套东西,建议把模型调用走 Coding Plan 通道,这样日常编码和 Skill 里的模型请求可以共用一套额度,管理起来更省心:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite需要调试模型返回时,可以用模型对话页面直接测 prompt:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite接入文档在这里,遇到协议细节可以对照:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite最后说几个实战经验。第一,任务栏位置不要写死 9,如果你经常变动图标顺序,可以在脚本启动时用pygetwindow扫描一次微信窗口并记录位置,或者干脆固定一个专用任务栏槽位。第二,批量发送时每条之间至少间隔 3 秒,太快会被微信风控盯上。第三,消息内容里如果有换行,剪贴板方案能正常处理,但SendKeys不行,这也是选剪贴板的另一个理由。第四,把发送结果写日志,失败时记录联系人和时间,方便重试。这套 Skill 我跑了一段时间,最实用的场景是每天定时给几个群发固定格式的提醒,省下来的时间比搭它的时间多得多。