Chuyển đến nội dung chính

Lesson 16: Multi-Channel Integration — Omnichannel Gateway, Messenger, Zalo, Web Widget

Omnichannel gateway architecture, channel adapter pattern, Facebook Messenger, Zalo OA, Web Widget, LINE, Slack, Microsoft Teams, mobile SDK, message normalization.

🏗️ Architecture — Lesson 16 Lesson 16: Multi-Channel Integration — Omnichannel Gateway, Messenger, Zalo, Web Widgets

Enterprise AI Chatbot Platform Architecture — From Prototype to Production

Part 5: Multi-Channel & Scale

xdev.asia

1. Omnichannel Gateway Architecture

Enterprise chatbot needs to work on multiple channels simultaneously — Web, Messenger, Zalo, Slack, Teams — with unified conversation across channels.


┌────────── OMNICHANNEL GATEWAY ────────────────────────┐
│                                                        │
│  ┌────────┐ ┌────────┐ ┌──────┐ ┌──────┐ ┌─────────┐  │
│  │  Web   │ │Messengr│ │ Zalo │ │Slack │ │  Teams  │  │
│  │Widget  │ │        │ │  OA  │ │      │ │         │  │
│  └───┬────┘ └───┬────┘ └──┬───┘ └──┬───┘ └────┬────┘  │
│      │          │         │        │          │       │
│  ┌───▼──────────▼─────────▼────────▼──────────▼───┐   │
│  │            CHANNEL ADAPTER LAYER               │   │
│  │   (Normalize → Unified Message → Enrich)       │   │
│  └────────────────────┬───────────────────────────┘   │
│                       │                               │
│            ┌──────────▼──────────┐                    │
│            │  UNIFIED MESSAGE    │                    │
│            │  {text, images,     │                    │
│            │   channel, userId}  │                    │
│            └──────────┬──────────┘                    │
│                       │                               │
│            ┌──────────▼──────────┐                    │
│            │   CHAT ENGINE       │                    │
│            │   (Same for all     │                    │
│            │    channels)        │                    │
│            └─────────────────────┘                    │
└────────────────────────────────────────────────────────┘

2. Channel Adapter Pattern


interface UnifiedMessage {
  channelType: 'web' | 'messenger' | 'zalo' | 'slack' | 'teams' | 'line';
  channelUserId: string;
  platformMessageId: string;
  text?: string;
  images?: { url: string; mimeType: string }[];
  audio?: { url: string; duration: number }[];
  location?: { lat: number; lng: number };
  quickReplyPayload?: string;
  metadata: Record<string, unknown>;
}

interface ChannelAdapter {
  channelType: string;
  handleWebhook(req: Request): Promise<UnifiedMessage>;
  sendResponse(userId: string, response: BotResponse): Promise<void>;
  getCapabilities(): ChannelCapabilities;
}

interface ChannelCapabilities {
  maxTextLength: number;
  supportsImages: boolean;
  supportsButtons: boolean;
  supportsCarousel: boolean;
  supportsQuickReplies: boolean;
  supportsAudio: boolean;
  supportsMarkdown: boolean;
  supportsRichCards: boolean;
}

class ChannelRouter {
  private adapters = new Map<string, ChannelAdapter>();

  register(adapter: ChannelAdapter): void {
    this.adapters.set(adapter.channelType, adapter);
  }

  async handleIncoming(channelType: string, req: Request): Promise<void> {
    const adapter = this.adapters.get(channelType);
    if (!adapter) throw new Error(`Unknown channel: ${channelType}`);

    // 1. Parse incoming message
    const message = await adapter.handleWebhook(req);

    // 2. Resolve or create conversation
    const conversation = await this.resolveConversation(message);

    // 3. Process through chat engine (channel-agnostic)
    const response = await this.chatEngine.processMessage(
      conversation.id,
      message.text ?? '',
      { images: message.images, audio: message.audio },
    );

    // 4. Adapt response to channel capabilities
    const adaptedResponse = this.adaptResponse(response, adapter.getCapabilities());

    // 5. Send back via channel
    await adapter.sendResponse(message.channelUserId, adaptedResponse);
  }

  private adaptResponse(
    response: BotResponse,
    capabilities: ChannelCapabilities,
  ): BotResponse {
    let adapted = { ...response };

    // Truncate text if exceeds channel limit
    if (adapted.text && adapted.text.length > capabilities.maxTextLength) {
      adapted.text = adapted.text.slice(0, capabilities.maxTextLength - 3) + '...';
    }

    // Convert markdown to plain text if not supported
    if (!capabilities.supportsMarkdown && adapted.text) {
      adapted.text = this.stripMarkdown(adapted.text);
    }

    // Remove buttons if not supported
    if (!capabilities.supportsButtons) {
      adapted.buttons = undefined;
    }

    return adapted;
  }
}

3. Facebook Messenger Adapter


class MessengerAdapter implements ChannelAdapter {
  channelType = 'messenger';

  async handleWebhook(req: Request): Promise<UnifiedMessage> {
    const body = req.body;

    // Verify signature
    this.verifySignature(req.headers['x-hub-signature-256'], req.rawBody);

    const entry = body.entry[0];
    const event = entry.messaging[0];

    return {
      channelType: 'messenger',
      channelUserId: event.sender.id,
      platformMessageId: event.message.mid,
      text: event.message?.text,
      images: event.message?.attachments
        ?.filter(a => a.type === 'image')
        .map(a => ({ url: a.payload.url, mimeType: 'image/jpeg' })),
      quickReplyPayload: event.message?.quick_reply?.payload,
      metadata: { pageId: entry.id },
    };
  }

