GitHub 项目自动 Star + Issue 监控

GitHub 项目自动 Star + Issue 监控

实时监控项目 Star 增长、自动回复常见 Issue、检测恶意 Spam,开源项目维护效率提升 10 倍


需求背景

为什么写这个工具

2025 年 7 月 12 日,周六凌晨 1 点 30 分,上海。

我正在改一个开源项目的 Issue,手机突然震动。打开一看,GitHub 通知:"Your repository received 50 new stars in the last hour."

我当时就精神了------这是要上 GitHub Trending 的节奏啊!

但接下来我就头疼了:同一时间,还有 12 个新 Issue 等着我处理。其中 8 个是重复问题(文档里明明写了),3 个是广告 Spam,只有 1 个是真正的 Bug 报告。

那天晚上我处理到凌晨 3 点,第二天还要参加一个技术分享。躺在床上我就想,这都什么年代了,怎么还在用这种原始方法管理开源项目?

回来我就花了两个周末写了这个工具。现在我的三个开源项目:

  • 实时监控 Star 增长,上热门第一时间知道
  • 自动回复常见 Issue,重复问题减少 70%
  • Spam 自动过滤,每周节省 2 小时清理时间
  • 每周自动生成项目报告,不用手动统计

真实案例: 2025 年 10 月 26 日,我的一个项目突然 Star 激增,工具 5 分钟内推送通知。我一看数据,发现是被一个技术大 V 转发了。我立刻跟进,当天新增了 200+ Stars,还收获了 15 个高质量 Issue。要是靠手动刷,大概率会错过这个热度。

谁需要这个功能

  • 开源项目维护者:需要监控项目增长、及时响应用户反馈
  • 技术团队 Leader:追踪团队开源项目的影响力
  • 开发者个人:关注自己项目的动态,了解用户使用情况
  • 技术博主:监控技术文章的配套代码仓库关注度
  • 企业开源办公室:管理公司多个开源项目的健康状况

真实时间成本

下面是我实际统计的数据(2025 年人工管理 vs 2026 年自动化):

任务 操作方式 频率 单次耗时 月耗时(人工) 月耗时(自动)
查看 Star 增长 访问 GitHub Insights 每日 5 分钟 2.5 小时 0.5 小时
检查新 Issue 查看 Notifications 每日 10 分钟 5 小时 0.5 小时
回复 Issue 逐条阅读并回复 每日 30 分钟 15 小时 1 小时
处理 PR 代码审查、合并 每周 60 分钟 4 小时 1 小时
清理 Spam 删除广告 Issue 每周 15 分钟 1 小时 0 小时
生成报告 整理项目数据 每月 60 分钟 1 小时 0.5 小时
总计 28.5 小时/月 3.5 小时/月

更麻烦的是:

  • 错过重要的 Issue 或 PR(有一次一个高质量 PR 躺了 3 天才看到)
  • 重复回答相同的问题(同一个安装问题回答了 20+ 次)
  • 难以追踪项目增长趋势(上没上热门全靠感觉)
  • 被恶意 Spam 打扰(每周至少 5 个广告 Issue)

💡 28.5 小时/月能干嘛?

  • 写完一个中型功能模块
  • 录 3 期技术视频教程
  • 陪孩子过 12 个周末(按 2.4 小时/周末)
  • 读完 2 本技术书籍

我选择第一个。省下来的时间用来优化核心功能,比手动管理项目有价值多了。

手动管理 vs 自动化监控对比

维度 手动管理 自动化监控 感受
时间成本 28.5 小时/月 3.5 小时/月(审核)
响应速度 数小时到数天 5 分钟内通知 及时
覆盖率 容易遗漏 100% 覆盖 全面
数据分析 手动统计 自动图表 方便
Spam 检测 人工识别 自动过滤 省心
历史记录 难以追溯 完整存档 安心
效率提升 1x 8x 真香

💡 2026 年新变化:今年开始,GitHub API 限流更严格了。代码里已经加了缓存和配额监控,但建议还是别太频繁请求,每个仓库间隔至少 5 分钟。


前置准备

需要的账号/API

  1. GitHub 账号:需要监控的仓库
  2. GitHub Personal Access Token :用于 API 访问
    • 创建路径:Settings → Developer settings → Personal access tokens
    • 需要权限:repo(私有仓库)或 public_repo(公开仓库)
  3. 飞书/钉钉/Slack:用于接收通知
  4. Vercel/服务器:用于部署监控服务(可选)

环境要求

  • Node.js 18+ 或 Python 3.9+
  • 能访问 GitHub API 的网络环境
  • 如需部署 Webhook,需要公网可访问的服务器

依赖安装

bash 复制代码
# 创建项目
mkdir github-monitor
cd github-monitor
npm init -y

# 安装核心依赖
npm install @octokit/rest @octokit/webhooks axios cron
npm install node-cron better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3

# 如果使用 Python
# pip install PyGithub requests schedule sqlite3

💡 性能提示 :监控多个仓库时,建议增加 Node.js 内存限制:node --max-old-space-size=512 dist/index.js


实现步骤

步骤 1: 项目结构设计

复制代码
github-monitor/
├── src/
│   ├── index.ts              # 主入口
│   ├── github/
│   │   ├── GitHubClient.ts   # GitHub API 客户端
│   │   ├── StarMonitor.ts    # Star 监控
│   │   ├── IssueMonitor.ts   # Issue 监控
│   │   └── PRMonitor.ts      # PR 监控
│   ├── analyzer/
│   │   ├── SpamDetector.ts   # Spam 检测
│   │   ├── AutoReplier.ts    # 自动回复
│   │   └── TrendAnalyzer.ts  # 趋势分析
│   ├── notifier/
│   │   ├── FeishuNotifier.ts # 飞书通知
│   │   └── SlackNotifier.ts  # Slack 通知
│   ├── storage/
│   │   └── Database.ts       # 数据存储
│   └── types/
│       └── index.ts          # 类型定义
├── config/
│   └── settings.json         # 配置
├── data/
│   └── github.db             # SQLite 数据库
├── scripts/
│   └── run-monitor.sh        # 启动脚本
└── package.json

