GitHub Issue 需求信號
取得競爭對手的公開 GitHub 倉庫。獲取其開放議題。在本地過濾雜訊。聚類為 6 個需求類別。根據真實參與度評分。輸出排名需求缺口報告與 GTM 訊息簡報。
關鍵規則: 輸出中的每個議題標題必須直接來自 GitHub API 回應,不得修改。每個聚類主題名稱必須源自該聚類中的實際議題標題。如果雜訊過濾後剩餘的議題少於 10 個,則停止並告知使用者——該倉庫太小,無法進行可靠的聚類。禁止在任何地方使用虛構的議題內容。
常見錯誤
| Agent 可能會想這麼做... | 為什麼這是錯的 |
|---|---|
| 將全部 200 個原始議題未經篩選就送給 AI | Bot 議題、 PR 和零參與度的雜訊會膨脹聚類數量並浪費上下文。應先在本地過濾。 |
| 使用評論數量作為主要需求信號 | 評論包含維護者回覆、離題討論和垃圾訊息。 reactions["+1"] 是最乾淨的買家信號。 |
| 在總結聚類時改寫議題標題 | 改寫會失去買家的原始表達,而這正是核心價值。應使用原封不動的議題標題。 |
| 在步驟 4 之後,如果過濾後剩餘少於 10 個議題仍繼續執行 | 少於 10 個議題表示倉庫太小或提供了錯誤的 URL。在稀疏資料上進行聚類會產生無意義的分類。 |
| 在分析中包含 Pull Requests | GitHub Issues 端點也會回傳 PR。需要透過檢查 issue 物件中是否缺少 pull_request 鍵來進行過濾。 |
| 在未檢查所有 3 個條件的情況下就將議題標記為被忽略的需求 | 三個條件必須同時成立: reactions >= 10、 age >= 180 天、沒有 planned/in-progress/roadmap 標籤。缺少任何一個條件就取消該議題的資格。 |
步驟 1:設定檢查
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set, unauthenticated rate limit applies (60 req/hr)}"
如果 GITHUB_TOKEN 未設定: 繼續執行。告訴使用者:"GITHUB_TOKEN 未設定。未經認證的速率限制為每小時 60 次請求——大約足夠在達到限制前進行 2 次獲取。如需重複使用,請到 github.com/settings/tokens 新增一個 token(存取公開倉庫不需要任何權限範圍)。"
步驟 2:收集輸入
你需要:
- GitHub 倉庫 URL (例如 https://github.com/owner/repo) 或 owner/repo 縮寫 (例如 facebook/react)
從輸入中解析 owner 和 repo:
python3 << 'PYEOF'
import re, sys, os
raw = "REPO_INPUT_HERE"
# 標準化為 owner/repo 格式
if raw.startswith("http"):
m = re.search(r"github\.com/([^/]+)/([^/?\s]+)", raw)
if not m:
print("ERROR: Could not parse GitHub URL. Expected format: https://github.com/owner/repo")
sys.exit(1)
owner, repo = m.group(1), m.group(2).rstrip("/")
elif "/" in raw:
parts = raw.strip().split("/")
owner, repo = parts[0], parts[1]
else:
print("ERROR: Input must be a GitHub URL or owner/repo slug (e.g. vercel/next.js)")
sys.exit(1)
print(f"Owner: {owner}")
print(f"Repo: {repo}")
with open("/tmp/ghd-target.txt", "w") as f:
f.write(f"{owner}/{repo}")
PYEOF
如果解析失敗: 停止。詢問:"請提供 GitHub 倉庫作為 URL (https://github.com/owner/repo) 或 owner/repo 縮寫 (例如 vercel/next.js)。"
步驟 3:從 GitHub REST API 獲取議題
獲取最多 200 個議題(2 頁,每頁 100 個)。在第一次獲取後檢查速率限制。
python3 << 'PYEOF'
import json, urllib.request, os, sys
from datetime import datetime, timezone
target = open("/tmp/ghd-target.txt").read().strip()
owner_repo = target
token = os.environ.get("GITHUB_TOKEN", "")
headers = {"Accept": "application/vnd.github+json", "User-Agent": "gh-issue-demand-signal/1.0"}
if token:
headers["Authorization"] = f"Bearer {token}"
all_issues = []
rate_limit_hit = False
for page in [1, 2]:
url = f"https://api.github.com/repos/{owner_repo}/issues?state=open&per_page=100&page={page}"
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
# 在第一頁之後檢查速率限制
if page == 1:
remaining = int(resp.headers.get("X-RateLimit-Remaining", 999))
reset_ts = resp.headers.get("X-RateLimit-Reset", "")
if remaining == 0:
reset_str = datetime.fromtimestamp(int(reset_ts), tz=timezone.utc).strftime("%H:%M UTC") if reset_ts else "unknown"
print(f"ERROR: GitHub rate limit exhausted. Resets at {reset_str}.")
print("Add GITHUB_TOKEN to your .env file to get 5000 req/hr. See github.com/settings/tokens (no scopes needed).")
sys.exit(1)
print(f"Rate limit remaining: {remaining}")
# 檢查 404/403 狀態
status = resp.status
if status == 404:
print(f"ERROR: Repo '{owner_repo}' not found. Check the URL or slug.")
sys.exit(1)
page_data = json.loads(resp.read())
if not page_data:
print(f"Page {page}: empty, stopping.")
break
all_issues.extend(page_data)
print(f"Page {page}: {len(page_data)} issues fetched")
except urllib.error.HTTPError as e:
if e.code == 404:
print(f"ERROR: Repo '{owner_repo}' not found (404). Check the URL or slug.")
elif e.code == 403:
print(f"ERROR: Access denied (403). Repo may be private or rate limit hit.")
else:
print(f"ERROR: GitHub API returned HTTP {e.code}")
sys.exit(1)
except Exception as e:
print(f"ERROR: Failed to fetch page {page}: {e}")
sys.exit(1)
print(f"Total raw issues fetched: {len(all_issues)}")
json.dump(all_issues, open("/tmp/ghd-raw-issues.json", "w"), indent=2)
PYEOF
如果 GitHub 回傳 404: 停止。告訴使用者:"找不到倉庫。請檢查 URL 或縮寫並重試。若未經認證和明確的 repo 權限,無法存取私有倉庫。"
如果 GitHub 回傳 403 並附帶速率限制標頭: 停止。顯示重置時間,並告訴使用者新增 GITHUB_TOKEN。
步驟 4:在本地進行預處理——過濾、評分、檢測被忽略的需求
不進行 API 呼叫。純 Python。在將任何資料送給 AI 之前執行。
python3 << 'PYEOF'
import json, re
from datetime import datetime, timezone
raw = json.load(open("/tmp/ghd-raw-issues.json"))
target = open("/tmp/ghd-target.txt").read().strip()
now = datetime.now(tz=timezone.utc)
noise_patterns = re.compile(
r"^(chore|deps|bump|renovate|dependabot|release|ci|build|revert)[\s:\[]",
re.IGNORECASE
)
pr_title_patterns = re.compile(
r"^(feat|fix|refactor|docs|test|style|perf|chore)(\(.+\))?:",
re.IGNORECASE
)
filtered = []
noise_count = 0
noise_reasons = {}
for issue in raw:
# 跳過 Pull Requests(GitHub Issues 端點也會回傳 PR)
if "pull_request" in issue:
noise_count += 1
noise_reasons["pull_request"] = noise_reasons.get("pull_request", 0) + 1
continue
title = issue.get("title", "")
reactions = issue.get("reactions", {}).get("+1", 0)
comments = issue.get("comments", 0)
user_type = (issue.get("user") or {}).get("type", "User")
# 跳過機器人建立的議題
if user_type == "Bot":
noise_count += 1
noise_reasons["bot_author"] = noise_reasons.get("bot_author", 0) + 1
continue
# 跳過機器人模式的標題
if noise_patterns.match(title):
noise_count += 1
noise_reasons["bot_title"] = noise_reasons.get("bot_title", 0) + 1
continue
# 跳過 PR 風格的議題標題
if pr_title_patterns.match(title):
noise_count += 1
noise_reasons["pr_as_issue"] = noise_reasons.get("pr_as_issue", 0) + 1
continue
# 跳過零信號的議題
if reactions == 0 and comments == 0:
noise_count += 1
noise_reasons["zero_signal"] = noise_reasons.get("zero_signal", 0) + 1
continue
# 計算需求分數
demand_score = (reactions * 2) + (comments * 0.5)
# 檢測被忽略的需求
created_at = issue.get("created_at", "")
if created_at:
created = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
age_days = (now - created).days
else:
age_days = 0
labels = [l.get("name", "").lower() for l in issue.get("labels", [])]
has_planned_label = any(
kw in label for label in labels
for kw in ["in-progress", "planned", "roadmap", "wip", "in progress"]
)
ignored_demand = (
reactions >= 10 and
age_days >= 180 and
not has_planned_label
)
filtered.append({
"number": issue["number"],
"title": title,
"url": issue.get("html_url", f"https://github.com/{target}/issues/{issue['number']}"),
"reactions_plus1": reactions,
"comments": comments,
"demand_score": demand_score,
"age_days": age_days,
"labels": labels,
"ignored_demand": ignored_demand,
"body_snippet": (issue.get("body") or "")[:300]
})
# 按需求分數降序排序
filtered.sort(key=lambda x: x["demand_score"], reverse=True)
print(f"Raw issues: {len(raw)}")
print(f"Noise filtered: {noise_count} ({', '.join(f'{k}: {v}' for k, v in noise_reasons.items())})")
print(f"Issues for analysis: {len(filtered)}")
if len(filtered) < 10:
print(f"ERROR: Only {len(filtered)} issues remain after filtering. This repo has too few engaged issues for reliable clustering.")
print("Try a larger repo or a repo with more community engagement.")
import sys; sys.exit(1)
# 被忽略的需求摘要
ignored = [i for i in filtered if i["ignored_demand"]]
print(f"Ignored demand issues: {len(ignored)}")
json.dump(filtered, open("/tmp/ghd-filtered-issues.json", "w"), indent=2)
print("Pre-processing complete.")
PYEOF
如果過濾後剩餘少於 10 個議題: 停止。告訴使用者總共找到和過濾了多少議題,以及為什麼該倉庫對於可靠的需求聚類來說太小了。
步驟 5:聚類議題
列印已過濾的議題以供分析:
python3 << 'PYEOF'
import json
filtered = json.load(open("/tmp/ghd-filtered-issues.json"))
target = open("/tmp/ghd-target.txt").read().strip()
issue_list = filtered[:150]
print(f"Repo: {target}")
print(f"Issues to cluster: {len(issue_list)}")
print()
for i in issue_list:
labels_str = f" [{', '.join(i['labels'][:3])}]" if i['labels'] else ""
print(f"#{i['number']} [score:{round(i['demand_score'],1)} reactions:{i['reactions_plus1']}] {i['title']}{labels_str}")
PYEOF
將上面列印的每個議題分類到以下 6 個類別之一:
feature_gap、 bug_pattern、 ux_complaint、 performance、 integration_missing、 docs_missing
規則:
- 將每個議題分類到恰好一個類別
- 使用使用者標題中的原始語言提取一句話的痛苦陳述——不要改寫
- 識別 5-8 個聚類主題:簡短詞組(3-6 個單字),捕捉所有議題中的主要抱怨模式
- 不要使用破折號。不要使用行銷語言。
將你的分析寫入 /tmp/ghd-clusters.json,使用以下精確結構:
{
"classified_issues": [
{"number": 123, "category": "feature_gap", "pain_statement": "Users need X which does not exist yet"}
],
"cluster_themes": [
{"theme_name": "Missing export options", "category": "feature_gap", "issue_numbers": [123, 456, 789]}
],
"category_counts": {"feature_gap": 5, "bug_pattern": 3, "ux_complaint": 4, "performance": 2, "integration_missing": 6, "docs_missing": 1}
}
寫入檔案後,使用以下指令確認:
python3 -c "
import json
d = json.load(open('/tmp/ghd-clusters.json'))
print(f'Classified: {len(d[\"classified_issues\"])} issues, {len(d[\"cluster_themes\"])} themes')
print('Categories:', d['category_counts'])
"
步驟 6:訊息簡報
計算每個聚類的總需求分數並列印前 3 名:
python3 << 'PYEOF'
import json
filtered = json.load(open("/tmp/ghd-filtered-issues.json"))
clusters = json.load(open("/tmp/ghd-clusters.json"))
target = open("/tmp/ghd-target.txt").read().strip()
demand_by_issue = {i["number"]: i["demand_score"] for i in filtered}
issue_titles = {i["number"]: i["title"] for i in filtered}
issue_reactions = {i["number"]: i["reactions_plus1"] for i in filtered}
enriched_themes = []
for theme in clusters.get("cluster_themes", []):
issue_nums = theme.get("issue_numbers", [])
total_demand = sum(demand_by_issue.get(n, 0) for n in issue_nums)
top_issues = sorted(issue_nums, key=lambda n: demand_by_issue.get(n, 0), reverse=True)[:3]
enriched_themes.append({
"theme_name": theme["theme_name"],
"category": theme["category"],
"issue_count": len(issue_nums),
"total_demand_score": round(total_demand, 1),
"top_issues": [
{"number": n, "title": issue_titles.get(n, ""), "reactions": issue_reactions.get(n, 0)}
for n in top_issues
]
})
enriched_themes.sort(key=lambda x: x["total_demand_score"], reverse=True)
json.dump(enriched_themes, open("/tmp/ghd-enriched-themes.json", "w"), indent=2)
print(f"Top 3 clusters for messaging brief (repo: {target}):")
for t in enriched_themes[:3]:
print(f"\n {t['theme_name']} ({t['category']}) -- total demand: {t['total_demand_score']}")
for ti in t["top_issues"]:
print(f" #{ti['number']}: \"{ti['title']}\" ({ti['reactions']} reactions)")
PYEOF
根據上面列印的前 3 個聚類生成 GTM 訊息簡報。
規則:
- 每個定位角度必須引用其來源的特定聚類
- 每個外展鉤子必須引用一個完全一致的議題標題,並加上引號
- 標題必須包含一個數字或具體命名的痛苦——禁止使用通用陳述
- 不要使用破折號。禁止使用以下詞彙:powerful、 robust、 seamless、 innovative、 game-changing、 streamline、 leverage、 transform
將你的簡報寫入 /tmp/ghd-brief.json,使用以下精確結構:
...