diff --git a/.github/repository-metrics-traffic.json b/.github/repository-metrics-traffic.json new file mode 100644 index 0000000..2211b7d --- /dev/null +++ b/.github/repository-metrics-traffic.json @@ -0,0 +1,7 @@ +{ + "unique_visitors_14d": 0, + "views_14d": 0, + "unique_cloners_14d": 11, + "clones_14d": 12, + "traffic_as_of": "2026-08-10T00:00:00Z" +} diff --git a/.github/workflows/repository-metrics.yml b/.github/workflows/repository-metrics.yml index 9110ba0..fe6e004 100644 --- a/.github/workflows/repository-metrics.yml +++ b/.github/workflows/repository-metrics.yml @@ -25,6 +25,7 @@ jobs: run: >- python3 scripts/render_repository_metrics.py --repository "$GITHUB_REPOSITORY" + --traffic-snapshot .github/repository-metrics-traffic.json --output "$RUNNER_TEMP/repository-metrics.svg" - name: Publish generated image to the metrics branch run: | diff --git a/README.md b/README.md index b4caeb4..b6513f6 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ credential store, and deterministic per-agent projections.
-Auto-refreshed after every new star and once a week. GitHub Traffic uses the rolling 14-day owner view. No long-lived token is embedded in this README. +Stars, forks, and commits refresh after every new star and once a week. Traffic cards use the dated aggregate owner snapshot shown in the chart because GitHub does not grant its Actions token access to repository Traffic. No long-lived token is embedded in this README. The native app follows the macOS language preference and ships complete **English** and **Simplified Chinese** interface catalogs. diff --git a/README.zh-CN.md b/README.zh-CN.md index 0af7816..5f66000 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -66,7 +66,7 @@ Agent Switch 用一个本地 MCP 注册表、一个私密凭据存储和可重 -每次获得新 Star 后自动刷新,并每周定时更新。GitHub Traffic 使用仓库所有者可见的 14 天滚动窗口;README 中不嵌入任何长期 Token。 +Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。由于 GitHub 不允许 Actions Token 读取仓库 Traffic,流量卡片使用图中注明日期的仓库所有者聚合快照;README 中不嵌入任何长期 Token。 原生应用会跟随 macOS 的语言偏好,并完整提供**英文**与**简体中文**界面。 diff --git a/scripts/render_repository_metrics.py b/scripts/render_repository_metrics.py index 758d2c7..7d2e371 100755 --- a/scripts/render_repository_metrics.py +++ b/scripts/render_repository_metrics.py @@ -12,6 +12,7 @@ from pathlib import Path import re from typing import Any +from urllib.error import HTTPError from urllib.parse import quote from urllib.request import Request, urlopen @@ -34,7 +35,11 @@ def _request_json(path: str, token: str, *, accept: str = "application/vnd.githu return json.load(response), response.headers -def fetch_snapshot(repository: str, token: str) -> dict[str, Any]: +def fetch_snapshot( + repository: str, + token: str, + traffic_fallback: dict[str, Any] | None = None, +) -> dict[str, Any]: if not token: raise RuntimeError("GITHUB_TOKEN is required when --snapshot is not supplied") if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository): @@ -42,8 +47,40 @@ def fetch_snapshot(repository: str, token: str) -> dict[str, Any]: encoded = "/".join(quote(part, safe="") for part in repository.split("/", 1)) repo, _ = _request_json(f"/repos/{encoded}", token) - views, _ = _request_json(f"/repos/{encoded}/traffic/views", token) - clones, _ = _request_json(f"/repos/{encoded}/traffic/clones", token) + generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") + traffic_live = True + try: + views, _ = _request_json(f"/repos/{encoded}/traffic/views", token) + clones, _ = _request_json(f"/repos/{encoded}/traffic/clones", token) + traffic = { + "unique_visitors_14d": int(views["uniques"]), + "views_14d": int(views["count"]), + "unique_cloners_14d": int(clones["uniques"]), + "clones_14d": int(clones["count"]), + "traffic_as_of": generated_at, + } + except HTTPError as error: + error.close() + if error.code != 403 or traffic_fallback is None: + raise + required = { + "unique_visitors_14d", + "views_14d", + "unique_cloners_14d", + "clones_14d", + "traffic_as_of", + } + missing = sorted(required - traffic_fallback.keys()) + if missing: + raise ValueError(f"traffic snapshot is missing: {', '.join(missing)}") from error + traffic_live = False + traffic = { + "unique_visitors_14d": int(traffic_fallback["unique_visitors_14d"]), + "views_14d": int(traffic_fallback["views_14d"]), + "unique_cloners_14d": int(traffic_fallback["unique_cloners_14d"]), + "clones_14d": int(traffic_fallback["clones_14d"]), + "traffic_as_of": str(traffic_fallback["traffic_as_of"]), + } _, commit_headers = _request_json(f"/repos/{encoded}/commits?per_page=1", token) link = commit_headers.get("Link", "") @@ -70,14 +107,12 @@ def fetch_snapshot(repository: str, token: str) -> dict[str, Any]: return { "repository": repository, "created_at": repo["created_at"], - "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"), + "generated_at": generated_at, "stars": int(repo["stargazers_count"]), "forks": int(repo["forks_count"]), "commits": commit_count, - "unique_visitors_14d": int(views["uniques"]), - "views_14d": int(views["count"]), - "unique_cloners_14d": int(clones["uniques"]), - "clones_14d": int(clones["count"]), + **traffic, + "traffic_live": traffic_live, "starred_at": sorted(starred_at), } @@ -171,6 +206,11 @@ def y_for(value: int) -> float: empty_note = '