import { runtimeConfig } from '../config/runtime.js' import crypto from 'node:crypto' import { createMessageDelivery, findLatestSuccessfulMessageDeliveryByTask, updateMessageDelivery, } from '../repositories/message-delivery-repo.js' import { nowIso } from '../utils/time.js' const AGISO_MESSAGE_CHANNEL = 'agiso_im' export async function ensureAgisoClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) { if (!order || !task || !claimUrl) { return { sent: false, skipped: true, reason: 'missing_message_context' } } const config = runtimeConfig.platforms?.agiso?.messaging || {} if (!config.enabled) { return { sent: false, skipped: true, reason: 'messaging_disabled' } } const endpoint = String(config.sendMessageEndpoint || '').trim() const accessToken = String(config.accessToken || '').trim() const appSecret = String(config.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim() if (!endpoint || !accessToken || !appSecret) { return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' } } const successful = findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_MESSAGE_CHANNEL) if (successful) { return { sent: false, skipped: true, reason: 'already_sent', deliveryId: successful.id } } const messageContent = renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template: config.messageTemplate }) const url = buildRequestUrl(endpoint) const requestBody = buildRequestBody({ tid: String(order.platform_order_id || ''), msg: messageContent, appSecret, }) const requestHeaders = buildRequestHeaders({ accessToken, apiVersion: String(config.apiVersion || '1').trim() || '1', }) const createdAt = nowIso() const delivery = createMessageDelivery({ platform: 'agiso', channel: AGISO_MESSAGE_CHANNEL, orderId: order.id, taskId: task.id, platformOrderId: order.platform_order_id, recipientKey: order.platform_order_id, messageContent, claimUrl, status: 'pending', requestUrl: url, requestHeadersJson: JSON.stringify(maskHeadersForStorage(requestHeaders)), requestBodyJson: JSON.stringify(maskBodyForStorage(requestBody)), responseStatus: 0, responseJson: '{}', errorMessage: '', sentAt: null, createdAt, updatedAt: createdAt, }) try { const response = await fetch(url, { method: 'POST', headers: requestHeaders, body: new URLSearchParams(requestBody).toString(), }) const rawText = await response.text() const parsed = safeParseJson(rawText) const success = isAgisoSendSuccess(response.status, parsed) const updated = updateMessageDelivery(delivery.id, { status: success ? 'success' : 'failed', response_status: response.status, response_json: JSON.stringify(parsed ?? { rawText }), error_message: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status), sent_at: success ? nowIso() : null, updated_at: nowIso(), }) return { sent: success, skipped: false, deliveryId: updated?.id || delivery.id, responseStatus: response.status, response: parsed ?? { rawText }, errorMessage: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status), } } catch (error) { const message = error instanceof Error ? error.message : String(error || '发送消息失败') updateMessageDelivery(delivery.id, { status: 'failed', response_status: 0, response_json: '{}', error_message: message, sent_at: null, updated_at: nowIso(), }) return { sent: false, skipped: false, deliveryId: delivery.id, errorMessage: message, } } } function buildRequestUrl(endpoint) { return new URL(endpoint).toString() } function buildRequestHeaders({ accessToken, apiVersion }) { return { Authorization: `Bearer ${accessToken}`, ApiVersion: apiVersion, 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', } } function buildRequestBody({ tid, msg, appSecret }) { const timestamp = String(Math.floor(Date.now() / 1000)) const payload = { tid, msg, timestamp, } payload.sign = generateSign(payload, appSecret) return payload } function generateSign(params, appSecret) { const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right)) let raw = appSecret for (const [key, value] of sortedEntries) { raw += `${key}${value}` } raw += appSecret return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase() } function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template }) { const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}' return source .replaceAll('{platformOrderId}', String(order.platform_order_id || '')) .replaceAll('{taskNo}', String(task.task_no || '')) .replaceAll('{claimUrl}', claimUrl) .replaceAll('{expiredAt}', String(expiredAt || '尽快')) } function isAgisoSendSuccess(statusCode, payload) { if (statusCode < 200 || statusCode >= 300) { return false } if (!payload || typeof payload !== 'object') { return true } if (payload.IsSuccess === true) { return true } if (Number(payload.Error_Code) === 0) { return true } if (Number(payload.code) === 0) { return true } return false } function resolveAgisoErrorMessage(payload, rawText, statusCode) { if (payload && typeof payload === 'object') { for (const value of [payload.Error_Msg, payload.msg, payload.message, payload.error]) { const normalized = String(value || '').trim() if (normalized) { return normalized } } } const text = String(rawText || '').trim() return text || `Agiso 发消息失败,HTTP ${statusCode}` } function safeParseJson(rawText) { const normalized = String(rawText || '').trim() if (!normalized) { return null } try { return JSON.parse(normalized) } catch { return null } } function maskHeadersForStorage(headers) { const output = { ...headers } if (output.Authorization) { output.Authorization = '[masked]' } return output } function maskBodyForStorage(body) { const output = { ...body } if (output.sign) { output.sign = '[masked]' } return output }