步骤 2: GitHub API 客户端

创建 src/github/GitHubClient.ts

typescript 复制代码
import { Octokit } from '@octokit/rest';
import { createAppAuth } from '@octokit/auth-app';

export class GitHubClient {
  private octokit: Octokit;

  constructor(token: string) {
    this.octokit = new Octokit({
      auth: token,
      userAgent: 'github-monitor/1.0.0'
    });
  }

  /**
   * 获取仓库基本信息
   */
  async getRepository(owner: string, repo: string) {
    const { data } = await this.octokit.repos.get({ owner, repo });
    return data;
  }

  /**
   * 获取 Star 数量
   */
  async getStarCount(owner: string, repo: string): Promise<number> {
    const { data } = await this.octokit.repos.get({ owner, repo });
    return data.stargazers_count;
  }

  /**
   * 获取 Star 历史(通过 GitHub API 限制,需要自己记录)
   */
  async getStargazers(owner: string, repo: string, per_page = 100) {
    const { data } = await this.octokit.activity.listStargazersForRepo({
      owner,
      repo,
      per_page,
      page: 1
    });
    return data;
  }

  /**
   * 获取 Issue 列表
   */
  async getIssues(owner: string, repo: string, options?: {
    state?: 'open' | 'closed' | 'all';
    since?: string;
  }) {
    const { data } = await this.octokit.issues.listForRepo({
      owner,
      repo,
      state: options?.state || 'open',
      since: options?.since,
      per_page: 100
    });
    return data;
  }

  /**
   * 获取单个 Issue
   */
  async getIssue(owner: string, repo: string, issue_number: number) {
    const { data } = await this.octokit.issues.get({
      owner,
      repo,
      issue_number
    });
    return data;
  }

  /**
   * 创建 Issue 评论
   */
  async createComment(owner: string, repo: string, issue_number: number, body: string) {
    const { data } = await this.octokit.issues.createComment({
      owner,
      repo,
      issue_number,
      body
    });
    return data;
  }

  /**
   * 关闭 Issue
   */
  async closeIssue(owner: string, repo: string, issue_number: number) {
    const { data } = await this.octokit.issues.update({
      owner,
      repo,
      issue_number,
      state: 'closed'
    });
    return data;
  }

  /**
   * 给 Issue 添加标签
   */
  async addLabels(owner: string, repo: string, issue_number: number, labels: string[]) {
    const { data } = await this.octokit.issues.addLabels({
      owner,
      repo,
      issue_number,
      labels
    });
    return data;
  }

  /**
   * 获取 PR 列表
   */
  async getPullRequests(owner: string, repo: string, options?: {
    state?: 'open' | 'closed' | 'all';
  }) {
    const { data } = await this.octokit.pulls.list({
      owner,
      repo,
      state: options?.state || 'open',
      per_page: 100
    });
    return data;
  }

  /**
   * 获取仓库活动趋势
   */
  async getRepoActivity(owner: string, repo: string) {
    const [stars, issues, prs] = await Promise.all([
      this.getStarCount(owner, repo),
      this.getIssues(owner, repo, { state: 'all' }),
      this.getPullRequests(owner, repo, { state: 'all' })
    ]);

    return {
      stars,
      totalIssues: issues.length,
      openIssues: issues.filter(i => i.state === 'open').length,
      totalPRs: prs.length,
      openPRs: prs.filter(p => p.state === 'open').length
    };
  }
}

步骤 3: Star 监控

创建 src/github/StarMonitor.ts

typescript 复制代码
import { GitHubClient } from './GitHubClient';
import { Database } from '../storage/Database';
import { logger } from '../utils/logger';

export class StarMonitor {
  private github: GitHubClient;
  private db: Database;

  constructor(github: GitHubClient, db: Database) {
    this.github = github;
    this.db = db;
  }

  /**
   * 检查 Star 变化
   */
  async check(owner: string, repo: string): Promise<{
    changed: boolean;
    current: number;
    previous: number;
    delta: number;
  }> {
    const current = await this.github.getStarCount(owner, repo);
    const previous = await this.db.getLastStarCount(owner, repo);

    if (previous === null) {
      // 首次记录
      await this.db.recordStarCount(owner, repo, current);
      return { changed: false, current, previous: current, delta: 0 };
    }

    const delta = current - previous;

    if (delta !== 0) {
      await this.db.recordStarCount(owner, repo, current);
      logger.info(`[Star 监控] ${owner}/${repo}: ${previous} → ${current} (${delta > 0 ? '+' : ''}${delta})`);
    }

    return {
      changed: delta !== 0,
      current,
      previous,
      delta
    };
  }

  /**
   * 获取 Star 增长趋势
   */
  async getTrend(owner: string, repo: string, days: number = 30) {
    const records = await this.db.getStarHistory(owner, repo, days);
    
    return {
      total: records[records.length - 1]?.count || 0,
      growth: records.length > 1 
        ? records[records.length - 1].count - records[0].count 
        : 0,
      dailyAverage: records.length > 1
        ? (records[records.length - 1].count - records[0].count) / records.length
        : 0,
      history: records
    };
  }

  /**
   * 检测异常增长(可能上热门了)
   */
  async detectSurge(owner: string, repo: string, threshold: number = 100): Promise<boolean> {
    const trend = await this.getTrend(owner, repo, 7); // 最近 7 天
    
    if (trend.dailyAverage > threshold) {
      logger.warn(`[Star 激增] ${owner}/${repo} 日均增长 ${trend.dailyAverage.toFixed(1)} Stars`);
      return true;
    }
    
    return false;
  }
}

步骤 4: Issue 监控

创建 src/github/IssueMonitor.ts

typescript 复制代码
import { GitHubClient } from './GitHubClient';
import { Database } from '../storage/Database';
import { SpamDetector } from '../analyzer/SpamDetector';
import { AutoReplier } from '../analyzer/AutoReplier';
import { logger } from '../utils/logger';

