UNPKG

overmind-mcp

Version:

Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour

243 lines • 11 kB
/** * ╔══════════════════════════════════════════════════════════════════════╗ * ║ OVERMIND BRIDGE — WebhookAdapter (Generic Webhook → Bridge) ║ * ║ ║ * ║ Normalise les webhooks de providers variés vers un format ║ * ║ uniforme consommable par l'API agent.run. ║ * ║ ║ * PROVIDERS SUPPORTÉS ║ * ──────────────────── ║ * - voipms : VoIP.ms (from, to, message, id, date, media) ║ * - twilio : Twilio (From, To, Body, MessageSid, MediaUrl0) ║ * - telegram : Telegram Bot API (message.chat.id, message.from.id) ║ * - discord : Discord-like (channelId, userId, content) ║ * - generic : Format customisable (mapping field-by-field) ║ * ║ ║ * ║ Chaque adapter retourne un NormalizedWebhook qui contient : ║ * ║ - externalKey : clé pour SessionStore (phone, user, etc.) ║ * ║ - prompt : texte à envoyer à l'agent (déjà contextualisé) ║ * ║ - metadata : données brutes pour audit ║ * ║ - mediaUrls : URLs de médias attachés ║ * ╚══════════════════════════════════════════════════════════════════════╝ */ import { createBridgeLogger } from './utils.js'; import { formatDiscordContext } from './utils.js'; // ─── WebhookAdapter ──────────────────────────────────────────────────────── export class WebhookAdapter { config; log; constructor(config = {}) { this.config = { provider: config.provider ?? 'voipms', promptPrefix: config.promptPrefix ?? '', fieldMap: config.fieldMap, discordContext: config.discordContext, }; this.log = config.logger ?? createBridgeLogger('webhook-adapter'); } /** * Adapte un payload brut vers un NormalizedWebhook. */ adapt(rawPayload) { switch (this.config.provider) { case 'voipms': return this.adaptVoipMs(rawPayload); case 'twilio': return this.adaptTwilio(rawPayload); case 'telegram': return this.adaptTelegram(rawPayload); case 'discord': return this.adaptDiscord(rawPayload); case 'generic': return this.adaptGeneric(rawPayload); default: { const _exhaustive = this.config.provider; throw new Error(`Unknown provider: ${String(_exhaustive)}`); } } } // ─── Provider Adapters ────────────────────────────────────────────────── /** * VoIP.ms callback format. * Params: from, to, message, id, date, media (URLs séparées par |) * Body peut être GET query ou POST form-encoded. */ adaptVoipMs(raw) { const from = stringOr(raw.from ?? raw.From, 'unknown'); const to = stringOr(raw.to ?? raw.To, 'unknown'); const message = stringOr(raw.message ?? raw.Message ?? raw.Body ?? raw.body, ''); const messageId = stringOr(raw.id ?? raw.MessageSid, ''); const date = stringOr(raw.date ?? raw.Date, new Date().toISOString()); const mediaRaw = stringOr(raw.media ?? raw.MediaUrl0 ?? '', ''); const mediaUrls = mediaRaw ? mediaRaw .split('|') .map((u) => u.trim()) .filter(Boolean) : []; const promptParts = []; if (this.config.promptPrefix) promptParts.push(this.config.promptPrefix); promptParts.push(`[Webhook VoIP.ms]`); promptParts.push(`From: ${from}`); promptParts.push(`To: ${to}`); promptParts.push(`Date: ${date}`); if (messageId) promptParts.push(`MessageId: ${messageId}`); promptParts.push('---'); promptParts.push(message); if (mediaUrls.length > 0) { promptParts.push('---'); promptParts.push(`Médias (${mediaUrls.length}):`); for (const url of mediaUrls) promptParts.push(`- ${url}`); } return { externalKey: from, prompt: promptParts.join('\n'), mediaUrls, metadata: { provider: 'voipms', from, to, messageId, date }, }; } /** * Twilio format (snake_case ou CamelCase). */ adaptTwilio(raw) { const from = stringOr(raw.From ?? raw.from, 'unknown'); const to = stringOr(raw.To ?? raw.to, 'unknown'); const body = stringOr(raw.Body ?? raw.body, ''); const messageSid = stringOr(raw.MessageSid ?? raw.SmsSid ?? raw.messageId, ''); const numMedia = Number(raw.NumMedia ?? 0); const mediaUrls = []; for (let i = 0; i < numMedia; i++) { const url = raw[`MediaUrl${i}`]; if (typeof url === 'string' && url) mediaUrls.push(url); } const promptParts = []; if (this.config.promptPrefix) promptParts.push(this.config.promptPrefix); promptParts.push(`[Webhook Twilio]`); promptParts.push(`From: ${from}`); promptParts.push(`To: ${to}`); if (messageSid) promptParts.push(`MessageSid: ${messageSid}`); promptParts.push('---'); promptParts.push(body); if (mediaUrls.length > 0) { promptParts.push('---'); promptParts.push(`Médias (${mediaUrls.length}):`); for (const url of mediaUrls) promptParts.push(`- ${url}`); } return { externalKey: from, prompt: promptParts.join('\n'), mediaUrls, metadata: { provider: 'twilio', from, to, messageSid, numMedia }, }; } /** * Telegram Bot API format. * Payload: { message: { chat: { id }, from: { id, username }, text, photo: [{ file_id }] } } */ adaptTelegram(raw) { const message = (raw.message ?? raw.edited_message); const chat = (message?.chat ?? {}); const from = (message?.from ?? {}); const chatId = stringOr(chat.id, ''); const userId = stringOr(from.id, ''); const username = stringOr(from.username ?? from.first_name, 'unknown'); const text = stringOr(message?.text, ''); const messageId = stringOr(message?.message_id, ''); // Photos/media: Telegram sends arrays of PhotoSize with file_id const mediaUrls = []; const photo = message?.photo; if (Array.isArray(photo) && photo.length > 0) { // Largest photo is last in array const largest = photo[photo.length - 1]; if (largest?.file_id) mediaUrls.push(`telegram://file/${largest.file_id}`); } const promptParts = []; if (this.config.promptPrefix) promptParts.push(this.config.promptPrefix); promptParts.push(`[Telegram]`); promptParts.push(`From: @${username} (${userId})`); promptParts.push(`Chat: ${chatId}`); if (messageId) promptParts.push(`MessageId: ${messageId}`); promptParts.push('---'); promptParts.push(text); if (mediaUrls.length > 0) { promptParts.push('---'); promptParts.push(`Médias (${mediaUrls.length}):`); for (const url of mediaUrls) promptParts.push(`- ${url}`); } return { externalKey: `telegram:${chatId}:${userId}`, prompt: promptParts.join('\n'), mediaUrls, metadata: { provider: 'telegram', chatId, userId, username, messageId }, }; } /** * Discord-like format (channel, user, message). */ adaptDiscord(raw) { const author = raw.author ?? undefined; const channelId = stringOr(raw.channelId ?? raw.channel_id, ''); const userId = stringOr(raw.userId ?? raw.user_id ?? author?.id, ''); const username = stringOr(raw.username ?? author?.username, 'unknown'); const message = stringOr(raw.content ?? raw.message ?? raw.body, ''); const prompt = formatDiscordContext({ channelId, userId, username, message }); return { externalKey: userId || channelId, prompt: this.config.promptPrefix ? `${this.config.promptPrefix}\n${prompt}` : prompt, mediaUrls: Array.isArray(raw.attachments) ? raw.attachments .map((a) => a.url) .filter((u) => Boolean(u)) : [], metadata: { provider: 'discord', channelId, userId, username }, }; } /** * Generic — applique le fieldMap custom. */ adaptGeneric(raw) { const map = this.config.fieldMap ?? {}; const get = (key) => { const sourceKey = map[key]; if (!sourceKey) return ''; const v = raw[sourceKey]; return typeof v === 'string' ? v : v != null ? JSON.stringify(v) : ''; }; const externalKey = get('externalKey') || 'default'; const prompt = get('prompt') || JSON.stringify(raw); const mediaStr = get('mediaUrls'); const mediaUrls = mediaStr ? mediaStr .split(',') .map((s) => s.trim()) .filter(Boolean) : []; return { externalKey, prompt: this.config.promptPrefix ? `${this.config.promptPrefix}\n${prompt}` : prompt, mediaUrls, metadata: { provider: 'generic', raw }, }; } } // ─── Helpers ─────────────────────────────────────────────────────────────── function stringOr(value, defaultValue) { if (typeof value === 'string') return value; if (value == null) return defaultValue; return String(value); } //# sourceMappingURL=WebhookAdapter.js.map