@starim-io/bot-sdk
v0.1.6
Published
Official Node.js SDK for StarIM Bot API and Webhooks
Readme
@starim-io/bot-sdk
StarIM 机器人开放平台的 官方 Node.js / TypeScript SDK,封装:
- Bot API:
Authorization: Bearer <bot_token>调用网关/api/v1/bots/** - Webhook:校验
X-StarIM-Signature-V2(HMAC-SHA256,带X-StarIM-Timestamp防重放)、update_id幂等、高阶事件路由
已发布到 npm:
@starim-io/bot-sdk·npm install @starim-io/bot-sdk(版本号以 npm registry 为准)
要求
- Node.js ≥ 18(依赖全局
fetch)
安装
npm install @starim-io/bot-sdk
# 或
pnpm add @starim-io/bot-sdk本仓库已将 botsdk/nodejs 加入根目录 pnpm workspace,在仓库根执行 pnpm install 即可安装依赖。单独构建/测试:
pnpm --filter @starim-io/bot-sdk run build
pnpm --filter @starim-io/bot-sdk run test对外即可在任意项目中:
npm install @starim-io/bot-sdk
# 或
pnpm add @starim-io/bot-sdk5 分钟快速上手
- 在 StarIM 开放平台开发者控制台提交机器人申请,由平台 Admin 审核通过后取得
sbot_...Token。 - 为机器人配置 HTTPS Webhook,并设置
secret_token(与下文代码中secretToken一致)。 - 编写 Webhook 服务(需保留 原始 JSON 字节 用于验签,见下文「Webhook 签名校验」)。
import { StarIMBot } from '@starim-io/bot-sdk';
const bot = new StarIMBot({
token: process.env.STARIM_BOT_TOKEN!,
secretToken: process.env.STARIM_WEBHOOK_SECRET!,
baseUrl: process.env.STARIM_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 并收到自动回复。
环境变量
| 变量 | 说明 |
|------|------|
| STARIM_API_BASE | 网关根 URL,须含 /api/v1,如 https://gw.example.com/api/v1 |
| STARIM_BOT_TOKEN | 审核通过后签发的 Bot Token(sbot_ 前缀) |
| STARIM_WEBHOOK_SECRET | setWebhook 时配置的 secret_token |
未设置 STARIM_API_BASE 时,客户端默认使用占位地址 https://api.starim.example/api/v1,请务必改为真实网关。
低阶客户端 StarIMBotClient
不跑 Webhook、仅调 Bot API 时使用:
import { StarIMBotClient } from '@starim-io/bot-sdk';
const client = new StarIMBotClient({
token: process.env.STARIM_BOT_TOKEN!,
baseUrl: process.env.STARIM_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 | 命令菜单(按 scope、language_code 分组) |
| 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 = manual;friendship_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 |
| listChatVirtualMembers / simulateSendMessage(v0.10.10,增值能力) | 列群内可调度成员 / 以指定成员身份在群里发文本;默认不对外开放,需联系开放平台支持申请开通 |
调度群内成员(v0.10.10,增值能力)
该能力默认不对外开放,需联系开放平台支持申请开通后使用。 首期仅支持文本(
type=text),单条 ≤ 1000 字;所有调用均会被记录用于合规追溯。
import { StarIMBotClient } from '@starim/bot-sdk';
const client = new StarIMBotClient({ token: process.env.STARIM_TOKEN! });
// 1) 列出群内可被调度的成员
const { items, pagination } = await client.listChatVirtualMembers({
chat_id: '<conversation_id>',
limit: 50
});
console.log('candidates', pagination.total, items.slice(0, 3));
// 2) 选一个成员让其在群里发一条文本
if (items[0]) {
await client.simulateSendMessage({
chat_id: '<conversation_id>',
virtual_user_id: items[0].virtual_user_id,
text: '大家好~'
});
}常见错误:
| HTTP | 触发 |
|------|------|
| 400 | 入参缺失 / virtual_user_id 格式不合法 / text 为空或超长 |
| 403 | 能力未开通 / 机器人不在该群 / 目标成员不在该群或状态不可用 |
| 404 | virtual_user_id 不存在 |
InlineKeyboard / Callback Query
bot.command('menu', async ctx => {
await ctx.reply('请选择:', {
reply_markup: {
inline_keyboard: [
[
{ text: '点赞', callback_data: 'like' },
{ text: '文档', url: 'https://open.starim.io/' }
]
]
}
});
});
bot.on('callback_query', async ctx => {
await ctx.answerCallback({ text: `收到: ${ctx.callbackQuery?.data}` });
});Webhook 签名校验原理
平台投递时(见 WebhookDeliveryWorker)下发的签名头:
| 头 | 算法 | 防 replay | 状态 |
| --- | --- | --- | --- |
| X-StarIM-Signature-V2 | sha256=HMAC(secret, "${ts}.${body}") | 是(结合 X-StarIM-Timestamp 判 5min 时窗) | 当前唯一推荐 |
| X-StarIM-Timestamp | unix 秒 | — | V2 必带 |
| X-StarIM-Update-Id | 透传 update.update_id | — | 用于幂等 |
SDK 的 verifyWebhookSignature / processWebhook / webhookMiddleware 默认使用 V2 校验。
服务端必须用 与计算签名时完全相同的字节 验签。若使用
express.json()默认中间件,会丢失原始字节,签名将无法对齐。推荐之一:
方式 A:express.raw
import express from 'express';
import { StarIMBot } from '@starim-io/bot-sdk';
const app = express();
const bot = new StarIMBot({ token: '...', secretToken: '...' });
app.post('/webhook', express.raw({ type: 'application/json' }), bot.webhookCallback());方式 B:express.json 的 verify 保存 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');
}
}多媒体:本地 / Buffer / URL
import {
StarIMBotClient,
sendPhotoFromBuffer,
sendPhotoFromUrl,
sendVideoFromUrl
} from '@starim-io/bot-sdk';
import { readFile } from 'node:fs/promises';
const client = new StarIMBotClient({ 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 统一抛出 StarIMApiError(含 statusCode、message、code 若存在)。文档与产品侧常见语义如下(具体以网关响应为准):
| 语义 | 可能场景 |
|------|----------|
| invalid_token / 401 | Token 错误、吊销、格式不对 |
| bot_disabled / 403 | 机器人未审核通过、已停用 |
| chat_forbidden / 4xx | 无权限向目标会话发消息 |
| rate_limited / 429 | 触发发送配额 |
| webhook_invalid | Webhook URL 非 HTTPS 或配置无效(配置接口返回消息) |
幂等与重复投递
平台重试 Webhook 时 保持同一 update_id。StarIMBot 内置 LRU(默认 1024 条)去重:重复 update_id 返回 200 ok 且不再执行业务 handler。可传入自定义 DedupeStore(见 processWebhook / 源码 webhook.ts)。
示例脚本
在 examples/ 目录:
echo-bot.ts— 回声slash-command.ts—/start、/helpsend-photo.ts— 本地图片上传并发送
cd botsdk/nodejs
pnpm install
npx tsx examples/echo-bot.ts构建
pnpm run build # 输出 dist/(ESM + CJS + 类型声明)
pnpm run test # VitestLicense
MIT © StarIM
