import asyncio MAX_TEXT_BYTES = 7400 class WebexOutbound: def __init__(self, api): self._api = api @staticmethod def build_mention(person_id: str = "", person_email: str = "", display_name: str = "") -> str: if person_email: return f"<@personEmail:{person_email}>" if person_id: return f"<@personId:{person_id}>" if display_name: return f"**{display_name}**" return "" @staticmethod def welcome_card_template(display_name: str = "", bot_name: str = "ForcePilot") -> dict: greeting = f"你好 {display_name}!" if display_name else "你好!" return { "type": "AdaptiveCard", "version": "1.3", "body": [ { "type": "TextBlock", "text": f"👋 {greeting}", "size": "Large", "weight": "Bolder", }, { "type": "TextBlock", "text": f"我是 **{bot_name}**,基于 Cisco Webex 的智能助手,有什么可以帮助你的?", "wrap": True, }, { "type": "ColumnSet", "columns": [ { "type": "Column", "width": "auto", "items": [ { "type": "TextBlock", "text": "🚀 可以尝试问我:", "wrap": True, }, { "type": "TextBlock", "text": "- 知识查询与解答\n- 数据分析与处理\n- 任务提醒与通知", "wrap": True, "spacing": "Small", }, ], } ], }, ], } @staticmethod def confirm_card_template( title: str, message: str, confirm_label: str = "确认", cancel_label: str = "取消" ) -> dict: return { "type": "AdaptiveCard", "version": "1.3", "body": [ { "type": "TextBlock", "text": title, "size": "Medium", "weight": "Bolder", }, { "type": "TextBlock", "text": message, "wrap": True, }, ], "actions": [ { "type": "Action.Submit", "title": confirm_label, "data": {"action": "confirm"}, }, { "type": "Action.Submit", "title": cancel_label, "style": "default", "data": {"action": "cancel"}, }, ], } async def send_text( self, room_id: str, content: str, *, parent_id: str | None = None, as_markdown: bool = True, mentions: list[str] | None = None, to_person_id: str | None = None, to_person_email: str | None = None, ) -> list[str]: text = content if mentions: text = " ".join(mentions) + " " + text chunks = self._chunk_text(text) sent_ids = [] for i, chunk in enumerate(chunks): pid = parent_id if i == 0 else None kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email) if pid: kwargs["parentId"] = pid if as_markdown: kwargs["markdown"] = chunk else: kwargs["text"] = chunk msg = self._api.messages.create(**kwargs) sent_ids.append(msg.id) if i < len(chunks) - 1: await asyncio.sleep(0.1) return sent_ids async def send_html( self, room_id: str, content: str, *, parent_id: str | None = None, to_person_id: str | None = None, to_person_email: str | None = None, ) -> str: kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email) if parent_id: kwargs["parentId"] = parent_id kwargs["html"] = content msg = self._api.messages.create(**kwargs) return msg.id async def send_media( self, room_id: str, media_url: str, *, parent_id: str | None = None, to_person_id: str | None = None, to_person_email: str | None = None, ) -> str: kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email) kwargs["files"] = [media_url] if parent_id: kwargs["parentId"] = parent_id msg = self._api.messages.create(**kwargs) return msg.id async def send_card( self, room_id: str, card_json: dict, *, parent_id: str | None = None, to_person_id: str | None = None, to_person_email: str | None = None, ) -> str: kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email) kwargs["attachments"] = [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": card_json, } ] if parent_id: kwargs["parentId"] = parent_id msg = self._api.messages.create(**kwargs) return msg.id async def edit_message( self, message_id: str, room_id: str, new_content: str, *, as_markdown: bool = True, ) -> str: kwargs = {"roomId": room_id} if as_markdown: kwargs["markdown"] = new_content else: kwargs["text"] = new_content updated = self._api.messages.update(message_id, **kwargs) return updated.id async def delete_message(self, message_id: str) -> bool: try: self._api.messages.delete(message_id) return True except Exception: return False async def list_messages( self, room_id: str, *, parent_id: str | None = None, before: str | None = None, max_results: int = 50, ) -> list[dict]: try: params = {"roomId": room_id, "max": min(max_results, 100)} if parent_id: params["parentId"] = parent_id if before: params["before"] = before messages = list(self._api.messages.list(**params)) return [ { "id": m.id, "text": m.text, "person_id": m.personId, "person_email": m.personEmail, "created": m.created, "parent_id": m.parentId, } for m in messages ] except Exception: return [] async def get_message(self, message_id: str) -> dict | None: try: m = self._api.messages.get(message_id) return { "id": m.id, "text": m.text, "markdown": m.markdown, "html": m.html, "person_id": m.personId, "person_email": m.personEmail, "room_id": m.roomId, "parent_id": m.parentId, "created": m.created, "files": m.files, } except Exception: return None async def get_room(self, room_id: str) -> dict | None: try: r = self._api.rooms.get(room_id) return { "id": r.id, "title": r.title, "type": r.type, "is_locked": getattr(r, "isLocked", False), "created": r.created, } except Exception: return None async def list_members(self, room_id: str) -> list[dict]: try: members = list(self._api.memberships.list(roomId=room_id)) return [ { "id": m.id, "person_id": m.personId, "person_email": m.personEmail, "person_display_name": m.personDisplayName, "room_id": m.roomId, "is_moderator": getattr(m, "isModerator", False), "created": m.created, } for m in members ] except Exception: return [] async def get_member(self, membership_id: str) -> dict | None: try: m = self._api.memberships.get(membership_id) return { "id": m.id, "person_id": m.personId, "person_email": m.personEmail, "person_display_name": m.personDisplayName, "room_id": m.roomId, "is_moderator": getattr(m, "isModerator", False), "created": m.created, } except Exception: return None def _chunk_text(self, content: str) -> list[str]: encoded = content.encode("utf-8") if len(encoded) <= MAX_TEXT_BYTES: return [content] chunks = [] pos = 0 while pos < len(encoded): end = min(pos + MAX_TEXT_BYTES, len(encoded)) chunk_bytes = encoded[pos:end] try: chunk = chunk_bytes.decode("utf-8") except UnicodeDecodeError as e: cut = e.start chunk_bytes = encoded[pos : pos + cut] chunk = chunk_bytes.decode("utf-8") chunks.append(chunk) if chunk_bytes: pos += len(chunk_bytes) else: pos += MAX_TEXT_BYTES return chunks @staticmethod def _build_target_kwargs( room_id: str, to_person_id: str | None = None, to_person_email: str | None = None, ) -> dict: if to_person_id: return {"toPersonId": to_person_id} if to_person_email: return {"toPersonEmail": to_person_email} return {"roomId": room_id}