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

@sochatlive/bot-sdk

v0.2.4

Published

Official Node.js SDK for SoChat Bot API and Webhooks

Readme

@sochatlive/bot-sdk

npm version npm downloads license

SoChat 机器人开放平台的 官方 Node.js / TypeScript SDK,封装:

  • Bot APIAuthorization: Bearer <bot_token> 调用网关 /api/v1/bots/**
  • Webhook:校验 X-SoChat-Signature-V2(HMAC-SHA256,带 X-SoChat-Timestamp 防重放)、update_id 幂等、高阶事件路由(V1 头并行下发以兼容旧实现)

已发布到 npm:@sochatlive/bot-sdk · 当前版本 0.1.2 · npm install @sochatlive/bot-sdk

设计说明见仓库 docs/机器人开放平台规划.md

要求

  • Node.js ≥ 18(依赖全局 fetch

安装

npm install @sochatlive/bot-sdk
# 或
pnpm add @sochatlive/bot-sdk

本仓库已将 botsdk/nodejs 加入根目录 pnpm workspace,在仓库根执行 pnpm install 即可安装依赖。单独构建/测试:

pnpm --filter @sochatlive/bot-sdk run build
pnpm --filter @sochatlive/bot-sdk run test

对外即可在任意项目中:

npm install @sochatlive/bot-sdk
# 或
pnpm add @sochatlive/bot-sdk

5 分钟快速上手

  1. 在 SoChat 开放平台 / App 提交机器人申请,由平台 Admin 审核通过后取得 sbot_... Token。
  2. 为机器人配置 HTTPS Webhook,并设置 secret_token(与下文代码中 secretToken 一致)。
  3. 编写 Webhook 服务(需保留 原始 JSON 字节 用于验签,见下文「Webhook 签名校验」)。
import { SoChatBot } from '@sochatlive/bot-sdk';

const bot = new SoChatBot({
  token: process.env.SOCHAT_BOT_TOKEN!,
  secretToken: process.env.SOCHAT_WEBHOOK_SECRET!,
  baseUrl: process.env.SOCHAT_API_BASE // 例如 https://gateway.example/api/v1
});

bot.on('message', async ctx => {
  if (ctx.message.text) {
    await ctx.reply(`你说: ${ctx.message.text}`);
  }
});

await bot.start({ port: 8787, path: '/webhook' });

将公网 HTTPS 反代到 http://<host>:8787/webhook,在开放平台把 Webhook URL 设为 https://你的域名/webhook。在会话中 @机器人 或发 /command,即可在控制台看到 Update 并收到自动回复。

环境变量

| 变量 | 说明 | |------|------| | SOCHAT_API_BASE | 网关根 URL,须含 /api/v1,如 https://gw.example.com/api/v1 | | SOCHAT_BOT_TOKEN | 审核通过后签发的 Bot Token(sbot_ 前缀) | | SOCHAT_WEBHOOK_SECRET | setWebhook 时配置的 secret_token |

未设置 SOCHAT_API_BASE 时,客户端默认使用占位地址 https://api.sochat.example/api/v1请务必改为真实网关

低阶客户端 SoChatBotClient

不跑 Webhook、仅调 Bot API 时使用:

import { SoChatBotClient } from '@sochatlive/bot-sdk';

const client = new SoChatBotClient({
  token: process.env.SOCHAT_BOT_TOKEN!,
  baseUrl: process.env.SOCHAT_API_BASE
});

await client.setWebhook({
  url: 'https://example.com/webhook',
  secret_token: 'your-secret',
  allowed_updates: ['message']
});

const me = await client.getMe();
await client.sendMessage({
  chat_id: '<conversationId>',
  text: 'hello',
  reply_markup: {
    inline_keyboard: [[{ text: '收到', callback_data: 'ack' }]]
  }
});

await client.setMyCommands({
  commands: [
    { command: 'start', description: '开始使用' },
    { command: 'help', description: '帮助' }
  ],
  scope: 'all_private_chats'
});

主要方法

| 方法 | 说明 | |------|------| | getMe() | GET /bots/me | | sendMessage | 文本消息(可选 reply_markup:InlineKeyboard、ReplyKeyboard、remove_keyboard) | | setMyCommands / getMyCommands / deleteMyCommands | 命令菜单(按 scopelanguage_code 分组,与 Telegram 语义对齐) | | sendPhoto / sendDocument / sendVideo / sendAudio | file_id 为上传完成后的文件 ID | | sendLocation | 经纬度 | | editMessage / editMessageReplyMarkup / deleteMessage | 仅允许操作本机器人发送的消息;editMessageReplyMarkup 可更新或清空 InlineKeyboard | | answerCallbackQuery | 回应 callback_query 按钮点击(toast / alert / url) | | answerInlineQuery | 应答 Inline Mode 查询结果 | | answerFriendRequest | 处理用户发起的 pending 好友申请(friend_request_mode = manualfriendship_id + accept/reject) | | getFriendRequests | 列出待处理好友申请(GET /bots/getFriendRequests,与 DB pending 同源) | | setMyFriendRequestMode / getMyFriendRequestMode | 配置 / 读取好友申请策略(auto_accept / auto_reject / manual) | | kickChatMember / banChatMember / unbanChatMember | 群治理(chat_id 为群会话 Mongo _id;机器人须为群主或群管理员;ban 为先踢后黑,非原子) | | setWebhook / deleteWebhook | Webhook 配置 | | issueUploadCredentials / completeUpload | S3 直传两步 | | sendPhotoFromFile / sendDocumentFromFile | 本地路径 → 上传 → 发送 | | verifyToken() | 公开接口校验 Token |

InlineKeyboard / Callback Query

bot.command('menu', async ctx => {
  await ctx.reply('请选择:', {
    reply_markup: {
      inline_keyboard: [
        [
          { text: '点赞', callback_data: 'like' },
          { text: '文档', url: 'https://open.sochatlive.com/' }
        ]
      ]
    }
  });
});

bot.on('callback_query', async ctx => {
  await ctx.answerCallback({ text: `收到: ${ctx.callbackQuery?.data}` });
});

Webhook 签名校验原理

平台投递时(见 WebhookDeliveryWorker)会同时下发 V1 与 V2 两份签名头,便于平滑升级:

| 头 | 算法 | 防 replay | 状态 | | --- | --- | --- | --- | | X-SoChat-Signature | sha256=HMAC(secret, body) | 否 | V1,兼容期保留,deprecated | | X-SoChat-Signature-V2 | sha256=HMAC(secret, "${ts}.${body}") | (结合 X-SoChat-Timestamp 判 5min 时窗) | 推荐使用 | | X-SoChat-Timestamp | unix 秒 | — | V2 必带 | | X-SoChat-Update-Id | 透传 update.update_id | — | 用于幂等 |

SDK 的 verifyWebhookSignature / processWebhook / webhookMiddleware 默认优先 V2,缺失 V2 时回退 V1。完成全网升级后可在 verify: { requireV2: true } 强制只接受 V2,彻底闭合 replay 风险。

服务端必须用 与计算签名时完全相同的字节 验签。若使用 express.json() 默认中间件,会丢失原始字节,签名将无法对齐。推荐之一:

方式 A:express.raw

import express from 'express';
import { SoChatBot } from '@sochatlive/bot-sdk';

const app = express();
const bot = new SoChatBot({ token: '...', secretToken: '...' });

app.post('/webhook', express.raw({ type: 'application/json' }), bot.webhookCallback());

方式 B:express.jsonverify 保存 rawBody

app.use(
  express.json({
    verify: (req: express.Request & { rawBody?: Buffer }, _res, buf) => {
      req.rawBody = buf;
    }
  })
);
// 再挂载 bot.webhookCallback(),SDK 会优先读取 req.rawBody

手写验签示例(V2,推荐):

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyV2(
  secret: string,
  rawBody: Buffer,
  sigHeader: string | undefined,
  tsHeader: string | undefined
) {
  if (!sigHeader?.startsWith('sha256=') || !tsHeader) throw new Error('bad sig');
  const ts = Number(tsHeader);
  if (!Number.isFinite(ts) || Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) {
    throw new Error('replay window');
  }
  const expected = Buffer.from(sigHeader.slice(7), 'hex');
  const actual = createHmac('sha256', secret)
    .update(`${ts}.`)
    .update(rawBody)
    .digest();
  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
    throw new Error('bad sig');
  }
}

V1 验签(仅在无 V2 头时回退):

function verifyV1(secret: string, rawBody: Buffer, header: string | undefined) {
  if (!header?.startsWith('sha256=')) throw new Error('bad sig');
  const expected = Buffer.from(header.slice(7), 'hex');
  const actual = createHmac('sha256', secret).update(rawBody).digest();
  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
    throw new Error('bad sig');
  }
}

多媒体:本地 / Buffer / URL

import {
  SoChatBotClient,
  sendPhotoFromBuffer,
  sendPhotoFromUrl,
  sendVideoFromUrl
} from '@sochatlive/bot-sdk';
import { readFile } from 'node:fs/promises';

const client = new SoChatBotClient({ token: '...', baseUrl: '...' });

await client.sendPhotoFromFile(chatId, './a.png', { caption: '本地文件' });

const buf = await readFile('./a.png');
await sendPhotoFromBuffer(client, chatId, buf, { fileName: 'a.png', fileType: 'image/png' });

await sendPhotoFromUrl(client, chatId, 'https://example.com/p.png', { fileName: 'p.png' });
await sendVideoFromUrl(client, chatId, 'https://example.com/v.mp4', {
  fileName: 'v.mp4',
  fileType: 'video/mp4',
  caption: '可选说明',
  duration: 10
});

错误码说明(常见)

平台与网关可能返回 success: false 或 HTTP 4xx/5xx,SDK 统一抛出 SoChatApiError(含 statusCodemessagecode 若存在)。文档与产品侧常见语义如下(具体以网关响应为准):

| 语义 | 可能场景 | |------|----------| | invalid_token / 401 | Token 错误、吊销、格式不对 | | bot_disabled / 403 | 机器人未审核通过、已停用 | | chat_forbidden / 4xx | 无权限向目标会话发消息 | | rate_limited / 429 | 触发发送配额 | | webhook_invalid | Webhook URL 非 HTTPS 或配置无效(配置接口返回消息) |

幂等与重复投递

平台重试 Webhook 时 保持同一 update_idSoChatBot 内置 LRU(默认 1024 条)去重:重复 update_id 返回 200 ok 且不再执行业务 handler。可传入自定义 DedupeStore(见 processWebhook / 源码 webhook.ts)。

示例脚本

examples/ 目录:

  • echo-bot.ts — 回声
  • slash-command.ts/start/help
  • send-photo.ts — 本地图片上传并发送
cd botsdk/nodejs
pnpm install
npx tsx examples/echo-bot.ts

构建

pnpm run build   # 输出 dist/(ESM + CJS + 类型声明)
pnpm run test    # Vitest

License

MIT