Files
inkreach-prefect/flows/shared/wecom.py
T
yeuimu 1d0da2a00b feat: 接入企微智能机器人——异常报警/数据核验日报/设备健康日报
- flows/shared/wecom.py: WSClient 按需连接(连接→认证→发送→断开),全容错,超长分段
- flows/monitor/alert_failure.py: Prefect 事件触发(Failed/Crashed→run deployment),发群报警
- flows/monitor/daily_report.py: 每日 18:30 近7天 RIIN/SDS 核验(复用 verify)+商品完整性+OCR+失败回顾
- flows/monitor/device_check.py: 每日 09:00 内存/磁盘/GPU(80%阈值)+Docker 容器异常+服务探活
- db.py: 新增 get_goods_integrity/ping; compose worker 挂载 docker.sock+/:/host:ro+GPU
- scripts/wecom_capture_chatid.py: 一次性捕获群 chat_id 工具
2026-08-27 09:40:53 +00:00

88 lines
3.1 KiB
Python

"""企业微信智能机器人通知层(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