export class IssueMonitor {
  private github: GitHubClient;
  private db: Database;
  private spamDetector: SpamDetector;
  private autoReplier: AutoReplier;

  constructor(
    github: GitHubClient,
    db: Database,
    spamDetector: SpamDetector,
    autoReplier: AutoReplier
  ) {
    this.github = github;
    this.db = db;
    this.spamDetector = spamDetector;
    this.autoReplier = autoReplier;
  }

  /**
   * 检查新 Issue
   */
  async checkNewIssues(owner: string, repo: string): Promise<Array<{
    number: number;
    title: string;
    author: string;
    isSpam: boolean;
    autoReplied: boolean;
  }>> {
    const results = [];
    
    // 获取最近 24 小时的 Issue
    const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
    const issues = await this.github.getIssues(owner, repo, { state: 'open', since });

    for (const issue of issues) {
      // 跳过已处理的
      const processed = await this.db.isIssueProcessed(issue.id);
      if (processed) continue;

      logger.info(`[新 Issue] #${issue.number}: ${issue.title} by @${issue.user.login}`);

      // 检测 Spam
      const isSpam = await this.spamDetector.detect(issue);
      
      // 自动回复
      let autoReplied = false;
      if (!isSpam) {
        const replyTemplate = await this.autoReplier.getReply(issue);
        if (replyTemplate) {
          await this.github.createComment(owner, repo, issue.number, replyTemplate);
          autoReplied = true;
          logger.info(`[自动回复] #${issue.number}`);
        }
      } else {
        // 标记为 Spam
        await this.github.addLabels(owner, repo, issue.number, ['spam']);
        logger.warn(`[Spam 检测] #${issue.number} 标记为垃圾信息`);
      }

      // 记录处理状态
      await this.db.markIssueProcessed(issue.id, { isSpam, autoReplied });

      results.push({
        number: issue.number,
        title: issue.title,
        author: issue.user.login,
        isSpam,
        autoReplied
      });
    }

    return results;
  }

  /**
   * 检查长时间未回复的 Issue
   */
  async checkStaleIssues(owner: string, repo: string, days: number = 7) {
    const issues = await this.github.getIssues(owner, repo, { state: 'open' });
    const staleThreshold = Date.now() - days * 24 * 60 * 60 * 1000;

    const staleIssues = issues.filter(issue => {
      const lastUpdate = new Date(issue.updated_at).getTime();
      return lastUpdate < staleThreshold;
    });

    if (staleIssues.length > 0) {
      logger.warn(`[过期 Issue] 发现 ${staleIssues.length} 个超过 ${days} 天未更新的 Issue`);
    }

    return staleIssues.map(issue => ({
      number: issue.number,
      title: issue.title,
      days: Math.floor((Date.now() - new Date(issue.updated_at).getTime()) / (24 * 60 * 60 * 1000))
    }));
  }
}

步骤 5: Spam 检测

创建 src/analyzer/SpamDetector.ts

typescript 复制代码
import { Issue } from '@octokit/rest';

export class SpamDetector {
  private spamKeywords = [
    'buy', 'sell', 'discount', 'promo', 'coupon',
    'crypto', 'bitcoin', 'investment', 'trading',
    'contact me', 'whatsapp', 'telegram', 'wechat',
    'click here', 'visit website', 'free download',
    'adult', 'xxx', 'casino', 'gambling',
    '🔥', '💰', '💎', '🚀' // 表情符号 spam
  ];

  private spamPatterns = [
    /https?:\/\/[^\s]+/g, // 链接
    /@\w+/g, // @提及
    /\b[A-Z]{2,}\b/g, // 全大写单词
  ];

  async detect(issue: Issue): Promise<boolean> {
    const title = issue.title || '';
    const body = issue.body || '';
    const content = `${title} ${body}`.toLowerCase();

    let spamScore = 0;

    // 检查关键词
    this.spamKeywords.forEach(keyword => {
      if (content.includes(keyword.toLowerCase())) {
        spamScore += 2;
      }
    });

    // 检查链接数量
    const links = content.match(this.spamPatterns[0]);
    if (links && links.length > 2) {
      spamScore += 5;
    }

    // 检查新用户(账号创建时间短)
    const userAge = Date.now() - new Date(issue.user.created_at).getTime();
    const userAgeDays = userAge / (24 * 60 * 60 * 1000);
    if (userAgeDays < 7) {
      spamScore += 3;
    }

    // 检查内容长度(太短可能是 spam)
    if (body.length < 20) {
      spamScore += 1;
    }

    // 检查全大写
    const allCaps = content.match(/\b[A-Z]{4,}\b/g);
    if (allCaps && allCaps.length > 2) {
      spamScore += 2;
    }

    const isSpam = spamScore >= 8;
    
    if (isSpam) {
      console.log(`[Spam 评分] ${spamScore} 分 - ${issue.title}`);
    }

    return isSpam;
  }
}

步骤 6: 自动回复

创建 src/analyzer/AutoReplier.ts

typescript 复制代码
import { Issue } from '@octokit/rest';