  async sendResponse(userId: string, response: BotResponse): Promise<void> {
    const messages = this.formatForMessenger(response);

    for (const msg of messages) {
      await this.client.post('/me/messages', {
        recipient: { id: userId },
        message: msg,
        messaging_type: 'RESPONSE',
      });
    }
  }

  private formatForMessenger(response: BotResponse): MessengerMessage[] {
    const messages: MessengerMessage[] = [];

    // Text message
    if (response.text) {
      messages.push({ text: response.text });
    }

    // Quick replies
    if (response.quickReplies?.length) {
      messages.push({
        text: response.quickReplyPrompt ?? 'Chọn một tùy chọn:',
        quick_replies: response.quickReplies.map(qr => ({
          content_type: 'text',
          title: qr.label,
          payload: qr.value,
        })),
      });
    }

    // Carousel
    if (response.cards?.length) {
      messages.push({
        attachment: {
          type: 'template',
          payload: {
            template_type: 'generic',
            elements: response.cards.map(card => ({
              title: card.title,
              subtitle: card.subtitle,
              image_url: card.imageUrl,
              buttons: card.buttons?.map(btn => ({
                type: btn.url ? 'web_url' : 'postback',
                title: btn.label,
                url: btn.url,
                payload: btn.payload,
              })),
            })),
          },
        },
      });
    }

    return messages;
  }

  getCapabilities(): ChannelCapabilities {
    return {
      maxTextLength: 2000,
      supportsImages: true,
      supportsButtons: true,
      supportsCarousel: true,
      supportsQuickReplies: true,
      supportsAudio: true,
      supportsMarkdown: false,
      supportsRichCards: true,
    };
  }
}

4. Zalo OA Adapter (Vietnam-specific)


class ZaloOAAdapter implements ChannelAdapter {
  channelType = 'zalo';

  async handleWebhook(req: Request): Promise<UnifiedMessage> {
    const event = req.body;

    // Verify MAC
    this.verifyMAC(event, req.headers['x-zalo-mac']);

    if (event.event_name === 'user_send_text') {
      return {
        channelType: 'zalo',
        channelUserId: event.sender.id,
        platformMessageId: event.message.msg_id,
        text: event.message.text,
        metadata: { oaId: event.recipient.id },
      };
    }

    if (event.event_name === 'user_send_image') {
      return {
        channelType: 'zalo',
        channelUserId: event.sender.id,
        platformMessageId: event.message.msg_id,
        images: [{ url: event.message.url, mimeType: 'image/jpeg' }],
        metadata: { oaId: event.recipient.id },
      };
    }

    throw new Error(`Unsupported Zalo event: ${event.event_name}`);
  }

  async sendResponse(userId: string, response: BotResponse): Promise<void> {
    if (response.text) {
      await this.client.post('/oa/message/cs', {
        recipient: { user_id: userId },
        message: { text: response.text },
      });
    }

    if (response.cards?.length) {
      await this.client.post('/oa/message/cs', {
        recipient: { user_id: userId },
        message: {
          attachment: {
            type: 'template',
            payload: {
              template_type: 'list',
              elements: response.cards.map(card => ({
                title: card.title,
                subtitle: card.subtitle,
                image_url: card.imageUrl,
                default_action: { type: 'oa.open.url', url: card.url },
              })),
            },
          },
        },
      });
    }
  }

  getCapabilities(): ChannelCapabilities {
    return {
      maxTextLength: 2000,
      supportsImages: true,
      supportsButtons: true,
      supportsCarousel: true,
      supportsQuickReplies: true,
      supportsAudio: false,
      supportsMarkdown: false,
      supportsRichCards: true,
    };
  }
}

5. Embeddable Web Widgets


// Widget embed script (1-line integration)
// <script src="https://chatbot.example.com/widget.js" data-tenant="tenant-id"></script>

class WebWidgetAdapter implements ChannelAdapter {
  channelType = 'web';

  // Web widget uses WebSocket directly (no webhook needed)
  async handleWebhook(req: Request): Promise<UnifiedMessage> {
    // Not used — web widget connects via WebSocket
    throw new Error('Web widget uses WebSocket, not webhooks');
  }

  // Widget configuration per tenant
  getWidgetConfig(tenantId: string): WidgetConfig {
    return {
      position: 'bottom-right',
      theme: {
        primaryColor: '#0066FF',
        botAvatar: '/avatar.png',
        headerText: 'Hỗ trợ khách hàng',
      },
      features: {
        fileUpload: true,
        voiceInput: true,
        quickReplies: true,
        typing: true,
        readReceipts: true,
      },
      i18n: { locale: 'vi' },
    };
  }

  getCapabilities(): ChannelCapabilities {
    return {
      maxTextLength: 10000,
      supportsImages: true,
      supportsButtons: true,
      supportsCarousel: true,
      supportsQuickReplies: true,
      supportsAudio: true,
      supportsMarkdown: true,    // Web supports full markdown
      supportsRichCards: true,
    };
  }
}

Summary of Lesson 16

  • Channel Adapter Pattern: Each channel has its own adapter — normalize → unified message → chat engine
  • Response Adaptation: Auto-adjust output according to channel capabilities (markdown, buttons, text length)
  • Messenger: Webhook verification, quick replies, carousel templates
  • Zalo OA: MAC verification, text + image messages, list templates (Vietnam-focused)
  • Web Widgets: WebSocket-based, full markdown + file upload + voice, 1-line embed

Next article: Human Handoff & Hybrid Support — escalation triggers, agent routing, live chat integration, seamless handoff UX.