from __future__ import annotations from dataclasses import dataclass from typing import Any from yuxi.channels.models import ChannelMessage, ChannelResponse, ChatType @dataclass class WeChatTargetResolver: @staticmethod def looks_like_id(raw: str) -> bool: return raw.startswith("wx:") or bool(raw.strip()) @staticmethod def resolve_target(raw: str) -> dict[str, Any]: raw = raw.strip() if raw.startswith("wx:"): return {"to": raw, "chatType": "direct"} return {"to": f"wx:{raw}", "chatType": "direct"} @property def hint(self) -> str: return "格式: wx: 或直接填 open_id" class WeChatMessagingRouter: def __init__(self): self.target_resolver = WeChatTargetResolver() @staticmethod def normalize_target(raw: str) -> str: raw = raw.strip() if not raw.startswith("wx:"): return f"wx:{raw}" return raw @staticmethod def parse_explicit_target(raw: str) -> dict[str, Any] | None: raw = raw.strip() if not raw: return None parts = raw.split(":", 2) if len(parts) >= 2 and parts[0] == "wx": return {"to": raw, "chatType": "direct"} return {"to": f"wx:{raw}", "chatType": "direct"} @staticmethod def infer_target_chat_type(target_id: str) -> ChatType: if target_id.startswith("wx:group:"): return ChatType.GROUP return ChatType.DIRECT @staticmethod def resolve_outbound_session_route( account_id: str, mode: str, chat_type: str, peer_id: str, default_agent_id: str = "1", ) -> str: if chat_type == "group": return f"agent:{default_agent_id}:wechat:{mode}:group:{peer_id}" return f"agent:{default_agent_id}:wechat:{mode}:direct:{peer_id}" @staticmethod def resolve_session_conversation(kind: str, raw_id: str) -> str: return f"{kind}:{raw_id}" @staticmethod def resolve_parent_conversation_candidates(message: ChannelMessage) -> list[str]: chat_id = message.identity.channel_chat_id sender_id = message.identity.channel_user_id return [f"chat:{chat_id}", f"direct:{sender_id}"] @staticmethod def resolve_session_target(message: ChannelMessage) -> dict[str, Any]: return { "to": message.identity.channel_user_id, "chatId": message.identity.channel_chat_id, "chatType": message.metadata.get("chat_type", "direct"), } @staticmethod def resolve_delivery_target(response: ChannelResponse) -> dict[str, Any]: return { "to": response.identity.channel_user_id, "chatId": response.identity.channel_chat_id, } @staticmethod def resolve_inbound_conversation(message: ChannelMessage) -> str: chat_id = message.identity.channel_chat_id chat_type = message.metadata.get("chat_type", "direct") return f"{chat_type}:{chat_id}" @staticmethod def format_target_display(target: dict[str, Any]) -> str: to = target.get("to", "unknown") chat_type = target.get("chatType", "direct") return f"[{chat_type}] {to}" @staticmethod def build_cross_context_presentation(source_ctx: dict[str, Any], target_ctx: dict[str, Any]) -> str: src_peer = source_ctx.get("peer_name", source_ctx.get("to", "unknown")) return f"[转发自 {src_peer}]" @staticmethod def transform_reply_payload(response: ChannelResponse) -> dict[str, Any]: payload: dict[str, Any] = { "touser": response.identity.channel_user_id, "msgtype": "text", "text": {"content": response.content}, } reply_to = response.metadata.get("reply_to_message_id") if reply_to: payload["reply_to_message_id"] = reply_to return payload @staticmethod def enable_interactive_replies(config: dict[str, Any]) -> bool: return config.get("enable_interactive_replies", False) @staticmethod def has_structured_reply_payload(response: ChannelResponse) -> bool: return bool(response.metadata.get("reply_to_message_id")) @staticmethod def resolve_inbound_attachment_roots(message: ChannelMessage) -> list[str]: roots: list[str] = [] for att in message.attachments: if att.file_id: roots.append(att.file_id) return roots @staticmethod def resolve_remote_inbound_attachment_roots(message: ChannelMessage) -> list[str]: return WeChatMessagingRouter.resolve_inbound_attachment_roots(message) @staticmethod def preserve_heartbeat_thread_id_for_group_route(config: dict[str, Any]) -> bool: return config.get("preserve_heartbeat_thread_id_for_group_route", False)