export class AutoReplier {
  private templates: Array<{
    keywords: string[];
    template: string;
  }> = [
    {
      keywords: ['install', '安装', 'dependency', '依赖'],
      template: `👋 感谢提问!

关于安装问题,请尝试以下步骤:

\`\`\`bash
npm install package-name
# 或
yarn add package-name
\`\`\`

如果仍有问题,请提供:
1. Node.js 版本
2. 完整的错误信息
3. 操作系统信息

我们会尽快帮你解决!`
    },
    {
      keywords: ['error', 'bug', '报错', '失败'],
      template: `👋 收到你的问题!

为了更好地帮你解决,请提供:

1. **复现步骤**:如何触发这个错误?
2. **预期行为**:你期望发生什么?
3. **实际行为**:实际发生了什么?
4. **环境信息**:
   - Node.js 版本:
   - 操作系统:
   - 浏览器(如适用):

我们会尽快排查!`
    },
    {
      keywords: ['feature', 'request', '建议', '希望'],
      template: `👋 感谢你的功能建议!

这个想法很有价值,我们会认真考虑。

为了更好评估,请补充:
1. **使用场景**:什么情况下需要这个功能?
2. **实现思路**:你期望如何使用?
3. **优先级**:这个功能对你的重要程度?

欢迎提交 PR 贡献代码!`
    },
    {
      keywords: ['how to', '怎么', '如何', 'question'],
      template: `👋 好问题!

建议你先查看:
- 📖 [文档](https://example.com/docs)
- ❓ [FAQ](https://example.com/faq)
- 🔍 [历史 Issue](https://github.com/owner/repo/issues?q=)

如果没找到答案,请详细描述:
1. 你想实现什么?
2. 已经尝试了什么?
3. 遇到了什么困难?

我们会尽力帮助你!`
    }
  ];

  async getReply(issue: Issue): Promise<string | null> {
    const content = `${issue.title} ${issue.body}`.toLowerCase();

    for (const { keywords, template } of this.templates) {
      const matched = keywords.some(keyword => 
        content.includes(keyword.toLowerCase())
      );

      if (matched) {
        return template;
      }
    }

    return null;
  }
}

步骤 7: 数据存储

创建 src/storage/Database.ts

typescript 复制代码
import Database from 'better-sqlite3';
import path from 'path';

export class Database {
  private db: Database.Database;

  constructor(dbPath: string = './data/github.db') {
    const fullPath = path.join(process.cwd(), dbPath);
    this.db = new Database(fullPath);
    this.init();
  }

  private init() {
    // Star 记录表
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS star_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        owner TEXT NOT NULL,
        repo TEXT NOT NULL,
        count INTEGER NOT NULL,
        timestamp INTEGER NOT NULL
      )
    `);

    // Issue 处理记录表
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS issue_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        issue_id INTEGER UNIQUE NOT NULL,
        owner TEXT NOT NULL,
        repo TEXT NOT NULL,
        issue_number INTEGER NOT NULL,
        is_spam INTEGER NOT NULL,
        auto_replied INTEGER NOT NULL,
        processed_at INTEGER NOT NULL
      )
    `);

    // 创建索引
    this.db.exec(`
      CREATE INDEX IF NOT EXISTS idx_star_owner_repo ON star_records(owner, repo);
      CREATE INDEX IF NOT EXISTS idx_star_timestamp ON star_records(timestamp);
      CREATE INDEX IF NOT EXISTS idx_issue_issue_id ON issue_records(issue_id);
    `);
  }

  /**
   * 记录 Star 数量
   */
  recordStarCount(owner: string, repo: string, count: number) {
    const stmt = this.db.prepare(`
      INSERT INTO star_records (owner, repo, count, timestamp)
      VALUES (?, ?, ?, ?)
    `);
    stmt.run(owner, repo, count, Date.now());
  }

  /**
   * 获取上次 Star 数量
   */
  getLastStarCount(owner: string, repo: string): number | null {
    const stmt = this.db.prepare(`
      SELECT count FROM star_records
      WHERE owner = ? AND repo = ?
      ORDER BY timestamp DESC
      LIMIT 1
    `);
    const result = stmt.get(owner, repo) as { count: number } | undefined;
    return result?.count || null;
  }

  /**
   * 获取 Star 历史
   */
  getStarHistory(owner: string, repo: string, days: number) {
    const since = Date.now() - days * 24 * 60 * 60 * 1000;
    const stmt = this.db.prepare(`
      SELECT count, timestamp FROM star_records
      WHERE owner = ? AND repo = ? AND timestamp >= ?
      ORDER BY timestamp ASC
    `);
    return stmt.all(owner, repo, since) as Array<{ count: number; timestamp: number }>;
  }

  /**
   * 检查 Issue 是否已处理
   */
  isIssueProcessed(issueId: number): boolean {
    const stmt = this.db.prepare(`
      SELECT 1 FROM issue_records WHERE issue_id = ?
    `);
    return !!stmt.get(issueId);
  }

  /**
   * 标记 Issue 已处理
   */
  markIssueProcessed(
    issueId: number,
    data: { isSpam: boolean; autoReplied: boolean },
    owner?: string,
    repo?: string,
    issueNumber?: number
  ) {
    const stmt = this.db.prepare(`
      INSERT OR REPLACE INTO issue_records 
      (issue_id, owner, repo, issue_number, is_spam, auto_replied, processed_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
    stmt.run(
      issueId,
      owner || '',
      repo || '',
      issueNumber || 0,
      data.isSpam ? 1 : 0,
      data.autoReplied ? 1 : 0,
      Date.now()
    );
  }

  /**
   * 获取统计数据
   */
  getStats(owner: string, repo: string, days: number = 30) {
    const since = Date.now() - days * 24 * 60 * 60 * 1000;

    const starRecords = this.getStarHistory(owner, repo, days);
    const issueStmt = this.db.prepare(`
      SELECT COUNT(*) as total, 
             SUM(is_spam) as spam_count,
             SUM(auto_replied) as replied_count
      FROM issue_records
      WHERE owner = ? AND repo = ? AND processed_at >= ?
    `);
    const issueStats = issueStmt.get(owner, repo, since) as any;

    return {
      stars: {
        current: starRecords[starRecords.length - 1]?.count || 0,
        growth: starRecords.length > 1 
          ? starRecords[starRecords.length - 1].count - starRecords[0].count 
          : 0
      },
      issues: {
        total: issueStats.total || 0,
        spam: issueStats.spam_count || 0,
        autoReplied: issueStats.replied_count || 0
      }
    };
  }
}

步骤 8: 通知适配器

创建 src/notifier/FeishuNotifier.ts

typescript 复制代码
import axios from 'axios';
import { logger } from '../utils/logger';

export class FeishuNotifier {
  private webhook: string;

  constructor(webhook: string) {
    this.webhook = webhook;
  }

  async sendStarUpdate(owner: string, repo: string, delta: number, current: number) {
    const card = {
      msg_type: 'interactive',
      card: {
        header: {
          template: delta > 0 ? 'green' : 'red',
          title: {
            tag: 'plain_text',
            content: `⭐ ${owner}/${repo} Star 更新`
          }
        },
        elements: [
          {
            tag: 'stat',
            data: [
              {
                name: '当前 Star',
                value: current.toString()
              },
              {
                name: '变化',
                value: `${delta > 0 ? '+' : ''}${delta}`
              }
            ]
          }
        ]
      }
    };

    await this.send(card);
  }

  async sendNewIssue(owner: string, repo: string, issue: {
    number: number;
    title: string;
    author: string;
    isSpam: boolean;
  }) {
    const card = {
      msg_type: 'interactive',
      card: {
        header: {
          template: issue.isSpam ? 'red' : 'blue',
          title: {
            tag: 'plain_text',
            content: `📝 新 Issue #${issue.number}`
          }
        },
        elements: [
          {
            tag: 'div',
            text: {
              tag: 'lark_md',
              content: `**仓库**: ${owner}/${repo}\n**标题**: ${issue.title}\n**作者**: @${issue.author}\n**Spam**: ${issue.isSpam ? '⚠️ 是' : '✅ 否'}`
            }
          },
          {
            tag: 'action',
            actions: [
              {
                tag: 'button',
                text: {
                  tag: 'plain_text',
                  content: '查看 Issue'
                },
                url: `https://github.com/${owner}/${repo}/issues/${issue.number}`,
                type: 'default'
              }
            ]
          }
        ]
      }
    };

    await this.send(card);
  }

  async sendWeeklyReport(stats: any) {
    const card = {
      msg_type: 'interactive',
      card: {
        header: {
          template: 'blue',
          title: {
            tag: 'plain_text',
            content: '📊 GitHub 项目周报'
          }
        },
        elements: [
          {
            tag: 'div',
            text: {
              tag: 'lark_md',
              content: this.buildReportText(stats)
            }
          }
        ]
      }
    };

    await this.send(card);
  }

  private async send(card: any) {
    try {
      await axios.post(this.webhook, card, {
        headers: { 'Content-Type': 'application/json' }
      });
      logger.success('[飞书通知] 发送成功');
    } catch (error) {
      logger.error(`[飞书通知] 发送失败:${error}`);
    }
  }

  private buildReportText(stats: any): string {
    let text = `**统计周期**: 最近 7 天\n\n`;
    text += `⭐ **Star 数据**\n`;
    text += `当前 Star: ${stats.stars.current}\n`;
    text += `本周增长:${stats.stars.growth}\n\n`;
    text += `📝 **Issue 数据**\n`;
    text += `新增 Issue: ${stats.issues.total}\n`;
    text += `Spam 过滤:${stats.issues.spam}\n`;
    text += `自动回复:${stats.issues.autoReplied}\n`;
    return text;
  }
}

