v0.5.84: matches origin filter, mobile gate, refresh reliability.

Ship Web refresh cache/lock, mobile demand gate, matches 职业/国服 filter, and related site updates through 0.5.84.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 18:31:55 +08:00
co-authored by Cursor
parent 7681fdb069
commit b01552ee6e
50 changed files with 3406 additions and 487 deletions
+317 -32
View File
@@ -20,12 +20,14 @@ Usage:
from __future__ import annotations
import argparse
import io
import json
import os
import re
import sys
import urllib.error
import urllib.request
import zipfile
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
@@ -155,6 +157,33 @@ def http_json_soft(
return e.code, raw
def http_response_soft(
url: str,
*,
headers: dict | None = None,
) -> tuple[int, bytes, str, dict[str, str]]:
request_headers = {"User-Agent": "climperor-monitor", "Accept": "*/*"}
request_headers.update(headers or {})
req = urllib.request.Request(url, headers=request_headers)
try:
with urllib.request.urlopen(req, timeout=60) as response:
return (
response.status,
response.read(),
response.headers.get_content_type(),
{k.lower(): v for k, v in response.headers.items()},
)
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError) as exc:
if isinstance(exc, urllib.error.HTTPError):
return (
exc.code,
exc.read(),
exc.headers.get_content_type(),
{k.lower(): v for k, v in exc.headers.items()},
)
return 0, str(exc).encode("utf-8", errors="replace"), "", {}
def gql(query: str, variables: dict) -> dict:
data = http_json(
"https://api.cloudflare.com/client/v4/graphql",
@@ -234,6 +263,18 @@ class DigestExtras:
workflows: list[WorkflowDayStatus] = field(default_factory=list)
deploys: list[PagesDeploy] = field(default_factory=list)
gitea_note: str = ""
refresh_summaries: dict[str, dict] = field(default_factory=dict)
production_health: "ProductionHealth | None" = None
@dataclass
class ProductionHealth:
site_version: str = "?"
ages_hours: dict[str, float | None] = field(default_factory=dict)
stale: list[str] = field(default_factory=list)
live_api_ok: bool = False
refresh_run_id: str = ""
note: str = ""
def zone_id() -> str:
@@ -437,6 +478,213 @@ def fetch_workflow_statuses(day: date) -> tuple[list[WorkflowDayStatus], str]:
return out, ""
def fetch_refresh_summaries(workflows: list[WorkflowDayStatus]) -> tuple[dict[str, dict], str]:
"""Download summary.json artifacts for the latest visible refresh runs."""
token = gitea_token()
if not token:
return {}, "无 Gitea token,未读取刷新摘要"
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
code, payload = http_json_soft(
f"{gitea_base()}/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}/actions/artifacts?limit=100",
headers=headers,
)
if code != 200 or not isinstance(payload, dict):
return {}, f"刷新摘要 API HTTP {code}"
artifacts = payload.get("artifacts") or []
by_run: dict[int, dict] = {}
for artifact in artifacts:
if not isinstance(artifact, dict) or artifact.get("expired"):
continue
run = artifact.get("workflow_run") or {}
try:
run_id = int(run.get("id") or artifact.get("workflow_run_id") or 0)
except (TypeError, ValueError):
continue
if run_id:
by_run[run_id] = artifact
summaries: dict[str, dict] = {}
for workflow in workflows:
latest = None
for run in workflow.runs:
try:
run_id = int(run.get("id") or 0)
except (TypeError, ValueError):
continue
if run.get("status") == "completed" and run_id in by_run:
latest = run
break
if not latest:
continue
artifact = by_run[int(latest["id"])]
artifact_id = artifact.get("id")
if not artifact_id:
continue
status, raw, _, _ = http_response_soft(
f"{gitea_base()}/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}/actions/artifacts/"
f"{artifact_id}/zip",
headers=headers,
)
if status != 200:
continue
try:
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
member = next(
name for name in archive.namelist() if name.endswith("summary.json")
)
summary = json.loads(archive.read(member).decode("utf-8"))
if isinstance(summary, dict):
summaries[workflow.file] = summary
except (ValueError, KeyError, StopIteration, zipfile.BadZipFile, json.JSONDecodeError):
continue
return summaries, ""
def _nested_timestamp(payload: dict, *path: str) -> datetime | None:
value: Any = payload
for key in path:
if not isinstance(value, dict):
return None
value = value.get(key)
return parse_gitea_time(value)
def _summary_proves_refresh(summary: dict, label: str, step: str) -> bool:
if not summary.get("ok"):
return False
step_ok = any(
isinstance(row, dict)
and row.get("step") == step
and row.get("ok")
for row in (summary.get("steps") or [])
)
if not step_ok:
return False
health = summary.get("health") or {}
if label == "主播资料":
return not (
int(health.get("streamer_profile_missing") or 0)
or int(health.get("streamer_live_probe_missing") or 0)
)
if label == "STRATZ Meta":
return not health.get("stratz_meta_stale")
if label == "STRATZ 对位":
return int(health.get("stratz_matchup_stale") or 0) == 0
return True
def fetch_production_health(
now: datetime | None = None,
refresh_summaries: dict[str, dict] | None = None,
) -> ProductionHealth:
"""Probe production payload freshness and the live-status Function."""
current = now or datetime.now(timezone.utc)
health = ProductionHealth()
status, config_raw, _, _ = http_response_soft(f"{SITE_URL}/config.js")
if status == 200:
match = re.search(
rb'SITE_VERSION\s*=\s*"([^"]+)"',
config_raw,
)
if match:
health.site_version = match.group(1).decode("utf-8", errors="replace")
status, data_raw, _, _ = http_response_soft(f"{SITE_URL}/data.json")
if status != 200:
health.note = f"生产 data.json HTTP {status}"
health.stale.append("data.json")
return health
try:
payload = json.loads(data_raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
health.note = "生产 data.json 无法解析"
health.stale.append("data.json")
return health
health.refresh_run_id = str((payload.get("meta") or {}).get("refresh_run_id") or "")
summaries = refresh_summaries or {}
checks = {
"英雄统计": (
("hero_stats", "fetched_at"),
36.0,
"web-daily.yml",
"fetch_hero_stats.py",
),
"排行榜": (
("leaderboards", "fetched_at"),
36.0,
"web-daily.yml",
"fetch_leaderboards.py",
),
"近期比赛": (
("hero_matches", "meta", "fetched_at"),
36.0,
"web-daily.yml",
"fetch_hero_matches.py",
),
"明星比赛": (
("pro_matches", "meta", "fetched_at"),
36.0,
"web-daily.yml",
"fetch_pro_matches.py",
),
"主播资料": (
("streamers", "fetched_at"),
36.0,
"web-daily.yml",
"fetch_streamers.py",
),
"STRATZ Meta": (
("stratz_hero_meta", "fetched_at"),
216.0,
"web-weekly.yml",
"fetch_stratz_meta.py",
),
"STRATZ 对位": (
("stratz_matchup_tops", "fetched_at"),
216.0,
"web-weekly.yml",
"fetch_stratz_meta.py",
),
"英雄装备": (
("hero_items", "meta", "fetched_at"),
216.0,
"web-weekly.yml",
"fetch_hero_items.py",
),
}
for label, (path, max_age, workflow_file, step) in checks.items():
timestamp = _nested_timestamp(payload, *path)
age = None
if timestamp is not None:
age = max(0.0, (current - timestamp.astimezone(timezone.utc)).total_seconds() / 3600)
health.ages_hours[label] = age
latest_refresh_ok = _summary_proves_refresh(
summaries.get(workflow_file) or {},
label,
step,
)
if (age is None or age > max_age) and not latest_refresh_ok:
health.stale.append(label)
live_status, live_raw, live_type, live_headers = http_response_soft(
f"{SITE_URL}/api/live-status"
)
try:
live_payload = json.loads(live_raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
live_payload = None
health.live_api_ok = (
live_status == 200
and live_type == "application/json"
and isinstance(live_payload, dict)
and live_headers.get("x-live-cache") not in ("error", "stale-override")
)
if not health.live_api_ok:
health.stale.append("直播状态 API")
return health
def cf_account_id() -> str:
d = http_json(
"https://api.cloudflare.com/client/v4/accounts",
@@ -495,7 +743,7 @@ def fmt_bytes(n: int) -> str:
def _fmt_workflow_line(w: WorkflowDayStatus) -> str:
if w.note and not w.runs:
return f"- **{w.label}**`{w.file}`{w.note}"
return f"- **{w.label}**{w.note}"
parts: list[str] = []
if w.ok_count:
parts.append(f"成功 {w.ok_count}")
@@ -528,49 +776,91 @@ def build_card(st: DayStats, extras: DigestExtras) -> dict:
estimate = st.human_home_vis
rum = st.rum_human
# Human estimate = known-browser UA hits on `/` (SPA hash routes are not paths).
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"
f"**非人类首页**{st.other_home_vis} visits\n"
f"**地区 Top**{countries}\n"
f"**状态码**{status_line}"
)
wf_lines = [_fmt_workflow_line(w) for w in extras.workflows]
if extras.gitea_note:
wf_lines.append(f"- _{extras.gitea_note}_")
for workflow_file, summary in extras.refresh_summaries.items():
label = next(
(label for file, label in REFRESH_WORKFLOWS if file == workflow_file),
workflow_file,
)
result = "已部署" if summary.get("deployed") else "无需部署"
if not summary.get("ok"):
result = "刷新失败"
stale_count = int(
(summary.get("health") or {}).get("stratz_matchup_stale") or 0
)
live_missing = int(
(summary.get("health") or {}).get("streamer_live_probe_missing") or 0
)
wf_lines.append(
f"- **{label}摘要**{result}"
+ (f" · 对位陈旧 {stale_count}" if stale_count else "")
+ (f" · 直播探测缺失 {live_missing}" if live_missing else "")
)
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)}**"
deploy_lines = [
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"
mark = "ok" if p.status == "success" else "!"
deploy_lines.append(
f"- [{mark}] {p.created_on.strftime('%H:%M')} "
f"`{p.commit or '?'}` {p.status}"
)
deploy_md = "\n".join(deploy_lines)
else:
deploy_md = "**Pages 生产部署**:当日无新部署(数据未变则 refresh 会跳过 deploy\n"
deploy_md = "**Pages**:当日无新部署"
health = extras.production_health
production_lines: list[str] = []
if health:
newest = [
f"{label} {age:.1f}h"
for label, age in health.ages_hours.items()
if age is not None
]
production_lines.append(
f"**生产探针**{'正常' if not health.stale else '异常'}"
f" · live API {'正常' if health.live_api_ok else '失败'}"
)
if newest:
production_lines.append("- " + " / ".join(newest))
if health.stale:
production_lines.append("- **超时/异常**" + "".join(health.stale))
if health.refresh_run_id:
production_lines.append(f"- run `{health.refresh_run_id}`")
refresh_md = (
f"**站点版本**`v{extras.site_version}`\n\n"
f"**自动刷新(Gitea Actions**\n"
f"**版本**`v{extras.site_version}`\n"
f"**Actions**\n"
+ "\n".join(wf_lines)
+ "\n\n"
+ deploy_md.strip()
+ "\n"
+ deploy_md
+ ("\n" + "\n".join(production_lines) if production_lines else "")
)
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
)
any_refresh_fail = any_refresh_fail or bool(health and health.stale)
if any_refresh_fail:
header_color = "red"
@@ -586,18 +876,7 @@ def build_card(st: DayStats, extras: DigestExtras) -> dict:
},
{
"tag": "div",
"text": {"tag": "lark_md", "content": "**自动更新**\n" + refresh_md},
},
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": (
f"**首页地区 Top**{countries}\n"
f"**状态码**{status_line}\n"
f"_模块:英雄 / 排行 / 主播 / 走势 / 物品 / 版本 / 机制_"
),
},
"text": {"tag": "lark_md", "content": "**刷新与部署**\n" + refresh_md},
},
{
"tag": "note",
@@ -605,8 +884,7 @@ def build_card(st: DayStats, extras: DigestExtras) -> dict:
{
"tag": "plain_text",
"content": (
f"{HOST} · Free 档无 BotScore · "
f"refresh=web-daily/weekly/patch · {now}"
f"{HOST} · 估数=已知浏览器 UA 打开 / · {now}"
),
}
],
@@ -674,15 +952,22 @@ def main(argv: list[str] | None = None) -> int:
args = ap.parse_args(argv)
day = parse_day(args.day)
refresh_day = datetime.now(CST).date()
zid = zone_id()
st = fetch_day(zid, day)
workflows, gitea_note = fetch_workflow_statuses(day)
workflows, gitea_note = fetch_workflow_statuses(refresh_day)
refresh_summaries, summary_note = fetch_refresh_summaries(workflows)
if summary_note:
gitea_note = "".join(x for x in (gitea_note, summary_note) if x)
deploys = fetch_pages_deploys(day)
production_health = fetch_production_health(refresh_summaries=refresh_summaries)
extras = DigestExtras(
site_version=read_site_version(),
site_version=production_health.site_version,
workflows=workflows,
deploys=deploys,
gitea_note=gitea_note,
refresh_summaries=refresh_summaries,
production_health=production_health,
)
payload = build_card(st, extras)