A small, self-hosted daemon that speaks the WeChat iLink bot protocol directly and exposes a clean per-account REST API.
查找文件
仓库文件(优先显示最新提交)
文件名 最新提交消息 最新提交日期
TMYTiMidlY a037f51916 docs: translate README to Chinese, bun-first run instructions
Rewrite the root README in Chinese (the project's only documentation file). Switch quick-start to bun (bun install / bun bin/ilinkd.js ...), note node also works, refresh the tests badge to 45, and point internal anchors at the Chinese headings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-26 03:17:38 +08:00
bin feat: systematic refactor to multi-account, hardened receive/send 2026-06-25 20:17:43 +08:00
src feat: systematic refactor to multi-account, hardened receive/send 2026-06-25 20:17:43 +08:00
test feat: systematic refactor to multi-account, hardened receive/send 2026-06-25 20:17:43 +08:00
.env.example feat: systematic refactor to multi-account, hardened receive/send 2026-06-25 20:17:43 +08:00
.gitignore Initial commit: ilinkd — minimal local WeChat iLink daemon 2026-06-14 12:36:26 +08:00
bun.lock build: standardize on bun; drop package-lock.json, update bun.lock 2026-06-26 02:52:33 +08:00
LICENSE Initial commit: ilinkd — minimal local WeChat iLink daemon 2026-06-14 12:36:26 +08:00
package.json feat: systematic refactor to multi-account, hardened receive/send 2026-06-25 20:17:43 +08:00
README.md docs: translate README to Chinese, bun-first run instructions 2026-06-26 03:17:38 +08:00

ilinkd

一个小巧、自托管的守护进程,直接讲 WeChat iLink 机器人协议,并暴露干净的「按账号」REST API。

扫码绑定一个或多个微信账号,然后通过普通 HTTP 收发消息(文本 · 图片 · 语音 · 文件 · 视频)。

License Node Runtime Tests WeChat Status


项目目标

ilinkd个人自托管的「微信 ↔ 你自己的程序」桥。微信的 "iLink" 功能让一个账号充当机器人; 它的官方客户端(openilink-hub)是一个完整的多租户平台,带 Web UI、账号体系、应用、市场、分析和 推送。对只想让自己的代码读写微信消息的单用户来说,那套太重了。

ilinkd 反其道而行:

  • 只做一件事。 绑定微信账号,然后用一个极小的本地 HTTP API 暴露收/发。无 Web UI、无数据库、 无任何多租户。
  • 轻量。 纯 Node、少量小依赖、JSON 文件存状态。毫秒级启动。
  • 易集成。 GET /messages(或挂起的 GET /stream)收,POST /send 发。守护进程替你处理 协议里那些麻烦事——游标、24 小时回复窗口、媒体加密、语音转码、去重、重试——你的代码不用碰。
  • 直连微信。 中间没有 hub;ilinkd 自己实现原始 iLink 协议。
  • 只拉不推。 ilinkd 从不主动回连你的服务。你来轮询它(或挂着一个流式连接)。少一处要操心的安全面。

它面向个人自动化:通知、聊天驱动的脚本、给 AI agent 当微信「嘴和耳朵」、"build 完 ping 我" 之类的钩子。

Note

为什么是守护进程而不是无状态库? 因为 iLink 的 context_token。给某人发消息,必须引用 最近一条入站消息带来的 context_token,有效期约 24 小时。所以必须有个常驻进程盯着入站消息、 记住每个联系人最新的 token。这正是 ilinkd 的核心职责——也是为什么 POST /send {to, text} 能 「开箱即用」、你不用自己管 token。

特性

  • 👥 多账号。 同时绑定多个微信账号。每个账号有自己的接收循环、状态目录和 /accounts/<id>/… 路由;可配置的默认账号支撑扁平别名(/messages/send…)。
  • 📥 两种接收方式。 游标轮询——GET /accounts/<id>/messages?since=<seq>,可选长轮询 (wait)——挂起的 SSE 流GET /accounts/<id>/stream),连接一直挂着、来一条吐一条。 curl -N …/stream 会一直挂到有消息。
  • 📤 发送 POST /send {to, text}——自动查 24 小时 context_token;没有有效窗口时返回明确的 409。长文本按序分片;发送按账号串行,瞬时失败时用固定 client_id 重试,重试不会 重复投递。
  • 🖼️ 媒体双向——图片、文件、视频。加密 CDN 上传/下载(AES-128-ECB)透明处理;入站媒体解密成 blob,从 /media/<id> 取。
  • 🎙️ 语音,做到位——入站语音同时给你微信的语音转文字音频:原始 SILK 加上解码好、 可直接播放的 WAV。出站时,WAV/PCM 文件会被编码成微信 SILK。(由 silk-wasm 驱动。)
  • ⌨️ 正在输入提示——POST /typing {to} 向联系人显示「正在输入…」。
  • 定时/延迟发送——给 POST /sendsendAt(ISO 或毫秒时间戳)或 delaySeconds;守护 进程持久化它、到点再发。
  • 🛡️ 稳健接收——入站去重(丢弃重复投递)、持久的单调 seq、可轮转的追加日志,以及 错误分类:会话被踢/token 失效会被识别并把账号标记为需重新登录,而不是空转。
  • 🔒 默认本地——绑 127.0.0.1;一个可选的全局 bearer 守住整个 API;密钥以 chmod 600 原子写入。

