'use strict'; const crypto = require('crypto'); const prom = require('./prom'); // Phase 0:验签 -> 去重 -> 只处理 contact 的 message.created -> 回一条占位消息。 // Phase 1 会把这里替换成 bridge AI 编排。 function verifySignature(rawBody, secret, signature) { if (!secret || !signature) return false; const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(String(signature)); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b); } function createLibreDeskWebhook(cfg, deps = {}) { const client = deps.client || require('./libredesk').createLibreDeskClient(cfg); const agent = deps.agent || null; const seen = new Map(); const TTL_MS = 10 * 60 * 1000; function markSeen(uuid) { const now = Date.now(); for (const [key, ts] of seen) if (now - ts > TTL_MS) seen.delete(key); if (seen.has(uuid)) return false; seen.set(uuid, now); return true; } async function handle(body) { if (!body || body.event !== 'message.created') return; const msg = body.payload || {}; prom.inc('bridge_webhook_events_total', { event: body.event || 'unknown' }); if (msg.sender_type !== 'contact' || msg.private) return; if (!msg.uuid || !msg.conversation_uuid) return; if (!markSeen(msg.uuid)) return; const conv = await client.getConversation(msg.conversation_uuid); const inboxId = String(conv && conv.inbox_id); const allowed = cfg.libredeskWebhookInboxIds || []; if (allowed.length && !allowed.includes(inboxId)) { console.log(JSON.stringify({ ts: new Date().toISOString(), webhook: 'ignored_inbox', inbox: inboxId, conversation: msg.conversation_uuid })); return; } const text = String(msg.text_content || msg.content || '').trim(); console.log(JSON.stringify({ ts: new Date().toISOString(), webhook: 'message.created', inbox: inboxId, conversation: msg.conversation_uuid, message: msg.uuid, text: text.slice(0, 120) })); if (agent) { try { await agent.run(conv, msg); } catch (err) { console.error(JSON.stringify({ ts: new Date().toISOString(), webhook: 'agent_error', conversation: msg.conversation_uuid, error: err.message })); } } else { // 没有 agent 时的占位回复(仅用于链路调试) const reply = `Bridge Phase 0 已收到:${text || '(空消息)'}`; await client.sendMessage(msg.conversation_uuid, reply, { echoId: `bridge-p0-${msg.uuid}` }); console.log(JSON.stringify({ ts: new Date().toISOString(), webhook: 'replied', conversation: msg.conversation_uuid, reply })); } } return { verifySignature, handle }; } module.exports = { createLibreDeskWebhook, verifySignature };