步骤 9: 主入口

创建 src/index.ts

typescript 复制代码
import { GitHubClient } from './github/GitHubClient';
import { StarMonitor } from './github/StarMonitor';
import { IssueMonitor } from './github/IssueMonitor';
import { SpamDetector } from './analyzer/SpamDetector';
import { AutoReplier } from './analyzer/AutoReplier';
import { Database } from './storage/Database';
import { FeishuNotifier } from './notifier/FeishuNotifier';
import { logger } from './utils/logger';
import { CronJob } from 'cron';

class GitHubMonitor {
  private github: GitHubClient;
  private db: Database;
  private starMonitor: StarMonitor;
  private issueMonitor: IssueMonitor;
  private notifier: FeishuNotifier;
  private config: any;

  constructor(config: any) {
    this.config = config;
    this.github = new GitHubClient(config.github.token);
    this.db = new Database();
    this.starMonitor = new StarMonitor(this.github, this.db);
    this.issueMonitor = new IssueMonitor(
      this.github,
      this.db,
      new SpamDetector(),
      new AutoReplier()
    );
    this.notifier = new FeishuNotifier(config.feishu.webhook);
  }

  async run() {
    logger.info('🚀 GitHub 监控启动');

    for (const repo of this.config.repos) {
      const [owner, name] = repo.split('/');

      // 检查 Star
      const starResult = await this.starMonitor.check(owner, name);
      if (starResult.changed && Math.abs(starResult.delta) >= 5) {
        await this.notifier.sendStarUpdate(owner, name, starResult.delta, starResult.current);
      }

      // 检查新 Issue
      const newIssues = await this.issueMonitor.checkNewIssues(owner, name);
      for (const issue of newIssues) {
        if (!issue.isSpam) {
          await this.notifier.sendNewIssue(owner, name, issue);
        }
      }

      // 检查过期 Issue
      const staleIssues = await this.issueMonitor.checkStaleIssues(owner, name);
      if (staleIssues.length > 0) {
        logger.warn(`发现 ${staleIssues.length} 个过期 Issue 需要处理`);
      }
    }

    logger.info('✅ 本轮监控完成');
  }

  async sendWeeklyReport() {
    logger.info('📊 生成周报');

    for (const repo of this.config.repos) {
      const [owner, name] = repo.split('/');
      const stats = this.db.getStats(owner, name, 7);
      await this.notifier.sendWeeklyReport(stats);
    }
  }

  start() {
    // 每 30 分钟检查一次
    const checkJob = new CronJob('0 */30 * * * *', () => {
      this.run().catch(console.error);
    }, null, true, 'Asia/Shanghai');

    // 每周五发送周报
    const reportJob = new CronJob('0 9 * * 5', () => {
      this.sendWeeklyReport().catch(console.error);
    }, null, true, 'Asia/Shanghai');

    logger.info('⏰ 定时任务已启动');
  }
}

// 启动
const config = {
  github: {
    token: process.env.GITHUB_TOKEN
  },
  feishu: {
    webhook: process.env.FEISHU_WEBHOOK
  },
  repos: [
    'your-username/your-repo'
  ]
};

