- JavaScript 100%
| 文件名 | 最新提交消息 | 最新提交日期 |
|---|---|---|
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> |
||
| bin | ||
| src | ||
| test | ||
| .env.example | ||
| .gitignore | ||
| bun.lock | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
ilinkd
一个小巧、自托管的守护进程,直接讲 WeChat iLink 机器人协议,并暴露干净的「按账号」REST API。
扫码绑定一个或多个微信账号,然后通过普通 HTTP 收发消息(文本 · 图片 · 语音 · 文件 · 视频)。
项目目标
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 /send加sendAt(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/—— NodehttpREST 接口(find-my-way 路由、zod 校验);绑定本地、可选 bearer。
设计借鉴
下面这些稳健性思路是从两个现有 iLink 项目里借鉴设计(不是复制代码)来的:
- 来自 cyberboss: 入站去重缓存(按服务端
message_id)、长出站文本分片,以及 typing-ticket 流程(getconfig→sendtyping)。 - 来自 openilink-hub: 把被踢/失效的会话当作终止态「需重新登录」、按账号串行发送,
以及解码 SILK 时以消息的语音
sample_rate为准。
环境要求
- Node.js ≥ 22 —— 用到内置
fetch和crypto.randomUUID。也能在 Bun 下跑。 - 几个小巧的纯 JS 依赖(无原生编译):
qrcode-terminal(二维码)、silk-wasm(语音)、 find-my-way(路由)、zod(校验)、lru-cache(去重)、write-file-atomic(安全写)。
本项目用 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 status;
bun test 跑测试(44+1 项,node 与 bun 下均通过),bun run check 做语法检查。
Warning
iLink 每个账号只允许一个活跃会话。在这里绑定会把该账号的 iLink 会话从任何其他客户端 (包括正在运行的 openilink-hub)那里抢过来。
也可以不用终端、走 HTTP 绑定:GET /qr 返回一个 qrUrl(liteapp.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—— 账号信息 + 接收循环状态(running、needsRelogin、计数器)。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 主机和你的本地客户端通信——绝不向任何第三方推送。
致谢
- cyberboss(
WenXiaoWendy/cyberboss,AGPL-3.0)—— iLink 协议层从其微信(weixin)渠道 适配器中提取,每个改写过的文件都保留了出处头。 openilink-sdk-go—— 用于对齐 ilinkd 协议细节(请求头、base_info、upload_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/* 里指明具体上游文件的出处头。