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.base import BaseChannelAdapter
from yuxi.channels.capabilities import ChannelCapabilities 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 ( from yuxi.channels.exceptions import (
ChannelAuthenticationError, ChannelAuthenticationError,
ChannelNotConnectedError, ChannelNotConnectedError,
DeliveryFailedError, DeliveryFailedError,
) )
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
from yuxi.channels.meta import ChannelMeta
from yuxi.channels.models import ( from yuxi.channels.models import (
Attachment, Attachment,
ChannelIdentity, ChannelIdentity,
@ -29,14 +29,14 @@ from yuxi.channels.models import (
DeliveryResult, DeliveryResult,
EventType, EventType,
HealthStatus, HealthStatus,
MessageType,
MentionsInfo, MentionsInfo,
MessageType,
) )
from yuxi.channels.registry import register_builtin_adapter from yuxi.channels.registry import register_builtin_adapter
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger
from .chunking import chunk_text 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 .event_queue import EventQueue
from .format import format_outbound from .format import format_outbound
from .monitor import YuanbaoMonitor from .monitor import YuanbaoMonitor
@ -47,7 +47,7 @@ from .security_audit import SecurityAuditLogger
from .send import send_with_retry from .send import send_with_retry
from .send_cache import SendMessageCache from .send_cache import SendMessageCache
from .token import YuanbaoTokenManager from .token import YuanbaoTokenManager
from .vision import download_and_analyze, VisionResult from .vision import VisionResult, download_and_analyze
from .yb_accounts import ( from .yb_accounts import (
YuanbaoAccountManager, YuanbaoAccountManager,
load_accounts_from_config, load_accounts_from_config,
@ -92,6 +92,13 @@ class YuanbaoAdapter(BaseChannelAdapter):
lane_streaming=True, lane_streaming=True,
reasoning_streaming=True, reasoning_streaming=True,
native_commands=True, native_commands=True,
edit=False,
unsend=False,
pin=False,
polls=False,
typing=False,
send_ephemeral=False,
effects=False,
) )
meta = ChannelMeta( meta = ChannelMeta(
id="yuanbao", id="yuanbao",
@ -663,6 +670,8 @@ class YuanbaoAdapter(BaseChannelAdapter):
await self._handle_bot_menu(event) await self._handle_bot_menu(event)
elif action == DispatchAction.READ_RECEIPT: elif action == DispatchAction.READ_RECEIPT:
self._handle_read_receipt(event) self._handle_read_receipt(event)
elif action == DispatchAction.SYSTEM_EVENT:
await self._handle_system_event(event)
else: else:
await self._dispatcher.dispatch(ctx) await self._dispatcher.dispatch(ctx)
except Exception as e: except Exception as e:
@ -682,6 +691,14 @@ class YuanbaoAdapter(BaseChannelAdapter):
msg_id = event.get("msg_id", "") msg_id = event.get("msg_id", "")
logger.debug(f"[Yuanbao] Read receipt: user={user_id} chat={chat_id} msg_id={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: async def _handle_card_action(self, event: dict) -> None:
card = CardAction.from_event(event) card = CardAction.from_event(event)
logger.info( logger.info(
@ -880,12 +897,9 @@ class YuanbaoAdapter(BaseChannelAdapter):
await asyncio.sleep(interval) await asyncio.sleep(interval)
async def edit_message(self, chat_id: str, msg_id: str, content: str) -> DeliveryResult: 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") return DeliveryResult(success=False, error="edit_message not supported by Yuanbao adapter")
async def delete_message(self, chat_id: str, msg_id: str) -> DeliveryResult: 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") 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: 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") return DeliveryResult(success=False, error="send_chat_action not supported by Yuanbao adapter")
async def send_typing_indicator(self, chat_id: str) -> DeliveryResult: 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]: async def get_user_info(self, channel_user_id: str) -> dict[str, Any]:
if not self._token_manager or not self._http_client: if not self._token_manager or not self._http_client:

View File

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

View File

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

View File

@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger
SENTINEL = object() SENTINEL = object()
@ -41,10 +40,7 @@ class EventQueue:
except asyncio.QueueFull: except asyncio.QueueFull:
event_type = event.get("type", "unknown") event_type = event.get("type", "unknown")
msg_id = event.get("msg_id", "N/A") msg_id = event.get("msg_id", "N/A")
logger.warning( logger.warning(f"[Yuanbao] Event queue full, dropping event: type={event_type} msg_id={msg_id}")
f"[Yuanbao] Event queue full, dropping event: "
f"type={event_type} msg_id={msg_id}"
)
self._dropped_count += 1 self._dropped_count += 1
async def _process_loop(self) -> None: 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.STICKER: "sticker",
MessageType.CARD: "card", MessageType.CARD: "card",
MessageType.COMMAND: "text", MessageType.COMMAND: "text",
MessageType.LOCATION: "location",
} }
return _type_map.get(message_type, "text") return _type_map.get(message_type, "text")

View File

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

View File

@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Awaitable from collections.abc import Awaitable, Callable
from typing import Any 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 from yuxi.utils.logging_config import logger

View File

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

View File

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

View File

@ -137,7 +137,9 @@ class YuanbaoTokenManager:
timeout=aiohttp.ClientTimeout(total=10), timeout=aiohttp.ClientTimeout(total=10),
) as resp: ) as resp:
if resp.status != 200: 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() data = await resp.json()
self._access_token = data["access_token"] self._access_token = data["access_token"]
expires_in = data.get("expires_in", 7200) expires_in = data.get("expires_in", 7200)

View File

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