const monitor = new GitHubMonitor(config);
monitor.start();

步骤 10: 配置文件

创建 config/settings.json

json 复制代码
{
  "github": {
    "token": "ghp_xxx",
    "app_id": null,
    "private_key": null
  },
  "feishu": {
    "webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
  },
  "repos": [
    {
      "name": "your-username/your-repo",
      "star_threshold": 10,
      "notify_stale_days": 7
    }
  ],
  "schedule": {
    "check_interval": "*/30 * * * *",
    "report_day": "5",
    "report_hour": "9"
  },
  "auto_reply": {
    "enabled": true,
    "templates": [
      {
        "keywords": ["install", "安装"],
        "template": "感谢提问!请尝试 npm install..."
      }
    ]
  },
  "spam_filter": {
    "enabled": true,
    "threshold": 8
  }
}

完整代码

项目已开源在 GitHub:

复制代码
https://github.com/your-username/github-monitor

快速开始

bash 复制代码
# 克隆项目
git clone https://github.com/your-username/github-monitor.git
cd github-monitor

# 安装依赖
npm install

# 配置环境变量
export GITHUB_TOKEN="ghp_xxx"
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"

# 运行
npm run start

部署与运行

本地运行

bash 复制代码
npm run start

服务器部署

bash 复制代码
# 使用 PM2
pm2 start dist/index.js --name github-monitor
pm2 save

Vercel 部署(Serverless)

typescript 复制代码
// api/monitor.ts
export default async function handler(req, res) {
  const monitor = new GitHubMonitor(config);
  await monitor.run();
  res.status(200).json({ success: true });
}

GitHub Actions 定时运行

yaml 复制代码
# .github/workflows/monitor.yml
name: GitHub Monitor

on:
  schedule:
    - cron: '0 */30 * * * *'

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm install
      - run: npm run start
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          FEISHU_WEBHOOK: ${{ secrets.FEISHU_WEBHOOK }}

运行效果

终端输出示例

bash 复制代码
$ npm run start

> github-monitor@1.0.0 start
> node dist/index.js

🚀 GitHub 监控启动
⏰ 定时任务已启动

[2026-04-16 14:30:00] [INFO] 开始检查:your-username/your-repo
[2026-04-16 14:30:02] [INFO] [Star 监控] your-username/your-repo: 1250 → 1267 (+17)
[2026-04-16 14:30:03] [SUCCESS] [飞书通知] 发送成功:Star 更新
[2026-04-16 14:30:04] [INFO] [新 Issue] #142: Installation fails on Windows by @user123
[2026-04-16 14:30:05] [INFO] [自动回复] #142
[2026-04-16 14:30:06] [INFO] [新 Issue] #143: 🚀🚀 Best crypto investment opportunity by @spammer
[2026-04-16 14:30:07] [WARN] [Spam 检测] #143 标记为垃圾信息
[2026-04-16 14:30:08] [SUCCESS] [飞书通知] 发送成功:新 Issue
[2026-04-16 14:30:09] [INFO] ✅ 本轮监控完成

飞书通知效果

Star 增长通知

复制代码
⭐ your-username/your-repo Star 更新

当前 Star: 1267
变化:+17

新 Issue 通知

复制代码
📝 新 Issue #142

仓库:your-username/your-repo
标题:Installation fails on Windows
作者:@user123
Spam: ✅ 否

[查看 Issue] → https://github.com/...

周报

复制代码
📊 GitHub 项目周报

统计周期:最近 7 天

⭐ Star 数据
当前 Star: 1267
本周增长:+89

📝 Issue 数据
新增 Issue: 23
Spam 过滤:8
自动回复:12

我踩过的坑

坑 1:GitHub API 限流,请求失败

刚开始我没注意 API 限流,疯狂请求,结果被限制了。那天是 2025 年 8 月 15 日,我正等着看 Star 增长,结果所有请求都返回 403。

查了文档才知道,Personal Access Token 每小时只有 5000 次调用。

解决方案

  1. 增加缓存,减少重复请求
  2. 监控剩余配额
  3. 使用 GitHub App(更高限额)
typescript 复制代码
// 检查剩余配额
const { remaining } = await octokit.rateLimit.get();
console.log(`剩余 API 调用:${remaining}`);

// 如果低于 100,暂停请求
if (remaining < 100) {
  logger.warn('API 配额不足,暂停请求');
  return;
}

坑 2:自动回复误伤正常用户

有次我的自动回复把一个新用户的问题当成了重复问题,回复了一个模板。结果用户在 Issue 里说:"你这是机器人吧?根本没看我的问题。"

我当时挺尴尬的。

解决方案

  1. 只在回复中说明是自动回复
  2. 添加白名单用户(贡献者不触发自动回复)
  3. 只回复首次 Issue,不回复评论
typescript 复制代码
// 在回复开头说明
const replyTemplate = `🤖 **自动回复**

${template}

---
*这是自动回复,如有问题请继续评论*`;

坑 3:Spam 检测太严格,误删正常 Issue

刚开始 Spam 检测阈值设得太低(5 分),结果有个新用户的 Issue 被误判了。他发了个功能建议,因为账号新、内容短,被当成 Spam 了。

解决方案

  1. 提高阈值到 8 分
  2. 标记为 Spam 但不自动关闭
  3. 人工审核后再处理
typescript 复制代码
// 阈值从 5 提高到 8
const isSpam = spamScore >= 8;

坑 4:Webhook 通知失败,没收到 Star 激增提醒

有次我的项目上了 GitHub Trending,但因为 Webhook 配置错了,没收到通知。等我发现的时候,热度已经过了。

解决方案

  1. 添加发送失败重试机制(重试 3 次)
  2. 设置失败通知(如发送邮件)
  3. 定期检查机器人状态
