- 新增 types/claim-identity,统一 ClaimIdentity / Payload / AdminSummary - 导出 KuaishouCloudFlow、KuaishouFeifeiFlow 作为 normalize 出口 - 生产路径 JsonObject 统一从 types/json 导入,去掉 rebind as any - 前后端 claimIdentity 类型字段对齐
284 lines
8.7 KiB
TypeScript
284 lines
8.7 KiB
TypeScript
import type { JsonObject } from '../../types/json.js'
|
|
import { logWarn } from '../../utils/logger.js'
|
|
import { sendInternalNotification } from './notification-service.js'
|
|
|
|
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
|
|
const notificationCooldownMap = new Map<string, number>()
|
|
|
|
type InternalNotificationPayload = {
|
|
title: string
|
|
body?: string
|
|
category?: string
|
|
url?: string
|
|
cooldownKey?: string
|
|
cooldownMs?: number
|
|
}
|
|
type KuaishouCloudAssetNotEnoughPayload = {
|
|
task?: unknown
|
|
flow?: unknown
|
|
assetBefore?: unknown
|
|
requiredAsset?: unknown
|
|
skuName?: unknown
|
|
}
|
|
type CloudtentaclesAuthExpiredPayload = {
|
|
pathname?: unknown
|
|
errorCode?: unknown
|
|
message?: unknown
|
|
cooldownSeconds?: unknown
|
|
sourceKey?: unknown
|
|
accountLabel?: unknown
|
|
}
|
|
type CloudtentaclesAssetLowPayload = {
|
|
asset?: unknown
|
|
threshold?: unknown
|
|
cooldownSeconds?: unknown
|
|
sourceKey?: unknown
|
|
accountLabel?: unknown
|
|
}
|
|
type Open91PendingConfigPayload = {
|
|
order?: unknown
|
|
orderNo?: unknown
|
|
productNo?: unknown
|
|
buyNum?: unknown
|
|
reason?: unknown
|
|
}
|
|
type TaskNotificationPayload = {
|
|
task?: unknown
|
|
reason?: unknown
|
|
source?: unknown
|
|
errorMessage?: unknown
|
|
}
|
|
type ClaimRedeemNeedsAttentionPayload = {
|
|
task?: unknown
|
|
order?: unknown
|
|
status?: unknown
|
|
errorMessage?: unknown
|
|
}
|
|
|
|
export async function notifyInternalSafely(payload: InternalNotificationPayload) {
|
|
const cooldownKey = String(payload.cooldownKey || '').trim()
|
|
if (cooldownKey && isNotificationCoolingDown(cooldownKey, payload.cooldownMs)) {
|
|
return {
|
|
skipped: true,
|
|
reason: 'cooldown',
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await sendInternalNotification({
|
|
title: payload.title,
|
|
...(payload.body !== undefined ? { body: payload.body } : {}),
|
|
...(payload.category !== undefined ? { category: payload.category } : {}),
|
|
...(payload.url !== undefined ? { url: payload.url } : {}),
|
|
})
|
|
markNotificationCooldown(cooldownKey)
|
|
return result
|
|
} catch (error) {
|
|
logWarn('[notification/internal]', '内部通知发送失败,已跳过', {
|
|
title: payload.title,
|
|
category: payload.category,
|
|
error,
|
|
})
|
|
markNotificationCooldown(cooldownKey)
|
|
return {
|
|
skipped: true,
|
|
reason: 'send_failed',
|
|
}
|
|
}
|
|
}
|
|
|
|
export function notifyKuaishouCloudAssetNotEnough({
|
|
task = {},
|
|
flow = {},
|
|
assetBefore = 0,
|
|
requiredAsset = 0,
|
|
skuName = '',
|
|
}: KuaishouCloudAssetNotEnoughPayload = {}) {
|
|
const taskRecord = toRecord(task)
|
|
const flowRecord = toRecord(flow)
|
|
const binding = toRecord(flowRecord.binding)
|
|
|
|
return notifyInternalSafely({
|
|
title: 'kuaishou-lewan 余额不足',
|
|
body: [
|
|
formatTaskLine(taskRecord),
|
|
`当前余额:${Number(assetBefore || 0)}`,
|
|
`最低需要:${Number(requiredAsset || 0)}`,
|
|
`商品:${String(skuName || binding.skuName || flowRecord.internalSkuName || '').trim() || '-'}`,
|
|
].join('\n'),
|
|
category: 'kuaishou_cloud_asset_not_enough',
|
|
cooldownKey: [
|
|
'kuaishou_cloud_asset_not_enough',
|
|
taskRecord.id || '',
|
|
binding.skuId || '',
|
|
].join(':'),
|
|
})
|
|
}
|
|
|
|
export function notifyCloudtentaclesAuthExpired({
|
|
pathname = '',
|
|
errorCode = '',
|
|
message = '',
|
|
cooldownSeconds = 600,
|
|
sourceKey = '',
|
|
accountLabel = '',
|
|
}: CloudtentaclesAuthExpiredPayload = {}) {
|
|
const normalizedSourceKey = String(sourceKey || '').trim()
|
|
const normalizedAccountLabel = String(accountLabel || '').trim()
|
|
|
|
return notifyInternalSafely({
|
|
title: normalizedAccountLabel
|
|
? `cloudtentacles 登录已过期:${normalizedAccountLabel}`
|
|
: 'cloudtentacles 登录已过期',
|
|
body: [
|
|
normalizedSourceKey ? `账号:${normalizedAccountLabel || normalizedSourceKey}` : '',
|
|
`接口:${String(pathname || '').trim() || '-'}`,
|
|
`错误码:${String(errorCode || '').trim() || '-'}`,
|
|
`原因:${String(message || '').trim() || '登录态已失效,请到后台重新登录'}`,
|
|
].filter(Boolean).join('\n'),
|
|
category: 'cloudtentacles_auth_expired',
|
|
cooldownKey: ['cloudtentacles_auth_expired', normalizedSourceKey || 'default', pathname, errorCode].join(':'),
|
|
cooldownMs: Number(cooldownSeconds || 600) * 1000,
|
|
})
|
|
}
|
|
|
|
export function notifyCloudtentaclesAssetLow({
|
|
asset = 0,
|
|
threshold = 500,
|
|
cooldownSeconds = 1800,
|
|
sourceKey = '',
|
|
accountLabel = '',
|
|
}: CloudtentaclesAssetLowPayload = {}) {
|
|
const normalizedSourceKey = String(sourceKey || '').trim()
|
|
const normalizedAccountLabel = String(accountLabel || '').trim()
|
|
|
|
return notifyInternalSafely({
|
|
title: normalizedAccountLabel
|
|
? `kuaishou-lewan 余额低于阈值:${normalizedAccountLabel}`
|
|
: 'kuaishou-lewan 余额低于阈值',
|
|
body: [
|
|
normalizedSourceKey ? `账号:${normalizedAccountLabel || normalizedSourceKey}` : '',
|
|
`当前余额:${Number(asset || 0)}`,
|
|
`提醒阈值:${Number(threshold || 0)}`,
|
|
'请及时补充 cloudtentacles 余额,避免自动履约失败。',
|
|
].filter(Boolean).join('\n'),
|
|
category: 'cloudtentacles_asset_low',
|
|
cooldownKey: ['cloudtentacles_asset_low', normalizedSourceKey || 'default', threshold].join(':'),
|
|
cooldownMs: Number(cooldownSeconds || 1800) * 1000,
|
|
})
|
|
}
|
|
|
|
export function notifyOpen91PendingConfig({
|
|
order = {},
|
|
orderNo = '',
|
|
productNo = '',
|
|
buyNum = 0,
|
|
reason = '未命中履约配置',
|
|
}: Open91PendingConfigPayload = {}) {
|
|
const orderRecord = toRecord(order)
|
|
|
|
return notifyInternalSafely({
|
|
title: '91卡券订单待补配置',
|
|
body: [
|
|
`订单:${String(orderNo || orderRecord.platform_order_id || '').trim() || '-'}`,
|
|
`商品:${String(productNo || '').trim() || '-'}`,
|
|
`数量:${Number(buyNum || 0) || 0}`,
|
|
`原因:${String(reason || '').trim() || '未命中履约配置'}`,
|
|
].join('\n'),
|
|
category: 'open91_pending_config',
|
|
cooldownKey: ['open91_pending_config', orderNo || orderRecord.platform_order_id || '', productNo].join(':'),
|
|
})
|
|
}
|
|
|
|
export function notifyKuaishouCloudBindUrlRefreshFailed({
|
|
task = {},
|
|
errorMessage = '',
|
|
}: TaskNotificationPayload = {}) {
|
|
const taskRecord = toRecord(task)
|
|
|
|
return notifyInternalSafely({
|
|
title: 'kuaishou-lewan 链接刷新失败',
|
|
body: [
|
|
formatTaskLine(taskRecord),
|
|
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
|
|
'旧虚拟号已退还,新绑定链接未准备成功。',
|
|
].join('\n'),
|
|
category: 'kuaishou_cloud_bind_url_refresh_failed',
|
|
cooldownKey: ['kuaishou_cloud_bind_url_refresh_failed', taskRecord.id || ''].join(':'),
|
|
})
|
|
}
|
|
|
|
export function notifyTaskAutoManualReview({
|
|
task = {},
|
|
reason = '',
|
|
source = '',
|
|
}: TaskNotificationPayload = {}) {
|
|
const taskRecord = toRecord(task)
|
|
|
|
return notifyInternalSafely({
|
|
title: '任务已自动转人工处理',
|
|
body: [
|
|
formatTaskLine(taskRecord),
|
|
`来源:${String(source || '').trim() || '-'}`,
|
|
`原因:${String(reason || taskRecord.last_error || '').trim() || '-'}`,
|
|
].join('\n'),
|
|
category: 'task_auto_manual_review',
|
|
cooldownKey: ['task_auto_manual_review', taskRecord.id || '', source].join(':'),
|
|
})
|
|
}
|
|
|
|
export function notifyClaimRedeemNeedsAttention({
|
|
task = {},
|
|
order = {},
|
|
status = '',
|
|
errorMessage = '',
|
|
}: ClaimRedeemNeedsAttentionPayload = {}) {
|
|
const taskRecord = toRecord(task)
|
|
const orderRecord = toRecord(order)
|
|
|
|
return notifyInternalSafely({
|
|
title: '兑换失败,任务需关注',
|
|
body: [
|
|
formatTaskLine(taskRecord),
|
|
`订单:${String(orderRecord.platform_order_id || taskRecord.platform_order_id || '').trim() || '-'}`,
|
|
`状态:${String(status || taskRecord.task_status || '').trim() || '-'}`,
|
|
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
|
|
].join('\n'),
|
|
category: 'claim_redeem_failed',
|
|
cooldownKey: ['claim_redeem_needs_attention', taskRecord.id || '', status].join(':'),
|
|
})
|
|
}
|
|
|
|
function formatTaskLine(task: unknown) {
|
|
const taskRecord = toRecord(task)
|
|
return [
|
|
`任务:${String(taskRecord.task_no || taskRecord.id || '').trim() || '-'}`,
|
|
`平台订单:${String(taskRecord.platform_order_id || '').trim() || '-'}`,
|
|
].join(' / ')
|
|
}
|
|
|
|
function toRecord(value: unknown): JsonObject {
|
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
? value as JsonObject
|
|
: {}
|
|
}
|
|
|
|
function isNotificationCoolingDown(cooldownKey: unknown, cooldownMs = DEFAULT_COOLDOWN_MS) {
|
|
const key = String(cooldownKey || '').trim()
|
|
if (!key) {
|
|
return false
|
|
}
|
|
|
|
const lastSentAt = Number(notificationCooldownMap.get(key) || 0)
|
|
return Date.now() - lastSentAt < Number(cooldownMs || DEFAULT_COOLDOWN_MS)
|
|
}
|
|
|
|
function markNotificationCooldown(cooldownKey: unknown) {
|
|
const key = String(cooldownKey || '').trim()
|
|
if (!key) {
|
|
return
|
|
}
|
|
|
|
notificationCooldownMap.set(key, Date.now())
|
|
}
|