为了账号安全,请及时绑定邮箱和手机立即绑定

我开发了一款工具,终结在有害开源项目上的时间浪费

标签:
开源

动机

在向多个开源项目贡献代码后,我发现其中一些项目存在严重问题。许多维护者在提交 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 上的有害沟通环境,以及维护者指导缺失导致的自动化代码审查困境。


核心功能:项目健康评分

系统采用混合方法,结合基于规则的确定性算法和定性的大语言模型(LLM)评判模块,全面评估项目健康状况。

评分算法(0-100 分)

基础健康评分采用自定义加权平均算法,灵感来自标准化 CHAOSS 指标。权重根据现代健康项目的特征进行调整:

分数 = (0.3 × 活跃度) + (0.25 × 维护度) + (0.2 × 社区) + (0.25 × 文档)

  • 活跃度 (30%): 提交频率、更新及时性、独立作者数量
  • 维护度 (25%): 问题响应时间、未解决问题比例、仓库年龄
  • 社区 (20%): 星标和复刻数量的对数尺度
  • 文档 (25%): README、LICENSE、CONTRIBUTING 文件的存在性

大语言模型调整

基于规则的算法常将"功能完备"项目误判为"已死亡"。为此,我增加了大语言模型评判层。

技术实现:

我设计了辅助逻辑层,让大语言模型分析仓库的目的(通过 README 内容和文件结构)。当模型检测到指标可能产生误导时,可以在±20分范围内调整最终分数。这是为了弥合原始数据与现实语境之间的差距。

目前 MVP 使用 GPT-4 Mini,因其成本效益和响应速度优势。这有助于验证方法可行性,为后续优化奠定基础。

  • 示例: 稳定工具库,6个月内无提交
  • 算法判定: "陈旧/已遗弃"
  • LLM 调整: 识别为"已完成/稳定",给予+20分稳定性加分

实现代码片段:

// 将计算分数输入 AI 提示词,请求调整
prompt += `
  "scoreInsights": {
    "adjustment": {
       "shouldAdjust": true, 
       "amount": 20, // 范围: -20 到 +20
       "reason": "这是处于维护模式的稳定工具库,低活跃度符合预期", 
       "confidence": "high"
    }
  }
`;

PR 指标分析

为解决开源协作中的沟通缺失问题,我构建了 PR 指标分析功能。贡献前需要了解:

  1. 响应速度: 平均合并时间(小时还是月?)
  2. 协作质量: 是与真人协作还是对抗机器人审查?
  3. 成长生态: 新贡献者是否会持续参与?

高效数据处理(后端并发)

为快速获取统计数据,采用 Promise.all 并行获取数据:

// 并行获取开放 PR、已关闭 PR 和模板检查
const [openPRs, closedPRs, template] = await Promise.all([
  fetchPRs(octokit, { owner, repo, state: "open" }),
  fetchPRs(octokit, { owner, repo, state: "closed" }),
  checkPRTemplate(octokit, { owner, repo }),
]);

留存率比数量更重要。使用 Sankey 图可视化贡献者流动路径:

// 贡献者流动可视化
const data = {
  nodes: [
    { id: "首次 PR", color: "#58a6ff" },
    { id: "二次贡献", color: "#3fb950" },
    { id: "常规贡献者 (3-9)", color: "#a371f7" },
    { id: "核心团队 (10+)", color: "#f0883e" }
  ],
  links: [
    {
      source: "首次 PR",
      target: "二次贡献",
      value: funnel.secondContribution + funnel.regular + funnel.coreTeam
    }
    // ... 流动映射逻辑
  ].filter((link) => link.value > 0)
};

经验教训:安全扫描器的取舍

最初基于 GitleaksTruffleHog 的灵感,构建了完整的密钥检测功能。

实现方案:

  • 正则模式匹配: 22个行业标准模式检测已知密钥
  • 随机性检测: 数学方法识别类似密钥的高随机字符串

放弃原因:

虽然安全扫描器是很好的工程挑战,但偏离了核心使命。为保持项目聚焦社区指标,最终决定移除该功能。软件工程的核心是解决实际问题,而非单纯的技术挑战。


智能问题分析

问题分析是了解项目活跃度的有效方法。我实现了多个指标为贡献者提供深度洞察:

  • 平均关闭时间: 衡量项目真实响应速度。短期平均关闭时间(如2天 vs 6个月)表明健康状态
  • 热门问题: 自定义算法优先展示近期更新(48小时内)、高参与度和安全相关讨论
  • 高价值问题: 突出"老旧但重要"的功能请求,适合作为首次贡献选择
  • 贡献难度评分: 基于文档质量、文件范围和测试要求的难度评级(0-100分)

