Harness Academy
Tài liệu gốc 16 20 phútTiếng Việt + English
docs/en/s13-platform-adapters.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

s13: Bộ điều hợp nền tảng

s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > [ s13 ] > s14 > s15 > s16 > s17 > s18 > s19 > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27

s12 đã giải thích lý do bạn cần bộ điều hợp. Chương này chỉ cho bạn cách viết một.

Mẫu bộ điều hợp nền tảng

Chương này giải quyết vấn đề gì

s12 đã sử dụng WeCom (Enterprise WeChat) làm ví dụ để thể hiện ba trách nhiệm cốt lõi của bộ chuyển đổi: kết nối với nền tảng, dịch tin nhắn và gửi phản hồi. Nhưng đó chỉ là một bộ chuyển đổi.

Khi bạn thực sự kết nối nền tảng thứ hai hoặc thứ ba, bạn sẽ gặp phải một loạt vấn đề mới:

  • Mỗi nền tảng có một phương thức kết nối khác nhau. WeCom sử dụng WebSocket trong khi WeChat cá nhân sử dụng tính năng bỏ phiếu dài HTTP -- các giao thức hoàn toàn khác nhau.
  • Người dùng gửi 5000 ký tự văn bản và ứng dụng khách nền tảng sẽ tự động chia nó thành hai tin nhắn. Nếu bạn xử lý riêng hai "nửa tin nhắn" thì câu trả lời của nhân viên sẽ không mạch lạc.
  • Người dùng gửi tin nhắn hình ảnh hoặc giọng nói. Bạn cần tải xuống, decode, lưu vào bộ nhớ đệm, cho phép agent sử dụng và khi trả lời, hãy mã hóa và tải lại.
  • Kết nối WebSocket bị rớt và cần kết nối lại. Yêu cầu HTTP đã hết thời gian chờ và cần thử lại. Cùng một tin nhắn có thể được đẩy hai lần.

Những vấn đề này không chỉ xảy ra ở bất kỳ nền tảng nào -- mọi bộ chuyển đổi đều gặp phải chúng. Bạn cần một lớp cơ sở để trích xuất những điểm chung để mỗi bộ chuyển đổi chỉ xử lý các phần dành riêng cho nền tảng của nó.

Đề xuất đọc

  • s12-gateway-architecture.md -- Bộ điều hợp ra đời như thế nào và logic định tuyến của GatewayRunner
  • data-structures.md -- Định nghĩa trường đầy đủ cho MessageEvent và SessionSource
  • glossary.md -- Thuật ngữ dành cho bộ điều hợp, khóa phiên, v.v.

Thuật ngữ chính

BasePlatformAdapter là gì

Lớp cơ sở trừu tượng cho tất cả các bộ điều hợp nền tảng. Nó xác định những gì bộ điều hợp phải làm (kết nối, gửi tin nhắn) và những gì nó có thể làm (gửi hình ảnh, gửi giọng nói), nhưng không quan tâm đến làm thế nào -- các chi tiết cụ thể được để lại cho các lớp con.

Phân nhóm văn bản là gì

Ứng dụng khách nền tảng tự động cắt ngắn văn bản dài khi gửi (WeCom ở 4000 ký tự, WeChat cá nhân ở 1500 ký tự). Một tin nhắn của người dùng sẽ trở thành hai hoặc thậm chí ba tin nhắn được gửi tới bạn. Bộ điều hợp cần hợp nhất chúng lại thành một trước khi chuyển nó cho GatewayRunner.

Chống trùng lặp là gì

Trong thời gian mạng không ổn định, cùng một thông báo có thể được nền tảng đẩy hai lần. Bộ chuyển đổi sử dụng message_id để theo dõi các tin nhắn đã xử lý và loại bỏ các tin nhắn trùng lặp.

Bộ nhớ đệm phương tiện là gì

