热点抓取 → 选题 → 框架 → 写作 → SEO → 视觉AI → 排版 → 微信草稿箱, 一句话触发完整流程。适用于 Claude Code skill 格式。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
5.6 KiB
Python
180 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Fetch WeChat article statistics and update history.yaml.
|
|
|
|
Uses WeChat Data Analytics API to pull article performance:
|
|
- /datacube/getarticlesummary (daily summary)
|
|
- /datacube/getarticletotal (cumulative)
|
|
|
|
Usage:
|
|
python3 fetch_stats.py --client demo
|
|
python3 fetch_stats.py --client demo --days 7
|
|
|
|
Requires: wechat appid/secret in config.yaml (skill root or toolkit dir)
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
import yaml
|
|
|
|
SKILL_DIR = Path(__file__).parent.parent
|
|
TOOLKIT_CONFIG_PATHS = [
|
|
SKILL_DIR / "config.yaml", # skill root
|
|
SKILL_DIR / "toolkit" / "config.yaml", # toolkit dir
|
|
Path.home() / ".config" / "media-agent" / "config.yaml",
|
|
Path.cwd() / "config.yaml",
|
|
]
|
|
|
|
|
|
def _load_toolkit_config() -> dict:
|
|
for p in TOOLKIT_CONFIG_PATHS:
|
|
if p.exists():
|
|
with open(p, "r", encoding="utf-8") as f:
|
|
return yaml.safe_load(f) or {}
|
|
return {}
|
|
|
|
|
|
def _get_access_token(appid: str, secret: str) -> str:
|
|
resp = requests.get(
|
|
"https://api.weixin.qq.com/cgi-bin/token",
|
|
params={"grant_type": "client_credential", "appid": appid, "secret": secret},
|
|
)
|
|
data = resp.json()
|
|
if "access_token" not in data:
|
|
raise ValueError(f"Token error: {data}")
|
|
return data["access_token"]
|
|
|
|
|
|
def fetch_article_summary(token: str, date: str) -> list[dict]:
|
|
"""
|
|
Fetch daily article summary.
|
|
API: POST /datacube/getarticlesummary
|
|
date format: "2026-03-23"
|
|
"""
|
|
resp = requests.post(
|
|
"https://api.weixin.qq.com/datacube/getarticlesummary",
|
|
params={"access_token": token},
|
|
json={"begin_date": date, "end_date": date},
|
|
)
|
|
data = resp.json()
|
|
if "list" not in data:
|
|
errcode = data.get("errcode", "unknown")
|
|
errmsg = data.get("errmsg", "")
|
|
if errcode == 61500:
|
|
# No data for this date (article not yet published or no reads)
|
|
return []
|
|
print(f"[warn] getarticlesummary error: {errcode} {errmsg}", file=sys.stderr)
|
|
return []
|
|
return data["list"]
|
|
|
|
|
|
def fetch_article_total(token: str, date: str) -> list[dict]:
|
|
"""
|
|
Fetch cumulative article stats.
|
|
API: POST /datacube/getarticletotal
|
|
"""
|
|
resp = requests.post(
|
|
"https://api.weixin.qq.com/datacube/getarticletotal",
|
|
params={"access_token": token},
|
|
json={"begin_date": date, "end_date": date},
|
|
)
|
|
data = resp.json()
|
|
if "list" not in data:
|
|
return []
|
|
return data["list"]
|
|
|
|
|
|
def update_history(client: str, stats_list: list[dict]):
|
|
"""Match stats to history.yaml entries and update."""
|
|
history_path = SKILL_DIR / "clients" / client / "history.yaml"
|
|
if not history_path.exists():
|
|
print(f"No history.yaml found for client: {client}")
|
|
return
|
|
|
|
with open(history_path, "r", encoding="utf-8") as f:
|
|
history = yaml.safe_load(f) or {}
|
|
|
|
articles = history.get("articles", [])
|
|
if not articles:
|
|
print("No articles in history to update.")
|
|
return
|
|
|
|
# Build a lookup by title for matching
|
|
title_to_idx = {}
|
|
for i, article in enumerate(articles):
|
|
title_to_idx[article.get("title", "")] = i
|
|
|
|
updated = 0
|
|
for stat in stats_list:
|
|
title = stat.get("title", "")
|
|
if title in title_to_idx:
|
|
idx = title_to_idx[title]
|
|
articles[idx]["stats"] = {
|
|
"read_count": stat.get("int_page_read_count", 0),
|
|
"share_count": stat.get("share_count", 0),
|
|
"like_count": stat.get("old_like_count", 0) + stat.get("like_count", 0),
|
|
"read_rate": round(
|
|
stat.get("int_page_read_count", 0)
|
|
/ max(stat.get("target_user", 1), 1)
|
|
* 100,
|
|
1,
|
|
),
|
|
}
|
|
updated += 1
|
|
|
|
if updated > 0:
|
|
history["articles"] = articles
|
|
with open(history_path, "w", encoding="utf-8") as f:
|
|
yaml.dump(history, f, allow_unicode=True, default_flow_style=False)
|
|
print(f"Updated stats for {updated} article(s).")
|
|
else:
|
|
print("No matching articles found in stats data.")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Fetch WeChat article stats")
|
|
parser.add_argument("--client", required=True, help="Client name")
|
|
parser.add_argument("--days", type=int, default=3, help="Days to look back")
|
|
args = parser.parse_args()
|
|
|
|
cfg = _load_toolkit_config()
|
|
wechat_cfg = cfg.get("wechat", {})
|
|
appid = wechat_cfg.get("appid")
|
|
secret = wechat_cfg.get("secret")
|
|
|
|
if not appid or not secret:
|
|
print("Error: wechat appid/secret not found in config.yaml", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
token = _get_access_token(appid, secret)
|
|
print(f"Fetching stats for client '{args.client}', last {args.days} days...")
|
|
|
|
all_stats = []
|
|
for i in range(args.days):
|
|
date = (datetime.now() - timedelta(days=i + 1)).strftime("%Y-%m-%d")
|
|
stats = fetch_article_summary(token, date)
|
|
if stats:
|
|
print(f" {date}: {len(stats)} article(s)")
|
|
all_stats.extend(stats)
|
|
|
|
if all_stats:
|
|
update_history(args.client, all_stats)
|
|
else:
|
|
print("No stats data found for the specified period.")
|
|
|
|
# Also print summary
|
|
print(f"\nTotal data points: {len(all_stats)}")
|
|
for s in all_stats:
|
|
title = s.get("title", "unknown")
|
|
reads = s.get("int_page_read_count", 0)
|
|
shares = s.get("share_count", 0)
|
|
print(f" [{reads} reads, {shares} shares] {title}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|