@openilink/openilink-sdk-node
v0.4.2
Published
Node.js SDK for the OpenILink Bot API.
Readme
@openilink/openilink-sdk-node
微信 iLink Bot API 的 Node.js SDK。
npm install @openilink/openilink-sdk-node特性
- 扫码登录,支持扫码/过期回调
- 长轮询消息监听,自动重试与退避,动态超时
- 主动推送(自动缓存
context_token) - 发送图片、视频、文件,MIME 自动路由
- CDN 加密上传/下载(AES-128-ECB)
- 语音消息解码(可插拔 SILK 解码器 + WAV 封装)
- 输入状态指示器、Bot 配置
- 可注入自定义
fetch实现,便于测试、代理或自定义 Agent fetch+AbortController传输层,便于在 Node 18+ 环境直接使用- 结构化错误类型(
APIError、HTTPError、NoContextTokenError、RequestError) - 零运行时依赖
要求
- Node.js 18+
快速开始
import { Client, extractText } from "@openilink/openilink-sdk-node";
import { readFileSync, writeFileSync } from "node:fs";
const client = new Client("");
const result = await client.loginWithQr({
on_qrcode: (img) => {
console.log(`请扫码:\n${img}`);
},
on_scanned: () => {
console.log("已扫码,请在微信中确认...");
},
});
if (!result.connected) {
throw new Error(result.message);
}
console.log(`已连接 BotID=${result.bot_id ?? ""}`);
const syncBufFile = new URL("./sync_buf.dat", import.meta.url);
let savedBuf = "";
try {
savedBuf = readFileSync(syncBufFile, "utf8");
} catch {}
await client.monitor(
async (message) => {
const text = extractText(message);
if (!text) {
return;
}
await client.push(String(message.from_user_id), `收到: ${text}`);
},
{
initial_buf: savedBuf,
on_response: (response) => {
console.log(response.sync_buf ?? "");
console.log(response.raw_response?.status_code ?? 0);
},
on_buf_update: (buf) => {
writeFileSync(syncBufFile, buf);
},
},
);API
创建客户端
import { Client } from "@openilink/openilink-sdk-node";
const client = new Client(token, {
base_url: "https://custom.endpoint.com",
cdn_base_url: "https://custom.cdn.com/c2c",
bot_type: "3",
version: "1.0.2",
route_tag: "my-route-tag",
fetch_impl: async (input, init) => {
return fetch(input, init);
},
silk_decoder: async (silkData, sampleRate) => {
return decodeSilkSomehow(silkData, sampleRate);
},
});扫码登录
const result = await client.loginWithQr({
on_qrcode: (imgContent) => {},
on_scanned: () => {},
on_expired: (attempt, max) => {},
});登录成功后,客户端的 token 和 base_url 会自动更新。
接收消息
import { extractText } from "@openilink/openilink-sdk-node";
await client.monitor(
async (message) => {
const text = extractText(message);
// message.from_user_id, message.context_token, message.item_list
},
{
initial_buf: savedBuf,
on_response: (response) => {},
on_buf_update: (buf) => {},
on_error: (error) => {},
on_session_expired: () => {},
should_continue: () => true,
},
);monitor() 会自动缓存每个用户的 context_token,供 push() 使用。服务端返回的 longpolling_timeout_ms 会被自动采纳;成功响应里的 sync_buf 和原始 HTTP 元数据可通过 on_response / raw_response 读取。
发送文本
await client.sendText(userId, "你好", contextToken);
await client.push(userId, "这是一条定时通知");发送媒体
import { MEDIA_IMAGE } from "@openilink/openilink-sdk-node";
import { readFileSync } from "node:fs";
const data = readFileSync("photo.jpg");
// 高级接口:自动识别 MIME 类型 -> 上传 -> 发送
await client.sendMediaFile(userId, contextToken, data, "photo.jpg", "看看这张图");
// 分步操作:上传 -> 发送
const uploaded = await client.uploadFile(data, userId, MEDIA_IMAGE);
await client.sendImage(userId, contextToken, uploaded);
await client.sendVideo(userId, contextToken, uploaded);
await client.sendFileAttachment(userId, contextToken, "report.pdf", uploaded);下载媒体
import { ITEM_TYPE_IMAGE, ITEM_TYPE_VOICE } from "@openilink/openilink-sdk-node";
for (const item of message.item_list ?? []) {
switch (item.type) {
case ITEM_TYPE_IMAGE:
await client.downloadFile(
item.image_item?.media?.encrypt_query_param ?? "",
item.image_item?.media?.aes_key ?? "",
);
break;
case ITEM_TYPE_VOICE:
await client.downloadVoice(item.voice_item);
break;
}
}语音解码
SDK 通过可插拔的 silk_decoder 支持语音消息解码,保持对外部解码器的开放性:
import { buildWAV, Client } from "@openilink/openilink-sdk-node";
const client = new Client(token, {
silk_decoder: async (silkData, sampleRate) => {
return decodeSilkSomehow(silkData, sampleRate);
},
});
const wav = await client.downloadVoice(voiceItem);也可以单独使用 WAV 封装:
const wav = buildWAV(pcmBytes, 24_000, 1, 16);其他
import {
CANCEL_TYPING,
TYPING,
ExtensionFromMIME,
MIMEFromFilename,
extractText,
isImageMIME,
isMediaItem,
isVideoMIME,
} from "@openilink/openilink-sdk-node";await client.sendTyping(userId, typingTicket, TYPING);
await client.sendTyping(userId, typingTicket, CANCEL_TYPING);
const config = await client.getConfig(userId, contextToken);
const text = extractText(message);
const media = isMediaItem(message.item_list?.[0]);
const mime = MIMEFromFilename("photo.jpg"); // image/jpeg
const ext = ExtensionFromMIME("image/jpg"); // .jpg
const image = isImageMIME("image/png"); // true
const video = isVideoMIME("video/mp4"); // true错误处理
import {
APIError,
HTTPError,
NoContextTokenError,
RequestError,
} from "@openilink/openilink-sdk-node";
try {
await client.push(userId, "hello");
} catch (error) {
if (error instanceof APIError && error.isSessionExpired()) {
// 需要重新登录
} else if (error instanceof HTTPError) {
console.log(error.statusCode);
} else if (error instanceof NoContextTokenError) {
// 该用户尚未发送过消息,无法主动推送
} else if (error instanceof RequestError && error.isTimeout()) {
// 请求超时
}
}常量
import {
ITEM_TYPE_FILE,
ITEM_TYPE_IMAGE,
ITEM_TYPE_TEXT,
ITEM_TYPE_VIDEO,
ITEM_TYPE_VOICE,
MEDIA_FILE,
MEDIA_IMAGE,
MEDIA_VIDEO,
MEDIA_VOICE,
MESSAGE_STATE_FINISH,
MESSAGE_STATE_GENERATING,
MESSAGE_STATE_NEW,
MESSAGE_TYPE_BOT,
MESSAGE_TYPE_USER,
} from "@openilink/openilink-sdk-node";
MEDIA_IMAGE; // 1
MEDIA_VIDEO; // 2
MEDIA_FILE; // 3
MEDIA_VOICE; // 4
MESSAGE_TYPE_USER; // 1
MESSAGE_TYPE_BOT; // 2
ITEM_TYPE_TEXT; // 1
ITEM_TYPE_IMAGE; // 2
ITEM_TYPE_VOICE; // 3
ITEM_TYPE_FILE; // 4
ITEM_TYPE_VIDEO; // 5
MESSAGE_STATE_NEW; // 0
MESSAGE_STATE_GENERATING; // 1
MESSAGE_STATE_FINISH; // 2许可证
MIT