typescript 复制代码
try {
  await axios.post(webhook, card);
} catch (error) {
  // 重试 3 次
  for (let i = 0; i < 3; i++) {
    await sleep(1000);
    try {
      await axios.post(webhook, card);
      break;
    } catch (e) {
      if (i === 2) throw e;
    }
  }
}

坑 5:数据库文件太大,查询变慢

跑了半年后,数据库文件到了 500MB,查询明显变慢。有次生成周报,等了 30 秒才出来。

解决方案

  1. 定期清理旧数据(保留最近 90 天)
  2. 添加数据库索引
  3. 分库分表(如果数据量特别大)
typescript 复制代码
// 清理 90 天前的数据
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000;
db.prepare('DELETE FROM star_records WHERE timestamp < ?').run(ninetyDaysAgo);

读者常问

@开源作者小王: "监控 5 个仓库,API 调用会不会超限?"

答:看监控频率。如果每 30 分钟检查一次,5 个仓库每小时 10 次调用,一天 240 次,一个月 7200 次,远低于 5000 次/小时的限制。

我目前监控 3 个仓库,每 30 分钟检查一次,一个月 API 调用约 4500 次,配额充足。

@技术团队 Leader: "能监控私有仓库吗?"

答:可以。Token 需要有 repo 权限:

复制代码
创建 Token 时勾选:repo (Full control of private repositories)

有个读者是技术团队 Leader,用这个工具监控公司 5 个私有仓库,说"终于不用手动查每个项目的数据了"。

@独立开发者: "自动回复会不会得罪用户?"

答:确实有这个风险。我的建议:

  1. 在回复中明确说明是自动回复
  2. 只针对常见问题(安装、报错)
  3. 复杂问题不触发自动回复

我用了半年,收到过 2 次用户反馈,都是说"谢谢自动回复,但我问的不是这个"。后来我优化了关键词匹配,这种情况少多了。

@较真的读者: "这个工具会不会被 GitHub 封号?"

答:好问题。我的理解:

  1. 遵守 API 限流:别超过配额,一般没问题
  2. 合理使用:别用来刷 Star、发 Spam
  3. 阅读条款:使用前看 GitHub 服务条款

⚠️ 免责声明:本工具仅供学习研究使用。使用前请阅读 GitHub 服务条款,合规使用。

@完美主义者: "周报模板能自定义吗?我们团队有固定格式。"

答:可以。修改 FeishuNotifier.ts 中的 buildReportText 方法,按你们团队格式调整。

有个读者是开源办公室的,他们公司有固定周报格式,花了一下午改代码,现在完全符合公司要求。

@新手小白: "对新手不太友好,希望能有更详细的教程。"

答:这个我的锅。已经录了视频教程,下周末发 B 站。包括:

  1. GitHub Token 创建演示
  2. 工具配置详细步骤
  3. 常见问题排查

@质疑的读者: "真的能省 28.5 小时吗?感觉有水分。"

答:这个数据是我实际统计的。我之前每周花 7 小时管理项目,一个月就是 28 小时。再加上生成报告、清理 Spam 的时间,28.5 小时是保守估计。

你可以自己试一周,记录手动管理花的时间,再跟自动化对比。

@安全专家: "Token 存储在环境变量里安全吗?"

答:好问题。建议:

  1. 不要把 Token 上传到 GitHub
  2. 使用加密存储
  3. 定期更换 Token

扩展思路

1. PR 自动审查

typescript 复制代码
// 检查 PR 是否符合规范
async function checkPR(pr: PullRequest) {
  const checks = [];
  
  // 检查标题格式
  if (!/^(feat|fix|docs|style|refactor|test|chore):/.test(pr.title)) {
    checks.push('❌ 标题不符合 Conventional Commits 规范');
  }
  
  // 检查描述
  if (!pr.body || pr.body.length < 50) {
    checks.push('❌ PR 描述过于简短');
  }
  
  // 检查关联 Issue
  if (!pr.body?.includes('Fixes #') && !pr.body?.includes('Closes #')) {
    checks.push('⚠️ 建议关联相关 Issue');
  }
  
  return checks;
}

2. 贡献者统计

typescript 复制代码
// 统计贡献者数据
async function getContributorStats(owner: string, repo: string) {
  const contributors = await github.repos.listContributors({ owner, repo });
  
  return contributors.map(c => ({
    login: c.login,
    contributions: c.contributions,
    avatar: c.avatar_url
  })).sort((a, b) => b.contributions - a.contributions);
}

3. Release 监控

typescript 复制代码
// 监控新版本发布
async function checkNewReleases(owner: string, repo: string) {
  const releases = await github.repos.listReleases({ owner, repo, per_page: 1 });
  const latest = releases[0];
  
  // 对比上次记录的版本
  const lastVersion = await db.getLastReleaseVersion(owner, repo);
  
  if (latest.tag_name !== lastVersion) {
    await notifier.sendReleaseNotification(latest);
    await db.recordReleaseVersion(owner, repo, latest.tag_name);
  }
}

4. 与 CI/CD 集成

  • Issue 创建时自动运行测试
  • PR 自动添加 reviewers
  • 合并后自动部署

5. 智能标签

typescript 复制代码
// 根据 Issue 内容自动添加标签
async function autoLabel(issue: Issue) {
  const content = `${issue.title} ${issue.body}`.toLowerCase();
  
  const labels = [];
  if (content.includes('bug') || content.includes('error')) labels.push('bug');
  if (content.includes('feature') || content.includes('request')) labels.push('enhancement');
  if (content.includes('help') || content.includes('question')) labels.push('question');
  
  if (labels.length > 0) {
    await github.issues.addLabels({ owner, repo, issue_number: issue.number, labels });
  }
}

使用效果

我的使用数据

从 2025 年 8 月 1 日开始用这个工具,到 2026 年 4 月 16 日,共 258 天:

  • 监控仓库: 3 个(从 1 个逐步增加)
  • Star 提醒: 89 次(平均每周 2.4 次)
  • Issue 处理: 412 个(自动回复 287 个,Spam 过滤 89 个)
  • 生成周报: 37 份(每周一份)
  • 节省时间: 约 244 小时(258 ÷ 30 × 28.5 小时)

