# perk-pushplus-nodejs-sdk **Repository Path**: perk-net/perk-pushplus-nodejs-sdk ## Basic Information - **Project Name**: perk-pushplus-nodejs-sdk - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-28 - **Last Updated**: 2026-05-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # @perk-net/perk-pushplus-sdk [pushplus(推送加)](https://www.pushplus.plus) 官方接口的 **JavaScript / TypeScript SDK**,覆盖 **消息接口** 与 **全部开放接口**。 - **同时支持 Node.js 与浏览器**:Node.js 18+ 使用内置 `fetch`,浏览器使用原生 `fetch`,无运行时依赖。 - **三种产物**:CommonJS (`.cjs`) + ESModule (`.js`) + 浏览器 IIFE (`.global.js`),可通过 npm / ` ``` > **作用域包**:本包在 npm 上为组织 **`@perk-net`** 下的公开包。安装与导入时请始终带上作用域前缀(见上文命令)。 > **维护者发布**:请使用具备 **`@perk-net` 组织发布权限** 的账号,并满足 npm 要求(如已开启账号 **2FA**,或使用可绕过写入 2FA 的 **Granular Access Token**)。仓库内已设置 `publishConfig.access: "public"`,首次发布通常无需再手动加 `--access public`。 ## 快速开始 ### 1. 构建客户端 ```ts import { PushPlusClient } from '@perk-net/perk-pushplus-sdk'; const client = new PushPlusClient({ token: 'your_user_token', // 个人中心 -> 一对一推送 secretKey: 'your_secret_key', // 个人中心 -> 开发设置(开放接口必填) }); // 也支持 Builder 风格 const client2 = PushPlusClient.builder() .token('your_user_token') .secretKey('your_secret_key') .build(); ``` > `PushPlusClient` 无状态,建议作为单例长期持有。 ### 2. 发送消息 ```ts import { Channel, Template, sendRequest } from '@perk-net/perk-pushplus-sdk'; // 最简:默认 wechat / html const shortCode = await client.sendSimple('标题', '内容'); // 完整:使用 Builder const code = await client.send( sendRequest() .title('CPU 告警') .content('# CPU > 90%\n请尽快处理') .template(Template.MARKDOWN) .channel(Channel.WECHAT) .topic('ops') .callbackUrl('https://your.host/pushplus/callback') .build(), ); // 也可以直接传普通对象 await client.send({ title: '部署完成', content: 'v1.0.0', template: Template.MARKDOWN, }); ``` ### 3. 多渠道发送 ```ts import { Channel, batchSendRequest } from '@perk-net/perk-pushplus-sdk'; const results = await client.batchSend( batchSendRequest() .title('多渠道告警') .content('CPU > 90%') .channel(Channel.WECHAT).option('') .channel(Channel.WEBHOOK).option('bark') .channel(Channel.EXTENSION).option('') .build(), ); for (const r of results) { console.log(r.channel, r.shortCode, r.code, r.message); } ``` `channel(...)` 与 `option(...)` 可累计调用,SDK 自动用逗号拼接,与官方文档示例语义一致。 ### 4. 开放接口(全量) > 需要在 PushPlus 后台「开发设置」中:开启开放接口、配置 `secretKey`、把调用方所在公网 IP 加入安全 IP 列表。 > AccessKey 完全自动管理 —— 直接调用就好。 ```ts // 用户 const me = await client.user.myInfo(); const limit = await client.user.getLimitTime(); const count = await client.user.getSendCount(); // 消息 const page = await client.openMessage.list({ current: 1, pageSize: 20 }); const result = await client.openMessage.queryResult('short-code'); const url = client.openMessage.detailUrl('short-code'); // 消息 token const newToken = await client.messageToken.add({ name: 'for-jenkins' }); // 群组 const topics = await client.topic.list({ current: 1, pageSize: 20, params: { topicType: 0 }, }); const detail = await client.topic.detail(123); const qr = await client.topic.qrCode(123, 86400, -1); // 群组用户 await client.topicUser.editRemark(456, '老张'); // 好友 const myQr = await client.friend.getQrCode({ content: 'welcome' }); const friends = await client.friend.list({ current: 1, pageSize: 20 }); // webhook 渠道 import { WebhookType } from '@perk-net/perk-pushplus-sdk'; await client.webhook.add({ webhookCode: 'bark', webhookName: '我的 Bark', webhookType: WebhookType.BARK, webhookUrl: 'https://api.day.app/xxxx', }); // 渠道(公众号 / 企业微信 / 邮箱) const mps = await client.channel.mpList(); // ClawBot const botQr = await client.clawBot.getBotQrcode(); // 设置 await client.setting.changeIsSend(1); // 启用发送 await client.setting.changeOpenMessageType(0); // 预处理(仅会员) const out = await client.pre.test({ content: '...', message: 'hi' }); // 图片服务(一行上传到 PushPlus 图床,30 天有效) import { readFile } from 'node:fs/promises'; const bytes = await readFile('/tmp/logo.png'); const uploaded = await client.image.uploadBytes(bytes, { fileName: 'logo.png' }); console.log(uploaded.url); // 直接拿到可访问的图片 URL const imgs = await client.image.list({ current: 1, pageSize: 10 }); await client.image.delete(imgs.list[0].id); ``` ### 图片服务 PushPlus 基于七牛云提供图片图床(30 天有效,可主动删除)。SDK 把「获取上传凭证 → multipart 表单上传 → 解析 URL」封装成一步: ```ts // Node.js:从文件读取 import { readFile } from 'node:fs/promises'; const bytes = await readFile('/tmp/a.png'); const r = await client.image.uploadBytes(bytes, { fileName: 'a.png' }); console.log(r.url); // 浏览器:input[type=file] const file = (document.querySelector('input[type=file]') as HTMLInputElement).files![0]; await client.image.uploadBytes(file, { fileName: file.name, contentType: file.type }); // 已上传图片列表 const page = await client.image.list({ current: 1, pageSize: 10 }); // 主动删除(未删除的图片默认 30 天后由系统自动清理) await client.image.delete(page.list![0].id!); ``` 需要自己控制凭证的获取与上传过程时(如缓存 token、分布式上传),可拆开调用: ```ts const token = await client.image.getUploadToken(); const r = await client.image.upload(token, bytes, { fileName: 'a.png', contentType: 'image/png' }); ``` > 上传图片的真正请求会按七牛云规范以 `multipart/form-data` 提交到 `uploadUrl`,**不会**携带 PushPlus 的 `access-key`;其余三个接口(获取凭证 / 列表 / 删除)走 PushPlus 开放接口,自动带上 `access-key`。 > > 接受的二进制形态:`Uint8Array`(Node 中 `Buffer` 是其子类,可直接传)、`ArrayBuffer`、`Blob`/`File`(浏览器 + Node 18+)。 ### 5. 回调解析 PushPlus 在消息发送完成、群组新增用户、新增好友时会回调你预置的 URL。SDK 提供类型安全的解析: ```ts import { CallbackEvent, parseCallback } from '@perk-net/perk-pushplus-sdk'; // Express app.post('/pushplus/callback', express.json(), (req, res) => { const payload = parseCallback(req.body); switch (payload.event) { case CallbackEvent.MESSAGE_COMPLETE: console.log('发送结果', payload.messageInfo?.shortCode, payload.messageInfo?.sendStatus); break; case CallbackEvent.ADD_TOPIC_USER: console.log('新订阅', payload.topicUserInfo?.openId); break; case CallbackEvent.ADD_FRIEND: console.log('新好友', payload.friendInfo?.token, payload.qrCode); break; } res.send('ok'); }); ``` `parseCallback` 接受字符串或已经解析过的对象,返回带类型的 `CallbackPayload`。 ## 配置 ```ts new PushPlusClient({ token: 'xxx', secretKey: 'xxx', baseUrl: 'https://www.pushplus.plus', connectTimeoutMs: 10_000, readTimeoutMs: 30_000, accessKeyRefreshAheadSeconds: 300, logRequest: false, rateLimitGuardEnabled: true, rateLimitCooldownMs: 0, // 0 表示「次日 0 点」自动解禁 userAgent: 'my-app/1.0', httpRequester: undefined, // 自定义 HTTP 客户端(可选) }); ``` | 字段 | 默认 | 说明 | | --- | --- | --- | | `token` | – | 用户 token / 消息 token,发送消息使用 | | `secretKey` | – | 用户 secretKey,调用开放接口使用 | | `baseUrl` | `https://www.pushplus.plus` | 服务地址 | | `connectTimeoutMs` | `10000` | 连接超时(毫秒) | | `readTimeoutMs` | `30000` | 请求/读超时(毫秒) | | `accessKeyRefreshAheadSeconds` | `300` | AccessKey 提前刷新秒数 | | `logRequest` | `false` | 开启 DEBUG 级请求/响应日志(写到 `console.debug`) | | `rateLimitGuardEnabled` | `true` | 是否启用本地限流守卫 | | `rateLimitCooldownMs` | `0` | 命中 `code=900` 后的本地禁推时长(毫秒);`0` 表示到「次日 0 点」 | | `userAgent` | `@perk-net/perk-pushplus-sdk/` | UA 头(仅 Node.js 生效,浏览器禁止设置) | | `httpRequester` | 内置 fetch 实现 | 自定义 HTTP 客户端 | ## 错误处理 所有错误都会包装成 `PushPlusError`: ```ts import { ErrorCode, PushPlusError } from '@perk-net/perk-pushplus-sdk'; try { await client.sendSimple('t', 'c'); } catch (e) { if (e instanceof PushPlusError) { if (e.isRateLimited()) { console.warn('PushPlus 限流,今天暂停推送:', e.message); return; } switch (e.errorCode) { case ErrorCode.INVALID_TOKEN: console.error('token 错误,立即排查配置'); break; case ErrorCode.NOT_VERIFIED: console.error('账号未实名认证'); break; case ErrorCode.INSUFFICIENT_POINTS: console.warn('积分不足'); break; default: console.warn(`PushPlus 失败: code=${e.code}, msg=${e.message}`); } } } ``` `ErrorCode` 已经把官方文档的全部业务码语义化(`OK / NOT_LOGIN / UNAUTHORIZED / IP_FORBIDDEN / SERVER_ERROR / DATA_ERROR / FORBIDDEN_VIEW / INSUFFICIENT_POINTS / RATE_LIMITED / INVALID_TOKEN / NOT_VERIFIED / VALIDATION_ERROR`)。 参考:[PushPlus 接口返回码说明](https://www.pushplus.plus/doc/guide/code.html)。 ## 限流守卫(code=900 自动短路) PushPlus 在请求次数过多时会返回 `code=900`,官方文档明确建议「根据返回值判断当天是否让程序继续调用发送消息接口,否则会让账号进一步受限」。SDK 默认替你做这件事: - 任意一次 `client.send(...)` / `client.batchSend(...)` 命中 `code=900` 后,SDK 会按 token 维度记下「禁推至 X 时刻」。 - 同 token 后续发送调用不再发起 HTTP,直接抛 `PushPlusError(code=900)`。 - 默认禁推到**系统时区的次日 0 点**;通过 `rateLimitCooldownMs` 可改为固定时长(例如文档示例的 2 天)。 - 仅作用于发送接口,开放接口不受影响。 - 进程内单例,**不跨进程共享**——多实例部署时每个进程最多被命中一次。 可观察 / 可干预: ```ts const guard = client.rateLimitGuard; const until = guard.blockedUntilAt('user_token'); // null 表示未被限流;否则为本地解禁时间戳(毫秒) guard.clear('user_token'); // 例如:人工确认服务端已解禁后立即放行 ``` 完全关闭这个行为(不推荐): ```ts new PushPlusClient({ token: 'xxx', rateLimitGuardEnabled: false }); ``` ## 自定义 HTTP 客户端 `fetch` 不满足需求(如想用 axios / undici / got / 浏览器代理)时,实现 `HttpRequester` 接口即可: ```ts import { HttpRequester, HttpResponse, PushPlusClient } from '@perk-net/perk-pushplus-sdk'; import axios from 'axios'; class AxiosHttpRequester implements HttpRequester { async execute({ method, url, headers, body }): Promise { const resp = await axios.request({ method, url, headers, data: body, validateStatus: () => true, // 自行处理状态码 transformResponse: r => r, // 直接拿到字符串 }); return { statusCode: resp.status, body: resp.data }; } } const client = PushPlusClient.builder() .token('xxx') .httpRequester(new AxiosHttpRequester()) .build(); ``` ## 兼容性 | 环境 | 要求 | 备注 | | --- | --- | --- | | Node.js | `>=18`(推荐) | 18+ 内置全局 `fetch` | | Node.js | `>=14` | 需自行注入 `HttpRequester` 或 `fetch` polyfill(如 `undici`) | | 浏览器 | 现代浏览器 | 使用原生 `fetch` + `AbortController`,注意 PushPlus 服务端 CORS 策略 | | TypeScript | `>=4.5` | 完整类型 | > ⚠️ **浏览器使用注意**:PushPlus 接口是否允许跨域取决于服务端响应头。如果生产环境无法直接从浏览器调用,请通过你自己的后端代理后再使用本 SDK。 ## 示例 更多示例见 [`examples/`](./examples) 目录: - [`examples/send.mjs`](./examples/send.mjs) — Node.js 发送消息 - [`examples/express-callback.mjs`](./examples/express-callback.mjs) — Express 接收回调 - [`examples/browser.html`](./examples/browser.html) — 浏览器中通过 `