在參與了幾個開源專案之後,我意識到其中一些專案存在嚴重問題。很多維護者在你提交 pull request 時並不提供幫助,最終你必須費盡周折地應對自動化程式碼審查,才能在 GitHub 個人資料上顯示一次提交記錄。因此,這次,我不再像之前那樣開發更多個人專案(我在部落格文章《用 TypeScript 建立我自己的 HTTP 伺服器》和《建立一個讓我的生活更輕鬆的 CLI 工具》中寫過),也不再隨意地為一些開源倉庫做貢獻,或者刷 LeetCode 題目,而是想為開源社群創造一些真正有意義的東西。我決定開發repo-health (線上演示) ,幫助貢獻者選擇他們能夠成功參與的專案,並了解開源協作中哪些方法有效,哪些無效。
前端:Next.js 16、React 19、Chakra UI
後端:tRPC、Octokit、Zod
資料:MySQL(Prisma)、Redis
我選擇這套技術堆疊是為了熟悉當前的行業標準工具,並了解它們在實際應用中是如何協同工作的(歸根結底,它們只是用來建立一些很酷的東西的工具)。
一開始,我只是展示一些GitHub上的資料,覺得挺酷的,直到我給朋友和大學生展示。我意識到,即使你花了很多時間開發一個功能或修復一個大bug,如果它不能解決實際問題,那就毫無意義。所以,我意識到,在寫任何程式碼之前,最好先坐下來思考為什麼要寫這個程式碼。於是,我挑戰自己,在兩週內把所有精力都投入在這個產品上。
我決定專注於幫助開源貢獻者解決我個人經歷以及我的同學在大學裡遇到的問題:GitHub 上有害的溝通環境,以及在缺乏維護者適當指導的情況下與自動化程式碼審查作鬥爭。
我的系統採用混合方法,將基於行業標準的確定性公式與定性語言模型判斷器相結合,以考慮現實世界的背景。
演算法(0-100 分)
基礎健康評分是使用我根據標準化的CHAOSS 指標設計的自訂加權平均值計算的。我根據自己對健康現代專案特徵的理解調整了權重:
Score = (0.3 × Activity) + (0.25 × Maintenance) + (0.2 × Community) + (0.25 × Docs)
活動(30%):提交頻率 + 更新時間 + 唯一作者。
維護(25%):問題回應時間 + 未解決問題比率 + 儲存庫年齡。
社群(20%):星形和叉形的對數尺度。
文件(25%): README、LICENSE 和 CONTRIBUTING 文件的存在。
語言模型調整
標準公式經常將「功能已完成」的專案誤判為「已終止」。為了解決這個問題,我加入了一個使用語言模型的判斷層。
我的貢獻:
我實作了一個輔助邏輯層,其中語言模型會分析倉庫的用途(透過README內容和文件結構)。我明確允許模型在檢測到指標具有誤導性時,將演算法評分提高 ±20 分。這樣做是為了彌合原始資料與實際應用場景之間的差距。對於 MVP(最小可行產品),我目前使用的是 GPT-4 Mini,因為它性價比高且反應速度快。這使我能夠在考慮擴展到其他模型(例如 Claude Sonnet)或提出更優方案之前,驗證該方法的有效性(期待您的寶貴意見!)。
例如:一個穩定的實用程式庫,6 個月內沒有提交任何內容。
算法:判定為「老舊/廢棄」。
語言模型評判員:認定其為“已完成/穩定”,並授予+20 穩定性獎勵。
實現程式碼片段:
// I feed the calculated score into the AI prompt and ask for an adjustment:
prompt += `
"scoreInsights": {
"adjustment": {
"shouldAdjust": true,
"amount": 20, // Range: -20 to +20
"reason": "This is a stable utility library in maintenance mode. Low activity is expected and healthy.",
"confidence": "high"
}
}
`;
我開發了PR 指標分析工具,旨在解決開源社群溝通不良的問題。在貢獻程式碼之前,您需要了解以下資訊:
速度:平均併線時間是幾小時還是幾個月?
人性:你是在和真人用戶打交道,還是在和機器人評論作鬥爭?
成長:新貢獻者真的會長期留存嗎?
為了快速取得這些統計資料,我無法逐一取得所有資料。我使用Promise.all並行取得開啟的 PR、關閉的 PR 和範本檢查結果,從而顯著縮短了載入時間。我在關於事件循環的 HTTP 伺服器部落格文章中深入探討了這個問題,其中耗時最長的操作決定了總執行時間。
// Efficiently fetching Open PRs, Closed PRs, and Template checks simultaneously
const [openPRs, closedPRs, template] = await Promise.all([
fetchPRs(octokit, { owner, repo, state: "open" }),
fetchPRs(octokit, { owner, repo, state: "closed" }),
checkPRTemplate(octokit, { owner, repo }),
]);
留住貢獻者比統計貢獻者數量更重要。我使用Sankey圖來視覺化從「新成員」到「核心團隊成員」的流動過程,這樣就能清楚地看出貢獻者是會留下還是會立即離開。
// Visualizing the contributor flow
const data = {
nodes: [
{ id: "First PR", color: "#58a6ff" },
{ id: "2nd Contribution", color: "#3fb950" },
{ id: "Regular (3-9)", color: "#a371f7" },
{ id: "Core Team (10+)", color: "#f0883e" },
],
links: [
{
source: "First PR",
target: "2nd Contribution",
value: funnel.secondContribution + funnel.regular + funnel.coreTeam,
},
// ... logic to map flows for Regular and Core contributors
].filter((link) => link.value > 0),
};
我最初建立了一個完整的金鑰偵測系統來捕捉暴露的 API 金鑰,其靈感來自Gitleaks和TruffleHog 。
工作原理:
正規表示式模式匹配:我使用了約 22 個行業標準模式來捕獲已知的密鑰(AWS 密鑰、GitHub 令牌、Stripe 密鑰)。
隨機性檢測:我運用數學方法檢測高度隨機的字串,這些字串「看起來」像秘密訊息,即使它們不匹配任何模式。
我刪除它的原因:
雖然開發安全掃描器是一項巨大的工程挑戰,但我意識到它偏離了我的核心使命。我決定砍掉這個功能,讓專案專注於社區指標,而不是安全審計。相信我,刪除它很痛苦,但事實就是如此。軟體工程的重點不是解決難題,而是解決現實生活中存在的問題。感謝朋友們的指點,我才幡然醒悟,意識到自己之前浪費了多少時間。
分析問題是了解專案活動最有效的方法。我實施了幾個具體的指標,以便為貢獻者提供切實可行的洞察:
平均關閉時間:這衡量的是專案的真實速度。即使有許多未解決的問題,如果平均關閉時間較短(例如,2 天而不是 6 個月),則表示程式碼庫仍然運作良好。我同時追蹤平均值和中間值,以排除異常情況(例如,耗時 2 年才關閉的問題)。
熱門問題:為了幫助貢獻者找到活躍的討論,我使用了一種自訂演算法,該演算法優先考慮最近的更新(最近 48 小時)、高參與度(評論/反應)和安全相關的關鍵字。
隱藏的寶藏:這部分著重介紹一些「老舊」但「影響巨大」的問題(例如被忽略的功能請求)。這些問題通常是理想的首次貢獻,因為它們既能提供價值,又不會捲入激烈的討論中。
可破解性評分:根據文件品質、文件範圍和測驗要求計算出的難度等級(0-100)。此評分將複雜的問題清單篩選成新手也能輕鬆完成的任務。
我以前從來沒接觸過前端,但這段經歷促使我深入鑽研。我想解決的一個問題是降低探索新專案的複雜性。當你面對一個有 500 個檔案的程式碼庫時,你甚至不知道該從何下手。
我的解決方案:基於語言模型的結構分析
我建立了一個系統,它可以遞歸地獲取整個文件樹,並將文件結構提供給 LLM。 LLM 接著會告訴你入口點、關鍵檔案以及哪些資料夾負責哪些功能。這樣,LLM 就能為你提供一個完整的專案概覽。
// Recursively fetch the entire file tree from GitHub
const { data } = await octokit.git.getTree({
owner,
repo,
tree_sha: "HEAD",
recursive: "true", // Fetches entire tree structure at once
});
文件-問題映射:將問題與程式碼關聯起來
此外,我還加入了文件-問題映射功能。在到達 LLM 之前,我使用正規表示式掃描所有問題描述中的檔案路徑。如果某個問題提到了src/components/Button.tsx ,我會將該問題直接連結到概覽中的該檔案。這樣,使用者點擊文件後即可立即查看該文件是否存在未解決的問題,以及該問題是否僅涉及單一文件,還是會影響多個文件。
// Extract file paths mentioned in issue text using regex
const FILE_PATTERN = /[\w\-\/\.]+\.(ts|tsx|js|jsx|py|go|rs|java|cpp|c)/gi;
function extractFilePaths(text: string): string[] {
const matches = text.match(FILE_PATTERN) || [];
return [...new Set(matches)]; // Deduplicate
}
專案樹可視化(前端)
專案結構圖的設計靈感來自repo-visualizer專案。前端方面,我實作了一個遞歸函數,用於從扁平的檔案列表中建立層級結構。此函數遍歷每個檔案路徑,以斜線 (/) 分割,並建立嵌套的父子關係以形成樹狀結構。
// Recursively build hierarchy from flat file paths
function buildHierarchy(files: FileNode[], repoName: string, maxDepth = 3): HierarchyNode {
const root: HierarchyNode = { name: repoName, path: "", children: [] };
files.forEach((file) => {
const parts = file.path.split("/");
let current = root;
// Traverse and build tree structure
parts.slice(0, maxDepth + 1).forEach((part, index) => {
let child = current.children?.find((c) => c.name === part);
if (!child) {
child = { name: part, children: [] };
current.children!.push(child);
}
current = child;
});
});
return root;
}
我還透過限制深度和文件數量來減少碰撞。這樣即使對於大型儲存庫,也能保持可視化效果的可讀性。
已知限制:我嘗試新增縮放和平移功能,但靈敏度設定不當,導致正常的滾動操作無法進行(歡迎提交 PR)。我決定保持視覺化介面的簡潔性和穩定性,而不是發布一個存在缺陷的互動版本。此功能需要在未來的迭代中進行進一步完善。
如您所知,尤其是在 Hacktoberfest 期間,有些人提交程式碼和拉取請求只是為了提交而提交。垃圾貢獻、大量刪除和可疑模式比比皆是。為了偵測這些行為,我基於 GitHub API 的提交指標建立了一個模式檢測系統。
什麼是可疑模式?
可疑模式是指任何與正常開發工作明顯不同的提交活動。我追蹤以下幾種類型:
大規模刪除檢測
「刪除率」衡量的是刪除程式碼量與總更改量的比率。如果一次提交刪除了 90% 的程式碼,且刪除行數超過 100 行,那就很可疑了。這可能是清理操作,但也可能是惡意破壞。
// Detect unusual deletion patterns
function detectChurnAnomalies(commits: CommitWithStats[]): PatternAnomaly[] {
for (const commit of commits) {
const total = commit.additions + commit.deletions;
const churnRatio = commit.deletions / total;
// Flag commits that delete >80% of touched code
if (churnRatio > 0.8 && commit.deletions > 100) {
anomalies.push({
type: "churn",
severity: churnRatio > 0.9 ? "critical" : "warning",
description: `Deleted ${Math.round(churnRatio * 100)}% of code (${commit.deletions} lines)`,
});
}
}
}
快速射擊式提交檢測
這可以偵測出提交轟炸現象,也就是有人在 10 分鐘內提交 10 次以上的程式碼。真正的開發並非如此運作。
// Detect rapid-fire commits (likely spam or farming)
function detectBurstActivity(commits: CommitWithStats[]): PatternAnomaly[] {
for (let i = 0; i < sorted.length - 4; i++) {
const windowStart = new Date(sorted[i].date).getTime();
const windowEnd = new Date(sorted[i + 4].date).getTime();
const diffMinutes = (windowEnd - windowStart) / (1000 * 60);
// 5+ commits in under 10 minutes = suspicious
if (diffMinutes <= 10) {
anomalies.push({
type: "velocity",
severity: count > 10 ? "critical" : "warning",
description: `Burst: ${count} commits in ${Math.round(diffMinutes)} minutes`,
});
}
}
}
等級 | 分數 | 意義 |
| ----- | ------ | ------------------ |
| A | 0-10 | 正常活動 |
| B | 11-30 | 輕微異常 |
| C | 31-50 | 建議複習 |
| D | 51-70 | 可疑 |
| F | 71-100 | 重要評論 |
專案開始,我只從貢獻者的角度考慮問題,但現實給了我沉重一擊。我閱讀了一些部落格文章和文章,例如:
[使用者提出的特殊要求](https://medium.com/@d4nyll/the-open-source-community-have-no-place-for-disrespect-70c85d473332)
從這些部落格中,我了解到開源社群的運作方式有多麼混亂。基本上,使用者和公司可能會提出一些與專案目標不符的功能要求,或者一些大公司會使用開源專案卻不提供任何贊助。此外,用戶中也存在一些不良風氣,有些人只是在README上簽名就沾沾自喜,但這並非真正的貢獻。因此,我決定從更宏觀的角度來看待開源社區,從維護者的角度來審視這些專案。
我發展了一個功能,可以分析被拒絕的 PR,並顯示其失敗的原因,幫助未來的貢獻者避免犯下同樣的錯誤。
垃圾郵件偵測
首先,我會過濾掉那些只是在README中加入名稱的明顯垃圾 PR:
const SPAM_TITLE_PATTERNS = [
/add(ed|ing)?\s+(my\s+)?name/i,
/update(d)?\s+readme/i,
/hacktoberfest/i,
];
function detectSpam(pr, files): { isSpam: boolean; reason: string } {
const isReadmeOnly = files.length === 1 &&
files[0].filename.toLowerCase().includes("readme");
if (isReadmeOnly && files[0].additions < 5) {
return { isSpam: true, reason: "Trivial README change" };
}
return { isSpam: false, reason: "" };
}
自動故障分析
對於合法被拒絕的 PR,我會將程式碼差異和審查者意見發送給語言模型。它會對每項失敗進行分類:
type PitfallAnalysis = {
prNumber: number;
mistake: string;
reviewFeedback: string;
advice: string;
category: "tests" | "style" | "scope" | "setup" | "breaking" | "docs";
};
| 類別 | 意義 |
| ---------- | ---------------------------------- |
| tests | 缺失或損壞的測試 |
| style | 程式碼格式違規 |
| scope | 變更過大或超出範圍 |
| setup | 建置/環境問題 |
| breaking | 引入了重大變更 |
| docs | 缺少文件 |
這樣就把被拒絕的 PR 變成了社區的學習資源。
剛開始的時候我對技術棧並不熟悉,犯了個錯誤,公共倉庫和私有倉庫都用了同一個快取鍵。快取確實讓使用者體驗更快更流暢,我當時還挺得意,直到我看到朋友的私人專案也出現在我的帳戶裡。
出於好奇,我問朋友要了他們的私人倉庫名稱,然後透過模糊搜尋和分析,我找到了這個專案。哇!當然,GitHub 不允許使用者存取他人的私有倉庫,但這仍然是一個安全漏洞。我意識到我沒有為每個用戶建立不同的快取鍵。
解決方案:基於令牌的快取隔離
我實現了一個函數,可以根據用戶的存取令牌生成唯一的哈希值:
import crypto from "crypto";
export function getTokenHash(token?: string | null): string {
if (!token) return "public";
return crypto.createHash("sha256").update(token).digest("hex").slice(0, 8);
}
如何使用
現在每個快取鍵都包含令牌雜湊值,以隔離每個使用者的私有儲存庫資料:
const tokenHash = getTokenHash(accessToken);
const cacheKey = `repo:info:${owner}:${repo}:${tokenHash}`;
| 場景 | 令牌哈希 | 快取鍵範例 |
| ------------------ | ---------- | ------------------------------------ |
| 公共倉庫 | public | repo:info:facebook:react:public |
| 使用者 A 私有 | k3m7p2q9 | repo:info:userA:secret:k3m7p2q9 |
| 使用者 B 私有 | x4y9z2a5 | repo:info:userA:secret:x4y9z2a5 |
這樣可以確保使用者 B 永遠無法看到使用者 A 快取的私有倉庫資料。
未來改善:快取管理器類
目前我在每個服務文件中都呼叫了getTokenHash()函數。我計劃建立一個集中式的CacheManager類別來統一處理這個問題。
我遇到的另一個問題是臭名昭著的 React 水合錯誤。登入按鈕導致伺服器渲染的內容與客戶端預期的內容不符。
問題
我使用了useSession() ,但沒有正確檢查其status :
// Before: Not checking status
const { data: session } = useSession();
// This caused hydration mismatch because:
// - Server: session is undefined → renders "Sign In" button
// - Client: session loads → renders user avatar
// React panics because the HTML doesn't match
修復方案
我新增了一個載入狀態,該狀態會在伺服器端和客戶端渲染相同的佔位符:
// After: Checking status properly
const { data: session, status } = useSession();
// In the JSX:
{status === "loading" ? (
// Loading state - same on server and client
<Box
w="100px"
h="32px"
bg="#21262d"
borderRadius="md"
opacity={0.5}
/>
) : session?.user ? (
// User is logged in - show avatar
<UserMenu />
) : (
// Not logged in - show sign in button
<SignInButton />
)}
為什麼這種方法有效
| 狀態 | 伺服器渲染 | 客戶端渲染 | 匹配? |
| ----------- | -------------- | -------------- | ------ |
| 載入中 | 佔位符 | 佔位符 | 是 |
| 已登入 | 佔位符 | 頭像 | 是 |
| 已登出 | 佔位符 | 登入 | 是 |
關鍵在於初始載入階段,伺服器和客戶端都會渲染同一個佔位符。只有載入完成後,客戶端才會更新以顯示實際狀態。
推薦資源
我強烈建議閱讀《水合的風險》這篇文章。它幫助我正確理解了 Next.js 伺服器端渲染和 React 水合問題。
我意識到,在這個專案中,我需要同時考慮貢獻者和維護者的角度,才能打造出一個合格的產品。因此,我未來的想法是:
將“最近提交”替換為“重要提交”:我不想顯示所有最近的提交,而是想突出顯示對專案產生重大影響的提交。這意味著要過濾掉無關緊要的更新(例如拼字錯誤修復或格式變更),只顯示新增功能、修復錯誤或進行架構改進的提交。
檢查功能請求是否適合專案範圍:幫助維護者辨識與專案目標不符的功能請求。
推廣資助平台:重點推薦 GitHub Sponsors、Open Collective 和 Buy Me a Coffee,支援開源專案(維護者值得支持,尤其是當我們廣泛使用他們的專案時)。
顯示專案生命週期狀態:顯示專案是處於活動狀態、維護模式、已歸檔、公司支援或由個人開發者維護。
展示常見設定錯誤:從CONTRIBUTING.md和失敗的 CI 建置中擷取模式,幫助貢獻者避免常見錯誤。我計劃移除我的依賴項部分,因為它與此範圍無關,儘管我花了大量時間來建立和思考此功能,並將 PR 與依賴項漏洞關聯起來。
包含 4 個等級的下拉式選單:初級貢獻者、新手、Stack 專家、我已掌握
增加全面的測試:實施單元測試、整合測試和端到端測試,以確保平台的可靠性,並使貢獻者能夠更有信心地加入功能。
我兩週前啟動了這個專案,如果你存取我的GitHub ,你會發現這個專案開發過程非常艱難 :)。所以,再次聲明,這個專案的程式碼並不完美。我當時的想法是快速完成並發布這個專案,以便向大家展示我的想法和方向。畢竟,程式碼勝於雄辯。順便一提,我使用了 LLM 來幫我完成一些重複的任務,這些任務我已經知道如何寫,並且可以從StackOverflow找到答案。否則,我不可能在這麼短的時間內寫出這麼多程式碼。但是,靈感是 LLM 無法創造的,而這正是我需要你們的幫助和遠見的地方。此外,靈感從來不會一蹴而就,大多數時候它們都是不完整的。讓我們一起改進這個專案,讓開源更安全、更美好。目前,這個計畫還有很多需要改進的地方。但是,有了開源社群的幫助,我們可以打造出一個能夠節省維護者和貢獻者時間的工具。最後,如果你願意,可以在GitHub上追蹤我。如果你能給repo-health點個星標,那就太棒了。你也可以在 Discord 上聯絡我,我的用戶名是:elshad_02838