URL phương tiện do nền tảng cung cấp thường là tạm thời và có thể yêu cầu decode. Khi bộ điều hợp nhận được một tin nhắn hình ảnh hoặc giọng nói, nó sẽ ngay lập tức tải xuống, decode và lưu trữ nó trong một thư mục bộ đệm cục bộ, chuyển đường dẫn cục bộ đến agent.

Mô hình tinh thần tối thiểu

Vòng đời hoàn chỉnh của bộ chuyển đổi:

                   connect()
                      |
                      v
         +-----------------------------+
         |   Platform message listener  |
         |                             |
         |  Platform JSON/XML          |
         |      |                      |
         |      v                      |
         |  _translate()               |
         |      |                      |
         |      v                      |
         |  Deduplication -> Batching  |
         |      |                      |
         |      v                      |
         |  handle_message(event)  ----+--> GatewayRunner
         |                             |
         |  send(chat_id, text)  <-----+--- GatewayRunner
         |      |                      |
         |      v                      |
         |  Format conversion ->       |
         |     Platform API            |
         +-----------------------------+
                      |
                      v
                 disconnect()

Thông tin chi tiết quan trọng: "Nền tảng JSON -> _translate() -> MessageEvent" của bên trái và "văn bản chung -> chuyển đổi định dạng -> API nền tảng" của bên phải là những phần duy nhất mà mỗi bộ chuyển đổi cần viết. Tính năng loại bỏ trùng lặp, phân nhóm và định tuyến ở giữa đều được chia sẻ.

Cấu trúc dữ liệu chính

BasePlatformAdapter

class BasePlatformAdapter(ABC):

    def __init__(self, platform_name: str):
        self.platform_name = platform_name
        self._on_message = None   # Injected by GatewayRunner at startup
        self._running = False

    # --- Three methods that must be implemented ---

    @abstractmethod
    async def connect(self) -> bool: ...

    @abstractmethod
    async def disconnect(self): ...

    @abstractmethod
    async def send(self, chat_id: str, content: str) -> bool: ...

    # --- Two optional media methods (default returns False = not supported) ---

    async def send_image(self, chat_id: str, image_path: str) -> bool:
        return False

    async def send_voice(self, chat_id: str, audio_path: str) -> bool:
        return False

    # --- No need to override ---

    async def handle_message(self, event: MessageEvent):
        if self._on_message:
            await self._on_message(event)

Ba phương thức bắt buộc là những gì mọi nền tảng đều có -- bất kể nền tảng nào, bạn luôn cần kết nối, ngắt kết nối và gửi tin nhắn. Hai phương thức tùy chọn mặc định là False; các nền tảng hỗ trợ chúng chỉ cần ghi đè.

Trình loại bỏ tin nhắn

class MessageDeduplicator:
    """Deduplicates by message_id with FIFO eviction of old records."""

    def __init__(self, max_size: int = 1000):
        self._seen: set[str] = set()
        self._order: list[str] = []
        self._max_size = max_size

    def is_duplicate(self, message_id: str) -> bool:
        if message_id in self._seen:
            return True
        self._seen.add(message_id)
        self._order.append(message_id)
        if len(self._order) > self._max_size:
            old_id = self._order.pop(0)
            self._seen.discard(old_id)
        return False

TextBatcher

class TextBatcher:
    """Buffers text fragments, merges them into a single MessageEvent after a quiet period."""

    async def enqueue(self, session_key, text, event, split_threshold=3900):
        self._buffers[session_key].append(text)
        # Close to the truncation threshold -> wait longer (a continuation is almost certain)
        delay = 2.0 if len(text) >= split_threshold else 0.6
        # Cancel the previous timer, restart the countdown
        self._restart_flush_timer(session_key, delay)

    async def _flush(self, session_key):
        merged_text = "".join(self._buffers.pop(session_key))
        event.text = merged_text
        await self._callback(event)

Triển khai tối thiểu: xây dựng bộ chuyển đổi WeCom từ đầu

Sử dụng WeCom (Enterprise WeChat) WebSocket Bot để thể hiện toàn bộ vòng đời.

