"""企业微信智能机器人通知层(wecom-aibot-sdk,WebSocket 按需连接)。 连接模式:每次发消息 建立→等认证→发送→断开,无状态,契合 flow run 级部署模型。 凭据存 Prefect Secret Block `wecom-aibot-credentials`,JSON: {"bot_id": "...", "secret": "...", "chat_id": "..."} chat_id 为目标群会话 ID(用 scripts/wecom_capture_chatid.py 一次性捕获)。 所有函数容错——发送失败只 print 警告并返回 False,绝不中断 flow(对齐 artifacts.py 风格)。 """ from __future__ import annotations import asyncio import json from prefect.blocks.system import Secret SECRET_BLOCK = "wecom-aibot-credentials" MAX_CONTENT_BYTES = 4000 # WeCom markdown 单条约 4096 字节上限,留余量 def load_credentials() -> dict[str, str]: value = Secret.load(SECRET_BLOCK).get() if isinstance(value, str): value = json.loads(value) missing = [k for k in ("bot_id", "secret", "chat_id") if not value.get(k)] if missing: raise ValueError(f"Secret Block {SECRET_BLOCK} 缺少字段: {missing}") return value async def _send_bodies(creds: dict[str, str], bodies: list[dict]) -> None: from wecom_aibot_sdk import WSClient ws = WSClient(bot_id=creds["bot_id"], secret=creds["secret"]) auth_ok = asyncio.Event() ws.on("authenticated", lambda: auth_ok.set()) await asyncio.wait_for(ws.connect(), timeout=15) try: # connect() 只等到认证帧发出;认证结果由 authenticated 事件确认 await asyncio.wait_for(auth_ok.wait(), timeout=10) for body in bodies: await ws.send_message(creds["chat_id"], body) finally: await ws.disconnect() def _split_markdown(content: str) -> list[str]: """按字节预算切段,优先在换行处切,避免拆开表格行。""" if len(content.encode("utf-8")) <= MAX_CONTENT_BYTES: return [content] parts: list[str] = [] cur = "" for line in content.splitlines(keepends=True): if cur and len((cur + line).encode("utf-8")) > MAX_CONTENT_BYTES: parts.append(cur) cur = line else: cur += line if cur: parts.append(cur) return parts def send_markdown(content: str) -> bool: """发送 markdown 消息到目标群(超长自动分段)。失败返回 False,不抛异常。""" try: creds = load_credentials() parts = _split_markdown(content) asyncio.run(_send_bodies(creds, [{"msgtype": "markdown", "markdown": {"content": p}} for p in parts])) if len(parts) > 1: print(f"[wecom] markdown 已发送 ({len(parts)} 段)") return True except Exception as err: # noqa: BLE001 print(f"[wecom] markdown 发送失败: {err}") return False def send_text(content: str) -> bool: """发送纯文本消息到目标群。失败返回 False,不抛异常。""" try: creds = load_credentials() asyncio.run(_send_bodies(creds, [{"msgtype": "text", "text": {"content": content}}])) return True except Exception as err: # noqa: BLE001 print(f"[wecom] text 发送失败: {err}") return False