diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000..78b8bd8 --- /dev/null +++ b/script/README.md @@ -0,0 +1,60 @@ +# 📡 GitHub Demand Radar — Scanner Script + +本目录包含命令行版本,可直接通过 cron 定时执行,无需 Agent 环境。 + +## 前置要求 + +```bash +# GitHub CLI(已认证) +brew install gh +gh auth login + +# 无需 jq — 本脚本使用纯 Python stdlib +python3 --version # 3.8+ +``` + +## 快速使用 + +```bash +# 默认扫描 4 个明星仓库,输出 markdown 报告到 reports/ +python3 script/scan.py + +# 指定单一仓库 +python3 script/scan.py --repo anthropics/claude-code + +# 指定扫描窗口 +python3 script/scan.py --days 7 + +# 指定输出目录 +python3 script/scan.py --output /path/to/reports/ +``` + +## Cron / 定时自动化 + +可以用 cron 每天定时执行,例如每天北京时间 09:00 运行: + +```cron +0 9 * * * cd /path/to/github-demand-radar && mkdir -p logs && python3 script/scan.py >> logs/demand-radar.log 2>&1 +``` + +## 输出 + +报告写入 `reports/report_YYYY-MM-DD.md`,格式: + +``` +# 📡 GitHub Demand Radar · 2026年05月25日 + +## 📊 概览 +## 🔥 HIGH 需求(独立开发者优先切入) +## 🟡 MEDIUM 需求(持续观察) +``` + +## 评分标准 + +| 评分 | 条件 | 说明 | +|------|------|------| +| HIGH | reactions ≥ 8 或 comments ≥ 6 | 强需求信号,独立开发者优先切入 | +| MEDIUM | reactions 5-7 或 comments 4-5 | 持续观察,等待更多信息 | +| 候选 | reactions ≥ 3 或 comments ≥ 3 | 进入评分池 | + +评分池过滤:reactions < 3 且 comments < 3 的 issue 视为噪音忽略。 diff --git a/script/scan.py b/script/scan.py new file mode 100644 index 0000000..8d1b412 --- /dev/null +++ b/script/scan.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +github-demand-radar — collect & score GitHub issues (Hermes cron version) + +Scan AI Coding Agent star repos for high-signal issues in the past 30 days. +Filters: reactions >= 3 OR comments >= 3 +Scores: + HIGH — reactions >= 8 OR comments >= 6 + MEDIUM — reactions 5-7 OR comments 4-5 + +Requirements: gh CLI (authenticated with `gh auth login`) + pip install — none (stdlib only) + +Usage: + python3 scan.py # prints markdown report to stdout + saves to reports/ + python3 scan.py --repo owner/repo # scan a specific repo only +""" +import subprocess, json, os, argparse +from datetime import datetime, timedelta + +REPOS = [ + ("anthropics", "claude-code"), + ("openclaw", "openclaw"), + ("nousresearch", "hermes-agent"), + ("obra", "superpowers"), +] + +def gh(args): + """Call 'gh api' with a list of arguments (avoids shell splitting issues).""" + cmd = ["gh", "api"] + args + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + return [] + try: + return json.loads(r.stdout) + except json.JSONDecodeError: + return [] + +def collect(repo, since): + data = gh([ + f"search/issues?q=repo:{repo}+is:issue+created:>={since}&sort=reactions&order:desc&per_page=50", + "--jq", ".items", + ]) + if not isinstance(data, list): + return [] + return [ + { + "repo": repo, + "number": i["number"], + "title": i["title"], + "reactions": i["reactions"]["total_count"], + "comments": i["comments"], + "labels": [l["name"] for l in i.get("labels", [])], + "created": i["created_at"][:10], + "url": i["html_url"], + } + for i in data + if i["reactions"]["total_count"] >= 3 or i["comments"] >= 3 + ] + +def issue_score(item): + return item["reactions"] + item["comments"] * 2 + +def is_high(item): + return item["reactions"] >= 8 or item["comments"] >= 6 + +def is_medium(item): + return 5 <= item["reactions"] <= 7 or 4 <= item["comments"] <= 5 + +def score(items): + high = sorted( + [i for i in items if is_high(i)], + key=lambda x: -issue_score(x) + ) + medium = sorted( + [i for i in items if not is_high(i) and is_medium(i)], + key=lambda x: -issue_score(x) + ) + return high, medium + +def build_report(items, since, today, repos): + high, medium = score(items) + hn, mn = len(high), len(medium) + total = len(items) + + lines = [ + f"# 📡 GitHub Demand Radar · {today}\n\n", + f"> 扫描窗口:{since} → 今天\n> 数据来源:GitHub Issues(30 天内)\n\n", + "---\n\n", + "## 📊 概览\n\n", + f"- 扫描仓库:**{len(repos)} 个**\n", + f"- 候选 issue:**{total} 条**\n", + f"- 🔥 HIGH:**{hn} 条**\n", + f"- 🟡 MEDIUM:**{mn} 条**\n\n", + "---\n\n", + ] + + if hn: + lines.append("## 🔥 HIGH 需求(独立开发者优先切入)\n\n") + for idx, item in enumerate(high, 1): + labels = "、".join(item["labels"]) if item["labels"] else "(无标签)" + lines += [ + f"### {idx}. {item['repo']} · Issue #{item['number']}\n\n", + f"- **Title**: {item['title']}\n", + f"- 👍 {item['reactions']} 💬 {item['comments']} 🏷️ {labels}\n", + f"- 📅 {item['created']}\n", + f"- 🔗 [查看 Issue]({item['url']})\n\n", + f"> 💡 **JTBD 穿透**:用户实际在完成什么任务?有没有丑陋的 workaround?\n\n", + "---\n\n", + ] + + if mn: + lines.append("## 🟡 MEDIUM 需求(持续观察)\n\n") + for item in medium: + lines.append( + f"- **{item['repo']} #{item['number']}** — {item['title']} " + f"(👍{item['reactions']} 💬{item['comments']} · {item['created']}) " + f"[→]({item['url']})\n" + ) + lines.append("\n") + + if hn == 0 and mn == 0: + lines.append("## 🟢 本期无高信号需求\n\n") + lines.append("近 30 天内这些仓库暂无 reactions ≥ 5 或 comments ≥ 3 的 issue。\n") + lines.append("可能是明星项目需求已在官方路线图中。明天再来。\n\n") + + return "".join(lines) + +def main(): + parser = argparse.ArgumentParser(description="GitHub Demand Radar scanner") + parser.add_argument("--repo", help="Scan a specific repo only (owner/repo)") + parser.add_argument("--days", type=int, default=30, help="Scan window in days (default: 30)") + parser.add_argument("--output", default="reports", help="Output directory for report .md file") + args = parser.parse_args() + + since = (datetime.now() - timedelta(days=args.days)).strftime("%Y-%m-%d") + today = datetime.now().strftime("%Y年%m月%d日") + + print(f"📡 GitHub Demand Radar · {today}") + print("━" * 50) + print(f"窗口期:{since} → 今天\n") + + scan_repos = [(args.repo.split("/")[0], args.repo.split("/")[1])] if args.repo else REPOS + all_items = [] + for owner, name in scan_repos: + repo = f"{owner}/{name}" + print(f"--► 扫描 {repo}") + items = collect(repo, since) + print(f" → {len(items)} 条候选") + all_items.extend(items) + + high, medium = score(all_items) + print(f"\n采集完成 | 共 {len(all_items)} 条候选 | HIGH {len(high)} | MEDIUM {len(medium)}\n") + + report = build_report(all_items, since, today, scan_repos) + + out_dir = os.path.expanduser(args.output) + os.makedirs(out_dir, exist_ok=True) + stamp = datetime.now().strftime("%Y-%m-%d") + out_path = os.path.join(out_dir, f"report_{stamp}.md") + with open(out_path, "w") as f: + f.write(report) + + print(f"✅ 报告 → {out_path}\n") + print("=" * 50) + print(report) + +if __name__ == "__main__": + main()