From 88b14ed574b576f16d8ca414da5e6f8214f0e53e Mon Sep 17 00:00:00 2001 From: vesperchinn Date: Mon, 25 May 2026 13:20:00 +0800 Subject: [PATCH 1/3] Add cron-ready scanner script + GitHub Actions workflow - script/scan.py: Pure-Python stdlib scanner (no jq required), compatible with hermes-agent cron and GitHub Actions. Fixes shell-arg-splitting issues by passing gh api args as a list instead of a string. - .github/workflows/demand-radar.yml: Runs daily at UTC 01:00 (Beijing 09:00) via schedule trigger, uploads report as artifact. - script/README.md: Documents CLI usage, CI setup, and scoring rubric. Closes: provides a zero-dependency automation path for users without an Agent environment. --- .github/workflows/demand-radar.yml | 29 ++++++ script/README.md | 66 ++++++++++++ script/scan.py | 160 +++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 .github/workflows/demand-radar.yml create mode 100644 script/README.md create mode 100644 script/scan.py diff --git a/.github/workflows/demand-radar.yml b/.github/workflows/demand-radar.yml new file mode 100644 index 0000000..5bdbb4e --- /dev/null +++ b/.github/workflows/demand-radar.yml @@ -0,0 +1,29 @@ +name: Daily GitHub Demand Radar + +on: + schedule: + # Run at 09:00 Asia/Shanghai (01:00 UTC) + - cron: '0 1 * * *' + workflow_dispatch: + inputs: + days: + description: 'Scan window in days' + required: false + default: '30' + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run scanner + run: python3 script/scan.py --output . + + - name: Upload report artifact + uses: actions/upload-artifact@v4 + with: + name: demand-radar-report + path: report_*.md + retention-days: 7 \ No newline at end of file diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000..848c89b --- /dev/null +++ b/script/README.md @@ -0,0 +1,66 @@ +# 📡 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 报告 +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/ +``` + +## CI / 定时自动化 + +项目内置 GitHub Actions 工作流,每天 UTC 01:00(= 北京时间 09:00)自动运行: + +```yaml +# .github/workflows/demand-radar.yml +on: + schedule: + - cron: '0 1 * * *' # UTC 01:00 = Asia/Shanghai 09:00 + workflow_dispatch: +``` + +在 GitHub 上启用:仓库 → Actions → 选择 "Daily GitHub Demand Radar" → Enable + +## 输出 + +报告写入 `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 视为噪音忽略。 \ No newline at end of file diff --git a/script/scan.py b/script/scan.py new file mode 100644 index 0000000..f713b8a --- /dev/null +++ b/script/scan.py @@ -0,0 +1,160 @@ +#!/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 score(items): + high = sorted( + [i for i in items if i["reactions"] >= 8 or i["comments"] >= 6], + key=lambda x: -(x["reactions"] + x["comments"] * 2) + ) + medium = sorted( + [i for i in items if 5 <= i["reactions"] <= 7 or 4 <= i["comments"] <= 5], + key=lambda x: -(x["reactions"] + x["comments"] * 2) + ) + 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=".", 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() \ No newline at end of file From 8517de4bc2473d4daccd794a2fdf50b09c186d47 Mon Sep 17 00:00:00 2001 From: vesperchinn Date: Mon, 25 May 2026 13:20:12 +0800 Subject: [PATCH 2/3] Add cron-ready scanner script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - script/scan.py: Pure-Python stdlib scanner (no jq required), compatible with hermes-agent cron. Fixes shell-arg-splitting by passing gh api args as list. - script/README.md: Documents CLI usage and scoring rubric. - Note: .github/workflows/ excluded — token needs 'workflow' scope to push. Will submit as separate PR or add via web UI. --- .github/workflows/demand-radar.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/workflows/demand-radar.yml diff --git a/.github/workflows/demand-radar.yml b/.github/workflows/demand-radar.yml deleted file mode 100644 index 5bdbb4e..0000000 --- a/.github/workflows/demand-radar.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Daily GitHub Demand Radar - -on: - schedule: - # Run at 09:00 Asia/Shanghai (01:00 UTC) - - cron: '0 1 * * *' - workflow_dispatch: - inputs: - days: - description: 'Scan window in days' - required: false - default: '30' - -jobs: - scan: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run scanner - run: python3 script/scan.py --output . - - - name: Upload report artifact - uses: actions/upload-artifact@v4 - with: - name: demand-radar-report - path: report_*.md - retention-days: 7 \ No newline at end of file From ab516601ac69bd5df5422734af1ef64e10ad6204 Mon Sep 17 00:00:00 2001 From: Vion <464672669@qq.com> Date: Mon, 25 May 2026 15:23:08 +0800 Subject: [PATCH 3/3] Fix scanner scoring and docs --- script/README.md | 18 ++++++------------ script/scan.py | 23 ++++++++++++++++------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/script/README.md b/script/README.md index 848c89b..78b8bd8 100644 --- a/script/README.md +++ b/script/README.md @@ -16,7 +16,7 @@ python3 --version # 3.8+ ## 快速使用 ```bash -# 默认扫描 4 个明星仓库,输出 markdown 报告 +# 默认扫描 4 个明星仓库,输出 markdown 报告到 reports/ python3 script/scan.py # 指定单一仓库 @@ -29,20 +29,14 @@ python3 script/scan.py --days 7 python3 script/scan.py --output /path/to/reports/ ``` -## CI / 定时自动化 +## Cron / 定时自动化 -项目内置 GitHub Actions 工作流,每天 UTC 01:00(= 北京时间 09:00)自动运行: +可以用 cron 每天定时执行,例如每天北京时间 09:00 运行: -```yaml -# .github/workflows/demand-radar.yml -on: - schedule: - - cron: '0 1 * * *' # UTC 01:00 = Asia/Shanghai 09:00 - workflow_dispatch: +```cron +0 9 * * * cd /path/to/github-demand-radar && mkdir -p logs && python3 script/scan.py >> logs/demand-radar.log 2>&1 ``` -在 GitHub 上启用:仓库 → Actions → 选择 "Daily GitHub Demand Radar" → Enable - ## 输出 报告写入 `reports/report_YYYY-MM-DD.md`,格式: @@ -63,4 +57,4 @@ on: | MEDIUM | reactions 5-7 或 comments 4-5 | 持续观察,等待更多信息 | | 候选 | reactions ≥ 3 或 comments ≥ 3 | 进入评分池 | -评分池过滤:reactions < 3 且 comments < 3 的 issue 视为噪音忽略。 \ No newline at end of file +评分池过滤:reactions < 3 且 comments < 3 的 issue 视为噪音忽略。 diff --git a/script/scan.py b/script/scan.py index f713b8a..8d1b412 100644 --- a/script/scan.py +++ b/script/scan.py @@ -58,14 +58,23 @@ def collect(repo, since): 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 i["reactions"] >= 8 or i["comments"] >= 6], - key=lambda x: -(x["reactions"] + x["comments"] * 2) + 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 5 <= i["reactions"] <= 7 or 4 <= i["comments"] <= 5], - key=lambda x: -(x["reactions"] + x["comments"] * 2) + [i for i in items if not is_high(i) and is_medium(i)], + key=lambda x: -issue_score(x) ) return high, medium @@ -121,7 +130,7 @@ 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=".", help="Output directory for report .md file") + 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") @@ -157,4 +166,4 @@ def main(): print(report) if __name__ == "__main__": - main() \ No newline at end of file + main()