增加xpush
This commit is contained in:
@@ -9,24 +9,6 @@
|
|||||||
"deviceId": "-",
|
"deviceId": "-",
|
||||||
"deviceType": 0
|
"deviceType": 0
|
||||||
},
|
},
|
||||||
"测试12": {
|
|
||||||
"token": "",
|
|
||||||
"baseUrl": "",
|
|
||||||
"username": "",
|
|
||||||
"phone": "",
|
|
||||||
"loggedInAt": "",
|
|
||||||
"deviceId": "-",
|
|
||||||
"deviceType": 0
|
|
||||||
},
|
|
||||||
"account3": {
|
|
||||||
"token": "",
|
|
||||||
"baseUrl": "",
|
|
||||||
"username": "",
|
|
||||||
"phone": "",
|
|
||||||
"loggedInAt": "",
|
|
||||||
"deviceId": "-",
|
|
||||||
"deviceType": 0
|
|
||||||
},
|
|
||||||
"account2": {
|
"account2": {
|
||||||
"token": "KU2draP6Rm7AQDIeN0SSIVeNBkdMdGODlN1ToNhAo0wYk6clo9EeHh8fE5cN2kIhxThC30gCZXZl6LQfs19K03S9",
|
"token": "KU2draP6Rm7AQDIeN0SSIVeNBkdMdGODlN1ToNhAo0wYk6clo9EeHh8fE5cN2kIhxThC30gCZXZl6LQfs19K03S9",
|
||||||
"baseUrl": "https://123.207.217.176",
|
"baseUrl": "https://123.207.217.176",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
{
|
{
|
||||||
"key": "default",
|
"key": "default",
|
||||||
"label": "默认账号",
|
"label": "默认账号",
|
||||||
|
"enabled": true,
|
||||||
"baseUrl": "https://123.207.217.176",
|
"baseUrl": "https://123.207.217.176",
|
||||||
"username": "17665234375",
|
"username": "17665234375",
|
||||||
"password": "yaochao11",
|
"password": "yaochao11",
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
{
|
{
|
||||||
"key": "account2",
|
"key": "account2",
|
||||||
"label": "备用账号 1",
|
"label": "备用账号 1",
|
||||||
|
"enabled": true,
|
||||||
"baseUrl": "https://123.207.217.176",
|
"baseUrl": "https://123.207.217.176",
|
||||||
"username": "18216161068",
|
"username": "18216161068",
|
||||||
"password": "yaochao11",
|
"password": "yaochao11",
|
||||||
|
|||||||
@@ -18,6 +18,23 @@
|
|||||||
"enabled": true
|
"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
|
? result.source.channels.bark.recipients.length
|
||||||
: 0,
|
: 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 = {}) {
|
export async function testAdminNotification(payload: AdminNotificationTestInput = {}) {
|
||||||
return sendInternalNotification({
|
return sendInternalNotification({
|
||||||
title: String(payload.title || '订单系统测试通知').trim() || '订单系统测试通知',
|
title: String(payload.title || '订单系统测试通知').trim() || '订单系统测试通知',
|
||||||
body: String(payload.body || '这是一条 Bark 内部通知测试。').trim() || '这是一条 Bark 内部通知测试。',
|
body: String(payload.body || '这是一条内部通知测试。').trim() || '这是一条内部通知测试。',
|
||||||
category: 'test',
|
category: 'test',
|
||||||
url: String(payload.url || '').trim(),
|
url: String(payload.url || '').trim(),
|
||||||
})
|
})
|
||||||
@@ -83,6 +83,7 @@ export async function runAdminScheduledJobNow(jobId: unknown) {
|
|||||||
|
|
||||||
function mapAdminNotificationConfig(config: JsonObject = {}) {
|
function mapAdminNotificationConfig(config: JsonObject = {}) {
|
||||||
const bark = config.channels?.bark || {}
|
const bark = config.channels?.bark || {}
|
||||||
|
const wpush = config.channels?.wpush || {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: config.enabled !== false,
|
enabled: config.enabled !== false,
|
||||||
@@ -98,6 +99,16 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
|
|||||||
enabled: item.enabled !== false,
|
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())
|
.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() {
|
function loadNotificationConfigFromFile() {
|
||||||
return readJsonFile(
|
return readJsonFile(
|
||||||
NOTIFICATION_CONFIG_FILE_PATH,
|
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 source = isPlainObject(rawValue) ? rawValue : {}
|
||||||
const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {}
|
const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {}
|
||||||
|
const wpush = isPlainObject(source.channels?.wpush) ? source.channels.wpush : {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
||||||
@@ -51,6 +61,12 @@ function normalizeNotificationConfig(rawValue: unknown) {
|
|||||||
.map((item) => normalizeBarkRecipient(item))
|
.map((item) => normalizeBarkRecipient(item))
|
||||||
.filter(Boolean),
|
.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(/\/+$/, '')
|
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 {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
channels: {
|
channels: {
|
||||||
@@ -90,6 +127,10 @@ function createDefaultNotificationConfig() {
|
|||||||
serverUrl: DEFAULT_BARK_SERVER_URL,
|
serverUrl: DEFAULT_BARK_SERVER_URL,
|
||||||
recipients: [],
|
recipients: [],
|
||||||
},
|
},
|
||||||
|
wpush: {
|
||||||
|
enabled: true,
|
||||||
|
recipients: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { logWarn } from '../../utils/logger.js'
|
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 { sendBarkNotification } from './bark-service.js'
|
||||||
|
import { sendWpushNotification } from './wpush-service.js'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
type NotificationInput = {
|
type NotificationInput = {
|
||||||
@@ -13,7 +18,8 @@ type NotificationInput = {
|
|||||||
export async function sendInternalNotification(input: NotificationInput = {}) {
|
export async function sendInternalNotification(input: NotificationInput = {}) {
|
||||||
const config = getNotificationConfig()
|
const config = getNotificationConfig()
|
||||||
const bark = (config.channels?.bark || {}) as { serverUrl?: string }
|
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 title = String(input.title || '订单系统通知').trim() || '订单系统通知'
|
||||||
const body = String(input.body || '').trim()
|
const body = String(input.body || '').trim()
|
||||||
const category = String(input.category || 'system').trim() || 'system'
|
const category = String(input.category || 'system').trim() || 'system'
|
||||||
@@ -21,44 +27,73 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
|||||||
if (config.enabled === false) {
|
if (config.enabled === false) {
|
||||||
return {
|
return {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
channel: 'bark',
|
channel: 'internal',
|
||||||
successCount: 0,
|
successCount: 0,
|
||||||
failedCount: 0,
|
failedCount: 0,
|
||||||
skippedCount: recipients.length,
|
skippedCount: barkRecipients.length + wpushRecipients.length,
|
||||||
results: [],
|
results: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(recipients.map(async (recipient) => {
|
const results = [
|
||||||
try {
|
...(await Promise.all(barkRecipients.map(async (recipient) => {
|
||||||
const result = await sendBarkNotification({
|
try {
|
||||||
serverUrl: bark.serverUrl,
|
const result = await sendBarkNotification({
|
||||||
recipient,
|
serverUrl: bark.serverUrl,
|
||||||
title,
|
recipient,
|
||||||
body,
|
title,
|
||||||
group: `订单系统/${category}`,
|
body,
|
||||||
url: input.url,
|
group: `订单系统/${category}`,
|
||||||
})
|
url: input.url,
|
||||||
return mapNotificationResult(recipient, true, '', result.status, result.response)
|
})
|
||||||
} catch (error) {
|
return mapNotificationResult('bark', recipient, recipient.deviceKey, true, '', result.status, result.response)
|
||||||
logWarn('[notification/bark]', 'Bark 内部通知发送失败', {
|
} catch (error) {
|
||||||
recipientId: recipient.id,
|
logWarn('[notification/bark]', 'Bark 内部通知发送失败', {
|
||||||
recipientName: recipient.name,
|
recipientId: recipient.id,
|
||||||
error,
|
recipientName: recipient.name,
|
||||||
})
|
error,
|
||||||
return mapNotificationResult(
|
})
|
||||||
recipient,
|
return mapNotificationResult(
|
||||||
false,
|
'bark',
|
||||||
error instanceof Error ? error.message : 'Bark 内部通知发送失败',
|
recipient,
|
||||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
recipient.deviceKey,
|
||||||
isPlainObject(error) ? error.context || null : null,
|
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 {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
channel: 'bark',
|
channel: 'internal',
|
||||||
successCount: results.filter((item) => item.ok).length,
|
successCount: results.filter((item) => item.ok).length,
|
||||||
failedCount: results.filter((item) => !item.ok).length,
|
failedCount: results.filter((item) => !item.ok).length,
|
||||||
skippedCount: 0,
|
skippedCount: 0,
|
||||||
@@ -67,16 +102,19 @@ export async function sendInternalNotification(input: NotificationInput = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapNotificationResult(
|
function mapNotificationResult(
|
||||||
|
channel: string,
|
||||||
recipient: JsonObject,
|
recipient: JsonObject,
|
||||||
|
recipientKey: unknown,
|
||||||
ok: boolean,
|
ok: boolean,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
status: number,
|
status: number,
|
||||||
response: unknown,
|
response: unknown,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
channel,
|
||||||
recipientId: String(recipient.id || '').trim(),
|
recipientId: String(recipient.id || '').trim(),
|
||||||
recipientName: String(recipient.name || '').trim(),
|
recipientName: String(recipient.name || '').trim(),
|
||||||
recipientKeyMasked: maskDeviceKey(recipient.deviceKey),
|
recipientKeyMasked: maskSecretKey(recipientKey),
|
||||||
ok,
|
ok,
|
||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -84,7 +122,7 @@ function mapNotificationResult(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maskDeviceKey(value: unknown) {
|
function maskSecretKey(value: unknown) {
|
||||||
const normalized = String(value || '').trim()
|
const normalized = String(value || '').trim()
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return ''
|
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
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminNotificationWpushRecipientInput = {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
apiKey?: string
|
||||||
|
apikey?: string
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type AdminNotificationConfigInput = {
|
export type AdminNotificationConfigInput = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
channels?: {
|
channels?: {
|
||||||
@@ -110,6 +118,10 @@ export type AdminNotificationConfigInput = {
|
|||||||
serverUrl?: string
|
serverUrl?: string
|
||||||
recipients?: AdminNotificationBarkRecipientInput[]
|
recipients?: AdminNotificationBarkRecipientInput[]
|
||||||
}
|
}
|
||||||
|
wpush?: {
|
||||||
|
enabled?: boolean
|
||||||
|
recipients?: AdminNotificationWpushRecipientInput[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export type {
|
|||||||
AdminAgisoMessagingDefaults,
|
AdminAgisoMessagingDefaults,
|
||||||
AdminAgisoObservedShopItem,
|
AdminAgisoObservedShopItem,
|
||||||
AdminNotificationBarkRecipient,
|
AdminNotificationBarkRecipient,
|
||||||
|
AdminNotificationWpushRecipient,
|
||||||
AdminNotificationConfig,
|
AdminNotificationConfig,
|
||||||
AdminNotificationSendResult,
|
AdminNotificationSendResult,
|
||||||
AdminNotificationTestResult,
|
AdminNotificationTestResult,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type {
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
AdminNotificationBarkRecipient,
|
AdminNotificationBarkRecipient,
|
||||||
|
AdminNotificationWpushRecipient,
|
||||||
AdminNotificationConfig,
|
AdminNotificationConfig,
|
||||||
AdminNotificationSendResult,
|
AdminNotificationSendResult,
|
||||||
AdminNotificationTestResult,
|
AdminNotificationTestResult,
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ export interface AdminNotificationBarkRecipient {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminNotificationWpushRecipient {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
apiKey: string
|
||||||
|
apiKeyMasked: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminNotificationConfig {
|
export interface AdminNotificationConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
channels: {
|
channels: {
|
||||||
@@ -14,10 +22,15 @@ export interface AdminNotificationConfig {
|
|||||||
serverUrl: string
|
serverUrl: string
|
||||||
recipients: AdminNotificationBarkRecipient[]
|
recipients: AdminNotificationBarkRecipient[]
|
||||||
}
|
}
|
||||||
|
wpush: {
|
||||||
|
enabled: boolean
|
||||||
|
recipients: AdminNotificationWpushRecipient[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminNotificationSendResult {
|
export interface AdminNotificationSendResult {
|
||||||
|
channel: string
|
||||||
recipientId: string
|
recipientId: string
|
||||||
recipientName: string
|
recipientName: string
|
||||||
recipientKeyMasked: string
|
recipientKeyMasked: string
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const {
|
|||||||
notificationFilePath,
|
notificationFilePath,
|
||||||
notificationForm,
|
notificationForm,
|
||||||
notificationRecipients,
|
notificationRecipients,
|
||||||
|
wpushRecipients,
|
||||||
notificationSaving,
|
notificationSaving,
|
||||||
notificationTesting,
|
notificationTesting,
|
||||||
notificationResultError,
|
notificationResultError,
|
||||||
@@ -263,10 +264,12 @@ onMounted(loadConfigs)
|
|||||||
{
|
{
|
||||||
key: 'notifications',
|
key: 'notifications',
|
||||||
label: '内部通知',
|
label: '内部通知',
|
||||||
tag: 'Bark / 值班',
|
tag: 'Bark / WPush',
|
||||||
value: notificationStats.enabledRecipientCount,
|
value:
|
||||||
|
notificationStats.barkEnabledRecipientCount +
|
||||||
|
notificationStats.wpushEnabledRecipientCount,
|
||||||
unit: '启用接收人',
|
unit: '启用接收人',
|
||||||
meta: `配置 ${notificationStats.configuredRecipientCount} 人 · 最近成功 ${notificationStats.lastSuccessCount} 个`,
|
meta: `Bark ${notificationStats.barkConfiguredRecipientCount} 人 · WPush ${notificationStats.wpushConfiguredRecipientCount} 人 · 最近成功 ${notificationStats.lastSuccessCount} 个`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ninetyone',
|
key: 'ninetyone',
|
||||||
@@ -356,6 +359,7 @@ onMounted(loadConfigs)
|
|||||||
:notification-file-path="notificationFilePath"
|
:notification-file-path="notificationFilePath"
|
||||||
:notification-form="notificationForm"
|
:notification-form="notificationForm"
|
||||||
:notification-recipients="notificationRecipients"
|
:notification-recipients="notificationRecipients"
|
||||||
|
:wpush-recipients="wpushRecipients"
|
||||||
:notification-saving="notificationSaving"
|
:notification-saving="notificationSaving"
|
||||||
:notification-testing="notificationTesting"
|
:notification-testing="notificationTesting"
|
||||||
:notification-result-error="notificationResultError"
|
:notification-result-error="notificationResultError"
|
||||||
|
|||||||
+9
-4
@@ -5,15 +5,20 @@ type NotificationForm = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
barkEnabled: boolean
|
barkEnabled: boolean
|
||||||
barkServerUrl: string
|
barkServerUrl: string
|
||||||
|
wpushEnabled: boolean
|
||||||
testTitle: string
|
testTitle: string
|
||||||
testBody: string
|
testBody: string
|
||||||
testUrl: string
|
testUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationStats = {
|
type NotificationStats = {
|
||||||
configuredRecipientCount: number
|
barkConfiguredRecipientCount: number
|
||||||
enabledRecipientCount: number
|
barkEnabledRecipientCount: number
|
||||||
|
wpushConfiguredRecipientCount: number
|
||||||
|
wpushEnabledRecipientCount: number
|
||||||
barkReady: boolean
|
barkReady: boolean
|
||||||
|
wpushReady: boolean
|
||||||
|
ready: boolean
|
||||||
lastSuccessCount: number
|
lastSuccessCount: number
|
||||||
lastFailedCount: number
|
lastFailedCount: number
|
||||||
scheduledJobEnabledCount: number
|
scheduledJobEnabledCount: number
|
||||||
@@ -40,8 +45,8 @@ defineProps<Props>()
|
|||||||
<div>
|
<div>
|
||||||
<span class="card-title">Bark 通知</span>
|
<span class="card-title">Bark 通知</span>
|
||||||
<p class="card-desc">
|
<p class="card-desc">
|
||||||
已配置 {{ notificationStats.configuredRecipientCount }} 人,启用
|
已配置 {{ notificationStats.barkConfiguredRecipientCount }} 人,启用
|
||||||
{{ notificationStats.enabledRecipientCount }} 人。
|
{{ notificationStats.barkEnabledRecipientCount }} 人。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
|||||||
+3
@@ -16,6 +16,9 @@ defineProps<Props>()
|
|||||||
<el-descriptions-item label="职责"
|
<el-descriptions-item label="职责"
|
||||||
>内部运营通知,仅发送给后台管理员、客服或值班人员。</el-descriptions-item
|
>内部运营通知,仅发送给后台管理员、客服或值班人员。</el-descriptions-item
|
||||||
>
|
>
|
||||||
|
<el-descriptions-item label="通道"
|
||||||
|
>当前支持 Bark 与 WPush,两种通道可同时启用。</el-descriptions-item
|
||||||
|
>
|
||||||
<el-descriptions-item label="配置文件">{{
|
<el-descriptions-item label="配置文件">{{
|
||||||
notificationFilePath || '-'
|
notificationFilePath || '-'
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
|
|||||||
+10
-4
@@ -5,15 +5,20 @@ type NotificationForm = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
barkEnabled: boolean
|
barkEnabled: boolean
|
||||||
barkServerUrl: string
|
barkServerUrl: string
|
||||||
|
wpushEnabled: boolean
|
||||||
testTitle: string
|
testTitle: string
|
||||||
testBody: string
|
testBody: string
|
||||||
testUrl: string
|
testUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationStats = {
|
type NotificationStats = {
|
||||||
configuredRecipientCount: number
|
barkConfiguredRecipientCount: number
|
||||||
enabledRecipientCount: number
|
barkEnabledRecipientCount: number
|
||||||
|
wpushConfiguredRecipientCount: number
|
||||||
|
wpushEnabledRecipientCount: number
|
||||||
barkReady: boolean
|
barkReady: boolean
|
||||||
|
wpushReady: boolean
|
||||||
|
ready: boolean
|
||||||
lastSuccessCount: number
|
lastSuccessCount: number
|
||||||
lastFailedCount: number
|
lastFailedCount: number
|
||||||
scheduledJobEnabledCount: number
|
scheduledJobEnabledCount: number
|
||||||
@@ -36,11 +41,11 @@ defineProps<Props>()
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div>
|
<div>
|
||||||
<span class="card-title">测试发送</span>
|
<span class="card-title">测试发送</span>
|
||||||
<p class="card-desc">测试会发送给所有已启用且填写了 Device Key 的接收人。</p>
|
<p class="card-desc">测试会发送给所有已启用且已填写 Bark Device Key 或 WPush API Key 的接收人。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<el-button
|
<el-button
|
||||||
:disabled="!notificationStats.barkReady"
|
:disabled="!notificationStats.ready"
|
||||||
:loading="notificationTesting"
|
:loading="notificationTesting"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleNotificationTest"
|
@click="handleNotificationTest"
|
||||||
@@ -93,6 +98,7 @@ defineProps<Props>()
|
|||||||
size="small"
|
size="small"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
>
|
>
|
||||||
|
<el-table-column prop="channel" label="通道" width="100" />
|
||||||
<el-table-column label="接收人">
|
<el-table-column label="接收人">
|
||||||
<template #default="{ row }">{{
|
<template #default="{ row }">{{
|
||||||
row.recipientName || row.recipientKeyMasked || '-'
|
row.recipientName || row.recipientKeyMasked || '-'
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { EditableWpushRecipient } from '../../composables/types'
|
||||||
|
|
||||||
|
type NotificationForm = {
|
||||||
|
enabled: boolean
|
||||||
|
barkEnabled: boolean
|
||||||
|
barkServerUrl: string
|
||||||
|
wpushEnabled: boolean
|
||||||
|
testTitle: string
|
||||||
|
testBody: string
|
||||||
|
testUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationStats = {
|
||||||
|
barkConfiguredRecipientCount: number
|
||||||
|
barkEnabledRecipientCount: number
|
||||||
|
wpushConfiguredRecipientCount: number
|
||||||
|
wpushEnabledRecipientCount: number
|
||||||
|
barkReady: boolean
|
||||||
|
wpushReady: boolean
|
||||||
|
ready: boolean
|
||||||
|
lastSuccessCount: number
|
||||||
|
lastFailedCount: number
|
||||||
|
scheduledJobEnabledCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
notificationForm: NotificationForm
|
||||||
|
wpushRecipients: EditableWpushRecipient[]
|
||||||
|
notificationSaving: boolean
|
||||||
|
notificationResultError: string
|
||||||
|
notificationStats: NotificationStats
|
||||||
|
addNotificationRecipient: () => void
|
||||||
|
removeNotificationRecipient: (id: string) => void
|
||||||
|
handleNotificationSaveConfig: () => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-card class="section-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<span class="card-title">WPush 通知</span>
|
||||||
|
<p class="card-desc">
|
||||||
|
已配置 {{ notificationStats.wpushConfiguredRecipientCount }} 人,启用
|
||||||
|
{{ notificationStats.wpushEnabledRecipientCount }} 人。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button @click="addNotificationRecipient">新增接收人</el-button>
|
||||||
|
<el-button
|
||||||
|
:loading="notificationSaving"
|
||||||
|
type="primary"
|
||||||
|
@click="handleNotificationSaveConfig"
|
||||||
|
>
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="notificationResultError"
|
||||||
|
:title="notificationResultError"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-form label-width="auto" label-position="top" inline>
|
||||||
|
<el-form-item>
|
||||||
|
<el-switch v-model="notificationForm.wpushEnabled" active-text="启用 WPush 通道" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table :data="wpushRecipients" stripe size="small" class="mt-4">
|
||||||
|
<el-table-column label="接收人" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.name" placeholder="例如:值班手机" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="API Key" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.apiKey" placeholder="WPush apikey" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch v-model="row.enabled" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="danger" text @click="removeNotificationRecipient(row.id)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="还没有 WPush 接收人。" :image-size="60" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.section-card {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+29
-7
@@ -1,8 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AdminNotificationTestResult, AdminScheduledJobRuntimeState } from '@/types/admin'
|
import type { AdminNotificationTestResult, AdminScheduledJobRuntimeState } from '@/types/admin'
|
||||||
import type { EditableNotificationRecipient, EditableScheduledJob } from '../../composables/types'
|
import type {
|
||||||
|
EditableNotificationRecipient,
|
||||||
|
EditableScheduledJob,
|
||||||
|
EditableWpushRecipient,
|
||||||
|
} from '../../composables/types'
|
||||||
import AdminNotificationInfoCard from './AdminNotificationInfoCard.vue'
|
import AdminNotificationInfoCard from './AdminNotificationInfoCard.vue'
|
||||||
import AdminNotificationBarkCard from './AdminNotificationBarkCard.vue'
|
import AdminNotificationBarkCard from './AdminNotificationBarkCard.vue'
|
||||||
|
import AdminNotificationWpushCard from './AdminNotificationWpushCard.vue'
|
||||||
import AdminNotificationScheduledJobsCard from './AdminNotificationScheduledJobsCard.vue'
|
import AdminNotificationScheduledJobsCard from './AdminNotificationScheduledJobsCard.vue'
|
||||||
import AdminNotificationTestCard from './AdminNotificationTestCard.vue'
|
import AdminNotificationTestCard from './AdminNotificationTestCard.vue'
|
||||||
|
|
||||||
@@ -10,6 +15,7 @@ type NotificationForm = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
barkEnabled: boolean
|
barkEnabled: boolean
|
||||||
barkServerUrl: string
|
barkServerUrl: string
|
||||||
|
wpushEnabled: boolean
|
||||||
testTitle: string
|
testTitle: string
|
||||||
testBody: string
|
testBody: string
|
||||||
testUrl: string
|
testUrl: string
|
||||||
@@ -20,9 +26,13 @@ type ScheduledJobsForm = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotificationStats = {
|
type NotificationStats = {
|
||||||
configuredRecipientCount: number
|
barkConfiguredRecipientCount: number
|
||||||
enabledRecipientCount: number
|
barkEnabledRecipientCount: number
|
||||||
|
wpushConfiguredRecipientCount: number
|
||||||
|
wpushEnabledRecipientCount: number
|
||||||
barkReady: boolean
|
barkReady: boolean
|
||||||
|
wpushReady: boolean
|
||||||
|
ready: boolean
|
||||||
lastSuccessCount: number
|
lastSuccessCount: number
|
||||||
lastFailedCount: number
|
lastFailedCount: number
|
||||||
scheduledJobEnabledCount: number
|
scheduledJobEnabledCount: number
|
||||||
@@ -32,6 +42,7 @@ type Props = {
|
|||||||
notificationFilePath: string
|
notificationFilePath: string
|
||||||
notificationForm: NotificationForm
|
notificationForm: NotificationForm
|
||||||
notificationRecipients: EditableNotificationRecipient[]
|
notificationRecipients: EditableNotificationRecipient[]
|
||||||
|
wpushRecipients: EditableWpushRecipient[]
|
||||||
notificationSaving: boolean
|
notificationSaving: boolean
|
||||||
notificationTesting: boolean
|
notificationTesting: boolean
|
||||||
notificationResultError: string
|
notificationResultError: string
|
||||||
@@ -43,8 +54,8 @@ type Props = {
|
|||||||
scheduledJobsSaving: boolean
|
scheduledJobsSaving: boolean
|
||||||
scheduledJobRunningId: string
|
scheduledJobRunningId: string
|
||||||
notificationStats: NotificationStats
|
notificationStats: NotificationStats
|
||||||
addNotificationRecipient: () => void
|
addNotificationRecipient: (channel: 'bark' | 'wpush') => void
|
||||||
removeNotificationRecipient: (id: string) => void
|
removeNotificationRecipient: (channel: 'bark' | 'wpush', id: string) => void
|
||||||
handleNotificationSaveConfig: () => void | Promise<void>
|
handleNotificationSaveConfig: () => void | Promise<void>
|
||||||
handleNotificationTest: () => void | Promise<void>
|
handleNotificationTest: () => void | Promise<void>
|
||||||
handleScheduledJobsSaveConfig: () => void | Promise<void>
|
handleScheduledJobsSaveConfig: () => void | Promise<void>
|
||||||
@@ -68,8 +79,19 @@ defineProps<Props>()
|
|||||||
:notification-saving="notificationSaving"
|
:notification-saving="notificationSaving"
|
||||||
:notification-result-error="notificationResultError"
|
:notification-result-error="notificationResultError"
|
||||||
:notification-stats="notificationStats"
|
:notification-stats="notificationStats"
|
||||||
:add-notification-recipient="addNotificationRecipient"
|
:add-notification-recipient="() => addNotificationRecipient('bark')"
|
||||||
:remove-notification-recipient="removeNotificationRecipient"
|
:remove-notification-recipient="(id) => removeNotificationRecipient('bark', id)"
|
||||||
|
:handle-notification-save-config="handleNotificationSaveConfig"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AdminNotificationWpushCard
|
||||||
|
:notification-form="notificationForm"
|
||||||
|
:wpush-recipients="wpushRecipients"
|
||||||
|
:notification-saving="notificationSaving"
|
||||||
|
:notification-result-error="notificationResultError"
|
||||||
|
:notification-stats="notificationStats"
|
||||||
|
:add-notification-recipient="() => addNotificationRecipient('wpush')"
|
||||||
|
:remove-notification-recipient="(id) => removeNotificationRecipient('wpush', id)"
|
||||||
:handle-notification-save-config="handleNotificationSaveConfig"
|
:handle-notification-save-config="handleNotificationSaveConfig"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ export type EditableNotificationRecipient = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EditableWpushRecipient = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
apiKey: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type EditableScheduledJob = {
|
export type EditableScheduledJob = {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
+68
-9
@@ -15,7 +15,11 @@ import type {
|
|||||||
AdminScheduledJobsResponse,
|
AdminScheduledJobsResponse,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
|
|
||||||
import type { EditableNotificationRecipient, EditableScheduledJob } from './types'
|
import type {
|
||||||
|
EditableNotificationRecipient,
|
||||||
|
EditableScheduledJob,
|
||||||
|
EditableWpushRecipient,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
export function useAdminNotificationPlatform() {
|
export function useAdminNotificationPlatform() {
|
||||||
const notificationFilePath = ref('')
|
const notificationFilePath = ref('')
|
||||||
@@ -23,11 +27,13 @@ export function useAdminNotificationPlatform() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
barkEnabled: true,
|
barkEnabled: true,
|
||||||
barkServerUrl: 'https://api.day.app',
|
barkServerUrl: 'https://api.day.app',
|
||||||
|
wpushEnabled: true,
|
||||||
testTitle: '订单系统测试通知',
|
testTitle: '订单系统测试通知',
|
||||||
testBody: '这是一条 Bark 内部通知测试。',
|
testBody: '这是一条内部通知测试。',
|
||||||
testUrl: '',
|
testUrl: '',
|
||||||
})
|
})
|
||||||
const notificationRecipients = ref<EditableNotificationRecipient[]>([])
|
const notificationRecipients = ref<EditableNotificationRecipient[]>([])
|
||||||
|
const wpushRecipients = ref<EditableWpushRecipient[]>([])
|
||||||
const notificationSaving = ref(false)
|
const notificationSaving = ref(false)
|
||||||
const notificationTesting = ref(false)
|
const notificationTesting = ref(false)
|
||||||
const notificationResultError = ref('')
|
const notificationResultError = ref('')
|
||||||
@@ -42,17 +48,37 @@ export function useAdminNotificationPlatform() {
|
|||||||
const scheduledJobRunningId = ref('')
|
const scheduledJobRunningId = ref('')
|
||||||
|
|
||||||
const notificationStats = computed(() => {
|
const notificationStats = computed(() => {
|
||||||
const enabledRecipients = notificationRecipients.value.filter(
|
const enabledBarkRecipients = notificationRecipients.value.filter(
|
||||||
(item) => item.enabled && item.deviceKey.trim(),
|
(item) => item.enabled && item.deviceKey.trim(),
|
||||||
)
|
)
|
||||||
|
const enabledWpushRecipients = wpushRecipients.value.filter(
|
||||||
|
(item) => item.enabled && item.apiKey.trim(),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
configuredRecipientCount: notificationRecipients.value.length,
|
barkConfiguredRecipientCount: notificationRecipients.value.length,
|
||||||
enabledRecipientCount: enabledRecipients.length,
|
barkEnabledRecipientCount: enabledBarkRecipients.length,
|
||||||
|
wpushConfiguredRecipientCount: wpushRecipients.value.length,
|
||||||
|
wpushEnabledRecipientCount: enabledWpushRecipients.length,
|
||||||
barkReady:
|
barkReady:
|
||||||
notificationForm.value.enabled &&
|
notificationForm.value.enabled &&
|
||||||
notificationForm.value.barkEnabled &&
|
notificationForm.value.barkEnabled &&
|
||||||
enabledRecipients.length > 0,
|
enabledBarkRecipients.length > 0,
|
||||||
|
wpushReady:
|
||||||
|
notificationForm.value.enabled &&
|
||||||
|
notificationForm.value.wpushEnabled &&
|
||||||
|
enabledWpushRecipients.length > 0,
|
||||||
|
ready:
|
||||||
|
(
|
||||||
|
notificationForm.value.enabled &&
|
||||||
|
notificationForm.value.barkEnabled &&
|
||||||
|
enabledBarkRecipients.length > 0
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
notificationForm.value.enabled &&
|
||||||
|
notificationForm.value.wpushEnabled &&
|
||||||
|
enabledWpushRecipients.length > 0
|
||||||
|
),
|
||||||
lastSuccessCount: notificationTestResult.value?.successCount || 0,
|
lastSuccessCount: notificationTestResult.value?.successCount || 0,
|
||||||
lastFailedCount: notificationTestResult.value?.failedCount || 0,
|
lastFailedCount: notificationTestResult.value?.failedCount || 0,
|
||||||
scheduledJobEnabledCount: scheduledJobs.value.filter((item) => item.enabled).length,
|
scheduledJobEnabledCount: scheduledJobs.value.filter((item) => item.enabled).length,
|
||||||
@@ -65,12 +91,19 @@ export function useAdminNotificationPlatform() {
|
|||||||
notificationForm.value.barkEnabled = data.source.channels.bark.enabled !== false
|
notificationForm.value.barkEnabled = data.source.channels.bark.enabled !== false
|
||||||
notificationForm.value.barkServerUrl =
|
notificationForm.value.barkServerUrl =
|
||||||
data.source.channels.bark.serverUrl || 'https://api.day.app'
|
data.source.channels.bark.serverUrl || 'https://api.day.app'
|
||||||
|
notificationForm.value.wpushEnabled = data.source.channels.wpush.enabled !== false
|
||||||
notificationRecipients.value = data.source.channels.bark.recipients.map((item) => ({
|
notificationRecipients.value = data.source.channels.bark.recipients.map((item) => ({
|
||||||
id: item.id || crypto.randomUUID(),
|
id: item.id || crypto.randomUUID(),
|
||||||
name: item.name,
|
name: item.name,
|
||||||
deviceKey: item.deviceKey,
|
deviceKey: item.deviceKey,
|
||||||
enabled: item.enabled !== false,
|
enabled: item.enabled !== false,
|
||||||
}))
|
}))
|
||||||
|
wpushRecipients.value = data.source.channels.wpush.recipients.map((item) => ({
|
||||||
|
id: item.id || crypto.randomUUID(),
|
||||||
|
name: item.name,
|
||||||
|
apiKey: item.apiKey,
|
||||||
|
enabled: item.enabled !== false,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function hydrateScheduledJobsConfig(data: AdminScheduledJobsResponse) {
|
function hydrateScheduledJobsConfig(data: AdminScheduledJobsResponse) {
|
||||||
@@ -91,7 +124,17 @@ export function useAdminNotificationPlatform() {
|
|||||||
scheduledJobRuntime.value = data.runtime || []
|
scheduledJobRuntime.value = data.runtime || []
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNotificationRecipient() {
|
function addNotificationRecipient(channel: 'bark' | 'wpush') {
|
||||||
|
if (channel === 'wpush') {
|
||||||
|
wpushRecipients.value.unshift({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: '',
|
||||||
|
apiKey: '',
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
notificationRecipients.value.unshift({
|
notificationRecipients.value.unshift({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: '',
|
name: '',
|
||||||
@@ -100,7 +143,12 @@ export function useAdminNotificationPlatform() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeNotificationRecipient(id: string) {
|
function removeNotificationRecipient(channel: 'bark' | 'wpush', id: string) {
|
||||||
|
if (channel === 'wpush') {
|
||||||
|
wpushRecipients.value = wpushRecipients.value.filter((item) => item.id !== id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
notificationRecipients.value = notificationRecipients.value.filter((item) => item.id !== id)
|
notificationRecipients.value = notificationRecipients.value.filter((item) => item.id !== id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +167,16 @@ export function useAdminNotificationPlatform() {
|
|||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
wpush: {
|
||||||
|
enabled: notificationForm.value.wpushEnabled,
|
||||||
|
recipients: wpushRecipients.value.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name.trim(),
|
||||||
|
apiKey: item.apiKey.trim(),
|
||||||
|
apiKeyMasked: '',
|
||||||
|
enabled: item.enabled,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +234,7 @@ export function useAdminNotificationPlatform() {
|
|||||||
if (response.data.successCount > 0) {
|
if (response.data.successCount > 0) {
|
||||||
showSuccess(`内部通知测试完成,成功 ${response.data.successCount} 个`)
|
showSuccess(`内部通知测试完成,成功 ${response.data.successCount} 个`)
|
||||||
} else {
|
} else {
|
||||||
showError('内部通知测试未成功发送,请检查 Bark 配置')
|
showError('内部通知测试未成功发送,请检查通知通道配置')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notificationResultError.value = error instanceof Error ? error.message : '内部通知测试失败'
|
notificationResultError.value = error instanceof Error ? error.message : '内部通知测试失败'
|
||||||
@@ -243,6 +301,7 @@ export function useAdminNotificationPlatform() {
|
|||||||
notificationFilePath,
|
notificationFilePath,
|
||||||
notificationForm,
|
notificationForm,
|
||||||
notificationRecipients,
|
notificationRecipients,
|
||||||
|
wpushRecipients,
|
||||||
notificationSaving,
|
notificationSaving,
|
||||||
notificationTesting,
|
notificationTesting,
|
||||||
notificationResultError,
|
notificationResultError,
|
||||||
|
|||||||
Reference in New Issue
Block a user