Enrich Feishu daily digest with auto-refresh status.
Include Gitea web-daily/weekly/patch conclusions and Cloudflare Pages deploys; ship the refresh workflows so those signals exist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+386
-39
@@ -1,18 +1,20 @@
|
||||
"""Daily site traffic digest -> Feishu webhook.
|
||||
"""Daily site digest -> Feishu webhook.
|
||||
|
||||
Pulls Cloudflare edge analytics for dota2.refining.dev, estimates human
|
||||
homepage visits (known browser UA on `/`, plus RUM beacons), and posts a
|
||||
Feishu custom-bot card.
|
||||
1) Cloudflare edge analytics for dota2.refining.dev (human homepage estimate)
|
||||
2) Auto-refresh status: Gitea Actions (web-daily / web-weekly / web-patch)
|
||||
+ Cloudflare Pages production deploys that day
|
||||
|
||||
Credentials (env, prefer keyzoo inject names):
|
||||
CLOUDFLARE_EMAIL / KEYZOO_ASSET_META_USERNAME
|
||||
CLOUDFLARE_API_KEY / KEYZOO_ASSET_SECRET_GLOBAL_API_KEY
|
||||
FEISHU_WEBHOOK_URL / KEYZOO_ASSET_SECRET_FEISHU_WEBHOOK_URL
|
||||
GITEA_TOKEN / KEYZOO_ASSET_SECRET_PERSONAL_ACCESS_TOKEN_GITEA_1 (optional;
|
||||
without it the refresh section shows \"未配置 token\")
|
||||
|
||||
Usage:
|
||||
python notify_site_traffic.py # yesterday (UTC date)
|
||||
python notify_site_traffic.py # yesterday CST
|
||||
python notify_site_traffic.py --day 2026-07-27
|
||||
python notify_site_traffic.py --dry-run # print card, do not POST
|
||||
python notify_site_traffic.py --dry-run
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,16 +22,30 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
HOST = "dota2.refining.dev"
|
||||
ZONE_NAME = "refining.dev"
|
||||
SITE_URL = "https://dota2.refining.dev"
|
||||
PAGES_PROJECT = "climperor-relations"
|
||||
GITEA_URL_DEFAULT = "https://gitea.refining.dev"
|
||||
GITEA_OWNER = "refining"
|
||||
GITEA_REPO = "climperor"
|
||||
CST = timezone(timedelta(hours=8))
|
||||
|
||||
# (workflow file basename, short label)
|
||||
REFRESH_WORKFLOWS: tuple[tuple[str, str], ...] = (
|
||||
("web-daily.yml", "每日数据"),
|
||||
("web-weekly.yml", "每周 Meta"),
|
||||
("web-patch.yml", "版本检测"),
|
||||
)
|
||||
|
||||
HUMAN_BROWSERS = frozenset(
|
||||
{
|
||||
@@ -81,17 +97,64 @@ def feishu_url() -> str:
|
||||
return url
|
||||
|
||||
|
||||
def http_json(url: str, *, method: str = "GET", body: dict | None = None, headers: dict | None = None) -> dict:
|
||||
def gitea_token() -> str | None:
|
||||
return _env(
|
||||
"GITEA_TOKEN",
|
||||
"KEYZOO_ASSET_SECRET_PERSONAL_ACCESS_TOKEN_GITEA_1",
|
||||
"KEYZOO_ASSET_SECRET_PERSONAL_ACCESS_TOKEN__GITEA_1",
|
||||
"KEYZOO_ASSET_TOKEN",
|
||||
)
|
||||
|
||||
|
||||
def gitea_base() -> str:
|
||||
return (
|
||||
_env("GITEA_URL", "KEYZOO_ASSET_META_URL") or GITEA_URL_DEFAULT
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def http_json(
|
||||
url: str,
|
||||
*,
|
||||
method: str = "GET",
|
||||
body: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
) -> dict | list:
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers or {})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read().decode())
|
||||
raw = r.read().decode()
|
||||
if not raw:
|
||||
return {}
|
||||
return json.loads(raw)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode(errors="replace")
|
||||
raise SystemExit(f"HTTP {e.code} {url}: {raw[:800]}") from e
|
||||
|
||||
|
||||
def http_json_soft(
|
||||
url: str,
|
||||
*,
|
||||
method: str = "GET",
|
||||
body: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
) -> tuple[int, Any]:
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers or {})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read().decode()
|
||||
if not raw:
|
||||
return r.status, {}
|
||||
return r.status, json.loads(raw)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode(errors="replace")
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return e.code, raw
|
||||
|
||||
|
||||
def gql(query: str, variables: dict) -> dict:
|
||||
data = http_json(
|
||||
"https://api.cloudflare.com/client/v4/graphql",
|
||||
@@ -99,11 +162,24 @@ def gql(query: str, variables: dict) -> dict:
|
||||
body={"query": query, "variables": variables},
|
||||
headers=cf_headers(),
|
||||
)
|
||||
if data.get("errors"):
|
||||
raise SystemExit("GraphQL errors: " + json.dumps(data["errors"], ensure_ascii=False)[:1500])
|
||||
if isinstance(data, dict) and data.get("errors"):
|
||||
raise SystemExit(
|
||||
"GraphQL errors: " + json.dumps(data["errors"], ensure_ascii=False)[:1500]
|
||||
)
|
||||
assert isinstance(data, dict)
|
||||
return data
|
||||
|
||||
|
||||
def read_site_version() -> str:
|
||||
path = Path(__file__).resolve().parent / "web" / "relations" / "config.js"
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return "?"
|
||||
m = re.search(r'SITE_VERSION\s*=\s*"([^"]+)"', text)
|
||||
return m.group(1) if m else "?"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DayStats:
|
||||
day: date
|
||||
@@ -115,15 +191,57 @@ class DayStats:
|
||||
rum_human: int = 0
|
||||
rum_other: int = 0
|
||||
bytes: int = 0
|
||||
top_countries: list[tuple[str, int, int]] = field(default_factory=list) # name, req, vis
|
||||
top_countries: list[tuple[str, int, int]] = field(default_factory=list)
|
||||
status: list[tuple[Any, int]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowDayStatus:
|
||||
file: str
|
||||
label: str
|
||||
runs: list[dict] = field(default_factory=list) # status/conclusion/started_at
|
||||
note: str = "" # e.g. missing token / none
|
||||
|
||||
@property
|
||||
def ok_count(self) -> int:
|
||||
return sum(1 for r in self.runs if r.get("conclusion") == "success")
|
||||
|
||||
@property
|
||||
def fail_count(self) -> int:
|
||||
return sum(
|
||||
1
|
||||
for r in self.runs
|
||||
if r.get("conclusion") not in (None, "success")
|
||||
and r.get("status") == "completed"
|
||||
)
|
||||
|
||||
@property
|
||||
def running_count(self) -> int:
|
||||
return sum(1 for r in self.runs if r.get("status") in ("queued", "in_progress", "waiting"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class PagesDeploy:
|
||||
created_on: datetime
|
||||
status: str
|
||||
commit: str
|
||||
url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class DigestExtras:
|
||||
site_version: str
|
||||
workflows: list[WorkflowDayStatus] = field(default_factory=list)
|
||||
deploys: list[PagesDeploy] = field(default_factory=list)
|
||||
gitea_note: str = ""
|
||||
|
||||
|
||||
def zone_id() -> str:
|
||||
d = http_json(
|
||||
f"https://api.cloudflare.com/client/v4/zones?name={ZONE_NAME}",
|
||||
headers=cf_headers(),
|
||||
)
|
||||
assert isinstance(d, dict)
|
||||
rows = d.get("result") or []
|
||||
if not rows:
|
||||
raise SystemExit(f"zone not found: {ZONE_NAME}")
|
||||
@@ -230,6 +348,142 @@ def fetch_day(zid: str, day: date) -> DayStats:
|
||||
return st
|
||||
|
||||
|
||||
def day_window_utc(day: date) -> tuple[datetime, datetime]:
|
||||
"""Inclusive start / exclusive end of CST calendar day, as aware UTC datetimes."""
|
||||
start_cst = datetime(day.year, day.month, day.day, tzinfo=CST)
|
||||
end_cst = start_cst + timedelta(days=1)
|
||||
return start_cst.astimezone(timezone.utc), end_cst.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def parse_gitea_time(s: str | None) -> datetime | None:
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
# Gitea may return +08:00 or Z
|
||||
if s.endswith("Z"):
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
||||
return datetime.fromisoformat(s)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def fetch_workflow_statuses(day: date) -> tuple[list[WorkflowDayStatus], str]:
|
||||
tok = gitea_token()
|
||||
if not tok:
|
||||
return (
|
||||
[
|
||||
WorkflowDayStatus(file=f, label=lab, note="未配置 GITEA_TOKEN")
|
||||
for f, lab in REFRESH_WORKFLOWS
|
||||
],
|
||||
"未配置 GITEA_TOKEN,跳过 Actions 查询",
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"token {tok}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
start_utc, end_utc = day_window_utc(day)
|
||||
code, payload = http_json_soft(
|
||||
f"{gitea_base()}/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}/actions/runs?limit=50",
|
||||
headers=headers,
|
||||
)
|
||||
if code != 200:
|
||||
return (
|
||||
[
|
||||
WorkflowDayStatus(file=f, label=lab, note=f"API {code}")
|
||||
for f, lab in REFRESH_WORKFLOWS
|
||||
],
|
||||
f"Gitea runs API HTTP {code}",
|
||||
)
|
||||
|
||||
if isinstance(payload, dict):
|
||||
items = payload.get("workflow_runs") or payload.get("runs") or []
|
||||
elif isinstance(payload, list):
|
||||
items = payload
|
||||
else:
|
||||
items = []
|
||||
|
||||
by_file: dict[str, list[dict]] = {f: [] for f, _ in REFRESH_WORKFLOWS}
|
||||
for x in items:
|
||||
path = str(x.get("path") or "")
|
||||
# path like "web-daily.yml@refs/heads/main"
|
||||
base = path.split("@", 1)[0]
|
||||
if base not in by_file:
|
||||
continue
|
||||
ts = parse_gitea_time(x.get("started_at") or x.get("completed_at"))
|
||||
if ts is None:
|
||||
continue
|
||||
ts_utc = ts.astimezone(timezone.utc)
|
||||
if not (start_utc <= ts_utc < end_utc):
|
||||
continue
|
||||
by_file[base].append(
|
||||
{
|
||||
"id": x.get("id"),
|
||||
"status": x.get("status"),
|
||||
"conclusion": x.get("conclusion"),
|
||||
"event": x.get("event"),
|
||||
"started_at": x.get("started_at"),
|
||||
"completed_at": x.get("completed_at"),
|
||||
"html_url": x.get("html_url"),
|
||||
}
|
||||
)
|
||||
|
||||
out: list[WorkflowDayStatus] = []
|
||||
for f, lab in REFRESH_WORKFLOWS:
|
||||
runs = by_file.get(f) or []
|
||||
note = "" if runs else "当日无运行"
|
||||
out.append(WorkflowDayStatus(file=f, label=lab, runs=runs, note=note))
|
||||
return out, ""
|
||||
|
||||
|
||||
def cf_account_id() -> str:
|
||||
d = http_json(
|
||||
"https://api.cloudflare.com/client/v4/accounts",
|
||||
headers=cf_headers(),
|
||||
)
|
||||
assert isinstance(d, dict)
|
||||
rows = d.get("result") or []
|
||||
if not rows:
|
||||
raise SystemExit("no Cloudflare accounts")
|
||||
return rows[0]["id"]
|
||||
|
||||
|
||||
def fetch_pages_deploys(day: date) -> list[PagesDeploy]:
|
||||
aid = cf_account_id()
|
||||
d = http_json(
|
||||
f"https://api.cloudflare.com/client/v4/accounts/{aid}/pages/projects/"
|
||||
f"{PAGES_PROJECT}/deployments",
|
||||
headers=cf_headers(),
|
||||
)
|
||||
assert isinstance(d, dict)
|
||||
start_utc, end_utc = day_window_utc(day)
|
||||
out: list[PagesDeploy] = []
|
||||
for x in d.get("result") or []:
|
||||
created = parse_gitea_time(x.get("created_on"))
|
||||
if created is None:
|
||||
continue
|
||||
created_utc = created.astimezone(timezone.utc)
|
||||
if not (start_utc <= created_utc < end_utc):
|
||||
continue
|
||||
if (x.get("environment") or "") != "production":
|
||||
continue
|
||||
stage = x.get("latest_stage") or {}
|
||||
trig = x.get("deployment_trigger") or {}
|
||||
meta = trig.get("metadata") or {}
|
||||
commit = str(meta.get("commit_hash") or "")[:10]
|
||||
out.append(
|
||||
PagesDeploy(
|
||||
created_on=created.astimezone(CST),
|
||||
status=str(stage.get("status") or "?"),
|
||||
commit=commit,
|
||||
url=str(x.get("url") or ""),
|
||||
)
|
||||
)
|
||||
out.sort(key=lambda p: p.created_on, reverse=True)
|
||||
return out
|
||||
|
||||
|
||||
def fmt_bytes(n: int) -> str:
|
||||
x = float(n)
|
||||
for unit in ("B", "KB", "MB", "GB"):
|
||||
@@ -239,39 +493,110 @@ def fmt_bytes(n: int) -> str:
|
||||
return f"{x:.1f} TB"
|
||||
|
||||
|
||||
def build_card(st: DayStats) -> dict:
|
||||
def _fmt_workflow_line(w: WorkflowDayStatus) -> str:
|
||||
if w.note and not w.runs:
|
||||
return f"- **{w.label}**(`{w.file}`):{w.note}"
|
||||
parts: list[str] = []
|
||||
if w.ok_count:
|
||||
parts.append(f"成功 {w.ok_count}")
|
||||
if w.fail_count:
|
||||
parts.append(f"失败 {w.fail_count}")
|
||||
if w.running_count:
|
||||
parts.append(f"进行中 {w.running_count}")
|
||||
if not parts:
|
||||
parts.append(w.note or "无结论")
|
||||
# Show latest conclusion time if any
|
||||
latest = ""
|
||||
if w.runs:
|
||||
r0 = w.runs[0]
|
||||
t = r0.get("completed_at") or r0.get("started_at") or ""
|
||||
if t:
|
||||
dt = parse_gitea_time(t)
|
||||
if dt:
|
||||
latest = " · 最近 " + dt.astimezone(CST).strftime("%H:%M")
|
||||
return f"- **{w.label}**:{' / '.join(parts)}{latest}"
|
||||
|
||||
|
||||
def build_card(st: DayStats, extras: DigestExtras) -> dict:
|
||||
"""Feishu interactive card (msg_type=interactive)."""
|
||||
countries = "、".join(
|
||||
f"{name} {vis}访/{req}次" for name, req, vis in st.top_countries[:5]
|
||||
) or "—"
|
||||
status_line = "、".join(f"{code}×{n}" for code, n in st.status[:6]) or "—"
|
||||
now = datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M CST")
|
||||
now = datetime.now(CST).strftime("%Y-%m-%d %H:%M CST")
|
||||
|
||||
# Free plan: no BotScore. Human ≈ known-browser homepage visits.
|
||||
estimate = st.human_home_vis
|
||||
rum = st.rum_human
|
||||
|
||||
traffic_md = (
|
||||
f"**真实访问(估)**:约 **{estimate}** 次首页打开\n"
|
||||
f"(已知浏览器 UA 打开 `/`;SPA Hash 路由不计路径;不含爬虫)\n\n"
|
||||
f"**RUM 页面浏览**:{rum}"
|
||||
+ (f" + {st.rum_other} 其他" if st.rum_other else "")
|
||||
+ "\n"
|
||||
f"**边缘总量**:{st.all_req} 请求 / {st.all_vis} visits / {fmt_bytes(st.bytes)}\n"
|
||||
f"**非人类首页命中**:{st.other_home_vis} visits"
|
||||
)
|
||||
|
||||
wf_lines = [_fmt_workflow_line(w) for w in extras.workflows]
|
||||
if extras.gitea_note:
|
||||
wf_lines.append(f"- _{extras.gitea_note}_")
|
||||
|
||||
ok_deploys = [p for p in extras.deploys if p.status == "success"]
|
||||
fail_deploys = [p for p in extras.deploys if p.status != "success"]
|
||||
if extras.deploys:
|
||||
deploy_md = (
|
||||
f"**Pages 生产部署**:成功 **{len(ok_deploys)}**"
|
||||
+ (f" / 异常 {len(fail_deploys)}" if fail_deploys else "")
|
||||
+ "\n"
|
||||
)
|
||||
for p in extras.deploys[:5]:
|
||||
mark = "[ok]" if p.status == "success" else "[!]"
|
||||
deploy_md += (
|
||||
f"- {mark} {p.created_on.strftime('%H:%M')} "
|
||||
f"`{p.commit or '?'}` {p.status}\n"
|
||||
)
|
||||
else:
|
||||
deploy_md = "**Pages 生产部署**:当日无新部署(数据未变则 refresh 会跳过 deploy)\n"
|
||||
|
||||
refresh_md = (
|
||||
f"**站点版本**:`v{extras.site_version}`\n\n"
|
||||
f"**自动刷新(Gitea Actions)**\n"
|
||||
+ "\n".join(wf_lines)
|
||||
+ "\n\n"
|
||||
+ deploy_md.strip()
|
||||
)
|
||||
|
||||
any_refresh_ok = any(w.ok_count > 0 for w in extras.workflows) or bool(ok_deploys)
|
||||
any_refresh_fail = any(w.fail_count > 0 for w in extras.workflows) or bool(
|
||||
fail_deploys
|
||||
)
|
||||
|
||||
if any_refresh_fail:
|
||||
header_color = "red"
|
||||
elif estimate > 0 or any_refresh_ok:
|
||||
header_color = "blue"
|
||||
else:
|
||||
header_color = "grey"
|
||||
|
||||
elements: list[dict] = [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": (
|
||||
f"**真实访问(估)**:约 **{estimate}** 次首页打开\n"
|
||||
f"(已知浏览器 UA 打开 `/`;不含 Unknown/Headless/爬虫)\n\n"
|
||||
f"**RUM 页面浏览**:{rum}(人类浏览器执行 JS)"
|
||||
+ (f" + {st.rum_other} 其他" if st.rum_other else "")
|
||||
+ "\n"
|
||||
f"**边缘总量**:{st.all_req} 请求 / {st.all_vis} visits / {fmt_bytes(st.bytes)}\n"
|
||||
f"**非人类首页命中**:{st.other_home_vis} visits"
|
||||
),
|
||||
},
|
||||
"text": {"tag": "lark_md", "content": "**访问**\n" + traffic_md},
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {"tag": "lark_md", "content": "**自动更新**\n" + refresh_md},
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": f"**首页地区 Top**:{countries}\n**状态码**:{status_line}",
|
||||
"content": (
|
||||
f"**首页地区 Top**:{countries}\n"
|
||||
f"**状态码**:{status_line}\n"
|
||||
f"_模块:英雄 / 排行 / 主播 / 走势 / 物品 / 版本 / 机制_"
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -280,7 +605,8 @@ def build_card(st: DayStats) -> dict:
|
||||
{
|
||||
"tag": "plain_text",
|
||||
"content": (
|
||||
f"{HOST} · Free 档无 BotScore,按 UA+路径估算 · {now}"
|
||||
f"{HOST} · Free 档无 BotScore · "
|
||||
f"refresh=web-daily/weekly/patch · {now}"
|
||||
),
|
||||
}
|
||||
],
|
||||
@@ -293,7 +619,13 @@ def build_card(st: DayStats) -> dict:
|
||||
"text": {"tag": "plain_text", "content": "打开站点"},
|
||||
"type": "primary",
|
||||
"url": SITE_URL,
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {"tag": "plain_text", "content": "Gitea Actions"},
|
||||
"type": "default",
|
||||
"url": f"{gitea_base()}/{GITEA_OWNER}/{GITEA_REPO}/actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -304,9 +636,9 @@ def build_card(st: DayStats) -> dict:
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": f"上分帝 Web 日活 · {st.day.isoformat()}",
|
||||
"content": f"上分帝 Web 日报 · {st.day.isoformat()}",
|
||||
},
|
||||
"template": "blue" if estimate > 0 else "grey",
|
||||
"template": header_color,
|
||||
},
|
||||
"elements": elements,
|
||||
},
|
||||
@@ -315,19 +647,19 @@ def build_card(st: DayStats) -> dict:
|
||||
|
||||
def post_feishu(payload: dict) -> dict:
|
||||
url = feishu_url()
|
||||
return http_json(
|
||||
resp = http_json(
|
||||
url,
|
||||
method="POST",
|
||||
body=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert isinstance(resp, dict)
|
||||
return resp
|
||||
|
||||
|
||||
def parse_day(s: str | None) -> date:
|
||||
if not s:
|
||||
# Yesterday in China time (site audience).
|
||||
cst = timezone(timedelta(hours=8))
|
||||
return (datetime.now(cst) - timedelta(days=1)).date()
|
||||
return (datetime.now(CST) - timedelta(days=1)).date()
|
||||
return date.fromisoformat(s)
|
||||
|
||||
|
||||
@@ -344,11 +676,23 @@ def main(argv: list[str] | None = None) -> int:
|
||||
day = parse_day(args.day)
|
||||
zid = zone_id()
|
||||
st = fetch_day(zid, day)
|
||||
payload = build_card(st)
|
||||
workflows, gitea_note = fetch_workflow_statuses(day)
|
||||
deploys = fetch_pages_deploys(day)
|
||||
extras = DigestExtras(
|
||||
site_version=read_site_version(),
|
||||
workflows=workflows,
|
||||
deploys=deploys,
|
||||
gitea_note=gitea_note,
|
||||
)
|
||||
payload = build_card(st, extras)
|
||||
|
||||
print(
|
||||
f"{HOST} {day}: human_home_vis={st.human_home_vis} "
|
||||
f"rum_human={st.rum_human} all_req={st.all_req}",
|
||||
f"rum_human={st.rum_human} all_req={st.all_req} "
|
||||
f"deploys={len(deploys)} "
|
||||
f"wf_ok={sum(w.ok_count for w in workflows)} "
|
||||
f"wf_fail={sum(w.fail_count for w in workflows)} "
|
||||
f"v={extras.site_version}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
@@ -357,10 +701,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 0
|
||||
|
||||
resp = post_feishu(payload)
|
||||
# Feishu returns {code:0, msg:success} on OK
|
||||
code = resp.get("code", resp.get("StatusCode"))
|
||||
if code not in (0, None) and code != 200:
|
||||
print("Feishu error:", json.dumps(resp, ensure_ascii=False)[:800], file=sys.stderr)
|
||||
print(
|
||||
"Feishu error:",
|
||||
json.dumps(resp, ensure_ascii=False)[:800],
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
print("feishu ok:", json.dumps(resp, ensure_ascii=False)[:200])
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user