SDK 採用追蹤器
輸入 SDK 名稱,搜尋 GitHub 上導入它的公開儲存庫。根據公司訊號、活躍度和雜訊指標對每個儲存庫進行評分。為高信號儲存庫補充擁有者和貢獻者資料。輸出排序後的採用報告,並針對公司採用者提供外展背景資訊。
關鍵規則: 輸出中的每個儲存庫都必須存在於 GitHub 程式碼搜尋 API 的回應中。每個公司名稱都必須來自 GitHub 使用者或組織 API 的 company 或 name 欄位。每個貢獻者帳號都必須來自 GitHub 貢獻者 API 的回應。如果 API 中任何欄位為空,請填寫「未列出」——不要推斷、猜測或推論。
常見錯誤
| 代理人可能會想要... | 為什麼那是錯的 |
|---|---|
在沒有 GITHUB_TOKEN 的情況下執行程式碼搜尋 | 未經身分驗證的程式碼搜尋會達到每分鐘 3 次請求的次要速率限制,並在任何有意義的掃描中失敗。需要 GITHUB_TOKEN。如果缺少,請在第 1 步停止並顯示明確錯誤。 |
| 包含 SDK 本身的分支 | 分支 SDK 的儲存庫是貢獻者或鏡像,而不是採用者。過濾掉 fork == true 且儲存庫名稱與 SDK 名稱相符的儲存庫。 |
| 將所有 500 筆原始搜尋結果發送給 AI | 程式碼搜尋最多可返回 500 筆結果,其中大部分是雜訊。先在本機過濾和評分。僅將前 20 個高信號儲存庫發送到 AI 分析步驟。 |
| 將範例和教學儲存庫回報為採用者 | 名稱或描述中包含「example」、「tutorial」、「demo」、「learn」、「sample」、「playground」、「starter」的儲存庫不是生產使用者。將其標記為 tutorial_noise 並從潛在客戶簡報中排除。 |
| 虛構公司名稱或聯絡人帳號 | 每個公司名稱都必須來自 GitHub 的 company 或組織 name 欄位。每個貢獻者帳號都必須來自貢獻者 API 的回應。如果某個欄位為空,請填寫「未列出」。 |
| 對所有生態系統使用一種導入模式 | require('sdk') 不會找到 Python 使用者。根據 SDK 名稱自動檢測生態系統,並建立生態系統特定的模式。如果自動檢測不明確,請詢問使用者。 |
步驟 1:設定檢查
if [ -z "$GITHUB_TOKEN" ]; then
echo "ERROR: GITHUB_TOKEN 是執行程式碼搜尋所必需的。"
echo "在 github.com/settings/tokens 新增一個權杖(公開儲存庫不需要任何範圍)。"
echo "沒有它,GitHub 程式碼搜尋會達到每分鐘 3 次請求的次要速率限制並失敗。"
exit 1
fi
echo "GITHUB_TOKEN: 已設定"
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/rate_limit" | python3 -c "
import json, sys
d = json.load(sys.stdin)
search = d['resources']['search']
core = d['resources']['core']
print(f'搜尋速率: {search[\"remaining\"]}/{search[\"limit\"]} 剩餘')
print(f'核心速率: {core[\"remaining\"]}/{core[\"limit\"]} 剩餘')
"
如果搜尋剩餘次數為 0:停止。告訴使用者來自 X-RateLimit-Reset 的重置時間。
步驟 2:收集輸入
從對話中收集:
- SDK 名稱(例如
@company/my-sdk、requests、github.com/org/go-sdk) - 選擇性:生態系統覆蓋(
npm、python、go、gem)——如果未提供則自動檢測 - 選擇性:要從結果中排除的組織/使用者(SDK 擁有者自己的儲存庫)
- 選擇性:產品背景字串(用於個人化外展訊息)
自動檢測生態系統:
- 開頭為
@或包含-:npm - 蛇形命名且不含
/或-:python - 包含
github.com/:go - 否則:詢問使用者
如果未提供 SDK 名稱: 詢問:「您想追蹤哪個 SDK 或函式庫?請提供在 import 陳述式中出現的套件名稱(例如 stripe、@clerk/nextjs、requests)。」
python3 << 'PYEOF'
import json, sys, re
sdk_name = "SDK_NAME_HERE"
ecosystem_override = "" # 留空以自動檢測
exclude_owner = "" # 選擇性:要排除的擁有者名稱(通常是 SDK 發布者)
product_context = "" # 選擇性:您的產品用途
# 自動檢測生態系統
if ecosystem_override:
ecosystem = ecosystem_override
elif sdk_name.startswith("@") or "-" in sdk_name:
ecosystem = "npm"
elif re.match(r'^[a-z][a-z0-9_]*$', sdk_name):
ecosystem = "python"
elif "github.com/" in sdk_name:
ecosystem = "go"
else:
ecosystem = "generic"
print(f"SDK: {sdk_name}")
print(f"生態系統: {ecosystem}")
print(f"排除擁有者: {exclude_owner or '(無)'}")
with open("/tmp/sat-input.json", "w") as f:
json.dump({
"sdk_name": sdk_name,
"ecosystem": ecosystem,
"exclude_owner": exclude_owner,
"product_context": product_context
}, f)
PYEOF
步驟 3:搜尋 GitHub 程式碼
首先檢查獨立腳本——它可以在一次呼叫中處理步驟 3-5。
ls scripts/fetch.py 2>/dev/null && echo "腳本可用" || echo "找不到腳本"
如果腳本可用,執行它並跳到步驟 6:
python3 scripts/fetch.py "$(python3 -c "import json; d=json.load(open('/tmp/sat-input.json')); print(d['sdk_name'])")" \
--ecosystem "$(python3 -c "import json; d=json.load(open('/tmp/sat-input.json')); print(d['ecosystem'])")" \
--exclude "$(python3 -c "import json; d=json.load(open('/tmp/sat-input.json')); print(d.get('exclude_owner',''))")" \
--output /tmp/sat-script-out.json
然後載入到步驟 6-8 預期的暫存檔案格式:
python3 << 'PYEOF'
import json
out = json.load(open("/tmp/sat-script-out.json"))
json.dump(out["raw_results"], open("/tmp/sat-raw-results.json", "w"), indent=2)
json.dump(out["scored"], open("/tmp/sat-scored.json", "w"), indent=2)
json.dump(out["enriched"], open("/tmp/sat-enriched.json", "w"), indent=2)
print(f"已載入: {len(out['raw_results'])} 原始 | {len(out['scored'])} 已評分 | {len(out['enriched'])} 已補充")
PYEOF
如果腳本不可用,執行以下內聯程式碼。
為每個建立導入模式並搜尋 GitHub 程式碼:
python3 << 'PYEOF'
import json, urllib.request, ssl, time, os
from datetime import datetime, timezone
ctx = ssl._create_unverified_context()
token = os.environ["GITHUB_TOKEN"]
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"User-Agent": "sdk-adoption-tracker/1.0"
}
data = json.load(open("/tmp/sat-input.json"))
sdk_name = data["sdk_name"]
ecosystem = data["ecosystem"]
# 根據生態系統建立查詢模式
if ecosystem == "npm":
# 去除 @ 前綴以進行原始搜尋
bare = sdk_name.lstrip("@").replace("/", "/")
queries = [
f'require("{sdk_name}")',
f"require('{sdk_name}')",
f'from "{sdk_name}"',
f"from '{sdk_name}'",
]
elif ecosystem == "python":
queries = [
f"import {sdk_name}",
f"from {sdk_name} import",
f"from {sdk_name}.",
]
elif ecosystem == "go":
queries = [f'"{sdk_name}"']
else:
queries = [sdk_name]
print(f"正在為 {sdk_name} ({ecosystem}) 建立查詢:")
for q in queries:
print(f" {q}")
seen_repos = {} # full_name -> 首次匹配
search_rate_remaining = 30
flags = []
for i, query in enumerate(queries):
if search_rate_remaining <= 2:
flags.append(f"程式碼搜尋速率限制低 ({search_rate_remaining}) -- 跳過剩餘模式")
print(f" 速率限制低 ({search_rate_remaining}),提前停止")
break
url = f"https://api.github.com/search/code?q={urllib.parse.quote(query)}&per_page=100"
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=20, context=ctx) as resp:
search_rate_remaining = int(resp.headers.get("X-RateLimit-Remaining", 10))
raw = json.loads(resp.read())
items = raw.get("items", [])
total = raw.get("total_count", 0)
print(f" 模式 '{query}': 共 {total} 筆,已取得 {len(items)} 筆 | 速率剩餘={search_rate_remaining}")
for item in items:
repo = item.get("repository", {})
full_name = repo.get("full_name", "")
if full_name and full_name not in seen_repos:
seen_repos[full_name] = {
"full_name": full_name,
"name": repo.get("name", ""),
"owner_login": repo.get("owner", {}).get("login", ""),
"owner_type": repo.get("owner", {}).get("type", ""),
"file_path": item.get("path", ""),
"matched_pattern": query,
"html_url": repo.get("html_url", ""),
"description": repo.get("description") or "",
}
except urllib.error.HTTPError as e:
if e.code == 403:
flags.append(f"在模式 '{query}' 上遇到程式碼搜尋速率限制")
print(f" 在 '{query}' 上遇到速率限制 (403)")
break
else:
print(f" 在 '{query}' 上遇到 HTTP {e.code}")
except Exception as e:
print(f" 在 '{query}' 上遇到錯誤: {e}")
# 遵守每分鐘 10 次請求的程式碼搜尋限制
if i < len(queries) - 1:
time.sleep(6)
results = list(seen_repos.values())
json.dump(results, open("/tmp/sat-raw-results.json", "w"), indent=2)
print(f"\n找到的唯一儲存庫總數: {len(results)}")
if flags:
print("標誌:", flags)
import urllib.parse # 確保已導入
PYEOF
如果結果為 0: 告訴使用者:「找不到導入 {sdk_name} 的儲存庫。GitHub 程式碼搜尋索引需要 1-4 週才能收錄新套件。如果 SDK 已建立,請檢查 references/import-patterns.md 中的導入模式。」
步驟 4:評分和分類儲存庫
無需 API 呼叫。純 Python。過濾雜訊,計算採用分數,對每個儲存庫進行分類。
python3 << 'PYEOF'
import json
from datetime import datetime, timezone
data = json.load(open("/tmp/sat-input.json"))
exclude_owner = data.get("exclude_owner", "").lower()
sdk_name = data["sdk_name"].lower().split("/")[-1].replace("@", "")
results = json.load(open("/tmp/sat-raw-results.json"))
TUTORIAL_WORDS = {"example", "tutorial", "demo", "learn", "sample", "starter",
"boilerplate", "template", "playground", "test", "course", "workshop"}
scored = []
now = datetime.now(tz=timezone.utc)
for repo in results:
full_name = repo["full_name"]
owner_login = repo.get("owner_login", "").lower()
repo_name = repo.get("name", "").lower()
description = (repo.get("description") or "").lower()
owner_type = repo.get("owner_type", "User")
# 排除 SDK 擁有者自己的儲存庫
if exclude_owner and owner_login == exclude_owner.lower():
continue
# 檢測教學雜訊
name_words = set(repo_name.replace("-", " ").replace("_", " ").split())
desc_words = set(description.split())
is_tutorial = bool((name_words | desc_words) & TUTORIAL_WORDS)
# 如果儲存庫名稱就是 SDK 名稱,也排除(很可能是分支)
if repo_name == sdk_name or repo_name.startswith(sdk_name + "-"):
is_tutorial = True # 視為雜訊
# 分類層級
if is_tutorial:
tier = "tutorial_noise"
elif owner_type == "Organization":
tier = "company_org"
else:
tier = "solo_dev" # 將在步驟 5 中升級為 affiliated_dev(如果 company 欄位已填入)
# 採用分數(現在用部分資料填入,在步驟 5 中補充)
score = 0
if owner_type == "Organization": score += 50
if not is_tutorial: score += 20
# stars, days_since_push, is_fork, is_archived 在步驟 5 中加入
scored.append({
**repo,
"tier": tier,
"is_tutorial": is_tutorial,
"adoption_score": score,
"enriched": False
})
# 排序:company_org 優先,然後依層級
tier_order = {"company_org": 0, "affiliated_dev": 1, "solo_dev": 2, "tutorial_noise": 3}
scored.sort(key=lambda x: (tier_order.get(x["tier"], 9), -x["adoption_score"]))
json.dump(scored, open("/tmp/sat-scored.json", "w"), indent=2)
tiers = {}
for r in scored:
tiers[r["tier"]] = tiers.get(r["tier"], 0) + 1
print(f"分類:")
for tier, count in sorted(tiers.items(), key=lambda x: tier_order.get(x[0], 9)):
print(f" {tier}: {count}")
print(f"總計: {len(scored)} 個儲存庫")
non_noise = [r for r in scored if r["tier"] != "tutorial_noise"]
print(f"\n要補充的熱門儲存庫(非雜訊): {len(non_noise)}")
for r in non_noise[:5]:
print(f" {r['full_name']} ({r['tier']}) -- {r.get('description','')[:60]}")
PYEOF
如果所有儲存庫都是 tutorial_noise: 停止。告訴使用者:「所有找到的儲存庫似乎是教學或範例。在公開的 GitHub 中未檢測到生產採用者。SDK 可能太新,或者套件名稱夠通用,以至於搜尋結果以範例為主。」
步驟 5:補充高信號儲存庫
取得非雜訊儲存庫的完整儲存庫元資料、擁有者個人資料和頂尖貢獻者。完全跳過 tutorial_noise 儲存庫。
python3 << 'PYEOF'
import json, urllib.request, ssl, os, time
from datetime import datetime, timezone
ctx = ssl._create_unverified_context()
token = os.environ["GITHUB_TOKEN"]
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"User-Agent": "sdk-adoption-tracker/1.0"
}
scored = json.load(open("/tmp/sat-scored.json"))
core_remaining = 5000
flags = []
def gh_get(path):
global core_remaining
req = urllib.request.Request(f"https://api.github.com{path}", headers=headers)
try:
with urllib.request.urlopen(req, timeout=15, context=ctx) as resp:
remaining = resp.headers.get("X-RateLimit-Remaining")
if remaining:
core_remaining = int(remaining)
return json.loads(resp.read())
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
except Exception:
return None
target = [r for r in scored if r["tier"] != "tutorial_noise"]
print(f"正在補充 {len(target)} 個儲存庫(跳過 tutorial_noise)...")
enriched = []
for item in target:
full_name = item["full_name"]
owner_login = item["owner_login"]
owner_type = item["owner_type"]
# 取得儲存庫資料
repo_data = gh_get(f"/repos/{full_name}")
if not repo_data:
enriched.append(item)
continue
# 取得擁有者個人資料
owner_data = gh_get(f"/users/{owner_login}")
company = (owner_data.get("company") or "").strip() if owner_data else ""
# 取得貢獻者(最多 10 個)
contributors = []
try:
req = urllib.request.Request(
f"https://api.github.com/repos/{full_name}/contributors?per_page=10",
headers=headers
)
# ... (貢獻者邏輯)
except Exception as e:
print(f" 取得貢獻者時出錯 {full_name}: {e}")
# ... (補充邏輯)
enriched.append({
**item,
"stars": repo_data.get("stargazers_count", 0),
"forks": repo_data.get("forks_count", 0),
"pushed_at": repo_data.get("pushed_at", ""),
"is_fork": repo_data.get("fork", False),
"is_archived": repo_data.get("archived", False),
"company": company or "未列出",
"contributors": [c["login"] for c in contributors[:5]],
"enriched": True
})
json.dump(enriched, open("/tmp/sat-enriched.json", "w"), indent=2)
print(f"已補充 {len(enriched)} 個儲存庫")
PYEOF
步驟 6:AI 分析和報告產生
將前 20 個高信號補充儲存庫發送給 AI。產生最終報告。
import json
enriched = json.load(open("/tmp/sat-enriched.json"))
input_data = json.load(open("/tmp/sat-input.json"))
# 排序:高信號優先
signal = sorted(enriched, key=lambda x: (x["adoption_score"], x["stars"]), reverse=True)
top20 = signal[:20]
report = f"""# SDK 採用報告
## SDK: {input_data['sdk_name']}
## 摘要
總計已掃描儲存庫: {len(enriched)}
公司組織: {sum(1 for r in enriched if r['tier'] == 'company_org')}
關聯開發者: {sum(1 for r in enriched if r['tier'] == 'affiliated_dev')}
個人開發者: {sum(1 for r in enriched if r['tier'] == 'solo_dev')}
教學雜訊: {sum(1 for r in enriched if r['tier'] == 'tutorial_noise')}
## 頂尖採用者
| 排名 | 儲存庫 | 類型 | 公司 | 分數 | 星數 |
|---|---|---|---|---|---|
"""
for i, r in enumerate(top20):
report += f"| {i+1} | {r['full_name']} | {r['tier']} | {r.get('company', '未列出')} | {r['adoption_score']} | {r.get('stars', 0)} |\n"
print(report)
步驟 7:產生潛在客戶簡報
對於公司組織,產生外展簡報。
company_signal = [r for r in top20 if r['tier'] == 'company_org']
for r in company_signal:
brief = f"""### {r['full_name']}
- **公司**: {r.get('company', '未列出')}
- **接觸點**: {', '.join(r.get('contributors', ['未列出']))}
- **採用分數**: {r['adoption_score']}
- **星數**: {r.get('stars', 0)}
- **背景**: {r.get('description', '')}
"""
print(brief)
步驟 8:最終輸出
以 JSON 格式輸出最終報告。
import json
final_output = {
"sdk": input_data['sdk_name'],
"summary": {
"total_scanned": len(enriched),
"company_org_count": sum(1 for r in enriched if r['tier'] == 'company_org'),
"affiliated_dev_count": sum(1 for r in enriched if r['tier'] == 'affiliated_dev'),
"solo_dev_count": sum(1 for r in enriched if r['tier'] == 'solo_dev'),
"tutorial_noise_count": sum(1 for r in enriched if r['tier'] == 'tutorial_noise')
},
"top_adopters": [{
"full_name": r['full_name'],
"tier": r['tier'],
"company": r.get('company', '未列出'),
"adoption_score": r['adoption_score'],
"stars": r.get('stars', 0),
"forks": r.get('forks', 0),
"contributors": r.get('contributors', [])
} for r in top20],
"execution_time": datetime.now(tz=timezone.utc).isoformat()
}
print(json.dumps(final_output, indent=2))
錯誤處理
- 速率限制: 如果
X-RateLimit-Remaining為 0,停止並告訴使用者重置時間。 - 無結果: 提供明確的訊息說明潛在原因。
- 所有結果為雜訊: 建議 SDK 太新或名稱太通用。
- API 錯誤: 優雅地處理且不中斷流程。