1. 从「跑完任务想推微信」说起ilink 协议到底能做什么如果你写过量化脚本、爬虫、定时任务大概率都遇到过同一个尴尬程序在服务器上跑得好好的结果只能自己看日志想让结果主动弹到微信里却发现路全被堵死了。公众号要企业认证、个人订阅号不能主动推、企业微信 webhook 要开 Server 酱、第三方框架itchat、wechaty 之类这两年基本全凉。我当初做量化选股系统6 个 Agent 分析跑完就想把结果推给自己结果折腾了一圈发现——微信官方其实留了一条口子藏在tencent-weixin/openclaw-weixin这个 npm 包里走的是微信内部的ilink AI Bot 平台接口。ilink 协议是什么简单说它是微信给 AI Bot 场景准备的一套 HTTP 接口基地址是https://ilinkai.weixin.qq.com全部 POST JSON一共 5 个核心接口getUpdates长轮询收消息、sendMessage发消息、getUploadUrlCDN 上传、getConfig拿 typing ticket、sendTyping正在输入状态。加上两个扫码登录接口就能拼出一个完整的收发闭环。它适合谁适合想给自己做「私人推送通道」的开发者——量化结果、爬虫告警、CI 构建通知、Agent 任务完成提醒都能用。不适合谁不适合想做群发、做营销、做多人群聊 Bot 的人ilink 只支持 1 对 1 direct chat群消息发不了。这篇我会把协议握手、消息上行、主动下发链路完整拆开给你一份能直接复制的 Python 骨架最后跑通一条主动推送消息。工具侧统一走 TaoToken 的 Key/API 通道做配置管理避免 token 散落在各个脚本里。2. 前置准备TaoToken 统一 Key 与 ilink 凭证的关系在动手写代码之前先把两件事分清楚不然很容易混。第一件是 ilink 的 bot_token。这是你扫码登录后微信服务端返回的凭证格式是一长串 base64后续所有/ilink/bot/*接口都要带Authorization: Bearer {bot_token}。它跟你的微信号绑定属于「通道凭证」。第二件是工具侧的 API Key。如果你像我一样Bot 背后要调大模型做回复、做分析那模型调用这一层建议统一走 TaoToken 的 API 通道把 Key 集中管理而不是每个脚本里硬编码一份。TaoToken 官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 两套凭证各管各的互不干扰。我踩过的坑是一开始把 ilink 的 bot_token 和模型 Key 混在一个 config 里结果换模型的时候不小心把 bot_token 覆盖了扫码重新登了一次。后来我改成两个文件wechat.json只放 ilink 凭证llm.json放模型侧配置清爽很多。注意ilink 的 bot_token 是扫码登录后一次性拿到的建议持久化到本地文件别每次启动都重新扫。扫码接口本身有频率限制扫太勤会被临时拦。3. 协议握手扫码登录拿到三个关键值ilink 的登录流程是「拿二维码 → 轮询扫码状态 → 拿到 token」两个接口都不在 bot 路径下import httpx, qrcode, time BASE https://ilinkai.weixin.qq.com # Step 1: 获取二维码 resp httpx.get(f{BASE}/ilink/bot/get_bot_qrcode?bot_type3) data resp.json() qrcode_key data[qrcode] qrcode_url data[qrcode_img_content] # Step 2: 终端打印二维码 qr qrcode.QRCode(border1) qr.add_data(qrcode_url) qr.make(fitTrue) qr.print_ascii(invertTrue) # Step 3: 长轮询等扫码确认 while True: status_resp httpx.get( f{BASE}/ilink/bot/get_qrcode_status?qrcode{qrcode_key}, headers{iLink-App-ClientVersion: 1}, timeout40, ) status status_resp.json() if status[status] scaned: print(已扫码请在手机上确认...) elif status[status] confirmed: bot_token status[bot_token] account_id status[ilink_bot_id] user_id status[ilink_user_id] print(f登录成功! token{bot_token[:20]}...) break elif status[status] expired: print(二维码过期请重新获取) break扫码确认后你会拿到三个值缺一不可字段含义用途bot_tokenBot 的认证令牌后续所有 API 的 Bearerilink_bot_idBot 账户 ID多账号区分ilink_user_id扫码人的微信 ID格式xxxim.wechat主动推送的默认目标get_qrcode_status超时是正常行为不是登录失败重试即可。二维码过期statusexpired就重新调get_bot_qrcode。4. 可复制配置Python 客户端骨架与请求头拿到 token 之后先别急着发消息。ilink 的请求头有几个固定字段少一个都可能出问题import base64, json, random, uuid, httpx def build_headers(token): uin base64.b64encode( str(random.randint(0, 0xFFFFFFFF)).encode() ).decode() return { Content-Type: application/json, AuthorizationType: ilink_bot_token, # 固定值 Authorization: fBearer {token}, X-WECHAT-UIN: uin, # 随机 uint32 的 base64 }AuthorizationType必须是ilink_bot_token不是Bearer也不是别的。X-WECHAT-UIN每次请求随机生成一个 uint32 再 base64服务端用它做请求去重和路由。然后是完整的客户端骨架我把收发都封进去了import base64, json, logging, random, time, uuid from pathlib import Path import httpx ILINK_BASE https://ilinkai.weixin.qq.com class WeChatBot: def __init__(self, token, to_user_id, context_token, config_pathwechat.json): self.base ILINK_BASE self.token token self.to_user_id to_user_id self.context_token context_token self.config_path config_path self._cursor classmethod def from_config(cls, pathwechat.json): with open(path) as f: cfg json.load(f) return cls( tokencfg[token], to_user_idcfg[to_user_id], context_tokencfg.get(context_token, ), config_pathpath, ) def _headers(self): uin base64.b64encode(str(random.randint(0, 0xFFFFFFFF)).encode()).decode() return { Content-Type: application/json, AuthorizationType: ilink_bot_token, Authorization: fBearer {self.token}, X-WECHAT-UIN: uin, } def _post(self, endpoint, body): body[base_info] {channel_version: 1.0.3} raw json.dumps(body, ensure_asciiFalse).encode(utf-8) headers self._headers() headers[Content-Length] str(len(raw)) resp httpx.post( f{self.base}/ilink/bot/{endpoint}, contentraw, headersheaders, timeout35, ) text resp.text.strip() return json.loads(text) if text and text ! {} else {ret: 0} def get_updates(self): result self._post(getupdates, {get_updates_buf: self._cursor}) self._cursor result.get(get_updates_buf, self._cursor) for msg in result.get(msgs, []): ct msg.get(context_token, ) if ct: self.context_token ct self._save_token(ct) return result.get(msgs, []) def send(self, text, toNone, context_tokenNone): return self._post(sendmessage, { msg: { from_user_id: , to_user_id: to or self.to_user_id, client_id: fbot-{uuid.uuid4().hex[:12]}, message_type: 2, message_state: 2, context_token: context_token or self.context_token, item_list: [{type: 1, text_item: {text: text}}], } }) def refresh_and_send(self, text): self.get_updates() return self.send(text) def _save_token(self, ct): try: p Path(self.config_path) if p.exists(): cfg json.loads(p.read_text()) cfg[context_token] ct p.write_text(json.dumps(cfg, indent2, ensure_asciiFalse)) except Exception: passwechat.json长这样{ token: 你的 bot_token, to_user_id: xxxim.wechat, context_token: }5. 消息上行与主动下发context_token 是命门ilink 的消息链路分两条上行是用户给 Bot 发消息Bot 通过getUpdates长轮询拉取下发是 Bot 主动调sendMessage推消息。两条链路都绕不开context_token。context_token是 ilink 的会话上下文令牌。每次用户给 Bot 发消息getUpdates返回的消息体里都会带一个{ msgs: [{ from_user_id: xxxim.wechat, context_token: AARzJW...(很长的base64)..., item_list: [{type: 1, text_item: {text: 你好}}] }], get_updates_buf: CgkI... }关键问题没有 context_token 能不能主动推答案是 API 不报错返回 200但消息不投递。这是 ilink 最阴险的设计——静默失败。那 context_token 会过期吗我一开始以为是一次性的因为用同一个 token 发第一条收到了发第二条就收不到。后来才发现真相context_token 可以无限复用收不到是因为第一条发送的格式就不对。补全client_id、message_type、message_state之后同一个 token 连发 10 条都能收到。所以主动推送的正确姿势是先get_updates刷新一次 context_token再send。这就是上面refresh_and_send方法存在的原因。6. 验证请求跑通第一条主动推送现在把代码跑起来。先确保你至少给 Bot 发过一条消息这样才有初始 context_token然后bot WeChatBot.from_config(wechat.json) # 主动推送一条消息 bot.refresh_and_send( 智能选股报告 2026-03-24 ━━━━━━━━━━━━━━ #1 AVGO $310.51 [分歧] 趋势↓ RSI:45 #2 NVDA $172.70 [分歧] 趋势↓ RSI:37 #3 AAPL $247.99 [看空] RSI:24 超卖! by QuantByQlib 6-Agent )成功的话微信上会立刻收到这条消息。注意sendMessage的响应体是{}空对象就是成功别以为没返回就是失败。如果你想做交互式 Bot加一个listen循环def handler(text, from_user): if text.startswith(分析): symbols text.split()[1:] return f收到正在分析 {symbols}... elif text 帮助: return 发送 分析 NVDA AAPL 开始分析 return None def listen(self, handler): while True: try: msgs self.get_updates() for msg in msgs: ct msg.get(context_token, ) from_user msg.get(from_user_id, ) text for item in msg.get(item_list, []): if item.get(type) 1: text item.get(text_item, {}).get(text, ) if ct and text: reply handler(text, from_user) if reply: self.send(reply, tofrom_user, context_tokenct) except Exception as e: logging.error(flisten error: {e}) time.sleep(5)7. 本篇常见错排查HTTP 200 不等于成功ilink 最坑的地方就是静默失败——返回 200 {}但消息根本没投递。下面这张表是我踩了两天坑总结出来的报表现象解法200 但不投递缺client_id每条消息生成唯一 UUID200 但不投递缺message_type固定传 2BOT200 但不投递缺message_state固定传 2FINISH200 但不投递缺base_info传{channel_version: 1.0.3}偶发超时缺Content-Length手动算 UTF-8 字节长度200 但不投递缺context_token先getUpdates拿响应体{}这是成功不是失败sendMessage无返回值get_qrcode_status超时正常行为重试即可二维码expired重新调get_bot_qrcode还有一个隐藏坑from_user_id必须传空字符串不是不传也不是传你的 ID。服务端靠这个字段判断消息方向。8. 边界与后续这个方案能做什么、不能做什么能做的个人微信 1 对 1 收发文本、图片、文件、视频媒体要 AES-128-ECB 加密后传 CDN、持续运行的交互 Bot、定时推送通知。不能做的群消息ilink 只支持 direct chat、未经扫码登录直接调用、用户没给 Bot 发过消息就主动推拿不到初始 context_token。注意事项token 有效期目前测试数天内正常但这是腾讯内部平台协议可能随时变更。建议把channel_version做成配置项方便后续跟版本。如果你 Bot 背后要接大模型做智能回复模型调用这一层建议走 TaoToken 的 API 通道统一管理Key 集中配置别散落在各个脚本里。接入文档在 https://taotoken.net/api 模型对话调试入口在 https://taotoken.net/api-keys 长期跑编码类 Agent 任务的话可以看 Coding Plan 页面。整个逆向过程最大的收获其实不是代码而是三个认知npm 包是个宝库很多「闭源」服务的官方 SDK 都以源码形式发布在 npm 上TypeScript 类型定义就是最好的 API 文档HTTP 200 不等于成功ilink 的sendMessage无论消息是否投递都返回 200 {}「可选字段」可能是必填的官方文档只列了to_user_id、context_token、item_list但client_id、message_type、message_state才是消息路由的关键。先读源码再写代码别猜。