from __future__ import annotations import logging from collections import defaultdict from typing import TYPE_CHECKING, Any import httpx from yuxi.channels.models import DeliveryResult if TYPE_CHECKING: from yuxi.channels.adapters.dingding.token import DingDingTokenManager logger = logging.getLogger(__name__) DINGTALK_API_BASE = "https://api.dingtalk.com" class CoolAppRouter: def __init__(self, config: dict[str, Any] | None = None): self._routes: dict[str, str] = {} self._sticky_cards: dict[str, dict[str, Any]] = defaultdict(dict) self._shortcuts: dict[str, list[dict[str, Any]]] = defaultdict(list) self._configure(config) def _configure(self, config: dict[str, Any] | None) -> None: if not config: return cool_app_config = config.get("cool_app", {}) if not isinstance(cool_app_config, dict): return routes = cool_app_config.get("routes", {}) if isinstance(routes, dict): for cool_code, agent_name in routes.items(): self.register(cool_code, agent_name) def register(self, cool_app_code: str, agent_name: str) -> None: self._routes[cool_app_code] = agent_name logger.info("[DingDing CoolApp] Registered route: %s → %s", cool_app_code, agent_name) def unregister(self, cool_app_code: str) -> None: self._routes.pop(cool_app_code, None) def resolve(self, cool_app_code: str) -> str | None: return self._routes.get(cool_app_code) def list_routes(self) -> dict[str, str]: return dict(self._routes) def set_sticky_card(self, chat_id: str, card_data: dict[str, Any]) -> None: self._sticky_cards[chat_id] = card_data def get_sticky_card(self, chat_id: str) -> dict[str, Any] | None: return self._sticky_cards.get(chat_id) def remove_sticky_card(self, chat_id: str) -> None: self._sticky_cards.pop(chat_id, None) def set_shortcuts(self, chat_id: str, items: list[dict[str, Any]]) -> None: self._shortcuts[chat_id] = items def get_shortcuts(self, chat_id: str) -> list[dict[str, Any]]: return list(self._shortcuts.get(chat_id, [])) async def send_cool_app_card( token_manager: DingDingTokenManager, robot_code: str, card_data: dict[str, Any], *, open_conversation_id: str = "", user_ids: list[str] | None = None, cool_app_code: str = "", http_client: httpx.AsyncClient | None = None, ) -> DeliveryResult: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/card/instances" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload: dict[str, Any] = { "robotCode": robot_code, "cardData": card_data.get("cardData", card_data), } if open_conversation_id: payload["openConversationId"] = open_conversation_id if user_ids: payload["userIds"] = user_ids if cool_app_code: payload["coolAppCode"] = cool_app_code card_biz_id = card_data.get("cardBizId", "") if card_biz_id: payload["cardBizId"] = card_biz_id private_data = card_data.get("privateData", {}) if private_data: payload["privateData"] = private_data try: if http_client is not None: resp = await http_client.post(url, json=payload, headers=headers) else: async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: resp = await client.post(url, json=payload, headers=headers) if resp.status_code != 200: return DeliveryResult( success=False, error=f"Cool App card API HTTP {resp.status_code}: {resp.text[:200]}" ) data = resp.json() return DeliveryResult( success=True, message_id=data.get("processQueryKey", data.get("cardInstanceId", "")), ) except Exception as e: logger.error(f"[DingDing CoolApp] send_cool_app_card failed: {e}") return DeliveryResult(success=False, error=str(e)) async def update_cool_app_card( token_manager: DingDingTokenManager, robot_code: str, card_instance_id: str, card_data: dict[str, Any], *, open_conversation_id: str = "", http_client: httpx.AsyncClient | None = None, ) -> DeliveryResult: token = await token_manager.get_token() url = f"{DINGTALK_API_BASE}/v1.0/card/instances/{card_instance_id}" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload: dict[str, Any] = { "robotCode": robot_code, "cardData": card_data.get("cardData", card_data), } if open_conversation_id: payload["openConversationId"] = open_conversation_id update_options = card_data.get("updateOptions", card_data.get("sendOptions")) if update_options: payload["updateOptions"] = update_options if "sendOptions" not in payload: payload["sendOptions"] = update_options private_data = card_data.get("privateData", {}) if private_data: payload["privateData"] = private_data try: if http_client is not None: resp = await http_client.put(url, json=payload, headers=headers) else: async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: resp = await client.put(url, json=payload, headers=headers) if resp.status_code != 200: return DeliveryResult( success=False, error=f"Cool App card update API HTTP {resp.status_code}: {resp.text[:200]}" ) return DeliveryResult(success=True) except Exception as e: logger.error(f"[DingDing CoolApp] update_cool_app_card failed: {e}") return DeliveryResult(success=False, error=str(e)) def parse_cool_app_code(raw: dict[str, Any]) -> str: return raw.get("coolAppCode", raw.get("outTrackId", "")) def parse_card_callback_data(raw: dict[str, Any]) -> dict[str, Any]: content = raw.get("content", "") or raw.get("text", "") import json if isinstance(content, str): try: parsed = json.loads(content) except (json.JSONDecodeError, TypeError): return {} return parsed if isinstance(content, dict): return content return {} def parse_card_private_data(raw: dict[str, Any]) -> dict[str, Any]: card_data = parse_card_callback_data(raw) return card_data.get("cardPrivateData", card_data.get("privateData", {}))