From ba060ca9c55b3b9e4e58c180765452bf629c47fb Mon Sep 17 00:00:00 2001 From: Kris <2893855659@qq.com> Date: Wed, 13 May 2026 16:17:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor(yuanbao):=20=E6=95=B4=E7=90=86?= =?UTF-8?q?=E5=85=83=E5=AE=9D=E9=80=82=E9=85=8D=E5=99=A8=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=B9=B6=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 调整导入顺序和导入项顺序优化代码结构 2. 新增位置消息类型映射支持 3. 新增频道帖子事件的消息分发处理 4. 重构WebSocket认证失败日志格式 5. 优化令牌刷新错误提示的换行格式 6. 简化事件队列满时的日志输出 7. 新增系统事件处理和打字状态上报支持 8. 实现打字指示器接口的实际调用逻辑 9. 更新通道能力配置,补充缺失的能力项 --- .../yuxi/channels/adapters/yuanbao/adapter.py | 56 ++++++++++++++++--- .../channels/adapters/yuanbao/directory.py | 3 +- .../channels/adapters/yuanbao/dispatch.py | 2 + .../channels/adapters/yuanbao/event_queue.py | 6 +- .../yuxi/channels/adapters/yuanbao/format.py | 1 + .../yuxi/channels/adapters/yuanbao/monitor.py | 2 +- .../adapters/yuanbao/outbound_queue.py | 4 +- .../adapters/yuanbao/security_audit.py | 2 +- .../yuxi/channels/adapters/yuanbao/setup.py | 3 +- .../yuxi/channels/adapters/yuanbao/token.py | 4 +- .../yuxi/channels/adapters/yuanbao/vision.py | 3 +- 11 files changed, 64 insertions(+), 22 deletions(-) diff --git a/backend/package/yuxi/channels/adapters/yuanbao/adapter.py b/backend/package/yuxi/channels/adapters/yuanbao/adapter.py index a0cc4042..146811f3 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/adapter.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/adapter.py @@ -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: diff --git a/backend/package/yuxi/channels/adapters/yuanbao/directory.py b/backend/package/yuxi/channels/adapters/yuanbao/directory.py index ef6af7a6..5afdc84e 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/directory.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/directory.py @@ -1,8 +1,9 @@ from __future__ import annotations -import aiohttp from dataclasses import dataclass, field +import aiohttp + from yuxi.utils.logging_config import logger diff --git a/backend/package/yuxi/channels/adapters/yuanbao/dispatch.py b/backend/package/yuxi/channels/adapters/yuanbao/dispatch.py index d13c43d1..781bafd4 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/dispatch.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/dispatch.py @@ -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": diff --git a/backend/package/yuxi/channels/adapters/yuanbao/event_queue.py b/backend/package/yuxi/channels/adapters/yuanbao/event_queue.py index e2bc653e..564b0c49 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/event_queue.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/event_queue.py @@ -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: diff --git a/backend/package/yuxi/channels/adapters/yuanbao/format.py b/backend/package/yuxi/channels/adapters/yuanbao/format.py index 4f173c0c..85a7cafd 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/format.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/format.py @@ -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") diff --git a/backend/package/yuxi/channels/adapters/yuanbao/monitor.py b/backend/package/yuxi/channels/adapters/yuanbao/monitor.py index 0182ad7b..99dd0a57 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/monitor.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/monitor.py @@ -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: diff --git a/backend/package/yuxi/channels/adapters/yuanbao/outbound_queue.py b/backend/package/yuxi/channels/adapters/yuanbao/outbound_queue.py index 0f733d2b..f1f3ca89 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/outbound_queue.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/outbound_queue.py @@ -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 diff --git a/backend/package/yuxi/channels/adapters/yuanbao/security_audit.py b/backend/package/yuxi/channels/adapters/yuanbao/security_audit.py index 3bbc19db..4bdc07b8 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/security_audit.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/security_audit.py @@ -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 diff --git a/backend/package/yuxi/channels/adapters/yuanbao/setup.py b/backend/package/yuxi/channels/adapters/yuanbao/setup.py index c383d83b..1e712f29 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/setup.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/setup.py @@ -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()) diff --git a/backend/package/yuxi/channels/adapters/yuanbao/token.py b/backend/package/yuxi/channels/adapters/yuanbao/token.py index 059cca78..601ebc3e 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/token.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/token.py @@ -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) diff --git a/backend/package/yuxi/channels/adapters/yuanbao/vision.py b/backend/package/yuxi/channels/adapters/yuanbao/vision.py index 2462cba3..a866a8c0 100644 --- a/backend/package/yuxi/channels/adapters/yuanbao/vision.py +++ b/backend/package/yuxi/channels/adapters/yuanbao/vision.py @@ -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