Diagnose PushPlus delivery #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Diagnose PushPlus delivery | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: pushplus-delivery-diagnostic | |
| cancel-in-progress: false | |
| jobs: | |
| diagnose: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| env: | |
| PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} | |
| PUSHPLUS_SECRET_KEY: ${{ secrets.PUSHPLUS_SECRET_KEY }} | |
| PUSHPLUS_WEBHOOK_URL: https://pushplus-sms-to-telegram.pages.dev/pushplus/webhook/${{ secrets.RELAY_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| - name: Inspect recent PushPlus records without message content | |
| run: | | |
| node <<'NODE' | |
| const { fetchRecentMessages } = require('./src/pushplus'); | |
| async function main() { | |
| const messages = await fetchRecentMessages({ | |
| token: process.env.PUSHPLUS_TOKEN, | |
| secretKey: process.env.PUSHPLUS_SECRET_KEY, | |
| baseUrl: 'https://www.pushplus.plus', | |
| pageSize: 20, | |
| titleKeyword: '短信转发', | |
| bodyKeyword: '', | |
| lookbackMinutes: 7 * 24 * 60, | |
| }); | |
| const dayAgo = Date.now() - 24 * 60 * 60 * 1000; | |
| const summary = { | |
| matchingRecordsSevenDays: messages.length, | |
| matchingRecordsTwentyFourHours: messages.filter(message => message.receivedAt >= dayAgo).length, | |
| latestUpdateTime: messages.at(-1)?.updateTime || null, | |
| readableDetails: messages.filter(message => message.text.length > 0).length, | |
| smsMarkerRecords: messages.filter(message => message.text.includes('#SMS')).length, | |
| senderFieldRecords: messages.filter(message => /发件(?:号码|人|号)?\s*[::]/.test(message.text)).length, | |
| }; | |
| console.log(JSON.stringify(summary)); | |
| } | |
| main().catch(error => { | |
| console.error(error.message); | |
| process.exit(1); | |
| }); | |
| NODE | |
| - name: Send end-to-end message through PushPlus | |
| run: | | |
| node <<'NODE' | |
| const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); | |
| async function requestJson(url, options = {}) { | |
| const response = await fetch(url, { | |
| ...options, | |
| signal: AbortSignal.timeout(15_000), | |
| }); | |
| const data = await response.json().catch(() => ({})); | |
| if (!response.ok) throw new Error(`PushPlus HTTP ${response.status}`); | |
| return data; | |
| } | |
| async function main() { | |
| const submitted = await requestJson('https://www.pushplus.plus/send', { | |
| method: 'POST', | |
| headers: { 'content-type': 'application/json', accept: 'application/json' }, | |
| body: JSON.stringify({ | |
| token: process.env.PUSHPLUS_TOKEN, | |
| title: '短信转发', | |
| content: [ | |
| 'PushPlus 完整生产链路诊断消息', | |
| '发件号码: DIAGNOSTIC-PUSHPLUS', | |
| `发件时间: ${new Date().toISOString()}`, | |
| '#SMS', | |
| ].join('\n'), | |
| }), | |
| }); | |
| if (submitted.code !== 200 || !submitted.data) { | |
| throw new Error(`PushPlus rejected the diagnostic request (code ${submitted.code ?? 'unknown'})`); | |
| } | |
| const access = await requestJson('https://www.pushplus.plus/api/common/openApi/getAccessKey', { | |
| method: 'POST', | |
| headers: { 'content-type': 'application/json', accept: 'application/json' }, | |
| body: JSON.stringify({ | |
| token: process.env.PUSHPLUS_TOKEN, | |
| secretKey: process.env.PUSHPLUS_SECRET_KEY, | |
| }), | |
| }); | |
| const accessKey = access?.data?.accessKey; | |
| if (access.code !== 200 || !accessKey) throw new Error('Could not obtain a PushPlus access key'); | |
| for (let attempt = 1; attempt <= 20; attempt += 1) { | |
| const result = await requestJson( | |
| `https://www.pushplus.plus/api/open/message/sendMessageResult?shortCode=${encodeURIComponent(submitted.data)}`, | |
| { headers: { accept: 'application/json', 'access-key': accessKey } }, | |
| ); | |
| if (result.code !== 200) { | |
| throw new Error(`Could not query PushPlus delivery status (code ${result.code ?? 'unknown'})`); | |
| } | |
| const status = Number(result?.data?.status); | |
| if (status === 2) { | |
| console.log('PushPlus accepted the message and completed webhook delivery to Telegram.'); | |
| return; | |
| } | |
| if (status === 3) { | |
| const detail = String(result?.data?.errorMessage || 'no detail returned') | |
| .replace(/\/pushplus\/webhook\/[^/?\s]+/gi, '/pushplus/webhook/[redacted]') | |
| .replace(/([?&](?:token|key|secret|authorization)=)[^&\s]+/gi, '$1[redacted]') | |
| .slice(0, 500); | |
| throw new Error(`PushPlus reported failed webhook delivery: ${detail}`); | |
| } | |
| if (attempt < 20) await sleep(2_000); | |
| } | |
| throw new Error('Timed out waiting for PushPlus webhook delivery'); | |
| } | |
| main().catch(error => { | |
| console.error(error.message); | |
| process.exit(1); | |
| }); | |
| NODE | |
| - name: Send production webhook-to-Telegram smoke message | |
| if: always() | |
| env: | |
| TEST_SOURCE_ID: delivery-smoke-${{ github.run_id }}-${{ github.run_attempt }} | |
| run: | | |
| node <<'NODE' | |
| async function main() { | |
| const response = await fetch(process.env.PUSHPLUS_WEBHOOK_URL, { | |
| method: 'POST', | |
| headers: { 'content-type': 'application/json' }, | |
| body: JSON.stringify({ | |
| shortCode: process.env.TEST_SOURCE_ID, | |
| title: '短信转发', | |
| content: [ | |
| 'PushPlus 到 Telegram 生产链路诊断消息', | |
| '发件号码: DIAGNOSTIC', | |
| `发件时间: ${new Date().toISOString()}`, | |
| '#SMS', | |
| ].join('\n'), | |
| }), | |
| }); | |
| const body = await response.text(); | |
| if (!response.ok) throw new Error(`Production webhook smoke failed: HTTP ${response.status}`); | |
| const data = JSON.parse(body); | |
| if (data.code !== 200) throw new Error('Production webhook smoke returned a non-success response'); | |
| console.log('Production webhook accepted the message and Telegram sendMessage succeeded.'); | |
| } | |
| main().catch(error => { | |
| console.error(error.message); | |
| process.exit(1); | |
| }); | |
| NODE |