Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions script/README.md
Original file line number Diff line number Diff line change
@@ -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 视为噪音忽略。
169 changes: 169 additions & 0 deletions script/scan.py
Original file line number Diff line number Diff line change
@@ -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()