项目结构与文件-问题映射

作为前端新手,这次开发推动我深入前端领域。重点解决新项目探索的复杂性难题。

解决方案:LLM 驱动的结构分析

系统递归获取完整文件树结构,输入大语言模型进行分析:

// 递归获取 GitHub 文件树
const { data } = await octokit.git.getTree({
  owner,
  repo,
  tree_sha: "HEAD",
  recursive: "true"  // 一次性获取完整结构
});

文件-问题映射

在 LLM 处理前,使用正则表达式扫描问题描述中的文件路径:

// 正则提取问题中的文件路径
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)]; // 去重
}

项目树可视化

repo-visualizer 启发,实现递归构建层次结构:

// 从扁平列表构建层次结构
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;
    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;
}

通过深度和文件数量限制确保可视化可读性。当前版本暂缓交互功能开发,优先保证核心稳定性。


活动模式检测

针对 Hacktoberfest 等时期的刷提交行为,构建基于 GitHub API 的模式检测系统。

可疑模式定义:

批量删除检测

// 检测异常删除模式
function detectChurnAnomalies(commits: CommitWithStats[]): PatternAnomaly[] {
  for (const commit of commits) {
    const total = commit.additions + commit.deletions;
    const churnRatio = commit.deletions / total;

    // 标记删除超过80%的提交
    if (churnRatio > 0.8 && commit.deletions > 100) {
      anomalies.push({
        type: "churn",
        severity: churnRatio > 0.9 ? "critical" : "warning",
        description: `删除${Math.round(churnRatio * 100)}%代码(${commit.deletions}行)`
      });
    }
  }
}

速射提交检测

// 检测提交轰炸
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);

    // 10分钟内5+次提交视为可疑
    if (diffMinutes <= 10) {
      anomalies.push({
        type: "velocity",
        severity: count > 10 ? "critical" : "warning",
        description: `爆发:${count}次提交(${Math.round(diffMinutes)}分钟)`
      });
    }
  }
}

风险等级

等级 分数 含义
A 0-10 正常活动
B 11-30 轻微异常
C 31-50 建议审查
D 51-70 可疑
F 71-100 需要严格审查

视角转变:维护者维度

初期仅从贡献者角度思考,但现实让我意识到维护者视角的重要性。通过阅读相关文章,我理解了开源维护的挑战:不合理的功能请求、企业无偿使用、用户毒性行为等。这促使我增加维护者维度的分析功能。

贡献洞察

分析被拒 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: "琐碎的 README 更改" };
  }
  return { isSpam: false, reason: "" };
}

自动化失败分析

type PitfallAnalysis = {
  prNumber: number;
  mistake: string;
  reviewFeedback: string;
  advice: string;
  category: "tests" | "style" | "scope" | "setup" | "breaking" | "docs";
};
类别 含义
tests 缺少或测试失败
style 代码格式问题
scope 改动过大或超范围
setup 环境配置问题
breaking 破坏性变更
docs 文档缺失

技术挑战与解决方案

缓存安全漏洞

初期对技术栈不熟悉,犯下缓存键重复使用的错误:

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

React Hydration 不匹配

// 修复方案
const { data: session, status } = useSession();

// JSX 实现
{status === "loading" ? (
  <Box
    w="100px"
    h="32px"
    bg="#21262d"
    borderRadius="md"
    opacity={0.5}
  />
) : session?.user ? (
  <UserMenu />
) : (
  <SignInButton />
)}

推荐阅读 The Perils of Rehydration 深入了解 hydration 机制。


未来规划

需要同时平衡贡献者和维护者视角,未来计划包括:

  • 重点提交展示: 过滤琐碎更新,突出功能添加和架构改进
  • 功能请求适配性检查: 识别不符合项目目标的请求
  • 资助平台推广: 突出 GitHub Sponsors 等资助渠道
  • 项目生命周期标识: 显示活跃、维护、归档等状态
  • 常见错误预防: 基于 CONTRIBUTING.md 和 CI 失败模式提供指导
  • 贡献者分级系统: 首次贡献者、初学者、技术专家等层级
  • 全面测试覆盖: 单元测试、集成测试和端到端测试

结语

这个两周内快速开发的项目,代码质量可能不是最优的,但快速迭代有助于验证核心想法。在实际开发中,我使用 LLM 辅助完成重复性编码任务,但所有核心创意和架构决策都来自深入思考。开源项目的价值在于持续改进,希望通过社区协作,共同构建对维护者和贡献者都有价值的工具。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消