Twikoo评论系统下即时通知推送增加原生纯Onebot_v11协议的支持

前言

因为go-cqhttp已差不多停止维护,go-cqhttp用户推荐大家迁移至无头NTQQ项目苟延残喘。而无头NTQQ的QQ登录协议器项目大部分是通过Onebot 11的API协议,go-cqhttp是扩展过和修改过后实现。其中推送结构有所不同,单纯使用go-cqhttp的推送方式给Onebot 11的正向HTTP会类型报错。故有此pushoo增加fork分支

私有部署和私有部署(docker)方式修改方式与步骤

本次修改替换一个文件,然后编译后再重启Twikoo服务,请注意备份

私有部署修改

第一步 取得你npm安装模块的根路径

  1. 如果你当时是按Twikoo官方的私有部署方式运行npm i -g tkserver形式
    则是运行npm root -g获得你的npm模块路径
    获得例如/root/nodejs/v18.19.0/lib/node_modules(具体按你获得的路径,此仅为示例)
  2. /node_modules下拥有tkserver文件夹,此为关键。如未找到tkserver,请回忆分清你当时是npm i -g tkserver还是 npm i tkserver安装

第二步 修改tkserver下的pushoo下的index.ts

  1. tkserver文件夹下路径 打开/tkserver/node_modules/pushoo,运行一遍npm install
  2. 打开文件pushoo/src文件夹下的index.ts例如
    /root/nodejs/v18.19.0/lib/node_modules/tkserver/node_modules/pushoo/src/index.ts
  3. 全部替换代码内容为如下代码,项目地址为Github
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    import axios from 'axios';
    import { marked } from 'marked';
    import markdownToTxt from 'markdown-to-txt';

    export interface NoticeOptions {
    /**
    * bark通知方式的参数配置
    */
    bark?: {
    /**
    * url 用于点击通知后跳转的地址
    */
    url?: string;
    };
    /**
    * IFTTT通知方式的参数配置
    */
    ifttt?: {
    value1?: string;
    value2?: string;
    value3?: string;
    };
    /**
    * Discord通知方式的参数配置
    */
    discord?: {
    userName?: string;
    avatarUrl?: string;
    };
    /**
    * WxPusher通知方式的参数配置
    */
    wxpusher?: {
    uids?: string[];
    url?: string;
    verifyPay?: boolean;
    };
    /**
    * QMsg酱通知方式的参数配置
    */
    qmsg?: {
    qq?: string;
    url?: string;
    group?: boolean;
    bot?: string;
    };
    onebot?: {
    /**
    * 群号(群发时必填)
    */
    group_id?: number;
    /**
    * QQ号(私聊时必填)
    */
    user_id?: number;
    /**
    * 消息类型(group/private)
    */
    message_type?: string;
    access_token?: string;
    };
    dingtalk?: {
    /**
    * 消息类型,目前支持 text、markdown。不设置,默认为 text。
    */
    msgtype?: string;
    };
    }
    export interface CommonOptions {
    token: string;
    title?: string;
    content: string;
    /**
    * 扩展选项
    */
    options?: NoticeOptions;
    }

    export type ChannelType =
    | 'qmsg'
    | 'serverchan'
    | 'serverchain'
    | 'pushplus'
    | 'pushplushxtrip'
    | 'dingtalk'
    | 'wecom'
    | 'bark'
    | 'gocqhttp'
    | 'onebot'
    | 'atri'
    | 'pushdeer'
    | 'igot'
    | 'telegram'
    | 'feishu'
    | 'ifttt'
    | 'wecombot'
    | 'discord'
    | 'wxpusher'
    | 'join';

    function checkParameters(options: any, requires: string[] = []) {
    requires.forEach((require) => {
    if (!options[require]) {
    throw new Error(`${require} is required`);
    }
    });
    }

    function getHtml(content: string) {
    return marked.parse(content);
    }

    function getTxt(content: string) {
    return markdownToTxt(content);
    }

    function getTitle(content: string) {
    return getTxt(content).split('\n')[0];
    }

    function removeUrlAndIp(content: string) {
    const urlRegex = /(https?:\/\/[^\s]+)/g;
    const ipRegex = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g;
    // 邮箱正则表达式来自 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
    const mailRegExp = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/g;
    return content
    .replace(urlRegex, '')
    .replace(ipRegex, '')
    .replace(mailRegExp, '');
    }

    /**
    * https://qmsg.zendee.cn/
    */
    async function noticeQmsg(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = options?.options?.qmsg?.url || 'https://qmsg.zendee.cn';
    let msg = getTxt(options.content);
    if (options.title) {
    msg = `${options.title}\n${msg}`;
    }
    // 移除网址和 IP 以避免 Qmsg 酱被 Tencent 封号
    msg = removeUrlAndIp(msg);
    const param = new URLSearchParams({ msg });
    const qq = options?.options?.qmsg?.qq || false;
    if (qq) {
    param.append('qq', qq);
    }
    const bot = options?.options?.qmsg?.bot || false;
    if (bot) {
    param.append('bot', bot);
    }
    const group = options?.options?.qmsg?.group || false;
    const response = await axios.post(`${url}/${group ? 'group' : 'send'}/${options.token}`, param.toString(), {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
    return response.data;
    }

    /**
    * https://github.com/Tianli0/push-bot-api/
    */
    async function noticeAtri(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = 'http://pushoo.tianli0.top/';
    let message = getTxt(options.content);
    if (options.title) {
    message = `${options.title}\n${message}`;
    }
    const param = new URLSearchParams({
    user_id: options.token,
    message,
    });
    const response = await axios.post(url, param.toString(), {
    headers: { 'X-Requested-By': 'pushoo' },
    });
    return response.data;
    }

    /**
    * Turbo: https://sct.ftqq.com/
    * V3: https://sc3.ft07.com/
    */
    async function noticeServerChan(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    let url: string;
    let param: URLSearchParams;
    if (options.token.startsWith('sctp')) {
    url = `https://${options.token.match(/^sctp(\d+)t/)[1]}.push.ft07.com/send`;
    param = new URLSearchParams({
    title: options.title || getTitle(options.content),
    desp: options.content,
    });
    } else if (options.token.substring(0, 3).toLowerCase() === 'sct') {
    url = 'https://sctapi.ftqq.com';
    param = new URLSearchParams({
    title: options.title || getTitle(options.content),
    desp: options.content,
    });
    } else {
    url = 'https://sc.ftqq.com';
    param = new URLSearchParams({
    text: options.title || getTitle(options.content),
    desp: options.content,
    });
    }
    const response = await axios.post(`${url}/${options.token}.send`, param.toString(), {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
    return response.data;
    }

    /**
    * https://www.pushplus.plus/
    */
    async function noticePushPlus(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const ppApiUrl = 'http://www.pushplus.plus/send';
    const ppApiParam = {
    token: options.token,
    title: options.title || getTitle(options.content),
    content: options.content,
    template: 'markdown',
    };
    const response = await axios.post(ppApiUrl, ppApiParam);
    return response.data;
    }

    /**
    * https://pushplus.hxtrip.com/
    */
    async function noticePushPlusHxtrip(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const ppApiUrl = 'http://pushplus.hxtrip.com/send';
    const ppApiParam = {
    token: options.token,
    title: options.title || getTitle(options.content),
    content: getHtml(options.content),
    template: 'html',
    };
    const response = await axios.post(ppApiUrl, ppApiParam);
    return response.data;
    }

    /**
    * 文档: https://open.dingtalk.com/document/group/custom-robot-access
    * 教程: https://blog.ljcbaby.top/article/Twikoo-DingTalk/
    */
    async function noticeDingTalk(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    let url = 'https://oapi.dingtalk.com/robot/send?access_token=';
    if (options.token.substring(0, 4).toLowerCase() === 'http') {
    url = options.token;
    } else {
    url += options.token;
    }

    const msgtype = options.options?.dingtalk?.msgtype || 'text';
    const content = msgtype === 'text'
    ? (options.title ? `${options.title}\n` : '') + getTxt(options.content)
    : options.content;

    const msgBody = {
    msgtype,
    };

    if (msgtype === 'text') {
    msgBody[msgtype] = { content };
    } else if (msgtype === 'markdown') {
    msgBody[msgtype] = { title: options.title || getTitle(options.content), text: content };
    }
    const response = await axios.post(url, msgBody);
    return response.data;
    }

    /**
    * 文档: https://developer.work.weixin.qq.com/document/path/90236
    * 教程: https://sct.ftqq.com/forward
    */
    async function noticeWeCom(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const [corpid, corpsecret, agentid, touser = '@all'] = options.token.split('#');
    checkParameters(
    {
    corpid,
    corpsecret,
    agentid,
    },
    ['corpid', 'corpsecret', 'agentid'],
    );
    // 获取 Access Token
    let accessToken;
    try {
    const accessTokenRes = await axios.get(
    `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpid}&corpsecret=${corpsecret}`,
    );
    accessToken = accessTokenRes.data.access_token;
    } catch (e) {
    console.error('获取企业微信 access token 失败,请检查 token', e);
    return {};
    }
    // 发送消息
    const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`;
    let content = getTxt(options.content);
    if (options.title) {
    content = `${options.title}\n${content}`;
    }
    const param = {
    touser,
    msgtype: 'text',
    agentid,
    text: { content },
    };
    const response = await axios.post(url, param);
    return response.data;
    }

    /**
    * https://github.com/Finb/Bark
    */
    async function noticeBark(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    let url = 'https://api.day.app/';
    if (options.token.substring(0, 4).toLowerCase() === 'http') {
    url = options.token;
    } else {
    url += options.token;
    }
    if (!url.endsWith('/')) url += '/';
    const title = encodeURIComponent(options.title || getTitle(options.content));
    const content = encodeURIComponent(getTxt(options.content));
    const params = new URLSearchParams({
    url: options?.options?.bark?.url || '',
    });
    const response = await axios.get(`${url}${title}/${content}/`, { params });
    return response.data;
    }

    /**
    * 文档: https://docs.go-cqhttp.org/api/
    * 教程: https://twikoo.js.org/QQ_API.html
    */
    async function noticeGoCqhttp(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = options.token;
    let message = getTxt(options.content);
    if (options.title) {
    message = `${options.title}\n${message}`;
    }
    const param = new URLSearchParams({ message });
    const response = await axios.post(url, param.toString());
    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<string, any> = {
    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';
    const response = await axios.post(url, {
    pushkey: options.token,
    text: options.title || getTitle(options.content),
    desp: options.content,
    });
    return response.data;
    }

    async function noticeIgot(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = `https://push.hellyw.com/${options.token}`;
    const response = await axios.post(url, {
    title: options.title || getTitle(options.content),
    content: getTxt(options.content),
    });
    return response.data;
    }

    /**
    * 文档: https://core.telegram.org/method/messages.sendMessage
    * 教程: https://core.telegram.org/bots#3-how-do-i-create-a-bot
    */
    async function noticeTelegram(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const [tgToken, chatId] = options.token.split('#');
    checkParameters(
    {
    tgToken,
    chatId,
    },
    ['tgToken', 'chatId'],
    );
    let text = options.content.replace(/([*_])/g, '\\$1'); // * 和 _ 似乎需要转义,否则会抛出 400 Bad Request 以及消息显示不正常
    if (options.title) {
    text = `${options.title}\n\n${text}`;
    }
    const response = await axios.post(`https://api.telegram.org/bot${tgToken}/sendMessage`, {
    text,
    chat_id: chatId,
    parse_mode: 'Markdown',
    });
    return response.data;
    }

    /**
    * https://www.feishu.cn/hc/zh-CN/articles/360024984973
    */
    async function noticeFeishu(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const v1 = 'https://open.feishu.cn/open-apis/bot/hook/';
    const v2 = 'https://open.feishu.cn/open-apis/bot/v2/hook/';
    let url;
    let params;
    if (options.token.substring(0, 4).toLowerCase() === 'http') {
    url = options.token;
    } else {
    url = v2 + options.token;
    }
    if (url.substring(0, v1.length) === v1) {
    params = {
    title: options.title || getTitle(options.content),
    text: getTxt(options.content),
    };
    } else {
    let text = getTxt(options.content);
    if (options.title) {
    text = `${options.title}\n${text}`;
    }
    params = {
    msg_type: 'text',
    content: { text },
    };
    }
    const response = await axios.post(url, params);
    return response.data;
    }

    /**
    * https://ifttt.com/maker_webhooks
    * http://ift.tt/webhooks_faq
    */
    async function noticeIfttt(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);

    const [token, eventName] = options.token.split('#');
    checkParameters(
    {
    token,
    eventName,
    },
    ['token', 'eventName'],
    );

    const url = `https://maker.ifttt.com/trigger/${eventName}/with/key/${token}`;

    const response = await axios.post(
    url,
    {
    value1: options.options?.ifttt?.value1 || getTxt(options.title),
    value2: options.options?.ifttt?.value2 || getTxt(options.content),
    value3: options.options?.ifttt?.value3,
    },
    {
    headers: { 'Content-Type': 'application/json' },
    },
    );
    return response.data;
    }

    /**
    * 文档: https://developer.work.weixin.qq.com/document/path/91770
    * 教程: https://developer.work.weixin.qq.com/tutorial/detail/54
    */
    async function noticeWecombot(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${options.token}`;
    const content = getTxt(options.content);

    const response = await axios.post(
    url,
    {
    msgtype: 'text',
    text: {
    content,
    },
    },
    {
    headers: { 'Content-Type': 'application/json' },
    },
    );

    return response.data;
    }

    /**
    * 文档:https://discord.com/developers/docs/resources/webhook#execute-webhook
    */
    async function noticeDiscord(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = options.token.startsWith('https://')
    ? options.token
    : `https://discord.com/api/webhooks/${options.token.replace(/#/, '/')}`;

    const response = await axios.post(
    url,
    {
    content: options.content,
    username: options.options?.discord?.userName,
    avatar_url: options.options?.discord?.avatarUrl,
    },
    {
    headers: { 'Content-Type': 'application/json' },
    },
    );
    return `Delivered successfully, code ${response.status}.`;
    }

    /**
    * WXPusher 推送
    * 教程:https://wxpusher.zjiecode.com/admin/
    * 文档: https://wxpusher.zjiecode.com/docs/#/
    */
    async function noticeWxPusher(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = 'http://wxpusher.zjiecode.com/api/send/message';
    const [appToken, topicIds] = options.token.split('#');
    checkParameters({ appToken, topicIds }, ['appToken', 'topicIds']);

    const response = await axios.post(
    url,
    {
    appToken,
    content: options.content,
    summary: options.title || getTitle(options.content),
    contentType: 3,
    topicIds: topicIds.split(',').map((id) => Number(id)),
    uids: options?.options?.wxpusher?.uids || [],
    url: options?.options?.wxpusher?.url || '',
    verifyPayload: options?.options?.wxpusher?.verifyPay || false,
    },
    {
    headers: {
    'Content-Type': 'application/json',
    },
    },
    );
    return response.data;
    }

    /**
    * Join 推送
    * 文档: https://joaoapps.com/join/api/
    */
    async function noticeJoin(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const [apiKey, deviceId] = options.token.split('#');
    checkParameters({ apiKey, deviceId }, ['apiKey', 'deviceId']);

    const url = 'https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush';
    const param = new URLSearchParams({
    apikey: apiKey,
    deviceId,
    title: options.title || getTitle(options.content),
    text: options.content,
    });
    const response = await axios.post(url, param.toString(), {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
    return response.data;
    }

    async function notice(channel: ChannelType, options: CommonOptions) {
    try {
    let data: any;
    const noticeFn = {
    qmsg: noticeQmsg,
    serverchan: noticeServerChan,
    serverchain: noticeServerChan,
    pushplus: noticePushPlus,
    pushplushxtrip: noticePushPlusHxtrip,
    dingtalk: noticeDingTalk,
    wecom: noticeWeCom,
    bark: noticeBark,
    gocqhttp: noticeGoCqhttp,
    onebot:noticeNodeOnebot,
    atri: noticeAtri,
    pushdeer: noticePushdeer,
    igot: noticeIgot,
    telegram: noticeTelegram,
    feishu: noticeFeishu,
    ifttt: noticeIfttt,
    wecombot: noticeWecombot,
    discord: noticeDiscord,
    wxpusher: noticeWxPusher,
    join: noticeJoin,
    }[channel.toLowerCase()];
    if (noticeFn) {
    data = await noticeFn(options);
    } else {
    throw new Error(`<${channel}> is not supported`);
    }
    console.debug(`[PUSHOO] Send to <${channel}> result:`, data);
    return data;
    } catch (e) {
    console.error('[PUSHOO] Got error:', e.message);
    return { error: e };
    }
    }

    export default notice;

    export {
    notice,
    noticeQmsg,
    noticeServerChan,
    noticePushPlus,
    noticePushPlusHxtrip,
    noticeDingTalk,
    noticeWeCom,
    noticeBark,
    noticeGoCqhttp,
    noticeNodeOnebot,
    noticeAtri,
    noticePushdeer,
    noticeIgot,
    noticeTelegram,
    noticeFeishu,
    noticeIfttt,
    noticeWecombot,
    noticeDiscord,
    noticeWxPusher,
    noticeJoin,
    };
  4. 替换好后回退至/root/nodejs/v18.19.0/lib/node_modules/tkserver/node_modules/pushoo
  5. 运行npm run build,其中无出错即为完成
    image.png

私有部署(docker)修改方式和步骤

前言

其中Docker 需要映射容器内的/app路径内容为例如-v /root/twikoo/app:/app。官方是少了这一个,只映射了-v ${PWD}/data:/app/data。这样我们没办法映射出容器项目内的_data\node_modules\pushoo文件夹的内容的。所以如果你没有映射,推荐重新docker run 加上-v /root/twikoo/app:/app这一部分。如果是可视化docker那就更方便,只加这一部分重启容器即可。

在前言的条件已满足的情况下

第一步 获取pushoo的文件夹并修改其文件

  1. 假设映射目录为/root/twikoo/app 则前往/root/twikoo/app/_data/node_modules/pushoo/src
  2. 修改src下的index.ts内容为
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    import axios from 'axios';
    import { marked } from 'marked';
    import markdownToTxt from 'markdown-to-txt';

    export interface NoticeOptions {
    /**
    * bark通知方式的参数配置
    */
    bark?: {
    /**
    * url 用于点击通知后跳转的地址
    */
    url?: string;
    };
    /**
    * IFTTT通知方式的参数配置
    */
    ifttt?: {
    value1?: string;
    value2?: string;
    value3?: string;
    };
    /**
    * Discord通知方式的参数配置
    */
    discord?: {
    userName?: string;
    avatarUrl?: string;
    };
    /**
    * WxPusher通知方式的参数配置
    */
    wxpusher?: {
    uids?: string[];
    url?: string;
    verifyPay?: boolean;
    };
    /**
    * QMsg酱通知方式的参数配置
    */
    qmsg?: {
    qq?: string;
    url?: string;
    group?: boolean;
    bot?: string;
    };
    onebot?: {
    /**
    * 群号(群发时必填)
    */
    group_id?: number;
    /**
    * QQ号(私聊时必填)
    */
    user_id?: number;
    /**
    * 消息类型(group/private)
    */
    message_type?: string;
    access_token?: string;
    };
    dingtalk?: {
    /**
    * 消息类型,目前支持 text、markdown。不设置,默认为 text。
    */
    msgtype?: string;
    };
    }
    export interface CommonOptions {
    token: string;
    title?: string;
    content: string;
    /**
    * 扩展选项
    */
    options?: NoticeOptions;
    }

    export type ChannelType =
    | 'qmsg'
    | 'serverchan'
    | 'serverchain'
    | 'pushplus'
    | 'pushplushxtrip'
    | 'dingtalk'
    | 'wecom'
    | 'bark'
    | 'gocqhttp'
    | 'onebot'
    | 'atri'
    | 'pushdeer'
    | 'igot'
    | 'telegram'
    | 'feishu'
    | 'ifttt'
    | 'wecombot'
    | 'discord'
    | 'wxpusher'
    | 'join';

    function checkParameters(options: any, requires: string[] = []) {
    requires.forEach((require) => {
    if (!options[require]) {
    throw new Error(`${require} is required`);
    }
    });
    }

    function getHtml(content: string) {
    return marked.parse(content);
    }

    function getTxt(content: string) {
    return markdownToTxt(content);
    }

    function getTitle(content: string) {
    return getTxt(content).split('\n')[0];
    }

    function removeUrlAndIp(content: string) {
    const urlRegex = /(https?:\/\/[^\s]+)/g;
    const ipRegex = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g;
    // 邮箱正则表达式来自 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
    const mailRegExp = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/g;
    return content
    .replace(urlRegex, '')
    .replace(ipRegex, '')
    .replace(mailRegExp, '');
    }

    /**
    * https://qmsg.zendee.cn/
    */
    async function noticeQmsg(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = options?.options?.qmsg?.url || 'https://qmsg.zendee.cn';
    let msg = getTxt(options.content);
    if (options.title) {
    msg = `${options.title}\n${msg}`;
    }
    // 移除网址和 IP 以避免 Qmsg 酱被 Tencent 封号
    msg = removeUrlAndIp(msg);
    const param = new URLSearchParams({ msg });
    const qq = options?.options?.qmsg?.qq || false;
    if (qq) {
    param.append('qq', qq);
    }
    const bot = options?.options?.qmsg?.bot || false;
    if (bot) {
    param.append('bot', bot);
    }
    const group = options?.options?.qmsg?.group || false;
    const response = await axios.post(`${url}/${group ? 'group' : 'send'}/${options.token}`, param.toString(), {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
    return response.data;
    }

    /**
    * https://github.com/Tianli0/push-bot-api/
    */
    async function noticeAtri(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = 'http://pushoo.tianli0.top/';
    let message = getTxt(options.content);
    if (options.title) {
    message = `${options.title}\n${message}`;
    }
    const param = new URLSearchParams({
    user_id: options.token,
    message,
    });
    const response = await axios.post(url, param.toString(), {
    headers: { 'X-Requested-By': 'pushoo' },
    });
    return response.data;
    }

    /**
    * Turbo: https://sct.ftqq.com/
    * V3: https://sc3.ft07.com/
    */
    async function noticeServerChan(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    let url: string;
    let param: URLSearchParams;
    if (options.token.startsWith('sctp')) {
    url = `https://${options.token.match(/^sctp(\d+)t/)[1]}.push.ft07.com/send`;
    param = new URLSearchParams({
    title: options.title || getTitle(options.content),
    desp: options.content,
    });
    } else if (options.token.substring(0, 3).toLowerCase() === 'sct') {
    url = 'https://sctapi.ftqq.com';
    param = new URLSearchParams({
    title: options.title || getTitle(options.content),
    desp: options.content,
    });
    } else {
    url = 'https://sc.ftqq.com';
    param = new URLSearchParams({
    text: options.title || getTitle(options.content),
    desp: options.content,
    });
    }
    const response = await axios.post(`${url}/${options.token}.send`, param.toString(), {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
    return response.data;
    }

    /**
    * https://www.pushplus.plus/
    */
    async function noticePushPlus(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const ppApiUrl = 'http://www.pushplus.plus/send';
    const ppApiParam = {
    token: options.token,
    title: options.title || getTitle(options.content),
    content: options.content,
    template: 'markdown',
    };
    const response = await axios.post(ppApiUrl, ppApiParam);
    return response.data;
    }

    /**
    * https://pushplus.hxtrip.com/
    */
    async function noticePushPlusHxtrip(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const ppApiUrl = 'http://pushplus.hxtrip.com/send';
    const ppApiParam = {
    token: options.token,
    title: options.title || getTitle(options.content),
    content: getHtml(options.content),
    template: 'html',
    };
    const response = await axios.post(ppApiUrl, ppApiParam);
    return response.data;
    }

    /**
    * 文档: https://open.dingtalk.com/document/group/custom-robot-access
    * 教程: https://blog.ljcbaby.top/article/Twikoo-DingTalk/
    */
    async function noticeDingTalk(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    let url = 'https://oapi.dingtalk.com/robot/send?access_token=';
    if (options.token.substring(0, 4).toLowerCase() === 'http') {
    url = options.token;
    } else {
    url += options.token;
    }

    const msgtype = options.options?.dingtalk?.msgtype || 'text';
    const content = msgtype === 'text'
    ? (options.title ? `${options.title}\n` : '') + getTxt(options.content)
    : options.content;

    const msgBody = {
    msgtype,
    };

    if (msgtype === 'text') {
    msgBody[msgtype] = { content };
    } else if (msgtype === 'markdown') {
    msgBody[msgtype] = { title: options.title || getTitle(options.content), text: content };
    }
    const response = await axios.post(url, msgBody);
    return response.data;
    }

    /**
    * 文档: https://developer.work.weixin.qq.com/document/path/90236
    * 教程: https://sct.ftqq.com/forward
    */
    async function noticeWeCom(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const [corpid, corpsecret, agentid, touser = '@all'] = options.token.split('#');
    checkParameters(
    {
    corpid,
    corpsecret,
    agentid,
    },
    ['corpid', 'corpsecret', 'agentid'],
    );
    // 获取 Access Token
    let accessToken;
    try {
    const accessTokenRes = await axios.get(
    `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpid}&corpsecret=${corpsecret}`,
    );
    accessToken = accessTokenRes.data.access_token;
    } catch (e) {
    console.error('获取企业微信 access token 失败,请检查 token', e);
    return {};
    }
    // 发送消息
    const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`;
    let content = getTxt(options.content);
    if (options.title) {
    content = `${options.title}\n${content}`;
    }
    const param = {
    touser,
    msgtype: 'text',
    agentid,
    text: { content },
    };
    const response = await axios.post(url, param);
    return response.data;
    }

    /**
    * https://github.com/Finb/Bark
    */
    async function noticeBark(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    let url = 'https://api.day.app/';
    if (options.token.substring(0, 4).toLowerCase() === 'http') {
    url = options.token;
    } else {
    url += options.token;
    }
    if (!url.endsWith('/')) url += '/';
    const title = encodeURIComponent(options.title || getTitle(options.content));
    const content = encodeURIComponent(getTxt(options.content));
    const params = new URLSearchParams({
    url: options?.options?.bark?.url || '',
    });
    const response = await axios.get(`${url}${title}/${content}/`, { params });
    return response.data;
    }

    /**
    * 文档: https://docs.go-cqhttp.org/api/
    * 教程: https://twikoo.js.org/QQ_API.html
    */
    async function noticeGoCqhttp(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = options.token;
    let message = getTxt(options.content);
    if (options.title) {
    message = `${options.title}\n${message}`;
    }
    const param = new URLSearchParams({ message });
    const response = await axios.post(url, param.toString());
    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<string, any> = {
    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';
    const response = await axios.post(url, {
    pushkey: options.token,
    text: options.title || getTitle(options.content),
    desp: options.content,
    });
    return response.data;
    }

    async function noticeIgot(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = `https://push.hellyw.com/${options.token}`;
    const response = await axios.post(url, {
    title: options.title || getTitle(options.content),
    content: getTxt(options.content),
    });
    return response.data;
    }

    /**
    * 文档: https://core.telegram.org/method/messages.sendMessage
    * 教程: https://core.telegram.org/bots#3-how-do-i-create-a-bot
    */
    async function noticeTelegram(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const [tgToken, chatId] = options.token.split('#');
    checkParameters(
    {
    tgToken,
    chatId,
    },
    ['tgToken', 'chatId'],
    );
    let text = options.content.replace(/([*_])/g, '\\$1'); // * 和 _ 似乎需要转义,否则会抛出 400 Bad Request 以及消息显示不正常
    if (options.title) {
    text = `${options.title}\n\n${text}`;
    }
    const response = await axios.post(`https://api.telegram.org/bot${tgToken}/sendMessage`, {
    text,
    chat_id: chatId,
    parse_mode: 'Markdown',
    });
    return response.data;
    }

    /**
    * https://www.feishu.cn/hc/zh-CN/articles/360024984973
    */
    async function noticeFeishu(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const v1 = 'https://open.feishu.cn/open-apis/bot/hook/';
    const v2 = 'https://open.feishu.cn/open-apis/bot/v2/hook/';
    let url;
    let params;
    if (options.token.substring(0, 4).toLowerCase() === 'http') {
    url = options.token;
    } else {
    url = v2 + options.token;
    }
    if (url.substring(0, v1.length) === v1) {
    params = {
    title: options.title || getTitle(options.content),
    text: getTxt(options.content),
    };
    } else {
    let text = getTxt(options.content);
    if (options.title) {
    text = `${options.title}\n${text}`;
    }
    params = {
    msg_type: 'text',
    content: { text },
    };
    }
    const response = await axios.post(url, params);
    return response.data;
    }

    /**
    * https://ifttt.com/maker_webhooks
    * http://ift.tt/webhooks_faq
    */
    async function noticeIfttt(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);

    const [token, eventName] = options.token.split('#');
    checkParameters(
    {
    token,
    eventName,
    },
    ['token', 'eventName'],
    );

    const url = `https://maker.ifttt.com/trigger/${eventName}/with/key/${token}`;

    const response = await axios.post(
    url,
    {
    value1: options.options?.ifttt?.value1 || getTxt(options.title),
    value2: options.options?.ifttt?.value2 || getTxt(options.content),
    value3: options.options?.ifttt?.value3,
    },
    {
    headers: { 'Content-Type': 'application/json' },
    },
    );
    return response.data;
    }

    /**
    * 文档: https://developer.work.weixin.qq.com/document/path/91770
    * 教程: https://developer.work.weixin.qq.com/tutorial/detail/54
    */
    async function noticeWecombot(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${options.token}`;
    const content = getTxt(options.content);

    const response = await axios.post(
    url,
    {
    msgtype: 'text',
    text: {
    content,
    },
    },
    {
    headers: { 'Content-Type': 'application/json' },
    },
    );

    return response.data;
    }

    /**
    * 文档:https://discord.com/developers/docs/resources/webhook#execute-webhook
    */
    async function noticeDiscord(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = options.token.startsWith('https://')
    ? options.token
    : `https://discord.com/api/webhooks/${options.token.replace(/#/, '/')}`;

    const response = await axios.post(
    url,
    {
    content: options.content,
    username: options.options?.discord?.userName,
    avatar_url: options.options?.discord?.avatarUrl,
    },
    {
    headers: { 'Content-Type': 'application/json' },
    },
    );
    return `Delivered successfully, code ${response.status}.`;
    }

    /**
    * WXPusher 推送
    * 教程:https://wxpusher.zjiecode.com/admin/
    * 文档: https://wxpusher.zjiecode.com/docs/#/
    */
    async function noticeWxPusher(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const url = 'http://wxpusher.zjiecode.com/api/send/message';
    const [appToken, topicIds] = options.token.split('#');
    checkParameters({ appToken, topicIds }, ['appToken', 'topicIds']);

    const response = await axios.post(
    url,
    {
    appToken,
    content: options.content,
    summary: options.title || getTitle(options.content),
    contentType: 3,
    topicIds: topicIds.split(',').map((id) => Number(id)),
    uids: options?.options?.wxpusher?.uids || [],
    url: options?.options?.wxpusher?.url || '',
    verifyPayload: options?.options?.wxpusher?.verifyPay || false,
    },
    {
    headers: {
    'Content-Type': 'application/json',
    },
    },
    );
    return response.data;
    }

    /**
    * Join 推送
    * 文档: https://joaoapps.com/join/api/
    */
    async function noticeJoin(options: CommonOptions) {
    checkParameters(options, ['token', 'content']);
    const [apiKey, deviceId] = options.token.split('#');
    checkParameters({ apiKey, deviceId }, ['apiKey', 'deviceId']);

    const url = 'https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush';
    const param = new URLSearchParams({
    apikey: apiKey,
    deviceId,
    title: options.title || getTitle(options.content),
    text: options.content,
    });
    const response = await axios.post(url, param.toString(), {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
    return response.data;
    }

    async function notice(channel: ChannelType, options: CommonOptions) {
    try {
    let data: any;
    const noticeFn = {
    qmsg: noticeQmsg,
    serverchan: noticeServerChan,
    serverchain: noticeServerChan,
    pushplus: noticePushPlus,
    pushplushxtrip: noticePushPlusHxtrip,
    dingtalk: noticeDingTalk,
    wecom: noticeWeCom,
    bark: noticeBark,
    gocqhttp: noticeGoCqhttp,
    onebot:noticeNodeOnebot,
    atri: noticeAtri,
    pushdeer: noticePushdeer,
    igot: noticeIgot,
    telegram: noticeTelegram,
    feishu: noticeFeishu,
    ifttt: noticeIfttt,
    wecombot: noticeWecombot,
    discord: noticeDiscord,
    wxpusher: noticeWxPusher,
    join: noticeJoin,
    }[channel.toLowerCase()];
    if (noticeFn) {
    data = await noticeFn(options);
    } else {
    throw new Error(`<${channel}> is not supported`);
    }
    console.debug(`[PUSHOO] Send to <${channel}> result:`, data);
    return data;
    } catch (e) {
    console.error('[PUSHOO] Got error:', e.message);
    return { error: e };
    }
    }

    export default notice;

    export {
    notice,
    noticeQmsg,
    noticeServerChan,
    noticePushPlus,
    noticePushPlusHxtrip,
    noticeDingTalk,
    noticeWeCom,
    noticeBark,
    noticeGoCqhttp,
    noticeNodeOnebot,
    noticeAtri,
    noticePushdeer,
    noticeIgot,
    noticeTelegram,
    noticeFeishu,
    noticeIfttt,
    noticeWecombot,
    noticeDiscord,
    noticeWxPusher,
    noticeJoin,
    };

第二步 回退到pushoo目录

  1. 回退到/root/twikoo/app/_data/node_modules/pushoo,依次运行以下命令即可
1
npm install
1
npm run build
  1. 如果显示无出错例如下图
    image.png
    则代表完成,重启你的twikoo的docker容器即可

其他部署说明

其他例如vercel和Hugging Face以及等等其他部署,如果你有条件能够访问器tkserver文件夹下的node_modules文件夹里的pushoo,你就完全可以参照以上私有部署的步骤参考进行更改代码后进行安装环境npm installnpm run build编译替代完成重启。

但如果你的环境不支持在里面编译,你也完全可以在有node环境下拷贝pushoo目录下的所有文件出来,进行npm installnpm run build然后拷贝新生成的/pushoo/dist的目录覆盖原有的/pushoo/dist。

其实有更好的解决方法就是,我已经pr给pushoo官方,如果他们愿意合并代码,那就更新最新的twikoo坐等即可。希望各位能顺利~

使用说明

上面聊好了如何部署等方法,那么如何引用使用该onebot 11协议呢?
onebot 11协议在Twikoo前端引入只支持正向HTTP通信,因为仅为推送通知而不是连接机器人。

  • 其实格式如下
    image.png
  • pushoo_channel选项填入onebot
  • Pushoo_token处填以下格式内容的url
    1. (发送到某个QQ号)
      http://你的IP或域名:端口号/send_private_msg?user_id=QQ号&access_token=你配置的token(QQ号)
    2. (发送到某个QQ群)
      http://你的IP或域名:端口号/send_group_msg?group_id=群号&access_token=你配置的鉴权 token(QQ群)
    3. 具体的各种API接口请移步官方文档 OneBot 11官方文档