Files
daily-robots/daily/webhook.py
2026-07-09 11:37:55 +08:00

128 lines
4.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.

"""推送早报至企业微信群 webhook超长自动分多条"""
from __future__ import annotations
import json
import re
import sys
import time
from pathlib import Path
import certifi
import httpx
from daily.config import OUTPUT_DIR, ROOT, env_int, wecom_chunk_bytes
from daily.wecom_split import split_wecom_messages
_PUSH_GAP_MS = 300
_DATE_RE = re.compile(r"(\d{4}-\d{2}-\d{2})\.wecom\.md$")
def _load_webhook_key() -> str:
from daily.config import env
key = (env("WECOM_WEBHOOK_KEY") or "").strip()
if not key:
raise RuntimeError("请设置 WECOM_WEBHOOK_KEY项目根 .env")
return key
def _resolve_report_path(arg: str | None) -> Path:
if arg:
path = Path(arg)
if not path.is_absolute():
path = ROOT / path
return path
candidates = sorted(
OUTPUT_DIR.glob("*.wecom.md"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if candidates:
return candidates[0]
legacy = sorted(ROOT.glob("*.wecom.md"), key=lambda p: p.stat().st_mtime, reverse=True)
if legacy:
return legacy[0]
raise RuntimeError("未找到 .wecom.md 报告,请先运行 python -m daily")
def _push_gate_for_report(path: Path) -> dict | None:
match = _DATE_RE.search(path.name)
if not match:
return None
data_path = path.parent / f"{match.group(1)}.data.json"
if not data_path.exists():
data_path = OUTPUT_DIR / f"{match.group(1)}.data.json"
if not data_path.exists():
return None
try:
payload = json.loads(data_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
meta = payload.get("meta") or {}
gate = meta.get("push_gate")
return gate if isinstance(gate, dict) else None
def should_skip_push(report_path: Path) -> bool:
gate = _push_gate_for_report(report_path)
if not gate:
return False
return bool(gate.get("silent")) and not gate.get("should_push")
def _post_markdown(client: httpx.Client, url: str, content: str) -> None:
payload = {"msgtype": "markdown", "markdown": {"content": content}}
resp = client.post(url, json=payload)
resp.raise_for_status()
data = resp.json()
if data.get("errcode", 0) != 0:
raise RuntimeError(f"推送失败: errcode={data.get('errcode')} errmsg={data.get('errmsg')}")
def send_report(report_path: Path | None = None) -> None:
path = _resolve_report_path(str(report_path) if report_path else None)
if should_skip_push(path):
print(f"[silent] no push gate matched for {path.name}")
return
if not path.exists():
raise RuntimeError(f"报告文件不存在: {path}")
content = path.read_text(encoding="utf-8").strip()
if not content:
raise RuntimeError(f"报告内容为空: {path}")
chunk_limit = wecom_chunk_bytes()
max_parts = env_int("DAILY_WECOM_MAX_PARTS", 5)
parts = split_wecom_messages(content, chunk_limit)
if len(parts) > max_parts:
raise RuntimeError(
f"早报需 {len(parts)} 条消息,超过 DAILY_WECOM_MAX_PARTS={max_parts},请调小各区块条数"
)
key = _load_webhook_key()
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}"
with httpx.Client(timeout=20.0, verify=certifi.where()) as client:
for i, part in enumerate(parts, 1):
if i > 1:
time.sleep(_PUSH_GAP_MS / 1000.0)
_post_markdown(client, url, part)
total_bytes = len(content.encode("utf-8"))
if len(parts) == 1:
print(f"已推送至企业微信: {path.name} ({total_bytes} bytes)")
else:
sizes = ", ".join(str(len(p.encode("utf-8"))) for p in parts)
print(f"已推送至企业微信: {path.name} ({total_bytes} bytes → {len(parts)} 条: {sizes})")
def main(argv: list[str] | None = None) -> int:
args = argv if argv is not None else sys.argv[1:]
try:
send_report(Path(args[0]) if args else None)
return 0
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1