Files
market_intel/packages/notify/wecom.py
T
2026-08-01 10:33:19 +08:00

164 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""企业微信群机器人通知(独立实现,不依赖策略仓)。"""
from __future__ import annotations
import logging
import time
from typing import Any
import httpx
from packages.config import get_settings
log = logging.getLogger("notify.wecom")
_last_fault_key: str | None = None
_last_fault_ms: float = 0.0
_fault_active: bool = False
def _as_bool(raw: Any, default: bool = False) -> bool:
if raw is None or raw == "":
return default
return str(raw).strip().lower() in ("1", "true", "yes", "on")
def wecom_enabled() -> bool:
s = get_settings()
return _as_bool(getattr(s, "wecom_enabled", False))
def wecom_webhook_url() -> str:
s = get_settings()
return (getattr(s, "wecom_webhook_url", "") or "").strip()
def wecom_machine_name() -> str:
s = get_settings()
return (getattr(s, "wecom_machine_name", "") or "").strip()[:64]
def alert_fail_threshold() -> int:
s = get_settings()
try:
return max(1, int(getattr(s, "alert_fail_threshold", 5) or 5))
except (TypeError, ValueError):
return 5
def build_markdown(*, tag: str, title: str, lines: list[str] | None = None) -> str:
machine = wecom_machine_name()
prefix = f"{machine}" if machine else ""
parts = [
f"## {prefix}{title}",
f"> **标识**: `{tag}`",
f"> **系统**: 比特骆驼行情采集分析",
f"> **时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}",
]
if machine:
parts.append(f"> **机器**: {machine}")
if lines:
parts.append("")
for ln in lines:
parts.append(f"> {ln}" if not ln.startswith(">") else ln)
return "\n".join(parts)
def post_markdown(content: str) -> tuple[bool, str]:
if not wecom_enabled():
return False, "未开启企业微信通知"
url = wecom_webhook_url()
if not url:
return False, "未配置 Webhook"
raw = content.encode("utf-8")
if len(raw) > 4000:
content = raw[:3900].decode("utf-8", errors="ignore") + "\n"
payload = {"msgtype": "markdown", "markdown": {"content": content}}
try:
with httpx.Client(timeout=10.0) as client:
r = client.post(url, json=payload)
body = r.json() if r.content else {}
if r.status_code != 200 or str(body.get("errcode", 0)) not in ("0", "0.0"):
return False, f"webhook failed status={r.status_code} body={body}"
return True, "ok"
except Exception as e: # noqa: BLE001
return False, str(e)
def notify_test() -> tuple[bool, str]:
md = build_markdown(
tag="TEST",
title="行情采集分析 · 测试推送",
lines=["这是一条测试消息,说明企微 Webhook 可用。"],
)
return post_markdown(md)
def notify_collector_fault(
*,
error: str,
consecutive_failures: int,
dedup_sec: float = 300.0,
) -> tuple[bool, str]:
"""连续失败告警;同错误键在 dedup_sec 内不重复推。"""
global _last_fault_key, _last_fault_ms, _fault_active
threshold = alert_fail_threshold()
if consecutive_failures < threshold:
return False, f"below threshold ({consecutive_failures}<{threshold})"
key = f"{consecutive_failures // threshold}:{(error or '')[:120]}"
now = time.time()
if (
_last_fault_key == key
and (now - _last_fault_ms) < dedup_sec
):
return False, "dedup"
_last_fault_key = key
_last_fault_ms = now
_fault_active = True
# 脱敏:避免日志/推送里出现完整密钥形态串
err_show = (error or "unknown").replace("\n", " ")[:300]
md = build_markdown(
tag="FAULT",
title="行情采集异常",
lines=[
f"**连续失败**: {consecutive_failures}(阈值 {threshold}",
f"**错误**: {err_show}",
"请检查 OKX 连通性 / 代理 / 合约是否可交易。",
],
)
ok, msg = post_markdown(md)
if ok:
log.info("wecom fault notified failures=%s", consecutive_failures)
else:
log.warning("wecom fault notify failed: %s", msg)
return ok, msg
def notify_collector_recovered(*, consecutive_failures: int = 0) -> tuple[bool, str]:
global _fault_active, _last_fault_key
if not _fault_active:
return False, "no active fault"
_fault_active = False
_last_fault_key = None
md = build_markdown(
tag="RECOVER",
title="行情采集已恢复",
lines=["采样已恢复正常。"],
)
ok, msg = post_markdown(md)
if ok:
log.info("wecom recovered notified")
else:
log.warning("wecom recover notify failed: %s", msg)
return ok, msg
def reset_alert_state() -> None:
"""测试用。"""
global _last_fault_key, _last_fault_ms, _fault_active
_last_fault_key = None
_last_fault_ms = 0.0
_fault_active = False