Bước 1: Kết nối

Bot WeCom gửi và nhận tin nhắn qua WebSocket liên tục. Kết nối yêu cầu đăng ký (aibot_subscribe), sau đó duy trì nhịp tim.

class WeComAdapter(BasePlatformAdapter):
    def __init__(self, bot_id: str, secret: str):
        super().__init__("wecom")
        self._bot_id = bot_id
        self._secret = secret
        self._ws_url = "wss://openws.work.weixin.qq.com"
        self._ws = None
        self._dedup = MessageDeduplicator()

    async def connect(self) -> bool:
        session = aiohttp.ClientSession()
        self._ws = await session.ws_connect(self._ws_url)

        # Subscribe: tell WeCom "I'm this bot, start pushing messages to me"
        await self._ws.send_json({
            "cmd": "aibot_subscribe",
            "headers": {"req_id": str(uuid.uuid4())},
            "body": {"bot_id": self._bot_id, "secret": self._secret},
        })

        self._running = True
        asyncio.create_task(self._listen_loop())
        asyncio.create_task(self._heartbeat_loop())
        return True

Bước 2: Nhận tin nhắn và dịch

Tin nhắn đến WeCom trông như thế này:

{
    "cmd": "aibot_msg_callback",
    "body": {
        "msgid": "msg_001",
        "msgtype": "text",
        "from": {"userid": "zhangsan"},
        "chatid": "zhangsan",
        "chattype": "single",
        "text": {"content": "Check today's weather for me"}
    }
}

Vòng nghe liên tục đọc các tin nhắn WebSocket, lọc để aibot_msg_callback, và dịch sang MessageEvent:

    async def _listen_loop(self):
        while self._running:
            try:
                raw = await self._ws.receive_json()
                if raw.get("cmd") != "aibot_msg_callback":
                    continue

                body = raw["body"]
                msg_id = body.get("msgid", "")

                # Deduplication
                if self._dedup.is_duplicate(msg_id):
                    continue

                event = self._translate(body)
                await self.handle_message(event)

            except Exception as exc:
                if self._running:
                    print(f"  [wecom] listen error: {exc}")
                    break  # Let the outer reconnection logic take over

    def _translate(self, body: dict) -> MessageEvent:
        """WeCom JSON -> unified format. This is the adapter's core job."""
        # Extract text: direct for text type, concatenate all fragments for mixed type
        if body.get("msgtype") == "mixed":
            parts = [
                item["text"]["content"]
                for item in body["mixed"]["msg_item"]
                if item.get("msgtype") == "text"
            ]
            text = "\n".join(parts)
        else:
            text = body.get("text", {}).get("content", "")

        user_id = body.get("from", {}).get("userid", "")
        chat_type = "dm" if body.get("chattype") == "single" else "group"

        return MessageEvent(
            message_id=body.get("msgid", ""),
            text=text,
            source=SessionSource(
                platform="wecom",
                chat_id=body["chatid"],
                chat_type=chat_type,
                user_id=user_id,
            ),
        )

Bước 3: Gửi trả lời

    async def send(self, chat_id: str, content: str) -> bool:
        # WeCom message length limit: 4000 characters
        content = content[:4000]

        await self._ws.send_json({
            "cmd": "aibot_send_msg",
            "headers": {"req_id": str(uuid.uuid4())},
            "body": {
                "chatid": chat_id,
                "msgtype": "markdown",
                "markdown": {"content": content},
            },
        })
        return True

Bước 4: Nhịp tim và kết nối lại

WeCom yêu cầu nhịp tim 30 giây một lần, nếu không máy chủ sẽ ngắt kết nối.

    async def _heartbeat_loop(self):
        while self._running:
            await asyncio.sleep(30)
            try:
                await self._ws.send_json({"cmd": "ping"})
            except Exception:
                break  # Connection lost, let reconnection logic handle it

