"""苏瑶飞书应用 API 服务 — 通过苏瑶应用发送消息到用户""" from __future__ import annotations import json import logging import time from typing import Optional import httpx from app.core.config import settings logger = logging.getLogger(__name__) # Token 缓存(tenant_access_token 有效期 2 小时,提前 5 分钟刷新) _token_cache: dict = {"token": None, "expires_at": 0} def _get_tenant_access_token() -> Optional[str]: """获取苏瑶应用的 tenant_access_token(带缓存)。""" now = time.time() if _token_cache["token"] and now < _token_cache["expires_at"] - 300: return _token_cache["token"] app_id = settings.SUYAO_APP_ID app_secret = settings.SUYAO_APP_SECRET if not app_id or not app_secret: logger.warning("苏瑶应用未配置(SUYAO_APP_ID / SUYAO_APP_SECRET)") return None try: with httpx.Client(timeout=10) as client: resp = client.post( "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", json={"app_id": app_id, "app_secret": app_secret}, ) result = resp.json() if resp.is_success and result.get("code") == 0: token = result["tenant_access_token"] expire = result.get("expire", 7200) _token_cache["token"] = token _token_cache["expires_at"] = now + expire logger.info("苏瑶 tenant_access_token 获取成功") return token else: logger.warning("苏瑶 token 获取失败: %s", result) return None except Exception as e: logger.warning("苏瑶 token 获取异常: %s", e) return None def send_message_to_user( open_id: str, title: str, content: str, status: str = "info", detail_link: Optional[str] = None, ) -> bool: """通过苏瑶应用向用户发送消息卡片。""" token = _get_tenant_access_token() if not token: return False color_map = {"success": "green", "failed": "red", "info": "blue"} color = color_map.get(status, "blue") elements = [ {"tag": "markdown", "content": content}, ] if detail_link: elements.append({ "tag": "action", "actions": [ { "tag": "button", "text": {"tag": "plain_text", "content": "查看详情"}, "url": detail_link, "type": "default", } ], }) card = { "config": {"wide_screen_mode": True}, "header": { "title": {"tag": "plain_text", "content": title}, "template": color, }, "elements": elements, } try: with httpx.Client(timeout=10) as client: resp = client.post( "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id", headers={"Authorization": f"Bearer {token}"}, json={ "receive_id": open_id, "msg_type": "interactive", "content": json.dumps(card, ensure_ascii=False), }, ) result = resp.json() if resp.is_success and result.get("code") == 0: logger.info("苏瑶消息发送成功: open_id=%s title=%s", open_id[:20], title) return True else: logger.warning("苏瑶消息发送失败: code=%s msg=%s", result.get("code"), result.get("msg")) return False except Exception as e: logger.warning("苏瑶消息发送异常: %s", e) return False def send_plain_text(open_id: str, text: str) -> bool: """通过苏瑶应用向用户发送纯文本消息。""" token = _get_tenant_access_token() if not token: return False try: with httpx.Client(timeout=10) as client: resp = client.post( "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id", headers={"Authorization": f"Bearer {token}"}, json={ "receive_id": open_id, "msg_type": "text", "content": json.dumps({"text": text}, ensure_ascii=False), }, ) result = resp.json() return resp.is_success and result.get("code") == 0 except Exception as e: logger.warning("苏瑶文本消息发送异常: %s", e) return False