export const WECHAT_CUSTOMER_QUOTA_ERRCODES = new Set([45047, 45015]); export const DEFERRED_DELIVERY_PRIORITY = { progress: 1, failure_notice: 5, formal_reply: 10, }; export function resolveDeferredDeliveryPriority(priority) { if (typeof priority === 'number' && Number.isFinite(priority)) return priority; const key = String(priority ?? 'formal_reply').trim(); return DEFERRED_DELIVERY_PRIORITY[key] ?? DEFERRED_DELIVERY_PRIORITY.formal_reply; } export function isWechatCustomerServiceQuotaError(errcode) { return WECHAT_CUSTOMER_QUOTA_ERRCODES.has(Number(errcode)); } export function parseWechatCustomerServiceErrcode(err) { const message = err instanceof Error ? err.message : String(err ?? ''); const match = message.match(/errcode=(\d+)/); return match ? Number(match[1]) : 0; } function memoryKey(appId, openid) { return `${String(appId)}:${String(openid)}`; } export function createWechatCustomerServiceDeferredStore({ mysqlPool = null, logger = console } = {}) { const memory = new Map(); let schemaReady = false; async function ensureSchema() { if (!mysqlPool?.query || schemaReady) return; await mysqlPool.query(` CREATE TABLE IF NOT EXISTS h5_wechat_mp_deferred_delivery ( app_id VARCHAR(32) NOT NULL, openid VARCHAR(64) NOT NULL, priority TINYINT NOT NULL DEFAULT 10, kind VARCHAR(32) NOT NULL DEFAULT 'text', content MEDIUMTEXT NOT NULL, metadata JSON NULL, source_msg_id VARCHAR(128) NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, PRIMARY KEY (app_id, openid), KEY idx_wechat_deferred_updated (updated_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); schemaReady = true; } async function upsertDeferred({ appId, openid, priority = 'formal_reply', kind = 'text', content = '', metadata = null, sourceMsgId = null, }) { const normalizedContent = String(content ?? '').trim(); if (!appId || !openid || !normalizedContent) { return { stored: false, reason: 'invalid_input' }; } const numericPriority = resolveDeferredDeliveryPriority(priority); const existing = await peekDeferred({ appId, openid }); if (existing && existing.priority > numericPriority) { return { stored: false, reason: 'lower_priority', existing }; } const now = Date.now(); const record = { appId: String(appId), openid: String(openid), priority: numericPriority, kind: String(kind ?? 'text'), content: normalizedContent, metadata: metadata ?? null, sourceMsgId: sourceMsgId ? String(sourceMsgId) : null, createdAt: existing?.createdAt ?? now, updatedAt: now, }; memory.set(memoryKey(appId, openid), record); if (mysqlPool?.query) { try { await ensureSchema(); await mysqlPool.query( `INSERT INTO h5_wechat_mp_deferred_delivery (app_id, openid, priority, kind, content, metadata, source_msg_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE priority = VALUES(priority), kind = VALUES(kind), content = VALUES(content), metadata = VALUES(metadata), source_msg_id = VALUES(source_msg_id), updated_at = VALUES(updated_at)`, [ record.appId, record.openid, record.priority, record.kind, record.content, metadata ? JSON.stringify(metadata) : null, record.sourceMsgId, record.createdAt, record.updatedAt, ], ); } catch (err) { logger.warn?.('WeChat deferred delivery persist failed:', err); } } return { stored: true, record }; } async function peekDeferred({ appId, openid }) { const key = memoryKey(appId, openid); if (memory.has(key)) return memory.get(key) ?? null; if (!mysqlPool?.query) return null; try { await ensureSchema(); const [rows] = await mysqlPool.query( `SELECT app_id AS appId, openid, priority, kind, content, metadata, source_msg_id AS sourceMsgId, created_at AS createdAt, updated_at AS updatedAt FROM h5_wechat_mp_deferred_delivery WHERE app_id = ? AND openid = ? LIMIT 1`, [String(appId), String(openid)], ); const row = rows?.[0]; if (!row) return null; const record = { ...row, metadata: typeof row.metadata === 'string' && row.metadata ? JSON.parse(row.metadata) : row.metadata ?? null, }; memory.set(key, record); return record; } catch (err) { logger.warn?.('WeChat deferred delivery peek failed:', err); return null; } } async function claimDeferred({ appId, openid }) { const record = await peekDeferred({ appId, openid }); if (!record) return null; memory.delete(memoryKey(appId, openid)); if (mysqlPool?.query) { try { await ensureSchema(); await mysqlPool.query( `DELETE FROM h5_wechat_mp_deferred_delivery WHERE app_id = ? AND openid = ?`, [String(appId), String(openid)], ); } catch (err) { logger.warn?.('WeChat deferred delivery claim delete failed:', err); } } return record; } async function hasFormalReplyPending({ appId, openid }) { const pending = await peekDeferred({ appId, openid }); return Boolean(pending && pending.priority >= DEFERRED_DELIVERY_PRIORITY.formal_reply); } return { ensureSchema, upsertDeferred, peekDeferred, claimDeferred, hasFormalReplyPending, }; } export function markWechatDeliveryDeferred(err) { if (err && typeof err === 'object') err.wechatDeliveryDeferred = true; return err; } export function wasWechatDeliveryDeferred(err) { return Boolean(err && typeof err === 'object' && err.wechatDeliveryDeferred); } export function composePassiveReplyWithDeferred(deferredContent, fallbackContent) { const deferred = String(deferredContent ?? '').trim(); const fallback = String(fallbackContent ?? '').trim(); if (!deferred) return fallback; if (!fallback) return deferred; return `${deferred}\n\n${fallback}`; }