refactor(yuanbao): 整理元宝适配器代码并新增功能支持

1. 调整导入顺序和导入项顺序优化代码结构
2. 新增位置消息类型映射支持
3. 新增频道帖子事件的消息分发处理
4. 重构WebSocket认证失败日志格式
5. 优化令牌刷新错误提示的换行格式
6. 简化事件队列满时的日志输出
7. 新增系统事件处理和打字状态上报支持
8. 实现打字指示器接口的实际调用逻辑
9. 更新通道能力配置,补充缺失的能力项
This commit is contained in:
Kris 2026-05-13 16:17:49 +08:00
parent 5c3611ff19
commit ba060ca9c5
11 changed files with 64 additions and 22 deletions

View File

@ -11,13 +11,13 @@ import aiohttp
from yuxi.channels.base import BaseChannelAdapter
from yuxi.channels.capabilities import ChannelCapabilities
from yuxi.channels.meta import ChannelMeta
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
from yuxi.channels.exceptions import (
ChannelAuthenticationError,
ChannelNotConnectedError,
DeliveryFailedError,
)
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
from yuxi.channels.meta import ChannelMeta
from yuxi.channels.models import (
Attachment,
ChannelIdentity,
@ -29,14 +29,14 @@ from yuxi.channels.models import (
DeliveryResult,
EventType,
HealthStatus,
MessageType,
MentionsInfo,
MessageType,
)
from yuxi.channels.registry import register_builtin_adapter
from yuxi.utils.logging_config import logger
from .chunking import chunk_text
from .dispatch import InteractiveDispatcher, DispatchAction, DispatchContext, CardAction, BotMenuAction
from .dispatch import BotMenuAction, CardAction, DispatchAction, DispatchContext, InteractiveDispatcher
from .event_queue import EventQueue
from .format import format_outbound
from .monitor import YuanbaoMonitor
@ -47,7 +47,7 @@ from .security_audit import SecurityAuditLogger
from .send import send_with_retry
from .send_cache import SendMessageCache
from .token import YuanbaoTokenManager
from .vision import download_and_analyze, VisionResult
from .vision import VisionResult, download_and_analyze
from .yb_accounts import (
YuanbaoAccountManager,
load_accounts_from_config,
@ -92,6 +92,13 @@ class YuanbaoAdapter(BaseChannelAdapter):
lane_streaming=True,
reasoning_streaming=True,
native_commands=True,
edit=False,
unsend=False,
pin=False,
polls=False,
typing=False,
send_ephemeral=False,
effects=False,
)
meta = ChannelMeta(
id="yuanbao",
@ -663,6 +670,8 @@ class YuanbaoAdapter(BaseChannelAdapter):
await self._handle_bot_menu(event)
elif action == DispatchAction.READ_RECEIPT:
self._handle_read_receipt(event)
elif action == DispatchAction.SYSTEM_EVENT:
await self._handle_system_event(event)
else:
await self._dispatcher.dispatch(ctx)
except Exception as e:
@ -682,6 +691,14 @@ class YuanbaoAdapter(BaseChannelAdapter):
msg_id = event.get("msg_id", "")
logger.debug(f"[Yuanbao] Read receipt: user={user_id} chat={chat_id} msg_id={msg_id}")
async def _handle_system_event(self, event: dict) -> None:
sys_type = event.get("type", "unknown")
logger.info(f"[Yuanbao] System event: {sys_type}")
if sys_type == "typing":
logger.debug(
f"[Yuanbao] User typing: open_id={event.get('open_id')} group_open_id={event.get('group_open_id')}"
)
async def _handle_card_action(self, event: dict) -> None:
card = CardAction.from_event(event)
logger.info(
@ -880,12 +897,9 @@ class YuanbaoAdapter(BaseChannelAdapter):
await asyncio.sleep(interval)
async def edit_message(self, chat_id: str, msg_id: str, content: str) -> DeliveryResult:
self._send_cache.mark_edited(msg_id, content[:200])
return DeliveryResult(success=False, error="edit_message not supported by Yuanbao adapter")
async def delete_message(self, chat_id: str, msg_id: str) -> DeliveryResult:
self._send_cache.mark_deleted(msg_id)
self._bot_message_ids.discard(msg_id)
return DeliveryResult(success=False, error="delete_message not supported by Yuanbao adapter")
async def send_reaction(self, chat_id: str, msg_id: str, emoji: str) -> DeliveryResult:
@ -929,7 +943,31 @@ class YuanbaoAdapter(BaseChannelAdapter):
return DeliveryResult(success=False, error="send_chat_action not supported by Yuanbao adapter")
async def send_typing_indicator(self, chat_id: str) -> DeliveryResult:
return DeliveryResult(success=False, error="send_typing_indicator not supported by Yuanbao adapter")
if not self._http_client or not self._token_manager:
return DeliveryResult(success=False, error="Client not initialized")
token = await self._token_manager.get_token()
api_base = self._token_manager.api_base
payload = {"open_id": chat_id}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
try:
async with self._http_client.post(
f"{api_base}/api/v1/bot/typing",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=5),
) as resp:
if resp.status == 200:
return DeliveryResult(success=True)
return DeliveryResult(
success=False,
error=f"send_typing_indicator failed: HTTP {resp.status}",
)
except Exception as e:
return DeliveryResult(success=False, error=str(e))
async def get_user_info(self, channel_user_id: str) -> dict[str, Any]:
if not self._token_manager or not self._http_client:

View File

@ -1,8 +1,9 @@
from __future__ import annotations
import aiohttp
from dataclasses import dataclass, field
import aiohttp
from yuxi.utils.logging_config import logger

View File

@ -126,6 +126,8 @@ class InteractiveDispatcher:
return DispatchAction.MESSAGE
elif event_type == "edited_message":
return DispatchAction.MESSAGE
elif event_type in ("channel_post", "edited_channel_post"):
return DispatchAction.MESSAGE
elif event_type in ("reaction_added", "reaction_removed"):
return DispatchAction.REACTION
elif event_type == "bot_menu":

View File

@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable
from yuxi.utils.logging_config import logger
SENTINEL = object()
@ -41,10 +40,7 @@ class EventQueue:
except asyncio.QueueFull:
event_type = event.get("type", "unknown")
msg_id = event.get("msg_id", "N/A")
logger.warning(
f"[Yuanbao] Event queue full, dropping event: "
f"type={event_type} msg_id={msg_id}"
)
logger.warning(f"[Yuanbao] Event queue full, dropping event: type={event_type} msg_id={msg_id}")
self._dropped_count += 1
async def _process_loop(self) -> None:

View File

@ -64,5 +64,6 @@ def _map_message_type(message_type: MessageType, content: str) -> str:
MessageType.STICKER: "sticker",
MessageType.CARD: "card",
MessageType.COMMAND: "text",
MessageType.LOCATION: "location",
}
return _type_map.get(message_type, "text")

View File

@ -131,7 +131,7 @@ class YuanbaoMonitor:
if not self._running:
break
if self._auth_failed:
logger.error(f"[Yuanbao] WebSocket auth permanently failed, stopping")
logger.error("[Yuanbao] WebSocket auth permanently failed, stopping")
break
self._reconnect_count += 1
if self._reconnect_count > self._max_reconnect:

View File

@ -1,10 +1,10 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Awaitable
from collections.abc import Awaitable, Callable
from typing import Any
from yuxi.channels.models import ChannelResponse, ChannelIdentity, DeliveryResult
from yuxi.channels.models import ChannelIdentity, ChannelResponse, DeliveryResult
from yuxi.utils.logging_config import logger

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import json
import time
from datetime import datetime, UTC
from datetime import UTC, datetime
from yuxi.utils.logging_config import logger

View File

@ -48,9 +48,10 @@ async def run_setup_wizard(
result.add_step("input_validation", True, "输入参数校验通过")
try:
import time
import hashlib
import hmac
import time
import aiohttp
timestamp = int(time.time())

View File

@ -137,7 +137,9 @@ class YuanbaoTokenManager:
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.status != 200:
raise ChannelAuthenticationError(f"Token refresh failed: HTTP {resp.status} {await resp.text()}")
raise ChannelAuthenticationError(
f"Token refresh failed: HTTP {resp.status} {await resp.text()}"
)
data = await resp.json()
self._access_token = data["access_token"]
expires_in = data.get("expires_in", 7200)

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import base64
import aiohttp
from dataclasses import dataclass
from typing import Any
import aiohttp
from yuxi.utils.logging_config import logger