@starim-io/bot-sdk 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) StarIM Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # @starim/bot-sdk
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@starim/bot-sdk.svg)](https://www.npmjs.com/package/@starim/bot-sdk)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@starim/bot-sdk.svg)](https://www.npmjs.com/package/@starim/bot-sdk)
5
+ [![license](https://img.shields.io/npm/l/@starim/bot-sdk.svg)](./LICENSE)
6
+
7
+ StarIM 机器人开放平台的 **官方 Node.js / TypeScript SDK**,封装:
8
+
9
+ - **Bot API**:`Authorization: Bearer <bot_token>` 调用网关 `/api/v1/bots/**`
10
+ - **Webhook**:校验 `X-StarIM-Signature-V2`(HMAC-SHA256,带 `X-StarIM-Timestamp` 防重放)、`update_id` 幂等、高阶事件路由
11
+
12
+ > 已发布到 npm:[`@starim/bot-sdk`](https://www.npmjs.com/package/@starim/bot-sdk) · 当前版本 `0.1.1` · `npm install @starim/bot-sdk`
13
+
14
+ ## 要求
15
+
16
+ - Node.js **≥ 18**(依赖全局 `fetch`)
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ npm install @starim/bot-sdk
22
+ # 或
23
+ pnpm add @starim/bot-sdk
24
+ ```
25
+
26
+ 本仓库已将 `botsdk/nodejs` 加入根目录 **pnpm workspace**,在仓库根执行 `pnpm install` 即可安装依赖。单独构建/测试:
27
+
28
+ ```bash
29
+ pnpm --filter @starim/bot-sdk run build
30
+ pnpm --filter @starim/bot-sdk run test
31
+ ```
32
+
33
+ 对外即可在任意项目中:
34
+
35
+ ```bash
36
+ npm install @starim/bot-sdk
37
+ # 或
38
+ pnpm add @starim/bot-sdk
39
+ ```
40
+
41
+ ## 5 分钟快速上手
42
+
43
+ 1. 在 StarIM **开放平台开发者控制台**提交机器人申请,由平台 Admin **审核通过**后取得 `sbot_...` Token。
44
+ 2. 为机器人配置 **HTTPS** Webhook,并设置 `secret_token`(与下文代码中 `secretToken` 一致)。
45
+ 3. 编写 Webhook 服务(需保留 **原始 JSON 字节** 用于验签,见下文「Webhook 签名校验」)。
46
+
47
+ ```ts
48
+ import { StarIMBot } from '@starim/bot-sdk';
49
+
50
+ const bot = new StarIMBot({
51
+ token: process.env.STARIM_BOT_TOKEN!,
52
+ secretToken: process.env.STARIM_WEBHOOK_SECRET!,
53
+ baseUrl: process.env.STARIM_API_BASE // 例如 https://gateway.example/api/v1
54
+ });
55
+
56
+ bot.on('message', async ctx => {
57
+ if (ctx.message.text) {
58
+ await ctx.reply(`你说: ${ctx.message.text}`);
59
+ }
60
+ });
61
+
62
+ await bot.start({ port: 8787, path: '/webhook' });
63
+ ```
64
+
65
+ 将公网 HTTPS 反代到 `http://<host>:8787/webhook`,在开放平台把 Webhook URL 设为 `https://你的域名/webhook`。在会话中 `@机器人` 或发 `/command`,即可在控制台看到 Update 并收到自动回复。
66
+
67
+ ## 环境变量
68
+
69
+ | 变量 | 说明 |
70
+ |------|------|
71
+ | `STARIM_API_BASE` | 网关根 URL,**须含** `/api/v1`,如 `https://gw.example.com/api/v1` |
72
+ | `STARIM_BOT_TOKEN` | 审核通过后签发的 Bot Token(`sbot_` 前缀) |
73
+ | `STARIM_WEBHOOK_SECRET` | `setWebhook` 时配置的 `secret_token` |
74
+
75
+ 未设置 `STARIM_API_BASE` 时,客户端默认使用占位地址 `https://api.starim.example/api/v1`,**请务必改为真实网关**。
76
+
77
+ ## 低阶客户端 `StarIMBotClient`
78
+
79
+ 不跑 Webhook、仅调 Bot API 时使用:
80
+
81
+ ```ts
82
+ import { StarIMBotClient } from '@starim/bot-sdk';
83
+
84
+ const client = new StarIMBotClient({
85
+ token: process.env.STARIM_BOT_TOKEN!,
86
+ baseUrl: process.env.STARIM_API_BASE
87
+ });
88
+
89
+ await client.setWebhook({
90
+ url: 'https://example.com/webhook',
91
+ secret_token: 'your-secret',
92
+ allowed_updates: ['message']
93
+ });
94
+
95
+ const me = await client.getMe();
96
+ await client.sendMessage({
97
+ chat_id: '<conversationId>',
98
+ text: 'hello',
99
+ reply_markup: {
100
+ inline_keyboard: [[{ text: '收到', callback_data: 'ack' }]]
101
+ }
102
+ });
103
+
104
+ await client.setMyCommands({
105
+ commands: [
106
+ { command: 'start', description: '开始使用' },
107
+ { command: 'help', description: '帮助' }
108
+ ],
109
+ scope: 'all_private_chats'
110
+ });
111
+ ```
112
+
113
+ ### 主要方法
114
+
115
+ | 方法 | 说明 |
116
+ |------|------|
117
+ | `getMe()` | `GET /bots/me` |
118
+ | `sendMessage` | 文本消息(可选 `reply_markup`:InlineKeyboard、ReplyKeyboard、`remove_keyboard`) |
119
+ | `setMyCommands` / `getMyCommands` / `deleteMyCommands` | 命令菜单(按 `scope`、`language_code` 分组) |
120
+ | `sendPhoto` / `sendDocument` / `sendVideo` / `sendAudio` | `file_id` 为上传完成后的文件 ID |
121
+ | `sendLocation` | 经纬度 |
122
+ | `editMessage` / `editMessageReplyMarkup` / `deleteMessage` | 仅允许操作本机器人发送的消息;`editMessageReplyMarkup` 可更新或清空 InlineKeyboard |
123
+ | `answerCallbackQuery` | 回应 `callback_query` 按钮点击(toast / alert / url) |
124
+ | `answerInlineQuery` | 应答 Inline Mode 查询结果 |
125
+ | `answerFriendRequest` | 处理用户发起的 pending 好友申请(`friend_request_mode = manual`;`friendship_id` + `accept`/`reject`) |
126
+ | `getFriendRequests` | 列出待处理好友申请(`GET /bots/getFriendRequests`,与 DB pending 同源) |
127
+ | `setMyFriendRequestMode` / `getMyFriendRequestMode` | 配置 / 读取好友申请策略(`auto_accept` / `auto_reject` / `manual`) |
128
+ | `kickChatMember` / `banChatMember` / `unbanChatMember` | 群治理(`chat_id` 为群会话 Mongo `_id`;机器人须为群主或群管理员;`ban` 为先踢后黑,非原子) |
129
+ | `setWebhook` / `deleteWebhook` | Webhook 配置 |
130
+ | `issueUploadCredentials` / `completeUpload` | S3 直传两步 |
131
+ | `sendPhotoFromFile` / `sendDocumentFromFile` | 本地路径 → 上传 → 发送 |
132
+ | `verifyToken()` | 公开接口校验 Token |
133
+
134
+ ### InlineKeyboard / Callback Query
135
+
136
+ ```ts
137
+ bot.command('menu', async ctx => {
138
+ await ctx.reply('请选择:', {
139
+ reply_markup: {
140
+ inline_keyboard: [
141
+ [
142
+ { text: '点赞', callback_data: 'like' },
143
+ { text: '文档', url: 'https://open.starim.io/' }
144
+ ]
145
+ ]
146
+ }
147
+ });
148
+ });
149
+
150
+ bot.on('callback_query', async ctx => {
151
+ await ctx.answerCallback({ text: `收到: ${ctx.callbackQuery?.data}` });
152
+ });
153
+ ```
154
+
155
+ ## Webhook 签名校验原理
156
+
157
+ 平台投递时(见 `WebhookDeliveryWorker`)下发的签名头:
158
+
159
+ | 头 | 算法 | 防 replay | 状态 |
160
+ | --- | --- | --- | --- |
161
+ | `X-StarIM-Signature-V2` | `sha256=HMAC(secret, "${ts}.${body}")` | **是**(结合 `X-StarIM-Timestamp` 判 5min 时窗) | **当前唯一推荐** |
162
+ | `X-StarIM-Timestamp` | unix 秒 | — | V2 必带 |
163
+ | `X-StarIM-Update-Id` | 透传 `update.update_id` | — | 用于幂等 |
164
+
165
+ SDK 的 `verifyWebhookSignature` / `processWebhook` / `webhookMiddleware` 默认使用 V2 校验。
166
+
167
+ > 服务端必须用 **与计算签名时完全相同的字节** 验签。若使用 `express.json()` 默认中间件,会丢失原始字节,签名将无法对齐。推荐之一:
168
+
169
+ **方式 A:`express.raw`**
170
+
171
+ ```ts
172
+ import express from 'express';
173
+ import { StarIMBot } from '@starim/bot-sdk';
174
+
175
+ const app = express();
176
+ const bot = new StarIMBot({ token: '...', secretToken: '...' });
177
+
178
+ app.post('/webhook', express.raw({ type: 'application/json' }), bot.webhookCallback());
179
+ ```
180
+
181
+ **方式 B:`express.json` 的 `verify` 保存 rawBody**
182
+
183
+ ```ts
184
+ app.use(
185
+ express.json({
186
+ verify: (req: express.Request & { rawBody?: Buffer }, _res, buf) => {
187
+ req.rawBody = buf;
188
+ }
189
+ })
190
+ );
191
+ // 再挂载 bot.webhookCallback(),SDK 会优先读取 req.rawBody
192
+ ```
193
+
194
+ 手写验签示例(V2):
195
+
196
+ ```ts
197
+ import { createHmac, timingSafeEqual } from 'node:crypto';
198
+
199
+ function verifyV2(
200
+ secret: string,
201
+ rawBody: Buffer,
202
+ sigHeader: string | undefined,
203
+ tsHeader: string | undefined
204
+ ) {
205
+ if (!sigHeader?.startsWith('sha256=') || !tsHeader) throw new Error('bad sig');
206
+ const ts = Number(tsHeader);
207
+ if (!Number.isFinite(ts) || Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) {
208
+ throw new Error('replay window');
209
+ }
210
+ const expected = Buffer.from(sigHeader.slice(7), 'hex');
211
+ const actual = createHmac('sha256', secret)
212
+ .update(`${ts}.`)
213
+ .update(rawBody)
214
+ .digest();
215
+ if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
216
+ throw new Error('bad sig');
217
+ }
218
+ }
219
+ ```
220
+
221
+ ## 多媒体:本地 / Buffer / URL
222
+
223
+ ```ts
224
+ import {
225
+ StarIMBotClient,
226
+ sendPhotoFromBuffer,
227
+ sendPhotoFromUrl,
228
+ sendVideoFromUrl
229
+ } from '@starim/bot-sdk';
230
+ import { readFile } from 'node:fs/promises';
231
+
232
+ const client = new StarIMBotClient({ token: '...', baseUrl: '...' });
233
+
234
+ await client.sendPhotoFromFile(chatId, './a.png', { caption: '本地文件' });
235
+
236
+ const buf = await readFile('./a.png');
237
+ await sendPhotoFromBuffer(client, chatId, buf, { fileName: 'a.png', fileType: 'image/png' });
238
+
239
+ await sendPhotoFromUrl(client, chatId, 'https://example.com/p.png', { fileName: 'p.png' });
240
+ await sendVideoFromUrl(client, chatId, 'https://example.com/v.mp4', {
241
+ fileName: 'v.mp4',
242
+ fileType: 'video/mp4',
243
+ caption: '可选说明',
244
+ duration: 10
245
+ });
246
+ ```
247
+
248
+ ## 错误码说明(常见)
249
+
250
+ 平台与网关可能返回 `success: false` 或 HTTP 4xx/5xx,SDK 统一抛出 `StarIMApiError`(含 `statusCode`、`message`、`code` 若存在)。文档与产品侧常见语义如下(具体以网关响应为准):
251
+
252
+ | 语义 | 可能场景 |
253
+ |------|----------|
254
+ | `invalid_token` / 401 | Token 错误、吊销、格式不对 |
255
+ | `bot_disabled` / 403 | 机器人未审核通过、已停用 |
256
+ | `chat_forbidden` / 4xx | 无权限向目标会话发消息 |
257
+ | `rate_limited` / 429 | 触发发送配额 |
258
+ | `webhook_invalid` | Webhook URL 非 HTTPS 或配置无效(配置接口返回消息) |
259
+
260
+ ## 幂等与重复投递
261
+
262
+ 平台重试 Webhook 时 **保持同一 `update_id`**。`StarIMBot` 内置 LRU(默认 1024 条)去重:重复 `update_id` 返回 `200 ok` 且不再执行业务 handler。可传入自定义 `DedupeStore`(见 `processWebhook` / 源码 `webhook.ts`)。
263
+
264
+ ## 示例脚本
265
+
266
+ 在 `examples/` 目录:
267
+
268
+ - `echo-bot.ts` — 回声
269
+ - `slash-command.ts` — `/start`、`/help`
270
+ - `send-photo.ts` — 本地图片上传并发送
271
+
272
+ ```bash
273
+ cd botsdk/nodejs
274
+ pnpm install
275
+ npx tsx examples/echo-bot.ts
276
+ ```
277
+
278
+ ## 构建
279
+
280
+ ```bash
281
+ pnpm run build # 输出 dist/(ESM + CJS + 类型声明)
282
+ pnpm run test # Vitest
283
+ ```
284
+
285
+ ## License
286
+
287
+ MIT © StarIM