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 工具
This commit is contained in:
@@ -17,6 +17,7 @@ from flows.shared.clients import (
|
||||
)
|
||||
from flows.shared.db import SyncDB
|
||||
from flows.shared.mappers import GoodsUpsertInput, OrderUpsertInput
|
||||
from flows.shared.wecom import send_markdown, send_text
|
||||
|
||||
__all__ = [
|
||||
"clients", "db", "mappers", "SyncDB",
|
||||
@@ -26,4 +27,5 @@ __all__ = [
|
||||
"get_channel_type_map", "refresh_channel_type_map", "resolve_courier_source",
|
||||
"format_date", "start_of_day", "floor_to_hour",
|
||||
"build_stats_rows", "emit_sync_summary", "emit_verify_report",
|
||||
"send_markdown", "send_text",
|
||||
]
|
||||
|
||||
@@ -86,6 +86,51 @@ class SyncDB:
|
||||
row = cur.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
def get_goods_integrity(self, system_name: str, start: datetime, end: datetime) -> dict[str, int]:
|
||||
"""商品数据完整性(与核验同一时间窗,按 orderedAt):
|
||||
- orders: 窗口内订单数
|
||||
- goods: 窗口内商品行数
|
||||
- zeroGoodsOrders: 窗口内没有任何商品行的订单数(可疑缺拉)
|
||||
- goodsNoName: 窗口内商品 itemName 为空的行数
|
||||
- orphanGoods: 全库没有对应订单的商品行数(脏数据)
|
||||
"""
|
||||
with self._conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
'SELECT COUNT(*), '
|
||||
'COALESCE(SUM(CASE WHEN NOT EXISTS ('
|
||||
' SELECT 1 FROM "Goods" g WHERE g."orderNo"=o."orderNo" AND g."createdAt"=o."createdAt"'
|
||||
') THEN 1 ELSE 0 END), 0) '
|
||||
'FROM "Orders" o WHERE o."systemName"=%s AND o."orderedAt">=%s AND o."orderedAt"<%s',
|
||||
(system_name, start, end),
|
||||
)
|
||||
orders, zero_goods = cur.fetchone()
|
||||
cur.execute(
|
||||
'SELECT COUNT(*), COALESCE(SUM(CASE WHEN "itemName" IS NULL OR "itemName"=\'\' '
|
||||
'THEN 1 ELSE 0 END), 0) FROM "Goods" g JOIN "Orders" o '
|
||||
'ON g."orderNo"=o."orderNo" AND g."createdAt"=o."createdAt" '
|
||||
'WHERE o."systemName"=%s AND o."orderedAt">=%s AND o."orderedAt"<%s',
|
||||
(system_name, start, end),
|
||||
)
|
||||
goods, no_name = cur.fetchone()
|
||||
cur.execute(
|
||||
'SELECT COUNT(*) FROM "Goods" g WHERE NOT EXISTS '
|
||||
'(SELECT 1 FROM "Orders" o WHERE o."orderNo"=g."orderNo")',
|
||||
)
|
||||
orphan = cur.fetchone()[0]
|
||||
return {
|
||||
"orders": int(orders), "goods": int(goods),
|
||||
"zeroGoodsOrders": int(zero_goods), "goodsNoName": int(no_name), "orphanGoods": int(orphan),
|
||||
}
|
||||
|
||||
def ping(self) -> str:
|
||||
"""连通性检查,返回 PG 版本号(供监控探活)。"""
|
||||
with self._conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT version()")
|
||||
row = cur.fetchone()
|
||||
return (row[0] or "").split(",")[0] if row else ""
|
||||
|
||||
def get_orders_with_100_prefix(self, system_name: str, statuses: list[str]) -> list[tuple[str, datetime]]:
|
||||
with self._conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""企业微信智能机器人通知层(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
|
||||
Reference in New Issue
Block a user