增加xpush
This commit is contained in:
@@ -9,24 +9,6 @@
|
||||
"deviceId": "-",
|
||||
"deviceType": 0
|
||||
},
|
||||
"测试12": {
|
||||
"token": "",
|
||||
"baseUrl": "",
|
||||
"username": "",
|
||||
"phone": "",
|
||||
"loggedInAt": "",
|
||||
"deviceId": "-",
|
||||
"deviceType": 0
|
||||
},
|
||||
"account3": {
|
||||
"token": "",
|
||||
"baseUrl": "",
|
||||
"username": "",
|
||||
"phone": "",
|
||||
"loggedInAt": "",
|
||||
"deviceId": "-",
|
||||
"deviceType": 0
|
||||
},
|
||||
"account2": {
|
||||
"token": "KU2draP6Rm7AQDIeN0SSIVeNBkdMdGODlN1ToNhAo0wYk6clo9EeHh8fE5cN2kIhxThC30gCZXZl6LQfs19K03S9",
|
||||
"baseUrl": "https://123.207.217.176",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
"key": "default",
|
||||
"label": "默认账号",
|
||||
"enabled": true,
|
||||
"baseUrl": "https://123.207.217.176",
|
||||
"username": "17665234375",
|
||||
"password": "yaochao11",
|
||||
@@ -14,6 +15,7 @@
|
||||
{
|
||||
"key": "account2",
|
||||
"label": "备用账号 1",
|
||||
"enabled": true,
|
||||
"baseUrl": "https://123.207.217.176",
|
||||
"username": "18216161068",
|
||||
"password": "yaochao11",
|
||||
|
||||
@@ -18,6 +18,23 @@
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"wpush": {
|
||||
"enabled": true,
|
||||
"recipients": [
|
||||
{
|
||||
"id": "1c1089bb-4a54-4ec9-a10f-15b8099fdfb0",
|
||||
"name": "y",
|
||||
"apiKey": "WPUSHcPSmgodKUwMCB13IFSHJsyBqUI4",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "5affc7d0-3944-43ee-90cb-8242ab9b34e7",
|
||||
"name": "yml",
|
||||
"apiKey": "WPUSHsa44qjF35klEZRqVHxAouoDE2A6",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ router.post(
|
||||
)
|
||||
? result.source.channels.bark.recipients.length
|
||||
: 0,
|
||||
wpushRecipientCount: Array.isArray(
|
||||
result.source?.channels?.wpush?.recipients
|
||||
)
|
||||
? result.source.channels.wpush.recipients.length
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ export function updateAdminNotificationConfig(payload: AdminNotificationConfigIn
|
||||
export async function testAdminNotification(payload: AdminNotificationTestInput = {}) {
|
||||
return sendInternalNotification({
|
||||
title: String(payload.title || '订单系统测试通知').trim() || '订单系统测试通知',
|
||||
body: String(payload.body || '这是一条 Bark 内部通知测试。').trim() || '这是一条 Bark 内部通知测试。',
|
||||
body: String(payload.body || '这是一条内部通知测试。').trim() || '这是一条内部通知测试。',
|
||||
category: 'test',
|
||||
url: String(payload.url || '').trim(),
|
||||
})
|
||||
@@ -83,6 +83,7 @@ export async function runAdminScheduledJobNow(jobId: unknown) {
|
||||
|
||||
function mapAdminNotificationConfig(config: JsonObject = {}) {
|
||||
const bark = config.channels?.bark || {}
|
||||
const wpush = config.channels?.wpush || {}
|
||||
|
||||
return {
|
||||
enabled: config.enabled !== false,
|
||||
@@ -98,6 +99,16 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
|
||||
enabled: item.enabled !== false,
|
||||
})),
|
||||
},
|
||||
wpush: {
|
||||
enabled: wpush.enabled !== false,
|
||||
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item) => ({
|
||||
id: String(item.id || '').trim(),
|
||||
name: String(item.name || '').trim(),
|
||||
apiKey: String(item.apiKey || item.apikey || '').trim(),
|
||||
apiKeyMasked: maskSecret(item.apiKey || item.apikey),
|
||||
enabled: item.enabled !== false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
createDefaultNotificationConfig,
|
||||
normalizeNotificationConfig,
|
||||
} from './config-service.js'
|
||||
|
||||
test('normalizeNotificationConfig normalizes bark and wpush recipients', () => {
|
||||
assert.deepEqual(
|
||||
normalizeNotificationConfig({
|
||||
enabled: false,
|
||||
channels: {
|
||||
bark: {
|
||||
enabled: true,
|
||||
serverUrl: 'https://api.day.app///',
|
||||
recipients: [
|
||||
{
|
||||
name: ' 值班A ',
|
||||
deviceKey: ' bark-key ',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: ' ',
|
||||
deviceKey: ' ',
|
||||
},
|
||||
],
|
||||
},
|
||||
wpush: {
|
||||
enabled: true,
|
||||
recipients: [
|
||||
{
|
||||
name: ' 值班B ',
|
||||
apikey: ' wpush-key ',
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
apiKey: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
enabled: false,
|
||||
channels: {
|
||||
bark: {
|
||||
enabled: true,
|
||||
serverUrl: 'https://api.day.app',
|
||||
recipients: [
|
||||
{
|
||||
id: 'bark-key',
|
||||
name: '值班A',
|
||||
deviceKey: 'bark-key',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
wpush: {
|
||||
enabled: true,
|
||||
recipients: [
|
||||
{
|
||||
id: 'wpush-key',
|
||||
name: '值班B',
|
||||
apiKey: 'wpush-key',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('createDefaultNotificationConfig includes wpush channel', () => {
|
||||
assert.deepEqual(createDefaultNotificationConfig(), {
|
||||
enabled: true,
|
||||
channels: {
|
||||
bark: {
|
||||
enabled: true,
|
||||
serverUrl: 'https://api.day.app',
|
||||
recipients: [],
|
||||
},
|
||||
wpush: {
|
||||
enabled: true,
|
||||
recipients: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -29,6 +29,15 @@ export function listEnabledBarkRecipients(config: JsonObject = getNotificationCo
|
||||
.filter((item) => item.enabled !== false && String(item.deviceKey || '').trim())
|
||||
}
|
||||
|
||||
export function listEnabledWpushRecipients(config: JsonObject = getNotificationConfig()) {
|
||||
if (config.enabled === false || config.channels?.wpush?.enabled === false) {
|
||||
return []
|
||||
}
|
||||
|
||||
return (Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : [])
|
||||
.filter((item) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim())
|
||||
}
|
||||
|
||||
function loadNotificationConfigFromFile() {
|
||||
return readJsonFile(
|
||||
NOTIFICATION_CONFIG_FILE_PATH,
|
||||
@@ -37,9 +46,10 @@ function loadNotificationConfigFromFile() {
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeNotificationConfig(rawValue: unknown) {
|
||||
export function normalizeNotificationConfig(rawValue: unknown) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {}
|
||||
const wpush = isPlainObject(source.channels?.wpush) ? source.channels.wpush : {}
|
||||
|
||||
return {
|
||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
||||
@@ -51,6 +61,12 @@ function normalizeNotificationConfig(rawValue: unknown) {
|
||||
.map((item) => normalizeBarkRecipient(item))
|
||||
.filter(Boolean),
|
||||
},
|
||||
wpush: {
|
||||
enabled: typeof wpush.enabled === 'boolean' ? wpush.enabled : true,
|
||||
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : [])
|
||||
.map((item) => normalizeWpushRecipient(item))
|
||||
.filter(Boolean),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -81,7 +97,28 @@ function normalizeBarkServerUrl(value: unknown) {
|
||||
return normalized.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function createDefaultNotificationConfig() {
|
||||
function normalizeWpushRecipient(rawValue: unknown) {
|
||||
if (!isPlainObject(rawValue)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const name = String(rawValue.name || '').trim()
|
||||
const apiKey = String(rawValue.apiKey || rawValue.apikey || '').trim()
|
||||
const id = String(rawValue.id || apiKey || name || '').trim()
|
||||
|
||||
if (!name && !apiKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
apiKey,
|
||||
enabled: typeof rawValue.enabled === 'boolean' ? rawValue.enabled : true,
|
||||
}
|
||||
}
|
||||
|
||||
export function createDefaultNotificationConfig() {
|
||||
return {
|
||||
enabled: true,
|
||||
channels: {
|
||||
@@ -90,6 +127,10 @@ function createDefaultNotificationConfig() {
|
||||
serverUrl: DEFAULT_BARK_SERVER_URL,
|
||||
recipients: [],
|
||||
},
|
||||
wpush: {
|
||||
enabled: true,
|
||||
recipients: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { logWarn } from '../../utils/logger.js'
|
||||
import { getNotificationConfig, listEnabledBarkRecipients } from './config-service.js'
|
||||
import {
|
||||
getNotificationConfig,
|
||||
listEnabledBarkRecipients,
|
||||
listEnabledWpushRecipients,
|
||||
} from './config-service.js'
|
||||
import { sendBarkNotification } from './bark-service.js'
|
||||
import { sendWpushNotification } from './wpush-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
type NotificationInput = {
|
||||
@@ -13,7 +18,8 @@ type NotificationInput = {
|
||||
export async function sendInternalNotification(input: NotificationInput = {}) {
|
||||
const config = getNotificationConfig()
|
||||
const bark = (config.channels?.bark || {}) as { serverUrl?: string }
|
||||
const recipients = listEnabledBarkRecipients(config)
|
||||
const barkRecipients = listEnabledBarkRecipients(config)
|
||||
const wpushRecipients = listEnabledWpushRecipients(config)
|
||||
const title = String(input.title || '订单系统通知').trim() || '订单系统通知'
|
||||
const body = String(input.body || '').trim()
|
||||
const category = String(input.category || 'system').trim() || 'system'
|
||||
@@ -21,44 +27,73 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
||||
if (config.enabled === false) {
|
||||
return {
|
||||
enabled: false,
|
||||
channel: 'bark',
|
||||
channel: 'internal',
|
||||
successCount: 0,
|
||||
failedCount: 0,
|
||||
skippedCount: recipients.length,
|
||||
skippedCount: barkRecipients.length + wpushRecipients.length,
|
||||
results: [],
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(recipients.map(async (recipient) => {
|
||||
try {
|
||||
const result = await sendBarkNotification({
|
||||
serverUrl: bark.serverUrl,
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
group: `订单系统/${category}`,
|
||||
url: input.url,
|
||||
})
|
||||
return mapNotificationResult(recipient, true, '', result.status, result.response)
|
||||
} catch (error) {
|
||||
logWarn('[notification/bark]', 'Bark 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
recipient,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'Bark 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}))
|
||||
const results = [
|
||||
...(await Promise.all(barkRecipients.map(async (recipient) => {
|
||||
try {
|
||||
const result = await sendBarkNotification({
|
||||
serverUrl: bark.serverUrl,
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
group: `订单系统/${category}`,
|
||||
url: input.url,
|
||||
})
|
||||
return mapNotificationResult('bark', recipient, recipient.deviceKey, true, '', result.status, result.response)
|
||||
} catch (error) {
|
||||
logWarn('[notification/bark]', 'Bark 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'bark',
|
||||
recipient,
|
||||
recipient.deviceKey,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'Bark 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}))),
|
||||
...(await Promise.all(wpushRecipients.map(async (recipient) => {
|
||||
try {
|
||||
const result = await sendWpushNotification({
|
||||
recipient,
|
||||
title,
|
||||
body,
|
||||
})
|
||||
return mapNotificationResult('wpush', recipient, recipient.apiKey, true, '', result.status, result.response)
|
||||
} catch (error) {
|
||||
logWarn('[notification/wpush]', 'WPush 内部通知发送失败', {
|
||||
recipientId: recipient.id,
|
||||
recipientName: recipient.name,
|
||||
error,
|
||||
})
|
||||
return mapNotificationResult(
|
||||
'wpush',
|
||||
recipient,
|
||||
recipient.apiKey,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'WPush 内部通知发送失败',
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}))),
|
||||
]
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
channel: 'bark',
|
||||
channel: 'internal',
|
||||
successCount: results.filter((item) => item.ok).length,
|
||||
failedCount: results.filter((item) => !item.ok).length,
|
||||
skippedCount: 0,
|
||||
@@ -67,16 +102,19 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
||||
}
|
||||
|
||||
function mapNotificationResult(
|
||||
channel: string,
|
||||
recipient: JsonObject,
|
||||
recipientKey: unknown,
|
||||
ok: boolean,
|
||||
errorMessage: string,
|
||||
status: number,
|
||||
response: unknown,
|
||||
) {
|
||||
return {
|
||||
channel,
|
||||
recipientId: String(recipient.id || '').trim(),
|
||||
recipientName: String(recipient.name || '').trim(),
|
||||
recipientKeyMasked: maskDeviceKey(recipient.deviceKey),
|
||||
recipientKeyMasked: maskSecretKey(recipientKey),
|
||||
ok,
|
||||
status,
|
||||
errorMessage,
|
||||
@@ -84,7 +122,7 @@ function mapNotificationResult(
|
||||
}
|
||||
}
|
||||
|
||||
function maskDeviceKey(value: unknown) {
|
||||
function maskSecretKey(value: unknown) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return ''
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import http from 'node:http'
|
||||
|
||||
import { sendWpushNotification } from './wpush-service.js'
|
||||
|
||||
test('sendWpushNotification sends POST json payload', async () => {
|
||||
const captured = {
|
||||
method: '',
|
||||
contentType: '',
|
||||
body: '',
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
captured.method = String(req.method || '')
|
||||
captured.contentType = String(req.headers['content-type'] || '')
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
req.on('data', (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
})
|
||||
req.on('end', () => {
|
||||
captured.body = Buffer.concat(chunks).toString('utf8')
|
||||
res.writeHead(200, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify({ code: 0, message: 'ok' }))
|
||||
})
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => resolve())
|
||||
})
|
||||
|
||||
try {
|
||||
const address = server.address()
|
||||
assert.ok(address && typeof address === 'object')
|
||||
|
||||
const result = await sendWpushNotification({
|
||||
endpoint: `http://127.0.0.1:${address.port}/send`,
|
||||
recipient: {
|
||||
apiKey: 'test-key-1',
|
||||
},
|
||||
title: '测试标题',
|
||||
body: '测试内容',
|
||||
})
|
||||
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(captured.method, 'POST')
|
||||
assert.equal(captured.contentType, 'application/json')
|
||||
assert.deepEqual(JSON.parse(captured.body), {
|
||||
apikey: 'test-key-1',
|
||||
title: '测试标题',
|
||||
content: '测试内容',
|
||||
})
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('sendWpushNotification rejects missing api key', async () => {
|
||||
await assert.rejects(
|
||||
() => sendWpushNotification({
|
||||
recipient: {},
|
||||
title: '测试标题',
|
||||
body: '测试内容',
|
||||
}),
|
||||
(error: any) => {
|
||||
assert.equal(error?.errorCode, 'wpush_api_key_required')
|
||||
return true
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send'
|
||||
const DEFAULT_TIMEOUT_MS = 10000
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
type NodeHttpResponse = {
|
||||
ok: boolean
|
||||
status: number
|
||||
bodyText: string
|
||||
}
|
||||
type WpushSendInput = {
|
||||
recipient?: {
|
||||
id?: string
|
||||
name?: string
|
||||
apiKey?: string
|
||||
}
|
||||
title?: string
|
||||
body?: string
|
||||
endpoint?: string
|
||||
}
|
||||
|
||||
export async function sendWpushNotification(input: WpushSendInput) {
|
||||
const apiKey = String(input.recipient?.apiKey || '').trim()
|
||||
const title = String(input.title || '').trim()
|
||||
const body = String(input.body || '').trim()
|
||||
const endpoint = String(input.endpoint || DEFAULT_WPUSH_ENDPOINT).trim() || DEFAULT_WPUSH_ENDPOINT
|
||||
|
||||
if (!apiKey) {
|
||||
throw createHttpError('WPush 接收人缺少 apikey', {
|
||||
statusCode: 400,
|
||||
errorCode: 'wpush_api_key_required',
|
||||
})
|
||||
}
|
||||
|
||||
if (!title && !body) {
|
||||
throw createHttpError('WPush 通知标题或内容不能为空', {
|
||||
statusCode: 400,
|
||||
errorCode: 'wpush_message_required',
|
||||
})
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
apikey: apiKey,
|
||||
title: title || body,
|
||||
content: body || title,
|
||||
})
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const response = await requestViaNodeHttp(new URL(endpoint), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
accept: 'application/json, text/plain, */*',
|
||||
'content-length': String(Buffer.byteLength(payload)),
|
||||
},
|
||||
body: payload,
|
||||
signal: controller.signal,
|
||||
})
|
||||
const parsed = parseJsonResponse(response.bodyText)
|
||||
|
||||
if (!response.ok || isWpushFailure(parsed)) {
|
||||
throw createHttpError(resolveWpushErrorMessage(parsed, response.bodyText, response.status), {
|
||||
statusCode: response.ok ? 502 : response.status,
|
||||
errorCode: 'wpush_send_failed',
|
||||
context: {
|
||||
status: response.status,
|
||||
response: parsed || response.bodyText,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
response: parsed || response.bodyText,
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
throw createHttpError('WPush 通知发送超时', {
|
||||
statusCode: 503,
|
||||
errorCode: 'wpush_send_timeout',
|
||||
cause: error,
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
async function requestViaNodeHttp(
|
||||
url: URL,
|
||||
{ method, headers, body, signal }: {
|
||||
method: string
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
signal?: AbortSignal
|
||||
},
|
||||
): Promise<NodeHttpResponse> {
|
||||
const transport = url.protocol === 'https:' ? https : http
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = transport.request(url, {
|
||||
method,
|
||||
headers,
|
||||
rejectUnauthorized: false,
|
||||
}, (response) => {
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
})
|
||||
|
||||
response.on('end', () => {
|
||||
resolve({
|
||||
ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300,
|
||||
status: Number(response.statusCode || 0),
|
||||
bodyText: Buffer.concat(chunks).toString('utf8'),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
request.on('error', reject)
|
||||
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
const error = new Error('Request aborted')
|
||||
error.name = 'AbortError'
|
||||
request.destroy(error)
|
||||
} else {
|
||||
signal.addEventListener('abort', () => {
|
||||
const error = new Error('Request aborted')
|
||||
error.name = 'AbortError'
|
||||
request.destroy(error)
|
||||
}, { once: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body !== 'undefined') {
|
||||
request.write(body)
|
||||
}
|
||||
|
||||
request.end()
|
||||
})
|
||||
}
|
||||
|
||||
function parseJsonResponse(text: string) {
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isWpushFailure(parsed: unknown) {
|
||||
if (!isPlainObject(parsed)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const success = parsed.success
|
||||
if (typeof success === 'boolean') {
|
||||
return success === false
|
||||
}
|
||||
|
||||
const code = Number(parsed.code ?? parsed.status ?? 0)
|
||||
return Number.isFinite(code) && code !== 0 && code !== 200
|
||||
}
|
||||
|
||||
function resolveWpushErrorMessage(parsed: unknown, responseText: string, status: number) {
|
||||
if (isPlainObject(parsed)) {
|
||||
return String(parsed.message || parsed.msg || parsed.error || '').trim() || `WPush 通知发送失败,HTTP ${status}`
|
||||
}
|
||||
|
||||
return String(responseText || '').trim() || `WPush 通知发送失败,HTTP ${status}`
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown): error is Error {
|
||||
return error instanceof Error && error.name === 'AbortError'
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
@@ -102,6 +102,14 @@ export type AdminNotificationBarkRecipientInput = {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type AdminNotificationWpushRecipientInput = {
|
||||
id?: string
|
||||
name?: string
|
||||
apiKey?: string
|
||||
apikey?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type AdminNotificationConfigInput = {
|
||||
enabled?: boolean
|
||||
channels?: {
|
||||
@@ -110,6 +118,10 @@ export type AdminNotificationConfigInput = {
|
||||
serverUrl?: string
|
||||
recipients?: AdminNotificationBarkRecipientInput[]
|
||||
}
|
||||
wpush?: {
|
||||
enabled?: boolean
|
||||
recipients?: AdminNotificationWpushRecipientInput[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user