from __future__ import annotations import json import logging import uuid import httpx logger = logging.getLogger(__name__) CREATE_AND_DELIVER_URL = "https://api.dingtalk.com/v1.0/card/instances/createAndDeliver" STREAMING_UPDATE_URL = "https://api.dingtalk.com/v1.0/card/streamingUpdate" class DingTalkCardManager: def __init__(self, gateway, http: httpx.AsyncClient | None = None): self._gateway = gateway self._http = http self._card_instance_id: str | None = None self._out_track_id: str | None = None def set_http(self, http: httpx.AsyncClient) -> None: self._http = http async def _get_http(self) -> httpx.AsyncClient: if self._http is not None: return self._http client = httpx.AsyncClient(timeout=15.0) self._http = client return client async def create_and_deliver( self, open_conversation_id: str, robot_code: str, card_template_id: str, title: str = "AI 正在思考...", ) -> bool: token = await self._gateway.token_manager.get_access_token() if not token: return False self._out_track_id = str(uuid.uuid4()) body = { "cardTemplateId": card_template_id, "outTrackId": self._out_track_id, "robotCode": robot_code, "openConversationId": open_conversation_id, "cardData": json.dumps({"title": title, "content": ""}), "imGroupOpenDeliverModel": {"robotCode": robot_code}, "callbackType": "STREAM", } http = await self._get_http() resp = await http.post( CREATE_AND_DELIVER_URL, headers={ "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", }, json=body, ) data = resp.json() self._card_instance_id = data.get("cardInstanceId") return bool(self._card_instance_id) async def streaming_update(self, content: str, status: str = "PROCESSING") -> bool: if not self._card_instance_id: return False token = await self._gateway.token_manager.get_access_token() if not token: return False body = { "outTrackId": self._out_track_id, "cardInstanceId": self._card_instance_id, "content": content, "fullContent": False, "flowStatus": status, } http = await self._get_http() resp = await http.put( STREAMING_UPDATE_URL, headers={ "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", }, json=body, ) return resp.status_code == 200 def build_approval_card_buttons(request_id: str) -> list[dict]: return [ { "title": "批准", "actionURL": f"dingtalk://yuxi/approval/approve?req={request_id}", }, { "title": "拒绝", "actionURL": f"dingtalk://yuxi/approval/deny?req={request_id}", }, ] def build_action_card(title: str, text: str, buttons: list[dict], btn_orientation: str = "1") -> dict: return { "msgtype": "actionCard", "actionCard": { "title": title, "text": text, "btnOrientation": btn_orientation, "btns": buttons, }, } def build_markdown_message(title: str, text: str) -> dict: return { "msgtype": "markdown", "markdown": { "title": title, "text": text, }, } def build_image_reply_button(image_url: str, prompt_en: str = "") -> dict | None: if not image_url or not prompt_en: return None return { "title": "查看原图", "actionURL": image_url, } def apply_ai_card_monkey_patch(): try: from dingtalk_stream import AICardReplier, AICardStatus, CardReplier class CustomAICardReplier(CardReplier): async def start(self, card_instance_id: str) -> None: self._card_instance_id = card_instance_id await self._update_card( card_instance_id, { "flowStatus": AICardStatus.PROCESSING, "content": "", }, ) async def update_content(self, content: str) -> None: await self._update_card( self._card_instance_id, { "content": content, "flowStatus": AICardStatus.PROCESSING, }, ) async def finish(self) -> None: await self._update_card( self._card_instance_id, {"flowStatus": AICardStatus.FINISHED}, ) AICardReplier.start = CustomAICardReplier.start logger.info("DingTalk AICardReplier monkey patch applied") except ImportError: logger.debug("dingtalk_stream SDK not available, skipping monkey patch")