From 445f9323254e4865ecc717d31c5bbf7b5f6cc8a8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:02:23 -0700 Subject: [PATCH] refactor(gateway/yuanbao): compact imports/tables, unify _fetch_and_cache resolve path, drop padding blanks --- gateway/platforms/yuanbao.py | 222 ++++++++++------------------------- 1 file changed, 60 insertions(+), 162 deletions(-) diff --git a/gateway/platforms/yuanbao.py b/gateway/platforms/yuanbao.py index e22ebefd21..c6c0347278 100644 --- a/gateway/platforms/yuanbao.py +++ b/gateway/platforms/yuanbao.py @@ -42,50 +42,24 @@ except ImportError: from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( - BasePlatformAdapter, - MessageEvent, - MessageType, - SendResult, - cache_document_from_bytes, - cache_image_from_bytes, - cache_video_from_bytes, + BasePlatformAdapter, MessageEvent, MessageType, SendResult, + cache_document_from_bytes, cache_image_from_bytes, cache_video_from_bytes, ) from gateway.platforms import helpers as _mdchunk from gateway.platforms._shared import get_scoped_secret as _yb_secret from gateway.platforms.helpers import MessageDeduplicator from gateway.platforms.yuanbao_media import ( - download_url as media_download_url, - get_cos_credentials, - upload_to_cos, - build_image_msg_body, - build_file_msg_body, - guess_mime_type, - md5_hex, + download_url as media_download_url, get_cos_credentials, upload_to_cos, + build_image_msg_body, build_file_msg_body, guess_mime_type, md5_hex, ) from gateway.platforms.yuanbao_proto import ( - CMD_TYPE, - _fields_to_dict, - _get_string, - _get_varint, - _parse_fields, - WS_HEARTBEAT_RUNNING, - WS_HEARTBEAT_FINISH, - HERMES_INSTANCE_ID, - decode_conn_msg, - decode_inbound_push, - decode_forward_msg_data, - decode_query_group_info_rsp, - decode_get_group_member_list_rsp, - encode_auth_bind, - encode_ping, - encode_push_ack, - encode_send_c2c_message, - encode_send_group_message, - encode_send_private_heartbeat, - encode_send_group_heartbeat, - encode_query_group_info, - encode_get_group_member_list, - next_seq_no, + CMD_TYPE, WS_HEARTBEAT_RUNNING, WS_HEARTBEAT_FINISH, HERMES_INSTANCE_ID, + _fields_to_dict, _get_string, _get_varint, _parse_fields, + decode_conn_msg, decode_inbound_push, decode_forward_msg_data, + decode_query_group_info_rsp, decode_get_group_member_list_rsp, + encode_auth_bind, encode_ping, encode_push_ack, encode_send_c2c_message, encode_send_group_message, + encode_send_private_heartbeat, encode_send_group_heartbeat, encode_query_group_info, + encode_get_group_member_list, next_seq_no, ) from gateway.session import build_session_key @@ -159,7 +133,6 @@ def _cancel_all(tasks: Dict[str, asyncio.Task]) -> None: class MarkdownProcessor: """Thin delegates to the shared fence-aware chunker in gateway.platforms.helpers; method names kept for existing call sites and tests.""" - @staticmethod def has_unclosed_fence(text: str) -> bool: return _mdchunk.text_has_unclosed_fence(text) @@ -183,7 +156,6 @@ class MarkdownProcessor: class SignManager: """Sign-token acquisition, caching, signing and retry. All state is class-level so one shared client serves the whole process.""" - TOKEN_PATH = "/api/v5/robotLogic/sign-token" RETRYABLE_CODE = 10099 MAX_RETRIES = 3 @@ -239,19 +211,10 @@ class SignManager: for attempt in range(cls.MAX_RETRIES + 1): nonce = secrets.token_hex(16) timestamp = cls.build_timestamp() - payload = { - "app_key": app_key, - "nonce": nonce, - "signature": cls.compute_signature(nonce, timestamp, app_key, app_secret), - "timestamp": timestamp, - } - headers = { - "Content-Type": "application/json", - "X-AppVersion": _APP_VERSION, - "X-OperationSystem": _OPERATION_SYSTEM, - "X-Instance-Id": _YUANBAO_INSTANCE_ID, - "X-Bot-Version": _BOT_VERSION, - } + payload = {"app_key": app_key, "nonce": nonce, + "signature": cls.compute_signature(nonce, timestamp, app_key, app_secret), "timestamp": timestamp} + headers = {"Content-Type": "application/json", "X-AppVersion": _APP_VERSION, "X-OperationSystem": _OPERATION_SYSTEM, + "X-Instance-Id": _YUANBAO_INSTANCE_ID, "X-Bot-Version": _BOT_VERSION} if route_env: headers["X-Route-Env"] = route_env logger.info("Sign token request: url=%s%s", url, f" (retry {attempt}/{cls.MAX_RETRIES})" if attempt > 0 else "") @@ -282,11 +245,8 @@ class SignManager: data = await cls.fetch(app_key, app_secret, api_domain, route_env) duration: int = data.get("duration", 0) cls._cache[app_key] = { - "token": data.get("token", ""), - "bot_id": data.get("bot_id", ""), - "duration": duration, - "product": data.get("product", ""), - "source": data.get("source", ""), + "token": data.get("token", ""), "bot_id": data.get("bot_id", ""), "duration": duration, + "product": data.get("product", ""), "source": data.get("source", ""), "expire_ts": time.time() + (duration if duration > 0 else 3600), } @@ -318,7 +278,6 @@ class SignManager: @dataclass class InboundContext: """Mutable context passed through every inbound middleware in registration order.""" - adapter: Any # YuanbaoAdapter (forward-ref avoids circular import) raw_frames: list = dc_field(default_factory=list) # debounce-aggregated raw frames # DecodeMiddleware @@ -358,7 +317,6 @@ class InboundContext: class InboundMiddleware(ABC): """Set class-level ``name`` and implement ``handle(ctx, next_fn)``; ``await next_fn()`` continues the pipeline, returning without it stops.""" - name: str = "" @abstractmethod @@ -374,7 +332,6 @@ class InboundMiddleware(ABC): class InboundPipeline: """Onion-model middleware pipeline: named middlewares, ``when`` guards, use_before/use_after/ remove. Accepts ``InboundMiddleware`` instances or plain ``async def(ctx, next_fn)`` callables.""" - def __init__(self) -> None: self._middlewares: list = [] # (name, handler, when_fn | None) @@ -438,7 +395,6 @@ class InboundPipeline: class DecodeMiddleware(InboundMiddleware): """Decode raw inbound frames (JSON or protobuf via ``decode_inbound_push``) into ctx.push.""" - name = "decode" @staticmethod @@ -547,7 +503,6 @@ class DecodeMiddleware(InboundMiddleware): class ExtractFieldsMiddleware(InboundMiddleware): """Copy common push fields onto ctx.""" - name = "extract-fields" _FIELDS = ("from_account", "group_code", "group_name", "sender_nickname", "msg_id", "cloud_custom_data") @@ -576,7 +531,6 @@ def _session_store(adapter): class RecallGuardMiddleware(InboundMiddleware): """Recall callbacks (Group.CallbackAfterRecallMsg / C2C.CallbackAfterMsgWithDraw). A: in transcript → redact; B: not in transcript → system note; C: being processed → interrupt + delayed redact.""" - name = "recall_guard" _RECALL_COMMANDS = frozenset({"Group.CallbackAfterRecallMsg", "C2C.CallbackAfterMsgWithDraw"}) @@ -639,16 +593,12 @@ class RecallGuardMiddleware(InboundMiddleware): def _interrupt_for_recall(cls, adapter, session_key: str, recalled_id: str, group_code: str, from_account: str) -> None: where = f"group {group_code}" if group_code else f"direct chat with {from_account}" recall_text = ( - f"[CRITICAL — MESSAGE RECALLED] The user message that triggered " - f"your current task (message_id=\"{recalled_id}\") in {where} has " - f"been recalled/withdrawn by the sender. " - f"IGNORE any prior system note asking you to finish processing " - f"tool results — the original request is void. " - f"Do NOT continue the task, do NOT call more tools, do NOT " - f"reference the recalled content. " - f"Reply only with a brief acknowledgment such as " - f"\"The message has been recalled.\" in the " - f"language the user was using." + f"[CRITICAL — MESSAGE RECALLED] The user message that triggered your current task " + f"(message_id=\"{recalled_id}\") in {where} has been recalled/withdrawn by the sender. " + "IGNORE any prior system note asking you to finish processing tool results — the original request is void. " + "Do NOT continue the task, do NOT call more tools, do NOT reference the recalled content. " + "Reply only with a brief acknowledgment such as \"The message has been recalled.\" in the " + "language the user was using." ) # Set pending + signal directly (bypass handle_message to avoid busy-ack). # May overwrite a user message pending in the same ~200ms window — acceptable. @@ -729,16 +679,14 @@ class RecallGuardMiddleware(InboundMiddleware): return # Branch B: not found in transcript → append system note store.append_to_transcript(sid, { - "role": "system", + "role": "system", "timestamp": datetime.now(tz=timezone.utc).isoformat(), "content": f'[recall] message_id="{recalled_id}" has been recalled; do not quote or reference it.', - "timestamp": datetime.now(tz=timezone.utc).isoformat(), }) logger.info("[%s] Recall: system note for msg_id=%s (branch B)", adapter.name, recalled_id) class SkipSelfMiddleware(InboundMiddleware): """Drop the bot's own messages.""" - name = "skip-self" @staticmethod @@ -754,7 +702,6 @@ class SkipSelfMiddleware(InboundMiddleware): class ChatRoutingMiddleware(InboundMiddleware): """Derive chat_id / chat_type / chat_name from push fields.""" - name = "chat-routing" async def handle(self, ctx: InboundContext, next_fn) -> None: @@ -767,7 +714,6 @@ class ChatRoutingMiddleware(InboundMiddleware): class AccessPolicy: """DM / group access rules shared by inbound middleware and outbound ``send_dm``.""" - def __init__(self, dm_policy: str, dm_allow_from: list[str], group_policy: str, group_allow_from: list[str]) -> None: self._dm_policy = dm_policy self._dm_allow_from = dm_allow_from @@ -811,7 +757,6 @@ class AccessPolicy: class AccessGuardMiddleware(InboundMiddleware): """Platform-level DM/group access filter.""" - name = "access-guard" async def handle(self, ctx: InboundContext, next_fn) -> None: @@ -831,7 +776,6 @@ class AutoSetHomeMiddleware(InboundMiddleware): a group home is upgraded by the first DM. Runs after GroupAtGuard so unaddressed group traffic never claims it; only strictly-authorized senders (allowlist / open opt-in / pairing-approved) may — intake-only pairing forwards must not.""" - name = "auto-sethome" async def handle(self, ctx: InboundContext, next_fn) -> None: @@ -881,7 +825,6 @@ def _file_name(content: dict) -> str: class ExtractContentMiddleware(InboundMiddleware): """Extract raw text, media refs and forwarded records from msg_body.""" - name = "extract-content" _CARD_CONTENT_MAX_LENGTH = 1000 @@ -1068,13 +1011,9 @@ class ExtractContentMiddleware(InboundMiddleware): class PlaceholderFilterMiddleware(InboundMiddleware): """Skip pure placeholder messages (e.g. '[image]' with no media).""" - name = "placeholder-filter" - SKIPPABLE_PLACEHOLDERS: frozenset = frozenset({ - "[image]", "[图片]", "[file]", "[文件]", - "[video]", "[视频]", "[voice]", "[语音]", - }) + SKIPPABLE_PLACEHOLDERS: frozenset = frozenset({"[image]", "[图片]", "[file]", "[文件]", "[video]", "[视频]", "[voice]", "[语音]"}) @classmethod def is_skippable_placeholder(cls, text: str, media_count: int = 0) -> bool: @@ -1089,14 +1028,9 @@ class PlaceholderFilterMiddleware(InboundMiddleware): class OwnerCommandMiddleware(InboundMiddleware): """Bot-owner slash commands in groups: allowlisted commands skip @Bot; non-owner attempts are rejected.""" - name = "owner-command" - ALLOWLIST: frozenset = frozenset({ - "/new", "/reset", "/retry", "/undo", "/stop", - "/approve", "/deny", "/bg", - "/btw", "/queue", "/q", - }) + ALLOWLIST: frozenset = frozenset({"/new", "/reset", "/retry", "/undo", "/stop", "/approve", "/deny", "/bg", "/btw", "/queue", "/q"}) _rewrite_slash_command = staticmethod(ExtractContentMiddleware._rewrite_slash_command) @@ -1153,7 +1087,6 @@ class BuildSourceMiddleware(InboundMiddleware): class GroupAtGuardMiddleware(InboundMiddleware): """Group chat: observe non-@bot messages into the transcript; only @Bot (or owner commands) proceed.""" - name = "group-at-guard" @staticmethod @@ -1207,10 +1140,8 @@ class GroupAtGuardMiddleware(InboundMiddleware): if summary: body_text = f"{text}\n{summary}" if text else summary entry: dict = { - "role": "user", - "content": f"[{sender_display}|{source.user_id or 'unknown'}]\n{body_text}", - "timestamp": datetime.now(tz=timezone.utc).isoformat(), - "observed": True, + "role": "user", "content": f"[{sender_display}|{source.user_id or 'unknown'}]\n{body_text}", + "timestamp": datetime.now(tz=timezone.utc).isoformat(), "observed": True, } if msg_id: entry["message_id"] = msg_id @@ -1234,7 +1165,6 @@ class GroupAttributionMiddleware(InboundMiddleware): """Group @bot turns: build channel_prompt, rewrite raw_text to ``[nickname|user_id]\\n`` (matches observed-history format) and clear ``source.user_name`` to suppress the runner's ``[user_name]`` prefix.""" - name = "group-attribution" async def handle(self, ctx: InboundContext, next_fn) -> None: @@ -1248,21 +1178,15 @@ class GroupAttributionMiddleware(InboundMiddleware): class YuanbaoMessageType(Enum): """Yuanbao-local subtypes; coerced back to MessageType in DispatchMiddleware.""" - CHAT_RECORD = "chat_record" -_ELEM_MESSAGE_TYPES = { - "TIMImageElem": MessageType.PHOTO, - "TIMSoundElem": MessageType.VOICE, - "TIMVideoFileElem": MessageType.VIDEO, - "TIMFileElem": MessageType.DOCUMENT, -} +_ELEM_MESSAGE_TYPES = {"TIMImageElem": MessageType.PHOTO, "TIMSoundElem": MessageType.VOICE, + "TIMVideoFileElem": MessageType.VIDEO, "TIMFileElem": MessageType.DOCUMENT} class ClassifyMessageTypeMiddleware(InboundMiddleware): """MessageType (or yuanbao-local YuanbaoMessageType) from text and msg_body elements.""" - name = "classify-msg-type" @staticmethod @@ -1290,7 +1214,6 @@ class ClassifyMessageTypeMiddleware(InboundMiddleware): class QuoteContextMiddleware(InboundMiddleware): """Extract quote/reply context from cloud_custom_data.""" - name = "quote-context" def _extract_quote_context(self, cloud_custom_data: str) -> Tuple[Optional[str], Optional[str]]: @@ -1344,7 +1267,6 @@ class ForwardedRecordsParseMiddleware(InboundMiddleware): render media as ``[kind|ybres:RID]``, append refs to ``ctx.media_refs`` and rewrite raw_text. No run-time fallback for earlier forwards — GroupAtGuard already rendered summaries at observe time. On any failure raw_text is left untouched.""" - name = "forwarded-records-parse" FORWARD_MSG_TEXT_MAX_CHARS = 1000 # per-record text cap; record count is NOT capped @@ -1448,7 +1370,6 @@ class MediaResolveMiddleware(InboundMiddleware): """Resolve inbound media references to local cached files. Yuanbao COS hostnames resolve to private IPs (tripping vision_tools' SSRF guard), so we download ourselves and hand the model local paths.""" - name = "media-resolve" # Resource download cache keyed by resourceId: rid -> (local_path, mime, ts) @@ -1614,14 +1535,10 @@ class MediaResolveMiddleware(InboundMiddleware): return media_urls, media_types async def _resolve_one(kind: str, url: str, filename: str, rid: str) -> Optional[Tuple[str, str]]: - try: - fetch_url = await cls._resolve_download_url(adapter, url) - except Exception as exc: - logger.warning("[%s] inbound media resolve failed: kind=%s url=%s err=%s", adapter.name, kind, url, exc) - return None - return await cls._download_and_cache( - adapter, fetch_url=fetch_url, kind=kind, file_name=filename or None, - log_tag=f"placeholder_url={url[:80]}", resource_id=rid, + return await cls._fetch_and_cache( + adapter, lambda: cls._resolve_download_url(adapter, url), kind, filename, rid, + fail_log=("[%s] inbound media resolve failed: kind=%s url=%s err=%s", (adapter.name, kind, url)), + log_tag=f"placeholder_url={url[:80]}", ) def _crash_msg(item, result): @@ -1630,6 +1547,19 @@ class MediaResolveMiddleware(InboundMiddleware): await cls._gather_resolve(adapter, active, _resolve_one, _crash_msg, "media", media_urls, media_types) return media_urls, media_types + @classmethod + async def _fetch_and_cache(cls, adapter, get_url, kind: str, filename: str, rid: str, *, fail_log, log_tag: str) -> Optional[Tuple[str, str]]: + """``await get_url()`` for a fetchable URL (log *fail_log* + err and return None on failure), then download+cache.""" + try: + fetch_url = await get_url() + except Exception as exc: + fmt, args = fail_log + logger.warning(fmt, *args, exc) + return None + return await cls._download_and_cache( + adapter, fetch_url=fetch_url, kind=kind, file_name=filename or None, log_tag=log_tag, resource_id=rid, + ) + @staticmethod async def _gather_resolve(adapter, active, resolve_one, crash_msg, scope, out_paths, out_mimes) -> None: """Run *resolve_one(*item)* over *active* under bounded concurrency; append successes in @@ -1670,14 +1600,10 @@ class MediaResolveMiddleware(InboundMiddleware): return media_paths, mimes async def _resolve_one(rid: str, kind: str, filename: str) -> Optional[Tuple[str, str]]: - try: - fresh_url = await cls._fetch_resource_url(adapter, rid) - except Exception as exc: - logger.warning("[%s] %s resolve failed: rid=%s kind=%s err=%s", adapter.name, log_prefix, rid, kind, exc) - return None - return await cls._download_and_cache( - adapter, fetch_url=fresh_url, kind=kind, file_name=filename or None, - log_tag=f"{log_prefix} rid={rid}", resource_id=rid, + return await cls._fetch_and_cache( + adapter, lambda: cls._fetch_resource_url(adapter, rid), kind, filename, rid, + fail_log=("[%s] %s resolve failed: rid=%s kind=%s err=%s", (adapter.name, log_prefix, rid, kind)), + log_tag=f"{log_prefix} rid={rid}", ) def _crash_msg(item, result): @@ -1788,7 +1714,6 @@ class PatchAnchorsMiddleware(InboundMiddleware): """Replace ``[kind|ybres:RID]`` anchors in raw_text with the local paths MediaResolveMiddleware produced, so the transcript records usable paths. Only resolved media (paths starting with ``/``) are substituted; other anchors stay untouched.""" - name = "patch-anchors" @staticmethod @@ -1822,7 +1747,6 @@ class PatchAnchorsMiddleware(InboundMiddleware): class DispatchMiddleware(InboundMiddleware): """Build the MessageEvent and dispatch it (groups: serialised per session via a queue).""" - name = "dispatch" async def handle(self, ctx: InboundContext, next_fn) -> None: @@ -1895,7 +1819,6 @@ class DispatchMiddleware(InboundMiddleware): class InboundPipelineBuilder: """Assembles the default Yuanbao inbound pipeline (order matters).""" - _DEFAULT_MIDDLEWARES: list[type] = [ DecodeMiddleware, ExtractFieldsMiddleware, RecallGuardMiddleware, DedupMiddleware, SkipSelfMiddleware, ChatRoutingMiddleware, AccessGuardMiddleware, ExtractContentMiddleware, PlaceholderFilterMiddleware, @@ -1914,7 +1837,6 @@ class InboundPipelineBuilder: class ConnectionManager: """WebSocket lifecycle: open/close, AUTH_BIND, ping/pong heartbeat, receive loop, backoff reconnect.""" - _DEBOUNCE_WINDOW: float = 1.5 # seconds to wait for companion frames of a multi-part message _LOOPS = (("_heartbeat_task", "_heartbeat_loop", "heartbeat"), ("_recv_task", "_receive_loop", "recv")) @@ -1943,14 +1865,10 @@ class ConnectionManager: if self._ws is None: return False open_attr = getattr(self._ws, "open", None) - if open_attr is True: - return True - if callable(open_attr): - try: - return bool(open_attr()) - except Exception: - return False - return False + try: + return open_attr is True or (callable(open_attr) and bool(open_attr())) + except Exception: + return False async def open(self) -> bool: """sign-token → WS connect → AUTH_BIND → start loops. Returns True on success.""" @@ -2087,11 +2005,8 @@ class ConnectionManager: def _pop_pending(self, msg_id: str) -> Optional[asyncio.Future]: """Pop the not-yet-done future registered for *msg_id*, if any.""" - if msg_id and msg_id in self._pending_acks: - fut = self._pending_acks.pop(msg_id) - if not fut.done(): - return fut - return None + fut = self._pending_acks.pop(msg_id, None) if msg_id else None + return fut if fut is not None and not fut.done() else None def _extract_connect_id(self, decoded_msg: dict) -> Optional[str]: """connectId from a decoded BIND_ACK, or None (logs AuthBindRsp errors).""" @@ -2344,7 +2259,6 @@ def _read_local_file(adapter, label: str, path: str, default_name: str, default_ class MediaSendHandler(ABC): """Media send strategy: subclasses provide acquire_file() and build_msg_body(); handle() runs the shared flow (check ws → cancel notifier → validate → COS upload → lock → dispatch).""" - def needs_cos_upload(self) -> bool: """Override to return False for non-COS media (sticker).""" return True @@ -2398,7 +2312,6 @@ class MediaSendHandler(ABC): class _ImageHandler(MediaSendHandler): """Shared TIMImageElem body builder for image handlers.""" - def build_msg_body(self, upload_result, **kwargs): return build_image_msg_body( url=upload_result["url"], uuid=kwargs["file_uuid"], filename=kwargs["filename"], size=upload_result["size"], @@ -2408,7 +2321,6 @@ class _ImageHandler(MediaSendHandler): class ImageUrlHandler(_ImageHandler): """Image from a URL (download → COS → TIMImageElem).""" - async def acquire_file(self, adapter, **kwargs): image_url: str = kwargs["image_url"] logger.info("[%s] ImageUrlHandler: downloading %s", adapter.name, image_url) @@ -2421,14 +2333,12 @@ class ImageUrlHandler(_ImageHandler): class ImageFileHandler(_ImageHandler): """Image from a local path (read → COS → TIMImageElem).""" - async def acquire_file(self, adapter, **kwargs): return _read_local_file(adapter, "ImageFileHandler", kwargs["image_path"], "image.jpg", "image/jpeg") class DocumentHandler(MediaSendHandler): """Local file/document (read → COS → TIMFileElem).""" - async def acquire_file(self, adapter, **kwargs): return _read_local_file(adapter, "DocumentHandler", kwargs["file_path"], "document", "application/octet-stream", kwargs.get("filename")) @@ -2438,7 +2348,6 @@ class DocumentHandler(MediaSendHandler): class StickerHandler(MediaSendHandler): """Sticker/emoji (TIMFaceElem, no COS upload).""" - def needs_cos_upload(self) -> bool: return False @@ -2463,7 +2372,6 @@ class StickerHandler(MediaSendHandler): class GroupQueryService: """Low-level WS group queries (group info, member list); populates adapter._member_cache.""" - def __init__(self, adapter: "YuanbaoAdapter") -> None: self._adapter = adapter @@ -2507,7 +2415,6 @@ class GroupQueryService: class HeartbeatManager: """Reply heartbeat lifecycle: RUNNING every 2s, auto-FINISH after 30s idle, explicit stop.""" - def __init__(self, adapter: "YuanbaoAdapter") -> None: self._adapter = adapter self._reply_heartbeat_tasks: Dict[str, asyncio.Task] = {} @@ -2583,7 +2490,6 @@ class HeartbeatManager: class SlowResponseNotifier: """Per-chat timer that sends a courtesy 'please wait' after SLOW_RESPONSE_TIMEOUT_S without a reply.""" - def __init__(self, adapter: "YuanbaoAdapter", sender: "MessageSender") -> None: self._adapter = adapter self._sender = sender @@ -2615,7 +2521,6 @@ class SlowResponseNotifier: class MessageSender: """Outbound dispatcher: per-chat locks (serial ordering), chunked text with retry, C2C/group encoding, media handler strategies, and send_direct for the send_message tool.""" - IMAGE_EXTS: ClassVar[frozenset] = frozenset({".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}) CHAT_DICT_MAX_SIZE: ClassVar[int] = 1000 # Max distinct chat IDs in _chat_locks # @nickname bounded by whitespace / line edges @@ -2628,10 +2533,7 @@ class MessageSender: self._on_send_start: Optional[Callable[[str], Any]] = None self._on_send_finish: Optional[Callable[[str], Any]] = None self._media_handlers: Dict[str, MediaSendHandler] = { - "image_url": ImageUrlHandler(), - "image_file": ImageFileHandler(), - "document": DocumentHandler(), - "sticker": StickerHandler(), + "image_url": ImageUrlHandler(), "image_file": ImageFileHandler(), "document": DocumentHandler(), "sticker": StickerHandler(), } def get_chat_lock(self, chat_id: str) -> asyncio.Lock: @@ -2760,10 +2662,8 @@ class MessageSender: entry = nickname_to_uid.get(nickname.lower()) if entry: real_nick, uid = entry - msg_body.append({ - "msg_type": "TIMCustomElem", - "msg_content": {"data": json.dumps({"elem_type": 1002, "text": f"@{real_nick}", "user_id": uid})}, - }) + msg_body.append({"msg_type": "TIMCustomElem", + "msg_content": {"data": json.dumps({"elem_type": 1002, "text": f"@{real_nick}", "user_id": uid})}}) else: msg_body.append(_text_elem(f"@{nickname}")) last_idx = match.end() @@ -2833,7 +2733,6 @@ class MessageSender: class OutboundManager: """Composes MessageSender, HeartbeatManager and SlowResponseNotifier and wires their coordination hooks.""" - CHAT_DICT_MAX_SIZE: ClassVar[int] = MessageSender.CHAT_DICT_MAX_SIZE def __init__(self, adapter: "YuanbaoAdapter") -> None: @@ -2869,7 +2768,6 @@ class OutboundManager: class YuanbaoAdapter(BasePlatformAdapter): """Yuanbao AI Bot adapter backed by a persistent WebSocket connection.""" - PLATFORM = Platform.YUANBAO MAX_TEXT_CHUNK: int = 4000 # Yuanbao single message character limit splits_long_messages = True # send() auto-chunks via truncate_message(MAX_TEXT_CHUNK)