Kết nối lại với độ trễ theo cấp số nhân:

    async def run_with_reconnect(self):
        """Main loop with automatic reconnection."""
        backoff = [2, 5, 10, 30, 60]
        attempt = 0

        while self._running:
            try:
                await self.connect()
                attempt = 0  # Connected, reset counter

                # Wait for the listener loop to end (meaning disconnection)
                while self._running and not self._ws.closed:
                    await asyncio.sleep(1)

            except Exception as exc:
                print(f"  [wecom] connection error: {exc}")

            if not self._running:
                break

            delay = backoff[min(attempt, len(backoff) - 1)]
            print(f"  [wecom] reconnecting in {delay}s...")
            await asyncio.sleep(delay)
            attempt += 1

Bước 5: Ngắt kết nối

    async def disconnect(self):
        self._running = False
        if self._ws and not self._ws.closed:
            await self._ws.close()

Toàn bộ bộ điều hợp có khoảng 100 dòng mã lõi. Nó thực hiện chính xác những gì sơ đồ mô hình tinh thần thể hiện: kết nối, nhận tin nhắn, dịch, gửi câu trả lời, đo nhịp tim, kết nối lại khi ngắt kết nối.

Phân nhóm văn bản: cạm bẫy mà mọi bộ chuyển đổi đều gặp phải

Ứng dụng khách WeCom tự động cắt ngắn văn bản dài 4000 ký tự. Người dùng nghĩ rằng họ đã gửi "một tin nhắn", nhưng nền tảng lại thúc ép cả hai bạn.

Nếu bạn xử lý chúng một cách riêng biệt thì quy trình đầu tiên vẫn chưa xử lý xong khi quy trình thứ hai đến -- kích hoạt cơ chế ngắt từ s12. Người đại diện nhìn thấy một nửa tin nhắn.

Giải pháp: Hợp nhất cửa sổ thời gian.

Nhưng có một thách thức cơ bản: bạn không thể biết chắc chắn liệu hai tin nhắn đến nhanh là "một tin nhắn được tách ra" hay "hai tin nhắn độc lập". TextBatcher không cố gắng phân biệt -- nó sử dụng hai phương pháp phỏng đoán để quyết định thời gian chờ đợi:

Heuristic 1: Xem xét độ dài. Nếu văn bản nhận được gần với điểm cắt ngắn của nền tảng (WeCom >= 3900 ký tự), thì rất có thể văn bản đó đã bị cắt bớt -- gần như chắc chắn sẽ có phần tiếp theo. Đợi lâu hơn (2 giây). Nếu độ dài thấp hơn nhiều so với điểm cắt bớt thì có thể nó đã hoàn thành. Chờ ít hơn (0,6 giây).

Heuristic 2: Nhìn vào khoảng thời gian. Không ai có thể gửi hai tin nhắn độc lập trong vòng 0,6 giây. Nếu một tin nhắn khác đến trong vòng 0,6 giây, thì đó có thể là sự phân chia nền tảng hoặc người dùng tiếp tục suy nghĩ tương tự -- dù sao đi nữa, việc hợp nhất sẽ không gây ra vấn đề gì.

Đi qua ba kịch bản:

Scenario 1: Long text was truncated (most common)

User sends 6000 chars -> WeCom splits into two messages

t=0.000s  Receive first fragment (3998 chars)
          -> 3998 >= 3900 (near truncation point) -> wait 2.0 seconds
t=0.050s  Receive second fragment (2002 chars)
          -> 2002 < 3900 -> restart timer at 0.6 seconds
t=0.650s  No new messages within 0.6s -> merge into 6000 chars, hand to agent

Scenario 2: User sent a short message (simplest)

t=0.000s  Receive "hello" (5 chars)
          -> 5 is well below 3900 -> wait 0.6 seconds
t=0.600s  No new messages -> hand directly to agent

Scenario 3: User rapidly sends two unrelated messages (rare)

