refactor(yuanbao): 整理元宝适配器代码并新增功能支持
1. 调整导入顺序和导入项顺序优化代码结构 2. 新增位置消息类型映射支持 3. 新增频道帖子事件的消息分发处理 4. 重构WebSocket认证失败日志格式 5. 优化令牌刷新错误提示的换行格式 6. 简化事件队列满时的日志输出 7. 新增系统事件处理和打字状态上报支持 8. 实现打字指示器接口的实际调用逻辑 9. 更新通道能力配置,补充缺失的能力项
This commit is contained in:
parent
5c3611ff19
commit
ba060ca9c5
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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":
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user