npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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+ 环境直接使用
  • 结构化错误类型(APIErrorHTTPErrorNoContextTokenErrorRequestError
  • 零运行时依赖

要求

  • 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