t=0.000s  Receive "check the weather" (17 chars) -> wait 0.6 seconds
t=0.300s  Receive "also remind me about the meeting" (32 chars) -> restart 0.6 seconds
t=0.900s  No new messages -> merge into "check the weatheralso remind me about the meeting"
          -> Merged, but the agent can still understand it -- better than processing separately

Thiết kế này có thể tạo ra kết quả dương tính giả không? Có. Tình huống 3 là kết quả dương tính giả -- hai thông báo độc lập được hợp nhất. Nhưng việc gửi hai tin nhắn không liên quan trong vòng 0,6 giây là cực kỳ hiếm trên thực tế và kết quả hợp nhất vẫn có thể hiểu được đối với agent. Ngược lại, nếu bạn không hợp nhất, vấn đề nửa tin nhắn trong Kịch bản 1 sẽ nghiêm trọng.

Các thông số chính:

| Tham số | Giá trị | Cơ sở lý luận | |----------|-------|-------|----------| | Khoảng thời gian yên tĩnh mặc định | 0,6 giây | Con người không thể gõ hai tin nhắn độc lập nhanh như vậy | | Gần ngưỡng cắt ngắn | 2,0 giây | Độ dài văn bản >= 3900 ký tự -> có thể bị cắt ngắn, chờ phần tiếp theo | | Ngưỡng cắt ngắn | 3900 (WeCom) / 1400 (WeChat) | Thấp hơn một chút so với giới hạn nền tảng thực tế (4000/1500) về giới hạn an toàn |

Cơ chế triển khai TextBatcher

"Bộ hẹn giờ" không phải là một đối tượng hẹn giờ mà là một tác vụ không đồng bộ, ngủ rồi xả. "Đặt lại bộ hẹn giờ" có nghĩa là hủy tác vụ cũ và tạo tác vụ mới.

TextBatcher nội bộ chỉ có ba từ điển:

_buffers = {}   # session_key -> ["fragment1", "fragment2", ...]  buffer
_events  = {}   # session_key -> latest MessageEvent              preserve metadata
_tasks   = {}   # session_key -> a sleeping async task            "timer"

Mỗi lần có tin nhắn đến (enqueue), bốn bước xảy ra:

# 1. Store text in the buffer
_buffers[key].append(text)

# 2. If an old task is sleeping -> cancel it
#    The cancelled task won't proceed to "take buffer -> merge -> hand off"
if old_task and not old_task.done():
    old_task.cancel()

# 3. Determine wait time based on text length
delay = 2.0 if len(text) >= 3900 else 0.6

# 4. Create new task: sleep(delay) then flush
_tasks[key] = create_task(_flush_after(key, delay))

cái gì _flush_after làm:

async def _flush_after(key, delay):
    await asyncio.sleep(delay)   # Wait here
    # ^ If cancelled, sleep raises CancelledError; nothing below executes

    chunks = _buffers.pop(key)           # Take out all fragments
    event.text = "".join(chunks)         # Merge into one
    await callback(event)                # Hand to GatewayRunner

Tại sao tác vụ cũ phải bị hủy khi có tin nhắn mới đến? Vì tác vụ cũ không biết có văn bản mới trong bộ đệm. Nếu bạn để nó tiếp tục, nó sẽ kích hoạt khi quá trình đếm ngược kết thúc và chiếm bộ đệm -- nhưng bộ đệm hiện do tác vụ mới quản lý, tạo ra tình huống trong đó hai tác vụ tranh nhau trên cùng một bộ đệm. Việc hủy nhiệm vụ cũ để nhiệm vụ mới đảm nhận toàn bộ trách nhiệm “đợi yên tĩnh rồi bàn giao toàn bộ mảnh vỡ cùng nhau”.

Sơ đồ dòng chảy hoàn chỉnh:

Receive a message(text)
       |
       v
  Store in buffer
       |
       v
  Old task sleeping?
       |          |
      Yes         No
       |          |
       v          |
  cancel(old)     |
  Old task won't  |
  flush           |
       |          |
       +----------+
       v
  len(text) >= 3900?
       |          |
      Yes         No
       |          |
       v          v
  delay=2.0s   delay=0.6s
       |          |
       +----------+
       v
  Create new task: sleep(delay)
       |
       |  New message arrives during sleep?
       |          |
      Yes         No (sleep completes)
       |          |
       v          v
  Back to top   Take all fragments, merge
 (old task      Hand to GatewayRunner
  cancelled)

Trong một câu: Mỗi tin nhắn đến sẽ khởi động lại đồng hồ đếm ngược; chỉ khi quá trình đếm ngược kết thúc thì lô mới được chuyển giao.

Logic này hầu như giống hệt nhau trên các bộ điều hợp WeCom, WeChat cá nhân, Telegram và Discord. Sự khác biệt duy nhất là ngưỡng cắt ngắn.

Xử lý phương tiện: tải xuống, decode, bộ đệm

Tin nhắn văn bản chỉ liên quan đến "dịch" và "gửi". Tin nhắn đa phương tiện thêm "tải xuống và decode" trên đường vào và "mã hóa và tải lên" trên đường ra.

Trong nước: nền tảng -> tải xuống -> decode -> bộ đệm cục bộ

Tin nhắn hình ảnh WeCom bao gồm một url và aeskey. URL tải xuống một tệp được mã hóa cần được decode bằng aeskey:

async def _download_and_decrypt(self, media: dict) -> str | None:
    """Download and decrypt a WeCom media file, return the local cache path."""
    url = media.get("url")
    aeskey_b64 = media.get("aeskey")
    if not url:
        return None

    # 1. Download encrypted file
    async with aiohttp.ClientSession() as session:
        resp = await session.get(url)
        encrypted = await resp.read()

    # 2. Decrypt (AES-256-CBC, key is also the IV)
    if aeskey_b64:
        from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
        key = base64.b64decode(aeskey_b64)  # 32 bytes
        cipher = Cipher(algorithms.AES(key), modes.CBC(key))
        decryptor = cipher.decryptor()
        raw = decryptor.update(encrypted) + decryptor.finalize()
        # PKCS#7 unpadding
        pad_len = raw[-1]
        data = raw[:-pad_len]
    else:
        data = encrypted

    # 3. Cache locally
    cache_dir = HERMES_HOME / "cache" / "images"
    cache_dir.mkdir(parents=True, exist_ok=True)
    filename = f"{uuid.uuid4().hex[:12]}.jpg"
    path = cache_dir / filename
    path.write_bytes(data)
    return str(path)

Tại sao phải tải ngay vào adapter

URL phương tiện WeCom là tạm thời. Nếu bạn lưu trữ URL trong MessageEvent nhưng không tải xuống ngay lập tức thì vào thời điểm nhân viên hỗ trợ phân tích hình ảnh, URL có thể đã hết hạn.

Quy tắc: Tải tệp phương tiện xuống bộ nhớ đệm cục bộ ngay khi nhận được. Lưu trữ đường dẫn cục bộ trong MessageEvent.

Gửi đi: agent -> mã hóa -> tải lên từng đoạn -> gửi tin nhắn

Tải lên phương tiện WeCom là một quá trình gồm ba bước:

1. aibot_upload_media_init   -> Tell server "I'm uploading a file of size X"
2. Send chunks (512KB each)  -> Large files split into chunks sent one by one
3. aibot_upload_media_finish -> Upload complete, receive media_id
4. aibot_send_msg            -> Send message using the media_id

Tải lên phương tiện thay đổi đáng kể trên các nền tảng:

WeComWeChat cá nhân
Mã hóaAES-256-CBCAES-128-ECB
Kích thước phím32 byte16 byte
Tải lênKhối 512KBTải lên trực tiếp CDN
Giới hạn hình ảnh10 MB--
Định dạng giọng nóiChỉ AMRChỉ lụa