能力矩阵(均经端到端验证)

方向 文本 图片 语音 文件/视频
接收(对端 → 你) 转写 + WAV + 原始 SILK
发送(你 → 对端) WAV/PCM → SILK

工作原理

ilinkd 实现原始 iLink 机器人协议(官方 hub 用的那些 https://ilinkai.weixin.qq.com/ 下的 HTTP 端点),并把它包进几个小层里,每个绑定的账号一套独立组件:

                ┌──────────────────────────── ilinkd ────────────────────────────┐
  WeChat iLink  │  ilink/      protocol: QR login · getupdates · sendmessage ·    │   your code
  servers   ◄───┤            getuploadurl · CDN AES up/download · SILK<->WAV ·     │      │
  + CDN         │            sendtyping · error classification                    │   HTTP/JSON
            ───►│  loop/       per-account long-poll → dedup → record token →      │◄────►│
                │            download media → append to seq log                    │      │
                │  send/       per-account serial queue: chunk · retry · schedule  │      │
                │  state/      AccountsManager + per-account JSON/JSONL store       │      │
                │  server/     REST: /accounts/<id>/{messages,stream,send,…}        │      │
                └─────────────────────────────────────────────────────────────────┘
  • src/ilink/ —— 协议层:QR 登录、getupdates 长轮询、消息发送、带 AES 的媒体上传/下载、 SILK 语音转码、sendtyping,以及一套错误分类(transient · rate_limited · auth_session · fatal)。从 cyberboss 的微信适配器中提取,并对齐上游 openilink-sdk-go
  • src/loop.js + src/loop-manager.js —— 每账号一个后台接收循环:长轮询、推进游标、去重、 记录每个发送者的 context_token、下载并解密媒体(把语音解成 WAV)、追加到日志。鉴权/会话终止类 错误会停止该循环并置 needsRelogin
  • src/send/ —— 每账号一条串行发送流水线(文本分片、幂等重试、媒体),外加一个触发延迟发送的调度器。
  • src/state/ —— AccountsManager 管理各账号目录;每个 Store 把状态以普通文件原子写入 (见 状态布局)。
  • src/server/ —— Node http REST 接口(find-my-way 路由、zod 校验);绑定本地、可选 bearer。

设计借鉴

下面这些稳健性思路是从两个现有 iLink 项目里借鉴设计(不是复制代码)来的:

  • 来自 cyberboss 入站去重缓存(按服务端 message_id)、长出站文本分片,以及 typing-ticket 流程(getconfigsendtyping)。
  • 来自 openilink-hub 把被踢/失效的会话当作终止态「需重新登录」、按账号串行发送, 以及解码 SILK 时以消息的语音 sample_rate 为准。

环境要求

本项目用 bun 作为包管理器(仓库里是 bun.lock,不要再用 npm install,否则会多出一个 package-lock.json):

bun install

快速开始

运行时 bun、Node 都支持。下面用 bun;把 bun 换成 node 亦可(如 node bin/ilinkd.js start)。

# 1) 绑定一个微信账号 —— 扫描终端里打印的二维码(可重复绑定多个)
bun bin/ilinkd.js login

# 2) 启动守护进程 —— HTTP API + 每账号接收循环 + 调度器
bun bin/ilinkd.js start
# → ilinkd listening on http://127.0.0.1:9700

# 查看 / 管理账号
bun bin/ilinkd.js accounts
bun bin/ilinkd.js status            # 或:status <accountId>
bun bin/ilinkd.js unbind <accountId>
# 3) 跟它对话(扁平路由走默认账号)
curl -N  localhost:9700/stream                                  # 挂起并流式接收新消息
curl -s 'localhost:9700/messages?since=0&wait=30000'            # 或游标长轮询
curl -s  localhost:9700/send -H 'Content-Type: application/json' \
     -d '{"to":"<peer-user-id>","text":"hello from ilinkd"}'    # 发送

# 显式指定某个账号
curl -s 'localhost:9700/accounts/<accountId>/messages?since=0'

也可以用包脚本:bun run start / bun run login / bun run accounts / bun run statusbun test 跑测试(44+1 项,node 与 bun 下均通过),bun run check 做语法检查。

Warning

iLink 每个账号只允许一个活跃会话。在这里绑定会把该账号的 iLink 会话从任何其他客户端 (包括正在运行的 openilink-hub)那里抢过来。

也可以不用终端、走 HTTP 绑定:GET /qr 返回一个 qrUrlliteapp.weixin.qq.com 链接),你把 它渲染成二维码让微信扫,然后轮询 GET /qr/status?flow=<id> 直到 confirmed。通过正在运行的守护 进程的 /qr 绑定会热添加账号(其循环立即启动)。

配置

全部通过环境变量配置,且都可选。完整带注释列表见 .env.example。要点:

