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() 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()) }