docs/en/s13-platform-adapters.mdXem bản gốc 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.

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 GatewayRunnerdata-structures.md-- Định nghĩa trường đầy đủ choMessageEventvàSessionSourceglossary.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:
| WeCom | WeChat cá nhân | |
|---|---|---|
| Mã hóa | AES-256-CBC | AES-128-ECB |
| Kích thước phím | 32 byte | 16 byte |
| Tải lên | Khối 512KB | Tải lên trực tiếp CDN |
| Giới hạn hình ảnh | 10 MB | -- |
| Định dạng giọng nói | Chỉ AMR | Chỉ 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ối | WebSocket liên tục | Bỏ phiếu dài HTTP (thời gian chờ 35 giây) |
| Cắt bớt tin nhắn | 4000 ký tự | 1500 ký tự |
| Nhịp tim | Ping cứ sau 30 giây | Không cần thiết (mỗi cuộc thăm dò đóng vai trò như nhịp tim) |
| Kết nối lại | Rút lui theo cấp số nhân | Mỗi cuộc thăm dò tự động kết nối lại |
| Chống trùng lặp | tin nhắn_id | tin nhắn_id |
| Mã hóa phương tiện | AES-256-CBC | AES-128-ECB |
| Gửi yêu cầu | trò chuyện | context_token (phải được lặp lại) |
| Khoảng thời gian gửi chunk | Khô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:
- 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
- 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
- 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
BasePlatformAdaptercó? 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.