变量 默认 含义
ILINKD_HOST 127.0.0.1 REST 绑定地址(设成非回环地址即对外暴露)
ILINKD_PORT 9700 REST 端口
ILINKD_TOKEN (空) 可选 bearer,守整个 API
ILINKD_ALLOW_INSECURE_REMOTE 0 允许在没有 token 时绑定非回环地址
ILINKD_STATE_DIR ~/.ilinkd 所有「按账号」状态的根目录
ILINKD_CHANNEL_VERSION (内置) 微信抬版本号时覆盖 iLink channel_version
ILINKD_SEND_MAX_CHARS 3800 超过此长度的出站文本会被分片
ILINKD_DEDUP_TTL_MS / ILINKD_DEDUP_MAX 600000 / 5000 入站去重缓存
ILINKD_MESSAGES_MAX_BYTES / ILINKD_MESSAGES_KEEP 16 MiB / 5 messages.jsonl 轮转
ILINKD_CONTEXT_TTL_MS 86400000 context_token 有效期(24 小时)

设了 ILINKD_TOKEN 后,每个请求都要带 Authorization: Bearer <token>

API

路由按账号组织为 /accounts/<id>/…扁平别名/messages/stream/send/typing/contacts/media/<id>/scheduled/unbind)解析到默认账号

守护进程级

  • GET /status —— 所有账号 + 各自的实时循环状态、默认账号、监听地址。
  • GET /accounts —— 列出已绑账号。
  • GET /qr / GET /qr/status?flow=<id> —— 发起 / 轮询一个 QR 绑定流(新增一个账号)。

按账号(/accounts/<id>/…,或扁平 → 默认账号)

  • GET …/status —— 账号信息 + 接收循环状态(runningneedsRelogin、计数器)。
  • GET …/messages?since=<seq>&wait=<ms>&limit=<n> —— 游标轮询;wait 做长轮询(0–60000 毫秒)。
  • GET …/stream?since=<seq> —— Server-Sent Events;挂起并吐出每条新消息(id: 即 seq)。 省略 since 则只收此后的新消息。
  • POST …/send —— { to, text }{ to, filePath | fileBase64, fileName }。可选 sendAt(ISO / 毫秒时间戳)或 delaySeconds 会改成定时(→ 202)。对端 24 小时内没给你发过消息 则返回 409 no_active_window;账号未绑定则 409 not_bound
  • POST …/typing —— { to };显示正在输入提示(尽力而为)。
  • GET …/contacts —— 当前有有效发送窗口的联系人。
  • GET …/media/<mediaId> —— 下载已存的附件 blob(已解密)。
  • GET …/scheduled / DELETE …/scheduled/<id> —— 列出 / 取消待发的定时消息。
  • POST …/unbind —— 丢弃该账号(停其循环、删其状态)。

对入站语音消息,text 是微信的语音转文字,附件把音频带两份——url 是解码后的 WAV, rawUrl 是原始 SILK。

状态布局

一切以普通文件持久化在 $ILINKD_STATE_DIR(默认 ~/.ilinkd),原子写入(临时文件 + 重命名):

meta.json                         { defaultAccountId }
accounts/<accountId>/
  account.json    { accountId, token, baseUrl, userId, boundAt }   (chmod 600)
  cursor.txt      最新 get_updates_buf(长轮询游标)
  seq.txt         消息 seq 高水位(轮转后不丢号)
  context-tokens.json   { <from>: { token, updatedAt } }           (chmod 600)
  scheduled.json  待发的定时消息
  messages.jsonl  追加式归一化入站日志(+ .1 … .K 轮转段)
  media/<id>      解密后的附件 blob

account.json 里的 token 是你的 iLink 机器人凭证——把状态目录当机密对待。

安全

  • 默认绑 localhost;要对外暴露就设 ILINKD_TOKEN 要求 bearer。没有 token 时 ilinkd 拒绝绑定 非回环地址,除非你设 ILINKD_ALLOW_INSECURE_REMOTE=1。bearer 是单一全局闸——没有按账号的访问 控制(这是个人守护进程)。
  • token 以 chmod 600 原子写入;保持 $ILINKD_STATE_DIR 私密。
  • ilinkd 只跟微信自家的 API/CDN 主机和你的本地客户端通信——绝不向任何第三方推送。

致谢

  • cyberbossWenXiaoWendy/cyberboss,AGPL-3.0)—— iLink 协议层从其微信(weixin)渠道 适配器中提取,每个改写过的文件都保留了出处头。
  • openilink-sdk-go —— 用于对齐 ilinkd 协议细节(请求头、base_infoupload_full_url、 语音项、typing ticket)的权威参考。
  • openilink-hub —— ilinkd 自用时替代的那个更重的 hub,也是语音采样率思路和会话失效处理模式的来源。
  • silk-wasm —— SILK ⇄ PCM/WAV 编解码。qrcode-terminal —— 终端二维码。 find-my-way —— 路由。zod —— 校验。lru-cache —— 去重。 write-file-atomic —— 防崩溃写入。

许可证

AGPL-3.0-only —— ilinkd 的协议层衍生自 AGPL 许可的 cyberboss,因此适用相同条款。见 LICENSE,以及 src/ilink/* 里指明具体上游文件的出处头。