Sau webhook là một bài toán hợp đồng, không phải bài toán tích hợp

Một doanh nghiệp Việt vận hành Messenger, Zalo OA, TikTok Shop, Instagram, WhatsApp và website thường bắt đầu bằng sáu màn hình quản trị hoặc sáu kết nối rời rạc. Khi xây Hộp thư hội tụ đa kênh, phản xạ đầu tiên của nhiều đội là viết webhook cho từng nền tảng rồi đổ tất cả vào một bảng messages. Điều này có thể chạy được ở giai đoạn demo, nhưng sẽ trở thành khoản nợ kỹ thuật lớn ngay khi kênh thứ ba, thứ tư xuất hiện.

Vấn đề không nằm ở việc nhận một payload JSON. Mỗi kênh có một định nghĩa khác nhau về người dùng, cuộc hội thoại, tin nhắn, trạng thái đã đọc, tệp đính kèm, cửa sổ được phép gửi chủ động, cách retry và mã lỗi. Một phản hồi dạng text có thể gửi được ở kênh này nhưng phải biến thành template đã duyệt ở kênh khác. Một 200 OK từ API có thể chỉ có nghĩa hệ thống bên ngoài đã nhận yêu cầu, chưa bảo đảm tin đến điện thoại khách. Nếu các khác biệt ấy lan thẳng vào code AI, CRM, phân luồng và giao diện nhân viên, mỗi lần nhà cung cấp đổi API sẽ làm rung chuyển toàn bộ hệ thống.

Bài viết này trình bày lớp kiến trúc BEEDONI đặt trước các nền tảng bên ngoài: Channel Adapter và Canonical Event Contract. Đây là tầng biến sáu hệ thống có ngôn ngữ riêng thành một nền tảng điều hành thống nhất, để phần còn lại của sản phẩm chỉ làm việc với một mô hình dữ liệu, một luồng trạng thái và một tập năng lực đã được khai báo tường minh.

Lớp 1. Canonical Event Schema: một ngôn ngữ nội bộ cho mọi kênh

Canonical Event Schema là hợp đồng dữ liệu nội bộ, mô tả sự kiện theo nghĩa nghiệp vụ thay vì theo cách một nền tảng cụ thể đặt tên trường. Adapter của Messenger có thể nhận mid, adapter Zalo OA nhận msg_id, một adapter website tự sinh UUID. Nhưng sau bước chuẩn hóa, mọi thành phần phía sau chỉ nhìn thấy provider_message_id, channel, sender, conversation, content và occurred_at. Đây là nguyên lý anti-corruption layer trong Domain-Driven Design: mô hình ngoài biên được phép thay đổi, mô hình lõi của doanh nghiệp không được phép bị “nhiễm” từ vựng và bất thường của bên ngoài.

Điều quan trọng là schema không cố ép mọi nền tảng trở nên giống hệt nhau. Nó giữ một tập trường lõi có ngữ nghĩa ổn định, đồng thời chừa provider_metadata có kiểm soát cho năng lực riêng mà chỉ một kênh có. Nếu cố đưa mọi field độc quyền vào trường lõi, canonical schema nhanh chóng phình to và trở thành bản sao lộn xộn của sáu API. Nếu bỏ hết metadata, hệ thống lại không thể khai thác các tính năng có giá trị như quick reply, reaction hay trạng thái giao hàng của từng kênh.

