后端迁移通知服务

This commit is contained in:
yml
2026-05-21 16:31:03 +08:00
parent fb95f7dc55
commit 1c760f1dd3
5 changed files with 95 additions and 78 deletions
@@ -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)
}
@@ -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]'
}
@@ -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
@@ -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)
}