Nhưng đối với GatewayRunner, những khác biệt này được gói gọn đằng sau send_image() phương pháp. Nó chỉ gọi phương thức mà không quan tâm đến các giao thức mã hóa và tải lên bên trong.

Bộ điều hợp WeChat cá nhân: cùng một mẫu, giao thức khác nhau

WeCom sử dụng WebSocket; WeChat cá nhân sử dụng tính năng bỏ phiếu dài HTTP. Nhưng cấu trúc bộ chuyển đổi hoàn toàn giống nhau.

WeCom (WeComAdapter)WeChat cá nhân (WeixinAdapter)
Modal kết nốiWebSocket liên tụcBỏ phiếu dài HTTP (thời gian chờ 35 giây)
Cắt bớt tin nhắn4000 ký tự1500 ký tự
Nhịp timPing cứ sau 30 giâyKhông cần thiết (mỗi cuộc thăm dò đóng vai trò như nhịp tim)
Kết nối lạiRút lui theo cấp số nhânMỗi cuộc thăm dò tự động kết nối lại
Chống trùng lặptin nhắn_idtin nhắn_id
Mã hóa phương tiệnAES-256-CBCAES-128-ECB
Gửi yêu cầutrò chuyệncontext_token (phải được lặp lại)
Khoảng thời gian gửi chunkKhông có0,35 giây (giới hạn tốc độ WeChat)

WeChat cá nhân có thiết kế độc đáo: mỗi tin nhắn gửi đến mang một context_tokenvà các câu trả lời gửi đi phải bao gồm token này. Điều này có nghĩa là bộ điều hợp cần lưu vào bộ nhớ đệm context_token mới nhất cho mỗi người dùng:

# When receiving a message
context_token = message["context_token"]
self._context_tokens[user_id] = context_token

# When sending a reply
await self._http.post(f"{self._base_url}/ilink/bot/sendmessage", json={
    "to_user_id": chat_id,
    "context_token": self._context_tokens.get(chat_id, ""),
    "item_list": [{"type": 1, "text_item": {"text": content}}],
})

Nhưng những khác biệt này chỉ ảnh hưởng đến việc triển khai nội bộ của bộ chuyển đổi. Bản dịch MessageEvent định dạng hoàn toàn giống nhau. GatewayRunner và vòng lặp cốt lõi không biết tin nhắn đến từ WeCom hay WeChat cá nhân.

Cách nó kết nối với vòng lặp chính

Tương tự như s12 -- bộ chuyển đổi và vòng lặp lõi được phân tách bằng GatewayRunner.

WeComAdapter
  +-- _listen_loop -> _translate -> dedup -> batch -> handle_message
                                                       |
                                                       v
                                              GatewayRunner._handle_message
                                                       |
                                                       v
                                              build_session_key -> run_conversation -> send
                                                                       |
                                                                       |  Same function the CLI calls
                                                                       v
                                                                 Agent core loop (s01-s11)

Bộ điều hợp không biết vòng lặp lõi tồn tại. Vòng lặp lõi không biết bộ chuyển đổi tồn tại.

Những sai lầm phổ biến nhất của người mới bắt đầu

1. Quên việc gộp văn bản

Người dùng gửi một văn bản dài và nền tảng sẽ chia nó thành hai tin nhắn. Bạn bắt đầu xử lý cái đầu tiên và tổng đài viên đã trả lời khi cái thứ hai đến, gây ra sự gián đoạn. Người dùng nhìn thấy hai câu trả lời không liên quan.

Khắc phục: Chạy tất cả tin nhắn văn bản qua TextBatcher (khoảng thời gian im lặng 0,6 giây) trước khi chuyển sang GatewayRunner.

2. Tải xuống URL phương tiện sau khi chúng hết hạn

URL phương tiện WeCom là tạm thời. Nếu bạn lưu trữ URL trong MessageEvent mà không tải xuống ngay lập tức thì URL đó sẽ hết hạn vào thời điểm nhân viên cần.

