后端迁移通知服务
This commit is contained in:
+29
-25
@@ -1,26 +1,22 @@
|
||||
// @ts-check
|
||||
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 10000
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* serverUrl: string
|
||||
* recipient: {
|
||||
* id?: string
|
||||
* name?: string
|
||||
* deviceKey?: string
|
||||
* }
|
||||
* title: string
|
||||
* body?: string
|
||||
* group?: string
|
||||
* url?: string
|
||||
* }} BarkSendInput
|
||||
*/
|
||||
type JsonObject = Record<string, any>
|
||||
type BarkSendInput = {
|
||||
serverUrl?: string
|
||||
recipient?: {
|
||||
id?: string
|
||||
name?: string
|
||||
deviceKey?: string
|
||||
}
|
||||
title?: string
|
||||
body?: string
|
||||
group?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
/** @param {BarkSendInput} input */
|
||||
export async function sendBarkNotification(input) {
|
||||
export async function sendBarkNotification(input: BarkSendInput) {
|
||||
const serverUrl = String(input.serverUrl || 'https://api.day.app').trim().replace(/\/+$/, '')
|
||||
const deviceKey = String(input.recipient?.deviceKey || '').trim()
|
||||
const title = String(input.title || '').trim()
|
||||
@@ -78,7 +74,7 @@ export async function sendBarkNotification(input) {
|
||||
response: parsed || responseText,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
if (isAbortError(error)) {
|
||||
throw createHttpError('Bark 通知发送超时', {
|
||||
statusCode: 503,
|
||||
errorCode: 'bark_send_timeout',
|
||||
@@ -91,7 +87,7 @@ export async function sendBarkNotification(input) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildBarkEndpoint(serverUrl, deviceKey, title, body) {
|
||||
function buildBarkEndpoint(serverUrl: string, deviceKey: string, title: string, body: string) {
|
||||
const segments = [
|
||||
serverUrl,
|
||||
encodeURIComponent(deviceKey),
|
||||
@@ -105,7 +101,7 @@ function buildBarkEndpoint(serverUrl, deviceKey, title, body) {
|
||||
return segments.join('/')
|
||||
}
|
||||
|
||||
function parseJsonResponse(text) {
|
||||
function parseJsonResponse(text: string) {
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
@@ -117,8 +113,8 @@ function parseJsonResponse(text) {
|
||||
}
|
||||
}
|
||||
|
||||
function isBarkFailure(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
function isBarkFailure(parsed: unknown) {
|
||||
if (!isPlainObject(parsed)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -126,10 +122,18 @@ function isBarkFailure(parsed) {
|
||||
return Number.isFinite(code) && code !== 0 && code !== 200
|
||||
}
|
||||
|
||||
function resolveBarkErrorMessage(parsed, responseText, status) {
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
function resolveBarkErrorMessage(parsed: unknown, responseText: string, status: number) {
|
||||
if (isPlainObject(parsed)) {
|
||||
return String(parsed.message || parsed.msg || parsed.error || '').trim() || `Bark 通知发送失败,HTTP ${status}`
|
||||
}
|
||||
|
||||
return String(responseText || '').trim() || `Bark 通知发送失败,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)
|
||||
}
|
||||
+8
-8
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
@@ -8,6 +6,8 @@ import { PROJECT_ROOT } from '../../config/runtime.js'
|
||||
const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json')
|
||||
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
export function getNotificationConfigFilePath() {
|
||||
return NOTIFICATION_CONFIG_FILE_PATH
|
||||
}
|
||||
@@ -16,14 +16,14 @@ export function getNotificationConfig() {
|
||||
return loadNotificationConfigFromFile()
|
||||
}
|
||||
|
||||
export function saveNotificationConfig(rawValue) {
|
||||
export function saveNotificationConfig(rawValue: unknown) {
|
||||
const normalized = normalizeNotificationConfig(rawValue)
|
||||
fs.mkdirSync(path.dirname(NOTIFICATION_CONFIG_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(NOTIFICATION_CONFIG_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function listEnabledBarkRecipients(config = getNotificationConfig()) {
|
||||
export function listEnabledBarkRecipients(config: JsonObject = getNotificationConfig()) {
|
||||
if (config.enabled === false || config.channels?.bark?.enabled === false) {
|
||||
return []
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function loadNotificationConfigFromFile() {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeNotificationConfig(rawValue) {
|
||||
function normalizeNotificationConfig(rawValue: unknown) {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {}
|
||||
|
||||
@@ -63,7 +63,7 @@ function normalizeNotificationConfig(rawValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBarkRecipient(rawValue) {
|
||||
function normalizeBarkRecipient(rawValue: unknown) {
|
||||
if (!isPlainObject(rawValue)) {
|
||||
return null
|
||||
}
|
||||
@@ -84,7 +84,7 @@ function normalizeBarkRecipient(rawValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBarkServerUrl(value) {
|
||||
function normalizeBarkServerUrl(value: unknown) {
|
||||
const normalized = String(value || DEFAULT_BARK_SERVER_URL).trim() || DEFAULT_BARK_SERVER_URL
|
||||
return normalized.replace(/\/+$/, '')
|
||||
}
|
||||
@@ -102,6 +102,6 @@ function createDefaultNotificationConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
+24
-28
@@ -1,24 +1,20 @@
|
||||
// @ts-check
|
||||
|
||||
import { logWarn } from '../../utils/logger.js'
|
||||
import { sendInternalNotification } from './notification-service.js'
|
||||
|
||||
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
|
||||
const notificationCooldownMap = new Map()
|
||||
const notificationCooldownMap = new Map<string, number>()
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* title: string
|
||||
* body?: string
|
||||
* category?: string
|
||||
* url?: string
|
||||
* cooldownKey?: string
|
||||
* cooldownMs?: number
|
||||
* }} InternalNotificationPayload
|
||||
*/
|
||||
type JsonObject = Record<string, any>
|
||||
type InternalNotificationPayload = {
|
||||
title: string
|
||||
body?: string
|
||||
category?: string
|
||||
url?: string
|
||||
cooldownKey?: string
|
||||
cooldownMs?: number
|
||||
}
|
||||
|
||||
/** @param {InternalNotificationPayload} payload */
|
||||
export async function notifyInternalSafely(payload) {
|
||||
export async function notifyInternalSafely(payload: InternalNotificationPayload) {
|
||||
const cooldownKey = String(payload.cooldownKey || '').trim()
|
||||
if (cooldownKey && isNotificationCoolingDown(cooldownKey, payload.cooldownMs)) {
|
||||
return {
|
||||
@@ -56,7 +52,7 @@ export function notifyKuaishouCloudAssetNotEnough({
|
||||
assetBefore = 0,
|
||||
requiredAsset = 0,
|
||||
skuName = '',
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
const flowRecord = toRecord(flow)
|
||||
const binding = toRecord(flowRecord.binding)
|
||||
@@ -83,7 +79,7 @@ export function notifyCloudtentaclesAuthExpired({
|
||||
errorCode = '',
|
||||
message = '',
|
||||
cooldownSeconds = 600,
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
return notifyInternalSafely({
|
||||
title: 'cloudtentacles 登录已过期',
|
||||
body: [
|
||||
@@ -101,7 +97,7 @@ export function notifyCloudtentaclesAssetLow({
|
||||
asset = 0,
|
||||
threshold = 500,
|
||||
cooldownSeconds = 1800,
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
return notifyInternalSafely({
|
||||
title: '快手 Cloud 余额低于阈值',
|
||||
body: [
|
||||
@@ -121,7 +117,7 @@ export function notifyOpen91PendingConfig({
|
||||
productNo = '',
|
||||
buyNum = 0,
|
||||
reason = '未命中履约配置',
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
const orderRecord = toRecord(order)
|
||||
|
||||
return notifyInternalSafely({
|
||||
@@ -140,7 +136,7 @@ export function notifyOpen91PendingConfig({
|
||||
export function notifyKuaishouCloudBindUrlRefreshFailed({
|
||||
task = {},
|
||||
errorMessage = '',
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
|
||||
return notifyInternalSafely({
|
||||
@@ -162,7 +158,7 @@ export function notifyKuaishouCloudConsumeFailed({
|
||||
shopId = '',
|
||||
shopName = '',
|
||||
errorMessage = '',
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
const orderRecord = toRecord(order)
|
||||
|
||||
@@ -184,7 +180,7 @@ export function notifyTaskAutoManualReview({
|
||||
task = {},
|
||||
reason = '',
|
||||
source = '',
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
|
||||
return notifyInternalSafely({
|
||||
@@ -204,7 +200,7 @@ export function notifyClaimRedeemNeedsAttention({
|
||||
order = {},
|
||||
status = '',
|
||||
errorMessage = '',
|
||||
} = {}) {
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
const orderRecord = toRecord(order)
|
||||
|
||||
@@ -221,7 +217,7 @@ export function notifyClaimRedeemNeedsAttention({
|
||||
})
|
||||
}
|
||||
|
||||
function formatTaskLine(task) {
|
||||
function formatTaskLine(task: unknown) {
|
||||
const taskRecord = toRecord(task)
|
||||
return [
|
||||
`任务:${String(taskRecord.task_no || taskRecord.id || '').trim() || '-'}`,
|
||||
@@ -229,13 +225,13 @@ function formatTaskLine(task) {
|
||||
].join(' / ')
|
||||
}
|
||||
|
||||
function toRecord(value) {
|
||||
function toRecord(value: unknown): JsonObject {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? /** @type {Record<string, unknown>} */ (value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
}
|
||||
|
||||
function isNotificationCoolingDown(cooldownKey, cooldownMs = DEFAULT_COOLDOWN_MS) {
|
||||
function isNotificationCoolingDown(cooldownKey: unknown, cooldownMs = DEFAULT_COOLDOWN_MS) {
|
||||
const key = String(cooldownKey || '').trim()
|
||||
if (!key) {
|
||||
return false
|
||||
@@ -245,7 +241,7 @@ function isNotificationCoolingDown(cooldownKey, cooldownMs = DEFAULT_COOLDOWN_MS
|
||||
return Date.now() - lastSentAt < Number(cooldownMs || DEFAULT_COOLDOWN_MS)
|
||||
}
|
||||
|
||||
function markNotificationCooldown(cooldownKey) {
|
||||
function markNotificationCooldown(cooldownKey: unknown) {
|
||||
const key = String(cooldownKey || '').trim()
|
||||
if (!key) {
|
||||
return
|
||||
+23
-17
@@ -1,22 +1,18 @@
|
||||
// @ts-check
|
||||
|
||||
import { logWarn } from '../../utils/logger.js'
|
||||
import { getNotificationConfig, listEnabledBarkRecipients } from './config-service.js'
|
||||
import { sendBarkNotification } from './bark-service.js'
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* title?: string
|
||||
* body?: string
|
||||
* category?: string
|
||||
* url?: string
|
||||
* }} NotificationInput
|
||||
*/
|
||||
type JsonObject = Record<string, any>
|
||||
type NotificationInput = {
|
||||
title?: string
|
||||
body?: string
|
||||
category?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
/** @param {NotificationInput} input */
|
||||
export async function sendInternalNotification(input = {}) {
|
||||
export async function sendInternalNotification(input: NotificationInput = {}) {
|
||||
const config = getNotificationConfig()
|
||||
const bark = /** @type {{ serverUrl?: string }} */ (config.channels?.bark || {})
|
||||
const bark = (config.channels?.bark || {}) as { serverUrl?: string }
|
||||
const recipients = listEnabledBarkRecipients(config)
|
||||
const title = String(input.title || '订单系统通知').trim() || '订单系统通知'
|
||||
const body = String(input.body || '').trim()
|
||||
@@ -54,8 +50,8 @@ export async function sendInternalNotification(input = {}) {
|
||||
recipient,
|
||||
false,
|
||||
error instanceof Error ? error.message : 'Bark 内部通知发送失败',
|
||||
Number(error?.statusCode || 0) || 0,
|
||||
error?.context || null,
|
||||
isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0,
|
||||
isPlainObject(error) ? error.context || null : null,
|
||||
)
|
||||
}
|
||||
}))
|
||||
@@ -70,7 +66,13 @@ export async function sendInternalNotification(input = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function mapNotificationResult(recipient, ok, errorMessage, status, response) {
|
||||
function mapNotificationResult(
|
||||
recipient: JsonObject,
|
||||
ok: boolean,
|
||||
errorMessage: string,
|
||||
status: number,
|
||||
response: unknown,
|
||||
) {
|
||||
return {
|
||||
recipientId: String(recipient.id || '').trim(),
|
||||
recipientName: String(recipient.name || '').trim(),
|
||||
@@ -82,7 +84,7 @@ function mapNotificationResult(recipient, ok, errorMessage, status, response) {
|
||||
}
|
||||
}
|
||||
|
||||
function maskDeviceKey(value) {
|
||||
function maskDeviceKey(value: unknown) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return ''
|
||||
@@ -94,3 +96,7 @@ function maskDeviceKey(value) {
|
||||
|
||||
return `${normalized.slice(0, 6)}****${normalized.slice(-6)}`
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
Reference in New Issue
Block a user