From bf3176702478043acf14aae5c52f125ea5c2d7ce Mon Sep 17 00:00:00 2001 From: ayakasuki <27204037@163.com> Date: Fri, 13 Jun 2025 16:54:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0OneBot=5Fv11=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原go-cqhttp协议与纯onebot_v11协议不够兼容,gocqhttp有自己的修改,但最近gocqhttp已经归档,取而代之的是纯正onebot协议的yunzai for ws-plugin、llonebot、napcat等 --- src/index.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/index.ts b/src/index.ts index e4914a4..e08e10f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,21 @@ export interface NoticeOptions { group?: boolean; bot?: string; }; + onebot?: { + /** + * 群号(群发时必填) + */ + group_id?: number; + /** + * QQ号(私聊时必填) + */ + user_id?: number; + /** + * 消息类型(group/private) + */ + message_type?: string; + access_token?: string; + }; dingtalk?: { /** * 消息类型,目前支持 text、markdown。不设置,默认为 text。 @@ -71,6 +86,7 @@ export type ChannelType = | 'wecom' | 'bark' | 'gocqhttp' + | 'onebot' | 'atri' | 'pushdeer' | 'igot' @@ -336,6 +352,86 @@ async function noticeGoCqhttp(options: CommonOptions) { return response.data; } +/** + * 文档: https://github.com/botuniverse/onebot-11 + * 教程: https://ayakasuki.com/ + */ +async function noticeNodeOnebot(options: CommonOptions) { + checkParameters(options, ['token', 'content']); + + try { + // 1. 解析完整URL(包含action和参数) + const fullUrl = options.token; + const urlObj = new URL(fullUrl); + const baseUrl = `${urlObj.protocol}//${urlObj.host}`; + + // 2. 从URL路径提取action类型 + const actionPath = urlObj.pathname.split('/').pop() || ''; + let action: string; + + // 自动识别动作类型(群发/私聊) + if (actionPath.includes('group')) { + action = 'send_group_msg'; + } else if (actionPath.includes('private')) { + action = 'send_private_msg'; + } else { + action = actionPath; // 保留原始action + } + + // 3. 从URL查询参数获取关键数据 + const urlParams = new URLSearchParams(urlObj.search); + const accessToken = urlParams.get('access_token') || ''; + const groupId = urlParams.get('group_id'); + const userId = urlParams.get('user_id'); + + // 4. 构建消息参数(优先级:URL参数 > 配置参数) + const params: Record = { + message: options.title + ? `${options.title}\n${getTxt(options.content)}` + : getTxt(options.content) + }; + + // 根据参数类型设置目标 + if (groupId) { + params.group_id = Number(groupId); + } else if (userId) { + params.user_id = Number(userId); + } else if (options?.options?.onebot?.group_id) { + params.group_id = Number(options.options.onebot.group_id); + } else if (options?.options?.onebot?.user_id) { + params.user_id = Number(options.options.onebot.user_id); + } else { + throw new Error('OneBot 必须提供 group_id 或 user_id'); + } + + // 5. 构建最终请求URL(保留原始路径结构) + const apiUrl = `${baseUrl}/${actionPath}`; + + // 6. 发送HTTP请求 + const response = await axios.post(apiUrl, params, { + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) + } + }); + + // 7. 处理OneBot响应 + if (response.data?.retcode !== 0) { + throw new Error(`[${response.data.retcode}] ${response.data.message}`); + } + + return response.data; + } catch (e) { + // 增强错误日志(包含原始URL) + console.error('[ONEBOT] 请求失败:', { + originalUrl: options.token, + error: e.response?.data || e.message + }); + throw new Error(`OneBot推送失败: ${e.message}`); + } +} + async function noticePushdeer(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = 'https://api2.pushdeer.com/message/push'; @@ -563,6 +659,7 @@ async function notice(channel: ChannelType, options: CommonOptions) { wecom: noticeWeCom, bark: noticeBark, gocqhttp: noticeGoCqhttp, + onebot:noticeNodeOnebot, atri: noticeAtri, pushdeer: noticePushdeer, igot: noticeIgot, @@ -599,6 +696,7 @@ export { noticeWeCom, noticeBark, noticeGoCqhttp, + noticeNodeOnebot, noticeAtri, noticePushdeer, noticeIgot, From 33b13771e739f850c648afde0e63fb23095b27f8 Mon Sep 17 00:00:00 2001 From: ayakasuki <27204037@163.com> Date: Mon, 2 Mar 2026 22:56:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8Donebot=20403=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E7=B2=BE=E7=AE=80onebot=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 + README.md | 49 ++++++++++++++++++- package.json | 2 +- src/index.ts | 135 +++++++++++++++++++++++++++++---------------------- 4 files changed, 126 insertions(+), 61 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index bbb05e7..3e5a995 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,6 +22,7 @@ module.exports = { ], 'no-console': 'off', 'no-unused-vars': 'off', + 'no-param-reassign': 'off', }, settings: { 'import/resolver': { diff --git a/README.md b/README.md index 306feb6..6243fce 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Twikoo 评论系统对不同的消息推送平台做了大量的适配工作, ## 支持的消息推送平台 +- Webhook - [Qmsg](https://qmsg.zendee.cn/) - [Server 酱](https://sct.ftqq.com/r/13235) - [Push Plus](https://www.pushplus.plus/) @@ -39,7 +40,6 @@ Twikoo 评论系统对不同的消息推送平台做了大量的适配工作, - 阿里云短信 - 腾讯云短信 -- 自定义 Webhook ## 使用方法 @@ -71,7 +71,7 @@ console.log(result); | 参数 | 必填 | 默认 | 说明 | | ---- | ---- | ---- | ---- | -| 平台名称 | ✅ | 无 | 字符串,平台名称的缩写,支持:`qmsg`、`serverchan`、`pushplus`、`pushplushxtrip`、`dingtalk`、`wecom`、`bark`、`gocqhttp`、`atri`、`pushdeer`、`igot`、`telegram`、`feishu`、`ifttt`、`wecombot`、`discord`, `wxpusher` | +| 平台名称 | ✅ | 无 | 字符串,平台名称的缩写,支持:`webhook`、`qmsg`、`serverchan`、`pushplus`、`pushplushxtrip`、`dingtalk`、`wecom`、`bark`、`gocqhttp`、`atri`、`pushdeer`、`igot`、`telegram`、`feishu`、`ifttt`、`wecombot`、`discord`, `wxpusher` | | token | ✅ | 无 | 平台用户身份标识,通常情况下是一串数字和字母组合,详情和示例见下方详细说明 | | title | | 内容第一行 | 可选,消息标题,如果推送平台不支持消息标题,则会拼接在正文首行 | | content | ✅ | 无 | Markdown 格式的推送内容,如果推送平台不支持 Markdown,pushoo 会自动转换成支持的格式 | @@ -79,6 +79,19 @@ console.log(result); ```typescript interface NoticeOptions { + /** + * webhook通知方式的参数配置 + */ + webhook?: { + /** + * url 发送通知的地址 + */ + url: string; + /** + * method 请求方法,默认为 POST + */ + method?: 'GET' | 'POST'; + }; /** * bark通知方式的参数配置 */ @@ -122,6 +135,38 @@ interface NoticeOptions { ## 详细说明 +### 💬 Webhook 缩写: `webhook` + +Webhook 是一种用户定义的 HTTP 回调,通常用于将实时数据推送到指定的 URL。pushoo 可以通过 Webhook 方式将消息推送到你自定义的后端。 + +示例调用: + +```js +let respond = await pushoo('webhook', { + token: '', // 可选,暂不支持签名 + title: '', // 可选 + content: '推送内容', + options: { + webhook: { + url: 'https://example.com/webhook-endpoint', + method: 'POST' // 可选,默认为 POST,也可以设置为 GET + } + } +}); +``` + +特别地,为兼容 Twikoo 中现有的使用方式,可以直接把平台名称设置为 Webhook 的 URL 地址(以 `http://` 或 `https://` 开头),无需传入 `options`。 + +```js +let respond = await pushoo('https://example.com/webhook-endpoint', { + token: '', // 可选 + title: '', // 可选 + content: '推送内容' +}); +``` + +此时如果 URL 的末尾为 `:GET` 则使用 GET 方法发送请求(实际 URL 自动去掉 `:GET`),否则默认使用 POST 方法发送请求。 + ### 💬 [Qmsg](https://qmsg.zendee.cn/) 缩写: `qmsg` Qmsg 酱是 Zendee 提供的第三方 QQ 消息推送服务,免费,消息以 QQ 消息的形式推送,支持私聊推送和群推送。请注意,为避免 Qmsg 酱被 Tencent 冻结,pushoo 会自动删除消息中的网址和 IP 地址。 diff --git a/package.json b/package.json index 7640b8c..510d292 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pushoo", - "version": "0.1.9", + "version": "0.1.11", "description": "Instant Messaging Pushing SDK", "keywords": [ "pushoojs", diff --git a/src/index.ts b/src/index.ts index e08e10f..60f9a0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,19 @@ import { marked } from 'marked'; import markdownToTxt from 'markdown-to-txt'; export interface NoticeOptions { + /** + * Webhook通知方式的参数配置 + */ + webhook?: { + /** + * url 发送通知的地址 + */ + url: string; + /** + * method 请求方法,默认为 POST + */ + method?: 'GET' | 'POST'; + }; /** * bark通知方式的参数配置 */ @@ -57,7 +70,7 @@ export interface NoticeOptions { * 消息类型(group/private) */ message_type?: string; - access_token?: string; + access_token?: string; }; dingtalk?: { /** @@ -77,6 +90,7 @@ export interface CommonOptions { } export type ChannelType = + | 'webhook' | 'qmsg' | 'serverchan' | 'serverchain' @@ -129,6 +143,37 @@ function removeUrlAndIp(content: string) { .replace(mailRegExp, ''); } +/** + * 自定义 Webhook 推送 + */ +async function noticeWebhook(options: CommonOptions) { + checkParameters(options, ['content']); + const method = options?.options?.webhook?.method || 'POST'; + const url = options?.options?.webhook?.url; + if (!url) { + throw new Error('Webhook url is required'); + } + if (method === 'GET') { + const params = new URLSearchParams({ + ...(options.token ? { token: options.token } : {}), + ...(options.title ? { title: options.title } : {}), + content: options.content, + }); + const response = await axios.get(url, { params }); + return response.data; + } + if (method === 'POST') { + const payload: Record = { + ...(options.token && { token: options.token }), + ...(options.title && { title: options.title }), + content: options.content, + }; + const response = await axios.post(url, payload); + return response.data; + } + throw new Error(`Unsupported Webhook request method: ${method}`); +} + /** * https://qmsg.zendee.cn/ */ @@ -360,74 +405,38 @@ async function noticeNodeOnebot(options: CommonOptions) { checkParameters(options, ['token', 'content']); try { - // 1. 解析完整URL(包含action和参数) - const fullUrl = options.token; - const urlObj = new URL(fullUrl); - const baseUrl = `${urlObj.protocol}//${urlObj.host}`; - - // 2. 从URL路径提取action类型 - const actionPath = urlObj.pathname.split('/').pop() || ''; - let action: string; - - // 自动识别动作类型(群发/私聊) - if (actionPath.includes('group')) { - action = 'send_group_msg'; - } else if (actionPath.includes('private')) { - action = 'send_private_msg'; - } else { - action = actionPath; // 保留原始action - } + const urlObj = new URL(options.token); + const searchParams = urlObj.searchParams; - // 3. 从URL查询参数获取关键数据 - const urlParams = new URLSearchParams(urlObj.search); - const accessToken = urlParams.get('access_token') || ''; - const groupId = urlParams.get('group_id'); - const userId = urlParams.get('user_id'); + const groupId = searchParams.get('group_id'); + const userId = searchParams.get('user_id'); - // 4. 构建消息参数(优先级:URL参数 > 配置参数) - const params: Record = { + searchParams.delete('group_id'); + searchParams.delete('user_id'); + + const apiUrl = urlObj.toString(); + + const body: Record = { message: options.title ? `${options.title}\n${getTxt(options.content)}` - : getTxt(options.content) + : getTxt(options.content), }; - // 根据参数类型设置目标 - if (groupId) { - params.group_id = Number(groupId); - } else if (userId) { - params.user_id = Number(userId); - } else if (options?.options?.onebot?.group_id) { - params.group_id = Number(options.options.onebot.group_id); - } else if (options?.options?.onebot?.user_id) { - params.user_id = Number(options.options.onebot.user_id); - } else { - throw new Error('OneBot 必须提供 group_id 或 user_id'); - } + if (groupId) body.group_id = Number(groupId); + if (userId) body.user_id = Number(userId); - // 5. 构建最终请求URL(保留原始路径结构) - const apiUrl = `${baseUrl}/${actionPath}`; - - // 6. 发送HTTP请求 - const response = await axios.post(apiUrl, params, { + const response = await axios.post(apiUrl, body, { timeout: 5000, - headers: { - 'Content-Type': 'application/json', - ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) - } + headers: { 'Content-Type': 'application/json' }, }); - // 7. 处理OneBot响应 if (response.data?.retcode !== 0) { - throw new Error(`[${response.data.retcode}] ${response.data.message}`); + throw new Error(`[${response.data.retcode}] ${response.data.status || 'Unknown Error'}`); } - + return response.data; - } catch (e) { - // 增强错误日志(包含原始URL) - console.error('[ONEBOT] 请求失败:', { - originalUrl: options.token, - error: e.response?.data || e.message - }); + } catch (e: any) { + console.error('[ONEBOT] 推送失败:', e.response?.data || e.message); throw new Error(`OneBot推送失败: ${e.message}`); } } @@ -646,10 +655,11 @@ async function noticeJoin(options: CommonOptions) { return response.data; } -async function notice(channel: ChannelType, options: CommonOptions) { +async function notice(channel: ChannelType | string, options: CommonOptions) { try { let data: any; const noticeFn = { + webhook: noticeWebhook, qmsg: noticeQmsg, serverchan: noticeServerChan, serverchain: noticeServerChan, @@ -659,7 +669,7 @@ async function notice(channel: ChannelType, options: CommonOptions) { wecom: noticeWeCom, bark: noticeBark, gocqhttp: noticeGoCqhttp, - onebot:noticeNodeOnebot, + onebot: noticeNodeOnebot, atri: noticeAtri, pushdeer: noticePushdeer, igot: noticeIgot, @@ -673,6 +683,15 @@ async function notice(channel: ChannelType, options: CommonOptions) { }[channel.toLowerCase()]; if (noticeFn) { data = await noticeFn(options); + } else if (typeof channel === 'string' && (channel.startsWith('http://') || channel.startsWith('https://'))) { + options.options = options.options || {}; + options.options.webhook = { url: channel }; + if (channel.endsWith(':GET')) { + // hack: 如果 URL 以 :GET 结尾,则使用 GET 方法 + options.options.webhook.method = 'GET'; + options.options.webhook.url = channel.slice(0, -4); + } + data = await noticeWebhook(options); } else { throw new Error(`<${channel}> is not supported`); }