first commit

This commit is contained in:
dekun
2026-08-01 10:33:19 +08:00
commit d9a34d4f20
72 changed files with 5499 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
from packages.notify import wecom
__all__ = ["wecom"]
+163
View File
@@ -0,0 +1,163 @@
"""企业微信群机器人通知(独立实现,不依赖策略仓)。"""
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