Khắc phục: Tải xuống bộ đệm cục bộ ngay lập tức khi bộ điều hợp nhận được tin nhắn đa phương tiện.

3. Không tính đến sự khác biệt về nền tảng khi trả lời

WeCom hỗ trợ Markdown, nhưng WeChat cá nhân thì không. Nếu bạn gửi Markdown trực tiếp, người dùng sẽ thấy một mớ hỗn độn * và # nhân vật.

Khắc phục: Mỗi bộ chuyển đổi send() phương thức chịu trách nhiệm chuyển đổi định dạng. Vòng lặp lõi chỉ xuất ra văn bản chung.

4. Quên context_token để trả lời WeChat

Mọi câu trả lời cho WeChat cá nhân phải bao gồm thông tin mới nhất context_token đã nhận được hoặc tin nhắn sẽ không gửi được.

Khắc phục: Bộ chuyển đổi duy trì một user_id -> context_token ánh xạ, được cập nhật với mỗi tin nhắn đến.

Phạm vi của chương này

Chương này chỉ đề cập đến ba điều:

  1. Giao diện BasePlatformAdapter -- Ba phương thức bắt buộc, hai phương thức truyền thông tùy chọn
  2. Quy trình hoàn chỉnh để viết bộ điều hợp mới -- Kết nối, dịch, gửi câu trả lời, nhịp tim, kết nối lại
  3. Ba cơ chế được chia sẻ -- Phân nhóm văn bản, sao chép tin nhắn, tải xuống phương tiện vào bộ nhớ đệm

Không được bảo hiểm:

  • Tài liệu API hoàn chỉnh cho từng nền tảng -> xem tài liệu dành cho nhà phát triển tương ứng của họ
  • Nguyên tắc mật mã đằng sau mã hóa/decode AES -> bạn chỉ cần biết "tải về rồi decode"
  • Kênh thoại và cuộc gọi video -> vượt quá phạm vi hội thoại bằng văn bản
  • Các thành phần tương tác dành riêng cho nền tảng (bàn phím, nút, thẻ) -> Cải tiến UX không ảnh hưởng đến luồng cốt lõi

Chương này liên quan đến các chương sau như thế nào

  • s12 định nghĩa GatewayRunner và MessageEvent -> chương này sử dụng chúng
  • s14 bao gồm tính trừu tượng của phần backend terminal -> cùng một cách tiếp cận "đóng gói sự khác biệt để lớp trên không quan tâm", chỉ áp dụng cho một miền khác (nền tảng so với môi trường thực thi)
  • s15 bao gồm các tác vụ đã lên lịch -> kết quả của tác vụ đã lên lịch cần được gửi đến các nền tảng cụ thể thông qua bộ điều hợp

Sau chương này, bạn sẽ có thể trả lời

  • Có bao nhiêu phương pháp BasePlatformAdapter có? Những cái nào phải được thực hiện?
  • Nếu cần kết nối một nền tảng mới thì bước đầu tiên là gì?
  • Một người dùng gửi 5000 ký tự tới WeCom. Bộ điều hợp nhận được bao nhiêu tin nhắn? Nó chuyển tới GatewayRunner bao nhiêu?
  • Tại sao các tệp phương tiện phải được tải xuống ngay trong bộ điều hợp thay vì chuyển URL cho agent?
  • WeChat cá nhân là gì context_token? Điều gì xảy ra nếu bạn quên nó?
  • Sau khi WeCom ngắt kết nối, adapter sẽ phục hồi như thế nào?

Trong một câu: Bộ điều hợp thực hiện dịch -- gửi đến, chúng chuyển đổi các thông báo trên nền tảng thành MessageEvents; gửi đi, họ chuyển đổi văn bản chung thành định dạng nền tảng. Các lỗi thường gặp (phân mảnh, chống trùng lặp, lưu vào bộ nhớ đệm phương tiện) đều giống nhau trên mọi bộ chuyển đổi.

Mục lục nguồn