json
{
  "event_id": "evt_01HQ...",
  "type": "message.received",
  "channel": "zalo_oa",
  "tenant_id": "tenant_abc",
  "provider_message_id": "msg_847291",
  "conversation": { "external_id": "zalo_user_458" },
  "sender": { "external_id": "zalo_user_458", "role": "customer" },
  "content": { "kind": "text", "text": "Cho tôi xin giá niềng răng" },
  "occurred_at": "2023-08-17T09:21:14.052+07:00",
  "provider_metadata": { "raw_event_type": "user_send_text" }
}
Khái niệm lõiMessengerZalo OAMô hình canonical của BEEDONI
Định danh tin nhắnmidmsg_idprovider_message_id
Định danh người gửiPSID theo Pageuser_id_by_appsender.external_id + namespace kênh
Thời điểmtimestamp mili-giâytimestamp tùy sự kiệnoccurred_at ISO 8601 có timezone
Nội dungtext, attachment, postbacktext, image, stickercontent.kind + payload chuẩn hóa
Thông tin riêng kênhquick reply payloadOA metadataprovider_metadata, không rò rỉ vào core

Lớp 2. Channel Adapter: lớp hấp thụ biến động của nền tảng bên ngoài

Mỗi Channel Adapter thực hiện bốn trách nhiệm rõ ràng. Một là xác thực request đi vào bằng chữ ký hoặc token theo cơ chế của từng nhà cung cấp. Hai là dịch payload bên ngoài thành Canonical Event. Ba là thực thi lệnh gửi ra bằng API của kênh và chuyển kết quả về cùng một mô hình receipt. Bốn là công bố capability của kênh để phần AI và giao diện không bao giờ đề xuất một thao tác mà kênh không hỗ trợ.

Adapter không được mang luật nghiệp vụ như phân công nhân viên, định tuyến intent hay chính sách giá. Những luật đó thuộc lớp lõi và phải chạy giống nhau bất kể khách nhắn từ đâu. Ngược lại, adapter được phép biết rất rõ luật giao thức của kênh mình: header chữ ký, cấu trúc template, giới hạn media, rate limit, quy tắc retry và cách xác định lỗi vĩnh viễn. Ranh giới này giúp BEEDONI thay hoặc nâng cấp một adapter mà không cần chạm vào mô hình AI hay dữ liệu CRM.

ts
type ChannelAdapter = {
  verifyInbound(request: Request): Promise<boolean>;
  normalizeInbound(raw: unknown): CanonicalEvent[];
  capabilities(tenantId: string): Promise<ChannelCapabilities>;
  deliver(command: OutboundCommand): Promise<DeliveryReceipt>;
};

// Core chỉ biết interface này. Không biết payload Meta, Zalo hay TikTok cụ thể.
const receipt = await adapters[channel].deliver(command);

Cách chia này cũng giúp kiểm thử thực tế hơn. Core có thể chạy contract test với adapter giả lập để xác nhận mọi kênh đều trả về cùng bộ trạng thái chuẩn. Mỗi adapter thật có bộ fixture payload của riêng mình, gồm cả webhook hợp lệ, chữ ký sai, event đến trùng, tệp hỏng và callback trạng thái trễ. Khi một nền tảng thay đổi version API, phạm vi regression được cô lập trong adapter thay vì phải kiểm thử lại toàn bộ nền tảng.

Lớp 3. Capability Registry: biến giới hạn từng kênh thành dữ liệu có thể ra quyết định

Một Hộp thư hội tụ tốt không che giấu khác biệt giữa các kênh bằng cách giả vờ mọi kênh giống nhau. Nó làm ngược lại: công bố rõ Channel Capabilities để sản phẩm ra quyết định đúng trước khi gửi. Ví dụ, AI cần biết kênh hiện tại có hỗ trợ hình ảnh, danh sách chọn nhanh, nút CTA, tin chủ động ngoài phiên hay không. Nếu không có lớp này, LLM có thể soạn một câu trả lời hoàn hảo nhưng driver gửi mới phát hiện nó vượt giới hạn ký tự hoặc vi phạm cửa sổ nhắn tin, rồi thất bại ở bước cuối cùng trước mặt khách hàng.