最明显的好处是,我再也没有错过重要热度。2025 年 10 月 26 日,工具 5 分钟内推送 Star 激增通知。我一看数据,发现是被一个技术大 V 转发了。我立刻跟进,当天新增了 200+ Stars,还收获了 15 个高质量 Issue。

省下来的时间我用来:

  • 优化核心功能(最重要)
  • 写技术文档
  • 回复高质量 Issue
  • 陪家人

读者案例

@开源作者老王(5 年经验,3 个项目):

  • 使用时间:2025 年 9 月 - 至今
  • 每周节省:6 小时
  • 效果:Issue 响应时间从 2 天降到 2 小时
  • "以前下班还要刷 GitHub 看有没有新 Issue,现在自动通知,心里踏实多了。"

@技术团队 Leader 小美(管理 5 个私有仓库):

  • 使用时间:2026 年 1 月 - 至今
  • 每周节省:8 小时
  • 效果:团队周报自动生成,不用手动统计
  • "之前每周一花 2 小时整理项目数据,现在工具自动生成,省下来的时间用来跟团队成员沟通。"

@独立开发者老李(一人公司,1 个项目):

  • 使用时间:2026 年 2 月 - 至今
  • 每周节省:4 小时
  • 效果:Spam 自动过滤,不再被广告打扰
  • "我之前每周至少花 1 小时清理 Spam Issue,现在自动过滤,清净多了。"

统计数据

根据 15 位读者的反馈(2025-2026):

指标 平均值 最佳
监控仓库 4 个 12 个
每周节省 5 小时 10 小时
Spam 过滤率 92% 98%
自动回复准确率 85% 95%
满意度 4.5/5 5/5

批评意见

@较真的读者: "有些 Issue 自动回复不准确,还是得人工看。"

答:你说得对。自动回复不是万能的,准确率 85% 左右。建议:

  1. 自动回复作为初稿,人工审核
  2. 复杂问题不触发自动回复
  3. 持续优化关键词匹配

@完美主义者: "周报数据不够详细,希望能有更多维度。"

答:已经在改了。v2.0 会支持:

  1. 贡献者统计
  2. PR 分析
  3. 代码质量指标

@安全专家: "自动关闭 Spam Issue 会不会误删?"

答:好问题。我现在改为标记为 Spam 但不自动关闭,人工审核后再处理。建议:

  1. 不要自动关闭 Issue
  2. 添加 spam 标签即可
  3. 定期人工审核

常见问题

Q1: GitHub API 限流怎么办?

A:

  1. 使用 Personal Access Token(每小时 5000 次)
  2. 使用 GitHub App(更高限额)
  3. 缓存结果,减少请求频率
  4. 监控剩余配额

Q2: 如何监控私有仓库?

A : Token 需要有 repo 权限:

复制代码
创建 Token 时勾选:repo (Full control of private repositories)

Q3: 自动回复误判怎么办?

A:

  1. 调整关键词匹配规则
  2. 添加白名单用户
  3. 只回复首次 Issue,不回复评论
  4. 在回复中说明是自动回复

Q4: 如何监控多个仓库?

A: 在配置中添加:

json 复制代码
{
  "repos": [
    "owner/repo1",
    "owner/repo2",
    "owner/repo3"
  ]
}

Q5: 如何自定义通知内容?

A : 修改 FeishuNotifier.ts 中的卡片模板,参考飞书开放平台文档。

Q6: 数据库文件太大怎么办?

A:

  1. 定期清理旧数据(保留最近 90 天)
  2. 添加数据库索引
  3. 分库分表

写在最后

一些真心话

工具再好,也只是工具。最重要的是跟用户的互动,而不是冷冰冰的自动化。

我见过太多人(包括以前的我):

  • 迷信工具,不重视真实反馈
  • 追求 Star 数量,忽略了项目质量
  • 自动回复所有 Issue,失去了人情味

工具能帮你节省时间,但不能代替你思考。所以:

我的建议

  1. 用好工具:自动化管理,节省时间
  2. 做好互动:把省下的时间用来回复高质量 Issue
  3. 保持真诚:自动回复要说明,别让用户觉得被敷衍
  4. 长期主义:开源是一场马拉松

行动号召

  • 🎯 今天:创建 GitHub Token,配置第一个监控仓库
  • 🎯 明天:测试 Star 监控和 Issue 通知,确保飞书能收到
  • 🎯 本周:配置自动回复模板,根据常见问题调整
  • 🎯 本月:收到第一份周报,根据数据优化项目策略

长期目标

  • 📊 建立项目数据档案
  • 📈 每周回顾项目健康状况
  • 🎯 根据用户反馈优化功能
  • 🧠 培养开源维护直觉
相关推荐
汀、人工智能2 小时前
AI Compass前沿速览:聚焦 GPT-Image-2、Qwen3.6-Max-Preview、ClawLess 与 AgentScope Tuner
人工智能·gpt·chatgpt
05大叔2 小时前
语言模型学习-统计语言模型 神经语言模型
人工智能·语言模型·自然语言处理
IT观测2 小时前
2026年视频格式转换器哪个好?国内视频音频格式转换软件功能对比与选型指南
人工智能·音视频
醉卧考场君莫笑2 小时前
NLP(基于统计的任务范式与单词向量化)
人工智能·自然语言处理
xiaotao1312 小时前
03-深度学习基础:LangChain应用开发
人工智能·深度学习·langchain
knight_9___2 小时前
RAG面试题4
开发语言·人工智能·python·面试·agent·rag
newsxun2 小时前
布局大湾区“黄金内湾”,HECHTER CAFE亚洲首店落子万象滨海购物村
大数据·人工智能
Daydream.V2 小时前
github基础入门及git安装配置
git·github·git学习·github学习
Y学院2 小时前
Spring AI Alibaba 高质量实战教程(从入门到企业级落地)
java·人工智能·spring·自然语言处理