- 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 工具
88 lines
3.1 KiB
Python
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
|