CapabilityVí dụ quyết định sản phẩmHậu quả nếu kiểm tra quá muộn
max_text_lengthChia câu trả lời dài thành nhiều đoạn ở ranh giới câuAPI từ chối hoặc cắt thông tin quan trọng
supports_interactiveAI chọn quick reply thay vì yêu cầu khách gõ tự doTạo UI không hiển thị được trên kênh nhận
outbound_windowChuyển sang template đã duyệt hoặc tạo tác vụ cho nhân viênGửi lỗi, nhân viên tưởng khách đã nhận tin
supported_mediaĐổi tài liệu sang link an toàn khi kênh không gửi file trực tiếpTệp bị mất hoặc nội dung không thể đọc

Capability Registry được phiên bản hóa theo kênh và theo tenant. Một doanh nghiệp có thể chưa bật một tính năng dù nền tảng bên ngoài hỗ trợ, hoặc có chính sách nội bộ chặt hơn chính sách mặc định của kênh. Vì vậy, capability là dữ liệu cấu hình tại thời điểm quyết định, không phải một chuỗi if/else rải khắp code. Đây là tiền đề để Trợ lý AI tạo lệnh gửi có ràng buộc, thay vì sinh văn bản rồi hy vọng tầng dưới gửi được.

Lớp 4. Hợp đồng gửi ra: API accepted không phải delivered

Luồng đi vào thường được chú ý nhiều hơn, nhưng luồng gửi phản hồi mới là nơi tạo trải nghiệm cuối cùng. Một POST trả 200 hoặc 202 Accepted thường chỉ khẳng định nhà cung cấp đã tiếp nhận lệnh. Tin có thể bị từ chối ở bước sau vì chính sách, bị giới hạn tần suất, hoặc thất bại tạm thời trên đường truyền. Nếu hệ thống đánh dấu hội thoại là “đã trả lời” ngay sau response API đầu tiên, nhân viên sẽ tin khách đã được chăm sóc trong khi khách chưa nhận gì.

BEEDONI mô hình hóa gửi ra như một state machine riêng: queued, accepted, delivered, read, failed_retryable, failed_terminal. Lệnh gửi được ghi vào Transactional Outbox cùng giao dịch thay đổi trạng thái hội thoại. Worker đọc outbox, gọi adapter với idempotency key ổn định, sau đó receipt và callback trạng thái từ kênh cập nhật lại thực thể delivery. Cách này đảm bảo một lỗi tạm thời không làm mất tin và retry không gửi trùng câu trả lời tới khách.

text
QUEUED ──(worker gọi Channel Adapter)──▶ ACCEPTED
  │                                           │
  ├─(lỗi tạm thời)──▶ FAILED_RETRYABLE ───────┤
  │                                           ├─(receipt/callback)──▶ DELIVERED ──▶ READ
  └─(lỗi chính sách hoặc payload)──▶ FAILED_TERMINAL

Mọi transition mang: tenant_id + channel + idempotency_key + trace_id
“Hộp thư hội tụ không phải nơi sáu API cùng đổ vào. Nó là nơi sáu thế giới khác nhau được dịch sang một hợp đồng chung, để doanh nghiệp có thể vận hành một trải nghiệm khách hàng duy nhất.”

Kết luận: hợp đồng chuẩn hóa là điều kiện để AI và con người cùng vận hành

Canonical Event, Channel Adapter, Capability Registry và state machine giao tin không phải chi tiết triển khai ẩn phía sau giao diện. Chúng là nền móng để cùng một luồng Nhắn tin đa kênh có thể nhận đúng ngữ cảnh, kiểm tra trước khả năng gửi, cho AI trả lời hoặc chuyển người đúng lúc, rồi đồng bộ chính xác về Bee CRM. Bài viết về kiến trúc Hộp thư hội tụ Event-Driven đi sâu hơn vào ingestion, idempotency và kiểm soát trạng thái sau khi hợp đồng này đã được xác lập.

Đội ngũ kỹ thuật có thể xem bảng giá để chuẩn bị phạm vi triển khai, hoặc tìm hiểu thêm về phương pháp xây nền tảng của BEEDONI tại Về chúng tôi.