
Github Star Manager
- 85 installs
- 543 repo stars
- Updated August 5, 2026
- cat-xierluo/legal-skills
Auto-discover and star GitHub projects, track their new releases and commits, and generate a visual dashboard of updates.
About
Manages starred GitHub projects by auto-discovering and starring repos, detecting new versions, releases, and important commits, and producing structured update reports and a dashboard. A developer uses it to track updates across their starred projects.
- Detects new releases, versions and key commits
- Generates structured update reports and a dashboard
Github Star Manager by the numbers
- 85 all-time installs (skills.sh)
- Ranked #875 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cat-xierluo/legal-skills --skill github-star-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 85 |
|---|---|
| repo stars | ★ 543 |
| Last updated | August 5, 2026 |
| Repository | cat-xierluo/legal-skills ↗ |
What it does
Auto-discover and star GitHub projects, track their new releases and commits, and generate a visual dashboard of updates.
Files
GitHub Star 管理器 (GitHub Star Manager)
简介
专注于 GitHub Star 项目的更新追踪与同步工具。自动检测您 Star 的项目是否有新版本、新 Release、重要 Commit 变化,并生成结构化的更新报告。
核心特色: 提供 HTML 可视化 Dashboard,快速浏览所有 Star 项目。
何时使用
本技能在以下场景下触发:
- 用户需要从内容中自动发现并 Star GitHub 项目
- 用户需要追踪 GitHub Star 项目的更新
- 用户需要监控依赖库的版本发布
- 用户需要查看项目活跃度和健康度
- 用户需要管理大量 starred 仓库
- 用户需要快速浏览和筛选 Star 项目
- 用户需要生成项目的可视化报告
模块说明
本技能包含两个模块:
| 模块 | 触发方式 | 功能 |
|---|---|---|
| 对话模块 | 对话触发 | 从内容提取项目并 Star |
| 脚本模块 | 命令行/定时任务 | 同步、Dashboard、追踪、批量管理 |
核心功能
1. 对话模块:自动发现并 Star
从各种内容来源中自动提取 GitHub 仓库引用,并在你的 GitHub 账户上 star 它们。
支持的触发方式:
- "分析这篇文章并 star 里面的项目"
- "从这张截图里找 GitHub 项目并 star"
- "Star [内容] 中提到的所有 GitHub 项目"
工作流程:
1. 内容提取与上下文分析
- 文字/URL:使用 WebFetch 获取文章内容,解析 GitHub URL
- 截图/图片:使用图片分析 MCP 提取文字并识别项目引用
2. 仓库发现与智能匹配
- 直接匹配:内容中找到的完整 GitHub URL
- 按名称搜索:当只有项目名时使用
gh search repos - 上下文相关性验证:检查 topics、description、技术栈是否匹配
3. 检查是否已 Star
gh api user/starred/owner/repo 2>/dev/null4. Star 仓库
gh repo star owner/repo5. 生成报告
- 新 star 的仓库列表
- 已 star 的仓库(跳过)
- 需人工确认的仓库
2. 更新追踪
- 版本检测:检测项目的新 Release 和 Tag
- 活跃度监控:追踪最近的 Commit 活跃度
- 变更摘要:使用 AI 总结版本变更内容
4. 智能分析
- 项目摘要:自动生成项目核心功能说明
- 价值评估:分析项目与您的关注领域匹配度(高/中/低)
- 健康度指标:项目维护状态、Stars 增长趋势
5. HTML 可视化 Dashboard
- 信息密集卡片:一屏展示更多项目
- 颜色编码状态:活跃/近期更新/长期未更一目了然
- 筛选与搜索:按状态、价值、关键词过滤
- 展开详情:点击卡片查看更多信息
依赖
系统依赖
| 依赖 | 安装方式 |
|---|---|
| Python 3.8+ | macOS: brew install python3<br>Linux: sudo apt-get install python3 |
Python 包
| 包名 | 用途 | 安装命令 |
|---|---|---|
requests | HTTP 请求,调用 GitHub API | pip install requests |
python-dotenv | 从 .env 文件加载配置 | pip install python-dotenv |
openai | AI 摘要生成(可选) | pip install openai |
依赖包文件
pip install -r assets/requirements.txt使用方法
1. 环境准备
方式 1:使用 .env 配置文件(推荐)
# 1. 复制示例配置文件
cp assets/.env.example .env
# 2. 编辑 .env 文件,填入你的 API 密钥
# GITHUB_PAT=ghp_xxxxxxxxxxxxxxxxx
# OPENAI_API_KEY=sk-xxxxxxxxxxxx
# 3. 安装依赖
pip install -r assets/requirements.txt方式 2:环境变量
# 直接设置环境变量
export GITHUB_PAT="你的_github_pat_token"
export OPENAI_API_KEY="你的_openai_api_key"如何获取 GitHub PAT
1. 访问 https://github.com/settings/tokens 2. 点击 "Generate new token (classic)" 3. 选择权限:
public_repo(访问公开仓库)- 如果需要访问私有 Star,勾选
repo
4. 生成后复制 token(只显示一次!)
Token 作用:
- 无 Token:60 次/小时请求限制
- 有 Token:5000 次/小时请求限制
2. 启动 Dashboard(推荐)
# 导出数据并打开 Dashboard
python scripts/main.py --export --user 你的用户名 && open dashboard.html首次使用说明: 如果 dashboard.html 不存在,系统会自动从 assets/dashboard.example.html 复制一份。
Dashboard 功能:
- 📊 总览:总项目数、本周活跃数、新版本数
- 🔍 筛选:按状态(活跃/近期更新/长期未更)、价值(高/中)过滤
- 🔎 搜索:按项目名、描述、标签搜索
- 📦 卡片:显示项目名、描述、语言、标签、Stars、更新时间、价值评估
- 🖱️ 点击卡片:展开详细信息(创建时间、Forks、Issues、收藏理由)
- 🔗 快速跳转:点击 GitHub 图标直达项目
3. 首次同步(建立基准)
python scripts/main.py --init --user 你的用户名 --limit 50首次运行会保存所有 Star 的快照作为后续对比基准。
4. 检查更新
python scripts/main.py --check --user 你的用户名对比上次快照,生成更新报告。
5. 生成完整报告
python scripts/main.py --report --user 你的用户名 --days 7生成包含项目摘要和更新状态的完整报告。
6. 定期运行(推荐)
# 每周检查一次
python scripts/main.py --check --user 你的用户名 --weekly配置与自定义
配置文件
本技能使用以下配置文件:
| 文件 | 用途 |
|---|---|
assets/categories.yaml | 分类定义和关键词规则 |
assets/tags.json | 标签管理和别名配置 |
assets/.env.example | 环境变量模板 |
首次运行时,配置文件会自动复制到 ~/.github-star-manager/ 目录。
适用场景
- 开发者:及时了解依赖库的版本更新
- 技术爱好者:跟踪 AI/开源领域的最新动态
- 项目经理:监控竞品或相关项目的进展
与其他技能的区别
| 功能 | github-star-manager | repo-research |
|---|---|---|
| 焦点 | Star 项目发现 + 管理 + 追踪 | 单个仓库的深度研究 |
| 输出 | Dashboard + 变更摘要 | 架构分析、代码解读 |
| 用途 | 日常订阅更新 | 一次性深度调研 |
汇报格式规范
当 GitHub Star 同步任务需要汇报时,使用以下固定格式:
⭐ GitHub Stars 同步报告 — YYYY-MM-DD HH:MM
总项目数: XXX 个(±Y)
---
**🔄 仓库转移**(同一项目,owner/name 变了)
- ~~旧路径~~ → **新路径**(⭐ N)
**➕ 新增 Star (N)**
- 项目名(⭐ N)
描述(如果有)
**➖ 取消 Star (N)**
- 项目名
---
**📊 汇总**
- 新增: X 个
- 取消: Y 个
- 转移: Z 个
- 当前总数: XXX规则:
- 无变化时完全静默,不发送任何消息
- 新增和取消分开列出,清晰的 emoji 区分
- 仓库转移放在最前面(因为容易被忽略)
- 最后有汇总行,方便快速扫视
参考文档
- SKILL-GUIDE.md - 技能开发指南
- AGENTS.md - 项目协作规范
# GitHub Stars Tracker 配置文件
# 复制此文件为 .env 并填入你的实际值
# GitHub Personal Access Token (必填)
# 获取方式: https://github.com/settings/tokens
# 推荐权限: public_repo (仅读取)或 repo (需要 Star 管理功能)
# 注意: Token 不包含用户信息,程序会自动通过 API 获取用户名
GITHUB_PAT=ghp_your_token_here
# GitHub Star 分类配置
#
# 说明:
# - 此文件为默认模板,仅作为参考
# - 实际分类会在首次运行时根据你的 Star 数据动态生成
# - 生成的分类保存在 output/categories.yaml
#
# 使用方法:
# python scripts/generate_categories.py # 增量更新
# python scripts/generate_categories.py --force # 强制重新生成
# 保留一个最基础的分类作为兜底
categories:
- id: uncategorized
name: 未分类
icon: "📦"
color: "#6b7280"
description: "尚未匹配到分类的项目"
keywords: []
# 以下是动态生成的分类示例(首次运行后会被替换)
# 实际分类会根据你的 GitHub Star 数据中的 topics 频率自动生成
# 例如:
# - ai-agents: AI Agent 相关项目
# - claude-code: Claude Code 生态
# - llm: 大语言模型
# - chinese-platform: 中国平台(微信、抖音等)
# - devtools: 开发工具
# - workflow: 工作流自动化
# - productivity: 效率工具
# - content-processing: 内容处理(OCR、PDF、TTS等)
# - crawler: 爬虫/抓取
# - rss: RSS/订阅
{
"$schema": "./config.schema.json",
"check_interval_days": 7,
"min_stars": 0,
"focus_areas": [],
"ignore_repos": [],
"report_format": "markdown",
"include_forks": false,
"max_repos": 200,
"cache_expiry_hours": 24
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Stars Tracker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* ===== 主题变量 ===== */
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--border-color: #30363d;
--accent-blue: #58a6ff;
--accent-purple: #a371f7;
--accent-green: #22c55e;
--card-shadow: 0 1px 3px rgba(0,0,0,0.3);
--card-shadow-hover: 0 8px 24px rgba(0,0,0,0.4);
}
[data-theme="light"] {
--bg-primary: #f6f8fa;
--bg-secondary: #ffffff;
--bg-tertiary: #f0f3f6;
--text-primary: #1f2328;
--text-secondary: #57606a;
--text-muted: #8b949e;
--border-color: #d0d7de;
--card-shadow: 0 1px 3px rgba(0,0,0,0.08);
--card-shadow-hover: 0 8px 24px rgba(0,0,0,0.12);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
font-size: 14px;
transition: background 0.3s, color 0.3s;
}
.container { max-width: 1280px; margin: 0 auto; padding: 20px; }
/* ===== 头部 ===== */
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
margin-bottom: 20px;
}
.header-left { display: flex; align-items: center; gap: 16px; }
h1 {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.stats {
display: flex;
gap: 20px;
font-size: 13px;
color: var(--text-secondary);
}
.stat-value {
font-weight: 600;
color: var(--accent-blue);
margin-right: 4px;
}
/* 主题切换按钮 */
.theme-toggle {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.theme-toggle:hover {
background: var(--bg-secondary);
border-color: var(--accent-blue);
}
/* ===== 筛选栏 ===== */
.toolbar {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
.search-box {
flex: 1;
min-width: 240px;
position: relative;
}
.search-box input {
width: 100%;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 10px 14px;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s;
}
.search-box input:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
}
.search-box input::placeholder { color: var(--text-muted); }
select {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
select:hover { border-color: var(--accent-blue); }
select:focus { outline: none; border-color: var(--accent-blue); }
.page-size-select {
background: var(--bg-secondary);
padding: 8px 12px;
font-size: 12px;
}
/* ===== 时间维度筛选器(Tab 式) ===== */
.time-filter-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.time-filter-header {
display: flex;
gap: 4px;
margin-bottom: 12px;
background: var(--bg-tertiary);
padding: 4px;
border-radius: 8px;
}
.time-tab {
flex: 1;
padding: 10px 16px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.time-tab:hover { color: var(--text-primary); }
.time-tab.active {
background: var(--bg-secondary);
color: var(--text-primary);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.time-tab .tab-icon { font-size: 16px; }
.time-tab .tab-label { font-weight: 600; }
.time-tab .tab-count {
background: var(--bg-tertiary);
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
color: var(--text-muted);
}
.time-tab.active[data-type="activity"] .tab-count { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.time-tab.active[data-type="star"] .tab-count { background: rgba(163, 113, 247, 0.15); color: var(--accent-purple); }
.time-filter-content { display: none; }
.time-filter-content.active { display: block; }
.time-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.time-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 20px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.time-chip:hover { border-color: var(--accent-blue); color: var(--text-primary); }
.time-chip.active {
border-color: var(--accent-blue);
background: rgba(88, 166, 255, 0.1);
color: var(--accent-blue);
}
.time-chip[data-type="star"].active {
border-color: var(--accent-purple);
background: rgba(163, 113, 247, 0.1);
color: var(--accent-purple);
}
.chip-icon { font-size: 14px; }
.chip-label { font-weight: 500; }
.chip-count {
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 8px;
font-weight: 600;
font-size: 11px;
}
/* ===== 标签云 ===== */
.tag-section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 10px;
margin-bottom: 16px;
overflow: hidden;
}
.tag-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
}
.tag-header:hover { background: var(--bg-tertiary); }
.tag-title { font-size: 13px; font-weight: 500; color: var(--text-primary); }
.tag-toggle { color: var(--text-muted); font-size: 12px; }
.tag-cloud {
display: none;
flex-wrap: wrap;
gap: 6px;
padding: 0 16px 12px;
}
.tag-cloud.show { display: flex; }
.tag {
background: var(--bg-tertiary);
color: var(--accent-blue);
padding: 4px 10px;
border-radius: 14px;
cursor: pointer;
font-size: 12px;
transition: all 0.15s;
border: 1px solid transparent;
}
.tag:hover { background: var(--accent-blue); color: #fff; }
.tag.active { background: var(--accent-blue); color: #fff; }
/* ===== 卡片网格 ===== */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 16px;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 18px;
transition: all 0.25s ease;
cursor: pointer;
display: flex;
flex-direction: column;
min-height: 150px;
box-shadow: var(--card-shadow);
}
.card:hover {
border-color: var(--accent-blue);
transform: translateY(-3px);
box-shadow: var(--card-shadow-hover);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.repo-name {
font-size: 15px;
font-weight: 600;
color: var(--accent-blue);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.3;
}
.repo-name:hover { text-decoration: underline; }
.category-badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 12px;
white-space: nowrap;
flex-shrink: 0;
font-weight: 500;
}
.description {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 12px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
flex-grow: 1;
}
.topics-row {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 12px;
}
.topic-tag {
font-size: 10px;
padding: 2px 8px;
background: var(--bg-tertiary);
color: var(--text-muted);
border-radius: 4px;
white-space: nowrap;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-muted);
margin-top: auto;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.metrics { display: flex; gap: 14px; flex-wrap: wrap; }
.metric { display: flex; align-items: center; gap: 5px; }
.language-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.time-info {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
}
.star-time {
font-size: 11px;
color: var(--accent-purple);
font-weight: 500;
}
.updated-recent { color: var(--accent-green); font-weight: 600; }
/* ===== 分页 ===== */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 24px;
padding: 16px 0;
}
.page-btn {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled) { border-color: var(--accent-blue); }
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.page-btn.active {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: #fff;
}
.page-info { color: var(--text-muted); font-size: 13px; padding: 0 8px; }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
/* ===== 隐藏的下拉框 ===== */
.hidden-select { display: none; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="header-left">
<h1>⭐ GitHub Stars Tracker</h1>
<div class="stats">
<span><span class="stat-value" id="totalRepos">-</span>个项目</span>
<span><span class="stat-value" id="filteredCount">-</span>筛选中</span>
</div>
</div>
<button class="theme-toggle" onclick="toggleTheme()">
<span id="themeIcon">🌙</span>
<span id="themeText">浅色模式</span>
</button>
</header>
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索项目名、描述或标签...">
</div>
<select id="categoryFilter">
<option value="all">全部分类</option>
<option value="ai">🤖 AI/机器学习</option>
<option value="web">🌐 Web应用</option>
<option value="desktop">💻 桌面应用</option>
<option value="mobile">📱 移动应用</option>
<option value="devtools">🔧 开发工具</option>
<option value="productivity">⚡ 效率工具</option>
<option value="education">📚 教育学习</option>
<option value="reading">📖 阅读收藏</option>
</select>
<select id="pageSizeSelect" class="page-size-select">
<option value="20">20/页</option>
<option value="40" selected>40/页</option>
<option value="80">80/页</option>
</select>
<select id="activityFilter" class="hidden-select"><option value="all">全部</option></select>
<select id="starFilter" class="hidden-select"><option value="all">全部</option></select>
</div>
<!-- 时间维度筛选器(Tab 式) -->
<div class="time-filter-container">
<div class="time-filter-header">
<button class="time-tab active" data-type="star" onclick="switchTimeTab('star')">
<span class="tab-icon">⭐</span>
<span class="tab-label">我的Star时间</span>
<span class="tab-count" id="starTotal">0</span>
</button>
<button class="time-tab" data-type="activity" onclick="switchTimeTab('activity')">
<span class="tab-icon">📊</span>
<span class="tab-label">项目更新活跃度</span>
<span class="tab-count" id="activityTotal">0</span>
</button>
</div>
<div class="time-filter-content" id="activityContent">
<div class="time-chips" id="activityChips">
<div class="time-chip active" data-filter="all" data-type="activity">
<span class="chip-label">全部</span>
<span class="chip-count" id="activityAll">0</span>
</div>
<div class="time-chip" data-filter="week" data-type="activity">
<span class="chip-icon">🆕</span><span class="chip-label">1周内</span>
<span class="chip-count" id="countWeek">0</span>
</div>
<div class="time-chip" data-filter="month" data-type="activity">
<span class="chip-icon">🟢</span><span class="chip-label">1个月</span>
<span class="chip-count" id="countMonth">0</span>
</div>
<div class="time-chip" data-filter="quarter" data-type="activity">
<span class="chip-icon">🟡</span><span class="chip-label">3个月</span>
<span class="chip-count" id="countQuarter">0</span>
</div>
<div class="time-chip" data-filter="halfyear" data-type="activity">
<span class="chip-icon">🟠</span><span class="chip-label">半年</span>
<span class="chip-count" id="countHalfYear">0</span>
</div>
<div class="time-chip" data-filter="year" data-type="activity">
<span class="chip-icon">🔴</span><span class="chip-label">1年</span>
<span class="chip-count" id="countYear">0</span>
</div>
<div class="time-chip" data-filter="twoyears" data-type="activity">
<span class="chip-icon">🟣</span><span class="chip-label">2年</span>
<span class="chip-count" id="countTwoYears">0</span>
</div>
<div class="time-chip" data-filter="threeyears" data-type="activity">
<span class="chip-icon">⚫</span><span class="chip-label">3年+</span>
<span class="chip-count" id="countThreeYears">0</span>
</div>
<div class="time-chip" data-filter="stale" data-type="activity">
<span class="chip-icon">💀</span><span class="chip-label">更久</span>
<span class="chip-count" id="countStale">0</span>
</div>
</div>
</div>
<div class="time-filter-content active" id="starContent">
<div class="time-chips" id="starChips">
<div class="time-chip active" data-filter="all" data-type="star">
<span class="chip-label">全部</span>
<span class="chip-count" id="starAll">0</span>
</div>
<div class="time-chip" data-filter="week" data-type="star">
<span class="chip-icon">🆕</span><span class="chip-label">1周内</span>
<span class="chip-count" id="starCountWeek">0</span>
</div>
<div class="time-chip" data-filter="month" data-type="star">
<span class="chip-icon">🟢</span><span class="chip-label">1个月</span>
<span class="chip-count" id="starCountMonth">0</span>
</div>
<div class="time-chip" data-filter="quarter" data-type="star">
<span class="chip-icon">🟡</span><span class="chip-label">3个月</span>
<span class="chip-count" id="starCountQuarter">0</span>
</div>
<div class="time-chip" data-filter="halfyear" data-type="star">
<span class="chip-icon">🟠</span><span class="chip-label">半年</span>
<span class="chip-count" id="starCountHalfYear">0</span>
</div>
<div class="time-chip" data-filter="year" data-type="star">
<span class="chip-icon">🔴</span><span class="chip-label">1年</span>
<span class="chip-count" id="starCountYear">0</span>
</div>
<div class="time-chip" data-filter="twoyears" data-type="star">
<span class="chip-icon">🟣</span><span class="chip-label">2年</span>
<span class="chip-count" id="starCountTwoYears">0</span>
</div>
<div class="time-chip" data-filter="threeyears" data-type="star">
<span class="chip-icon">⚫</span><span class="chip-label">3年+</span>
<span class="chip-count" id="starCountThreeYears">0</span>
</div>
<div class="time-chip" data-filter="stale" data-type="star">
<span class="chip-icon">💀</span><span class="chip-label">更久</span>
<span class="chip-count" id="starCountStale">0</span>
</div>
</div>
</div>
</div>
<div class="tag-section">
<div class="tag-header" onclick="toggleTagCloud()">
<span class="tag-title">🏷 标签筛选 <span id="activeTagLabel"></span></span>
<span class="tag-toggle" id="tagToggleText">展开 ▾</span>
</div>
<div class="tag-cloud" id="tagCloud">
<span class="tag active" data-tag="all">全部</span>
</div>
</div>
<div class="grid" id="repoGrid"></div>
<div class="pagination" id="pagination"></div>
</div>
<script>
let projects = [], filteredProjects = [], activeTag = 'all', activeCategory = 'all';
let activeActivity = 'all', activeStarFilter = 'all', currentPage = 1, pageSize = 40;
let tagCloudVisible = false, currentTimeTab = 'star';
// 分类数据(稍后从 data.js 加载)
let categories = { uncategorized: { id: 'uncategorized', name: '未分类', icon: '📦', color: '#6b7280' } };
function toggleTheme() {
const html = document.documentElement;
const isDark = html.getAttribute('data-theme') !== 'light';
html.setAttribute('data-theme', isDark ? 'light' : 'dark');
document.getElementById('themeIcon').textContent = isDark ? '☀️' : '🌙';
document.getElementById('themeText').textContent = isDark ? '深色模式' : '浅色模式';
localStorage.setItem('theme', isDark ? 'light' : 'dark');
}
function loadTheme() {
if (localStorage.getItem('theme') === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
document.getElementById('themeIcon').textContent = '☀️';
document.getElementById('themeText').textContent = '深色模式';
}
}
function switchTimeTab(type) {
currentTimeTab = type;
document.querySelectorAll('.time-tab').forEach(t => t.classList.toggle('active', t.dataset.type === type));
document.getElementById('activityContent').classList.toggle('active', type === 'activity');
document.getElementById('starContent').classList.toggle('active', type === 'star');
if (type === 'activity') {
activeActivity = 'all';
activeStarFilter = 'all';
document.getElementById('activityFilter').value = 'all';
document.getElementById('starFilter').value = 'all';
} else {
activeActivity = 'all';
activeStarFilter = 'all';
document.getElementById('activityFilter').value = 'all';
document.getElementById('starFilter').value = 'all';
}
document.querySelectorAll('.time-chip').forEach(c => { c.classList.remove('active'); if (c.dataset.filter === 'all') c.classList.add('active'); });
filterProjects();
}
function getActivityCategory(dateStr) {
if (!dateStr) return 'stale';
const d = Math.floor((Date.now() - new Date(dateStr)) / 86400000);
if (d < 7) return 'week';
if (d < 30) return 'month';
if (d < 90) return 'quarter';
if (d < 180) return 'halfyear';
if (d < 365) return 'year';
if (d < 730) return 'twoyears'; // 2年以内
if (d < 1095) return 'threeyears'; // 3年以内
return 'stale'; // 3年以上
}
function updateActivityStats() {
const c = { all: projects.length, week: 0, month: 0, quarter: 0, halfyear: 0, year: 0, twoyears: 0, threeyears: 0, stale: 0 };
projects.forEach(p => c[getActivityCategory(p.updated_at || p.pushed_at)]++);
document.getElementById('activityAll').textContent = c.all;
document.getElementById('countWeek').textContent = c.week;
document.getElementById('countMonth').textContent = c.month;
document.getElementById('countQuarter').textContent = c.quarter;
document.getElementById('countHalfYear').textContent = c.halfyear;
document.getElementById('countYear').textContent = c.year;
document.getElementById('countTwoYears').textContent = c.twoyears;
document.getElementById('countThreeYears').textContent = c.threeyears;
document.getElementById('countStale').textContent = c.stale;
document.getElementById('activityTotal').textContent = c.all;
}
function updateStarStats() {
const c = { all: projects.length, week: 0, month: 0, quarter: 0, halfyear: 0, year: 0, twoyears: 0, threeyears: 0, stale: 0 };
projects.forEach(p => c[getActivityCategory(p.starred_at || p.updated_at)]++);
document.getElementById('starAll').textContent = c.all;
document.getElementById('starCountWeek').textContent = c.week;
document.getElementById('starCountMonth').textContent = c.month;
document.getElementById('starCountQuarter').textContent = c.quarter;
document.getElementById('starCountHalfYear').textContent = c.halfyear;
document.getElementById('starCountYear').textContent = c.year;
document.getElementById('starCountTwoYears').textContent = c.twoyears;
document.getElementById('starCountThreeYears').textContent = c.threeyears;
document.getElementById('starCountStale').textContent = c.stale;
document.getElementById('starTotal').textContent = c.all;
}
function loadProjects() {
// 加载分类数据(从 data.js)
if (typeof CATEGORIES_DATA !== 'undefined') {
categories = CATEGORIES_DATA;
}
if (typeof PROJECTS_DATA !== 'undefined') {
projects = PROJECTS_DATA; filteredProjects = [...projects];
updateCategoryFilter(); // 动态更新分类选项
renderTagCloud(); updateActivityStats(); updateStarStats();
switchTimeTab('star'); renderProjects(); updateStats();
} else {
document.getElementById('repoGrid').innerHTML = '<div class="empty-state">加载失败: data.js 文件不存在</div>';
}
}
// 动态更新分类筛选器选项
function updateCategoryFilter() {
const select = document.getElementById('categoryFilter');
const currentValue = select.value;
// 清空现有选项(保留 "全部")
select.innerHTML = '<option value="all">全部分类</option>';
// 从 categories 对象生成选项
Object.values(categories).forEach(cat => {
const option = document.createElement('option');
option.value = cat.id;
option.textContent = `${cat.icon} ${cat.name}`;
select.appendChild(option);
});
// 恢复之前的选中值
select.value = currentValue || 'all';
}
function toggleTagCloud() {
tagCloudVisible = !tagCloudVisible;
document.getElementById('tagCloud').classList.toggle('show', tagCloudVisible);
document.getElementById('tagToggleText').textContent = tagCloudVisible ? '收起 ▴' : '展开 ▾';
}
function renderTagCloud() {
const t = new Map();
// 使用 topics 字段生成标签云(如果没有 matched_keywords)
projects.forEach(p => {
// 尝试 matched_keywords(优先)
if (p.matched_keywords && p.matched_keywords.length > 0) {
p.matched_keywords.forEach(k => t.set(k, (t.get(k) || 0) + 1));
}
// 使用 topics 作为备选
else if (p.topics && p.topics.length > 0) {
p.topics.forEach(k => t.set(k, (t.get(k) || 0) + 1));
}
});
const s = [...t.entries()].sort((a, b) => b[1] - a[1]).slice(0, 30);
document.getElementById('tagCloud').innerHTML = '<span class="tag active" data-tag="all">全部</span>' +
s.map(([k, n]) => `<span class="tag" data-tag="${k}">${k} <small style="opacity:0.6">${n}</small></span>`).join('');
}
function filterProjects() {
const search = document.getElementById('searchInput').value.toLowerCase();
const cat = document.getElementById('categoryFilter').value;
// 直接使用变量,而不是从隐藏的 select 元素读取
const act = activeActivity;
const star = activeStarFilter;
filteredProjects = projects.filter(p => {
// 搜索匹配:项目名、描述、topics
const projectTopics = p.topics || [];
const ms = !search || p.full_name.toLowerCase().includes(search) || (p.description || '').toLowerCase().includes(search) || projectTopics.some(k => k.toLowerCase().includes(search));
// 支持多分类:检查项目是否属于选中的分类
const projectCategories = p.categories || (p.category ? [p.category?.id || p.category] : []);
const mc = cat === 'all' || projectCategories.includes(cat);
// 标签匹配:支持 topics 和 matched_keywords
const allTags = [...(p.topics || []), ...(p.matched_keywords || [])];
const mt = activeTag === 'all' || allTags.includes(activeTag);
// 时间筛选
const ma = act === 'all' || getActivityCategory(p.updated_at || p.pushed_at) === act;
const ms2 = star === 'all' || getActivityCategory(p.starred_at || p.updated_at) === star;
return ms && mc && mt && ma && ms2;
});
currentPage = 1; renderProjects(); updateStats();
}
function renderProjects() {
const grid = document.getElementById('repoGrid');
if (filteredProjects.length === 0) { grid.innerHTML = '<div class="empty-state">没有找到匹配的项目</div>'; document.getElementById('pagination').innerHTML = ''; return; }
const tp = Math.ceil(filteredProjects.length / pageSize);
const pp = filteredProjects.slice((currentPage - 1) * pageSize, currentPage * pageSize);
grid.innerHTML = pp.map(createCard).join('');
renderPagination(tp);
}
function renderPagination(total) {
if (total <= 1) { document.getElementById('pagination').innerHTML = ''; return; }
let h = `<button class="page-btn" onclick="goToPage(${currentPage-1})" ${currentPage===1?'disabled':''}>‹</button>`;
const r = total <= 7 ? [...Array(total)].map((_, i) => i + 1) : currentPage <= 3 ? [1,2,3,4,5,'...',total] : currentPage >= total - 2 ? [1,'...',total-4,total-3,total-2,total-1,total] : [1,'...',currentPage-1,currentPage,currentPage+1,'...',total];
r.forEach(p => h += p === '...' ? '<span class="page-info">...</span>' : `<button class="page-btn ${p===currentPage?'active':''}" onclick="goToPage(${p})">${p}</button>`);
h += `<button class="page-btn" onclick="goToPage(${currentPage+1})" ${currentPage===total?'disabled':''}>›</button><span class="page-info">${currentPage}/${total}</span>`;
document.getElementById('pagination').innerHTML = h;
}
function goToPage(p) { const t = Math.ceil(filteredProjects.length / pageSize); if (p < 1 || p > t) return; currentPage = p; renderProjects(); window.scrollTo({ top: 0, behavior: 'smooth' }); }
function createCard(p) {
// 支持多分类显示
const projectCategories = p.categories || (p.category ? [p.category?.id || p.category] : []);
const categoryBadges = projectCategories.slice(0, 3).map(catId => {
const c = categories[catId] || { icon: '📁', color: '#8b949e' };
return `<span class="category-badge" style="background:${c.color}20;color:${c.color}">${c.icon}</span>`;
}).join('');
const uc = isRecent(p.updated_at) ? 'updated-recent' : '';
const th = (p.topics || []).slice(0, 3).map(t => `<span class="topic-tag">${t}</span>`).join('');
const st = p.starred_at ? `<span class="star-time">⭐ ${formatDate(p.starred_at)} Star</span>` : '';
return `<div class="card" onclick="window.open('${p.html_url}', '_blank')">
<div class="card-header"><a href="${p.html_url}" target="_blank" class="repo-name" onclick="event.stopPropagation()">${p.full_name}</a>
${categoryBadges}</div>
<div class="description">${p.description || '暂无描述'}</div>${th ? `<div class="topics-row">${th}</div>` : ''}
<div class="card-footer"><div class="metrics">
<span class="metric"><span class="language-dot" style="background:${getLangColor(p.language)}"></span>${p.language || '-'}</span>
<span class="metric">⭐${formatNumber(p.stargazers_count)}</span><span class="metric">⑂${formatNumber(p.forks_count)}</span></div>
<div class="time-info">${st}<span class="${uc}">${formatDate(p.updated_at)} 更新</span></div></div></div>`;
}
function getLangColor(l) { const c = {Python:'#3572A5',JavaScript:'#f1e05a',TypeScript:'#3178c6',Go:'#00ADD8',Rust:'#dea584',Java:'#b07219','C++':'#f34b7d',HTML:'#e34c26',CSS:'#563d7c',Kotlin:'#A97BFF',Swift:'#F05138',Dart:'#00B4AB'}; return c[l] || '#6e7681'; }
function formatNumber(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : n; }
function isRecent(d) { return d && Math.floor((Date.now() - new Date(d)) / 86400000) < 7; }
function formatDate(d) { if (!d) return '-'; const n = Math.floor((Date.now() - new Date(d)) / 86400000); return n < 1 ? '今天' : n < 7 ? n + '天前' : n < 30 ? Math.floor(n / 7) + '周前' : n < 365 ? Math.floor(n / 30) + '月前' : new Date(d).getFullYear() + '年'; }
function updateStats() { document.getElementById('totalRepos').textContent = projects.length; document.getElementById('filteredCount').textContent = filteredProjects.length; }
document.getElementById('tagCloud').addEventListener('click', e => {
const t = e.target.classList.contains('tag') ? e.target : e.target.closest('.tag');
if (t) { activeTag = t.dataset.tag; document.querySelectorAll('.tag').forEach(x => x.classList.remove('active')); t.classList.add('active'); document.getElementById('activeTagLabel').textContent = activeTag === 'all' ? '' : ': ' + activeTag; filterProjects(); }
});
document.getElementById('categoryFilter').addEventListener('change', filterProjects);
document.getElementById('searchInput').addEventListener('input', filterProjects);
document.getElementById('pageSizeSelect').addEventListener('change', e => { pageSize = parseInt(e.target.value); currentPage = 1; renderProjects(); });
document.querySelectorAll('.time-chip').forEach(c => {
c.addEventListener('click', () => {
c.closest('.time-chips').querySelectorAll('.time-chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
if (c.dataset.type === 'activity') { activeActivity = c.dataset.filter; document.getElementById('activityFilter').value = c.dataset.filter; }
else { activeStarFilter = c.dataset.filter; document.getElementById('starFilter').value = c.dataset.filter; }
filterProjects();
});
});
loadTheme();
</script>
<script src="data.js"></script>
<script>loadProjects();</script>
</body>
</html>
/**
* 示例数据文件 - data.js
*
* 此文件展示了 dashboard.html 所需的数据格式。
* 运行 `python scripts/main.py --export` 会自动生成此文件。
*
* 注意:实际生成的 data.js 包含完整的 GitHub Stars 数据,
* 此文件仅作为格式参考,请勿提交包含个人数据的文件。
*/
const PROJECTS_DATA = [
{
"id": 1136152017,
"full_name": "owner/repo-name",
"name": "repo-name",
"owner": "owner",
"description": "项目描述",
"html_url": "https://github.com/owner/repo-name",
"language": "Python",
"topics": ["topic1", "topic2"],
"stargazers_count": 100,
"forks_count": 20,
"open_issues_count": 5,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2026-02-01T00:00:00Z",
"pushed_at": "2026-02-01T00:00:00Z",
"avatar_url": "https://avatars.githubusercontent.com/u/123456?v=4",
"has_new_release": false,
"value": "high",
"reason": "",
"category": "devtools",
"matched_keywords": ["keyword1", "keyword2"]
},
{
"id": 1234567890,
"full_name": "example/awesome-project",
"name": "awesome-project",
"owner": "example",
"description": "一个很棒的开源项目",
"html_url": "https://github.com/example/awesome-project",
"language": "TypeScript",
"topics": ["ai", "tools"],
"stargazers_count": 500,
"forks_count": 100,
"open_issues_count": 10,
"created_at": "2024-06-15T08:30:00Z",
"updated_at": "2026-02-10T12:00:00Z",
"pushed_at": "2026-02-09T18:00:00Z",
"avatar_url": "https://avatars.githubusercontent.com/u/987654?v=4",
"has_new_release": true,
"value": "high",
"reason": "值得关注的新版本",
"category": "ai",
"matched_keywords": ["ai", "machine-learning"]
}
];
# GitHub Star 追踪器依赖
# HTTP 请求
requests>=2.31.0
# 配置文件加载
python-dotenv>=1.0.0
# OpenAI API (可选,用于 AI 摘要)
openai>=1.0.0
# 如果需要更好的 JSON 处理
# orjson>=3.9.0
{
"version": "1.0",
"tags": [],
"tag_aliases": {}
}
变更日志 (CHANGELOG.md)
本文档记录 github-star-manager 技能的所有变更。
---
[0.6.2] - 2026-05-10
修复
- description 为 null 时自动重试:增量同步和首次同步时,对 description 为 null 的项目自动调用 GitHub API 重新抓取最新元数据,同时刷新 language、topics、homepage 等字段(Closes #9)
- star 变化触发元数据刷新:检测到新增 star 时,同时刷新该项目的 description、language、topics 等元数据,确保存量项目改名后数据同步更新
- 数据质量日志:新增
log_null_descriptions()方法,记录哪些项目 description 为 null 并输出到output/data_quality.log,方便排查数据质量问题
新增方法
| 方法 | 位置 | 功能 |
|---|---|---|
refresh_repo_metadata() | star_tracker.py | 通过 GitHub API 刷新单个仓库的最新元数据 |
log_null_descriptions() | star_tracker.py | 记录 description 为 null 的项目列表,写入数据质量日志 |
retry_null_descriptions() | star_tracker.py | 批量重试 description 为 null 的项目,自动刷新元数据 |
---
[0.6.1] - 2026-04-10
修复
- 文档路径修正:环境配置示例统一改为当前目录结构
cp .env.example .env修正为cp assets/.env.example .envpip install -r requirements.txt修正为pip install -r assets/requirements.txt- 配置目录口径统一:首次复制配置目录统一为
~/.github-star-manager/
文档完善
references/api-guide.md与references/dev-guide.md中的旧名称github-star-tracker已同步改为github-star-manager- 开发指南中的测试目录示例改为当前目录
skills/github-star-manager
---
[0.6.0] - 2026-02-28
重构(Skill 整合)
| 变更 | 说明 |
|---|---|
| Skill 整合 | 将 github-auto-star 整合到 github-star-tracker,统一为 github-star-manager |
| 目录重命名 | github-star-tracker → github-star-manager |
| 模块化设计 | 同一 Skill 包含两个模块:对话模块 + 脚本模块 |
新增(对话模块)
| 模块 | 触发方式 | 功能 |
|---|---|---|
| 对话模块 | 对话触发 | 从内容提取项目并 Star |
| 脚本模块 | 命令行/定时任务 | 同步、Dashboard、追踪、批量管理 |
对话模块功能
- 从各种内容来源(URL/截图/文字)中自动提取 GitHub 仓库引用
- 智能匹配:直接匹配 + 按名称搜索 + 上下文相关性验证
- 自动检查是否已 Star,避免重复
- 使用 GitHub CLI (
gh) 执行 Star 操作
删除
- 删除
github-auto-star独立 Skill,功能已整合到github-star-manager
---
[0.5.1] - 2026-02-14
修复(Dashboard UI 问题修复)
| 变更 | 说明 |
|---|---|
| 刷新后默认不显示项目 | 修复默认筛选状态,加载后自动显示项目列表 |
| 标签云不显示 | 修复 renderTagCloud() 使用错误的字段,改用 topics |
| Star 时间显示为零 | 修复统计函数未包含 twoyears/threeyears 选项 |
| 时间筛选器不工作 | 修复 filterProjects() 使用隐藏 select 而非变量的问题 |
增强(时间筛选扩展)
| 变更 | 说明 |
|---|---|
| 新增筛选选项 | 添加"2年"和"3年+"时间筛选选项 |
| emoji 同步 | 活动标签和 Star 时间的 emoji 保持一致 |
| 默认显示调整 | 将"我的Star时间"设为默认显示标签(左侧) |
重构(项目命名)
| 变更 | 说明 |
|---|---|
| 项目重命名 | github-star-tracker → github-stars-tracker(复数形式符合 GitHub 官方命名) |
| HTML 标题更新 | 页面标题和 H1 标题改为 GitHub Stars Tracker |
| 模板文件同步 | 更新 dashboard.example.html、.env.example 等模板 |
模板文件更新
assets/dashboard.example.html- 同步 Dashboard 修复和默认显示assets/.env.example- 更新标题为GitHub Stars Tracker
---
[0.5.0] - 2026-02-14
新增(数据维度增强 + 双重活跃度筛选)
| 变更 | 说明 |
|---|---|
| 数据字段扩展 | 从 3 个字段扩展到 27 个字段,丰富 AI 分析维度 |
| starred_at 获取 | 使用认证 API 获取用户 star 的具体时间 |
| 项目更新活跃度筛选 | 按项目更新时间筛选(1周/1月/3月/半年/1年/超1年) |
| Star 时间筛选 | 按用户 star 的时间筛选,了解近期关注领域 |
| 双维度统计卡片 | 同时显示项目活跃度和 Star 时间分布 |
数据字段清单
基本信息:
full_name,name,owner,html_url,description,homepage
时间维度(关键):
starred_at- 用户何时 star 的created_at- 仓库创建时间updated_at- 最后更新时间pushed_at- 最后推送时间local_fetched_at- 本地抓取时间
统计数据:
stargazers_count,forks_count,watchers_count,open_issues_count
分类信息:
language,topics,license,visibility
项目特征:
is_fork,is_archived,is_template,has_wiki,has_pages,has_discussions
使用示例
# 重新初始化获取完整数据(27个字段)
python scripts/main.py --init --user cat-xierluo
# 查看 Dashboard(包含活跃度筛选)
open output/dashboard.html技术改进
save_latest()方法重写,保存完整仓库信息get_starred_repos()添加include_starred_at参数,使用application/vnd.github.star+json获取 starred_at- Dashboard 添加活跃度统计卡片和筛选下拉框
- JavaScript 添加
getActivityCategory()和updateActivityStats()函数
---
[0.4.2] - 2026-02-12
新增(输出目录规范)
| 变更 | 说明 |
|---|---|
| output/ 目录 | 创建专用目录存放运行时生成的文件 |
| 文件组织 | dashboard.html 和 projects.json 移至 output/ |
| gitignore 更新 | 忽略整个 output/ 目录 |
使用说明
运行 --export 后,生成的文件位于 output/ 目录:
output/projects.json- 导出的项目数据output/dashboard.html- 可视化面板
打开 output/dashboard.html 即可查看数据可视化。
---
[0.4.1] - 2026-02-12
修复(目录结构规范化)
- 配置文件散落在根目录 → 移至
assets/目录符合 SKILL-GUIDE.md 规范 - 文档路径引用错误 → 更新 SKILL.md 和 CHANGELOG.md 中的路径
变更内容
- 移动
dashboard.example.html→assets/dashboard.example.html - 移动
projects.example.json→assets/projects.example.json - 移动
categories.yaml→assets/categories.yaml - 移动
tags.json→assets/tags.json - 更新
scripts/main.py中的模板路径引用 - 更新
SKILL.md配置文件说明
---
[0.4.0] - 2026-02-12
新增(分类与标签系统)
| 功能 | 说明 |
|---|---|
| 分类配置 | assets/categories.yaml - 8种预设分类,可自定义 |
| 标签管理 | assets/tags.json - 添加、编辑、删除标签 |
| 智能分类 | 根据关键词自动匹配分类(语言/topics/description) |
| 标签云 | Dashboard 顶部显示所有标签,点击筛选 |
| 分类筛选 | 按预设分类(AI/Web/桌面/移动等)筛选项目 |
| 搜索增强 | 支持标签名和关键词搜索 |
| 统计增强 | 显示已分类项目数量 |
使用示例
# 导出数据(现在包含分类和标签)
python scripts/main.py --export
# 编辑分类配置
vim ~/.github-star-tracker/categories.yaml
# 添加新标签
python scripts/main.py --add-tag "游戏引擎" --color "#f472b6"
# 按分类查看
# 然后在 Dashboard 中点击对应分类标签技术改进
- 新增
assets/categories.yaml和assets/tags.json配置文件 StarTracker添加_load_categories(),_load_tags(),get_categories_map()方法StarTracker添加_match_category()方法,根据关键词智能匹配分类get_starred_repos()返回的每个仓库包含category和matched_keywords字段- Dashboard 全面重构,支持标签云、分类筛选、搜索
---
[0.3.1] - 2026-02-12
修复
| 问题 | 解决方案 |
|---|---|
| 只能获取 100 个 Star | 移除 limit 限制,支持分页获取全部 Star |
| 用户名必须手动输入 | 添加 get_authenticated_user() 自动获取 Token 对应的用户名 |
language=None 导致崩溃 | 修复空值检查 |
| 符号链接报错 | 修复 save_snapshot() 文件覆盖逻辑 |
改进
- 默认 limit 从 100 提升到 9999(获取全部)
- Dashboard 修复本地文件加载问题
--user参数改为可选,未提供时自动获取
---
[0.3.0] - 2026-02-12
新增(Star 管理功能)
| 命令 | 说明 |
|---|---|
--star owner/repo | 为单个仓库添加 Star |
--unstar owner/repo | 取消单个仓库的 Star |
--cleanup --days 90 | 查找并清理长期未更新的仓库 |
--batch-unstar file.json | 批量取消 Star(从 JSON 文件) |
--batch-star file.json | 批量添加 Star(从 JSON 文件) |
使用示例
# 单个操作
python scripts/main.py --star owner/repo # 添加 Star
python scripts/main.py --unstar owner/repo # 取消 Star
# 清理陈旧项目
python scripts/main.py --cleanup --days 90 # 预览(默认)
python scripts/main.py --cleanup --days 90 --execute # 实际执行
# 批量操作
python scripts/main.py --batch-unstar repos.json # 批量取消
python scripts/main.py --batch-star repos.json # 批量添加技术改进
StarTracker类新增star_repo(),unstar_repo(),is_starred()等方法- 添加
--execute安全确认机制,避免误操作 - 所有破坏性操作(取消 Star)默认预览,需要
--execute确认
---
[0.2.0] - 2026-02-12
新增
- HTML Dashboard:实现信息密集型可视化面板,支持一屏浏览大量项目
- 数据导出功能:新增
--export命令,生成 projects.json 供 Dashboard 使用 - 状态筛选:支持按活跃度(活跃/近期更新/长期未更)和价值(高/中)筛选
- 搜索功能:支持按项目名、描述、标签进行关键词搜索
- 示例文件机制:创建 dashboard.example.html 作为模板,首次运行自动复制
改进
- 从 knowledge-retention skill 重构,移除社交媒体截图转化功能
- 专注核心功能:更新追踪、版本检测、活跃度分析
- 卡片点击展开详情:显示创建时间、Forks、Issues 等信息
技术优化
- 使用 CSS 变量管理主题色值
- 响应式布局,支持移动端浏览
- 本地 JSON 数据缓存,减少 API 请求
MIT License
Copyright (c) 2025 杨卫薪律师(微信ywxlaw)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
GitHub API 使用指南
本文档说明 github-star-manager 中使用的 GitHub API 端点。
基础信息
API 基础 URL
https://api.github.com/认证方式
Authorization: token GITHUB_PAT使用的 API 端点
1. 获取用户 Star 的仓库
GET /users/{username}/starred
参数:per_page(默认 100,最大 100)2. 获取仓库 README
GET /repos/{owner}/{repo}/readme
返回:JSON 包含 download_url3. 获取最新 Release
GET /repos/{owner}/{repo}/releases/latest
返回:完整的 Release 信息4. 获取仓库详情
GET /repos/{owner}/{repo}
返回:stargazers_count, forks_count, open_issues_count, language, topics 等5. 获取 Commits
GET /repos/{owner}/{repo}/commits
参数:per_page, sha(分支)使用限制
| 限制 | 无 Token | 有 Token |
|---|---|---|
| 请求频率 | 60 次/小时 | 5000 次/小时 |
| 单页结果数 | 最大 100 | 最大 100 |
最佳实践
1. 缓存策略:优先使用本地快照,减少 API 调用 2. 错误处理:遇到 403/429 等待后重试 3. 增量更新:只请求有变化的数据
开发指南
本文档面向 github-star-manager 的开发者,提供架构说明和开发指导。
代码架构
文件职责
| 文件 | 职责 |
|---|---|
scripts/main.py | CLI 入口,命令解析和分发 |
scripts/star_tracker.py | 核心逻辑:API 调用、数据处理 |
assets/dashboard.example.html | Dashboard 模板 |
数据流向
用户输入 (CLI)
↓
main.py (命令解析)
↓
star_tracker.py (API 调用)
↓
获取数据 (repos, releases, commits)
↓
生成 JSON 数据
↓
dashboard.html (浏览器渲染)核心类设计
StarTracker 类
class StarTracker:
def __init__(self, github_token): # 初始化请求头
def get_starred_repos(self, username): # 获取 Star 列表
def get_latest_release(self, repo_full_name): # 获取最新 Release
def get_recent_commits(self, repo_full_name, days): # 获取最近 Commits
def get_repo_health(self, repo_full_name): # 获取仓库健康度
def get_readme(self, repo_full_name): # 获取 README
def load_snapshot(self, username): # 加载本地快照
def save_snapshot(self, username, repos): # 保存快照
def check_updates(self, username): # 检查更新
def get_commits_between_releases(self, ...): # 获取版本间 Commits (新增)
def export_config(self, ...): # 导出配置 (新增)
def import_config(self, ...): # 导入配置 (新增)
def get_version_diff(self, ...): # 获取版本差异 (新增)扩展指南
添加新功能
1. 在 StarTracker 类中添加新方法 2. 更新 scripts/main.py 中的命令处理 3. 如需新依赖,添加到 assets/requirements.txt 4. 更新 SKILL.md 和 CHANGELOG.md
代码规范
1. 错误处理:所有 API 调用必须有 try-except 2. 日志输出:使用 print() 显示进度,不是 logging 3. 类型提示:函数签名使用类型注解 4. 常量管理:API URL 等定义为类常量
测试
手动测试
# 进入脚本目录
cd skills/github-star-manager
# 测试获取 Star(不使用 Token)
python3 -c "from star_tracker import StarTracker; t = StarTracker(); print(t.get_starred_repos('yourname'))"
# 测试导出配置
python3 -c "from star_tracker import StarTracker; t = StarTracker(); print(t.export_config('testuser'))"集成测试
确保添加新功能后: 1. 不破坏现有功能 2. 兼容现有配置文件格式 3. Dashboard 正确显示新数据
#!/usr/bin/env python3
"""
Dashboard 生成器 - 将 GitHub Star 数据导出为 HTML Dashboard
负责:
1. 保存项目数据为 JSON 文件
2. 复制模板到输出目录(纯前端加载 JSON,不内嵌数据)
3. 创建简版 HTML(当模板不存在时)
"""
import json
from pathlib import Path
def generate_dashboard(projects: list, output_dir: str = "output") -> dict:
"""
生成 Dashboard HTML 和数据文件
Args:
projects: 项目数据列表
output_dir: 输出目录(相对于项目根目录)
Returns:
dict: 包含生成的文件的路径信息
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# 生成 data.js 文件(HTML 直接引用,无需 HTTP 服务器)
data_js_path = output_path / "data.js"
json_str = json.dumps(projects, ensure_ascii=False)
# 修复 JavaScript 不兼容的字符:U+2028 (Line Separator) 和 U+2029 (Paragraph Separator)
json_str = json_str.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
with open(data_js_path, "w", encoding="utf-8") as f:
f.write(f"const PROJECTS_DATA = {json_str};\n")
# 查找模板文件
example_path = Path("assets/dashboard.example.html")
if example_path.exists():
dashboard_path = _generate_from_template(projects, example_path, output_path)
else:
dashboard_path = _generate_simple_html(projects, output_path)
return {
"data_js_path": str(data_js_path),
"dashboard_path": str(dashboard_path),
"project_count": len(projects)
}
def _generate_from_template(projects: list, template_path: Path, output_dir: Path) -> Path:
"""从模板生成 HTML(纯前端加载 JSON,不内嵌数据)"""
# 直接复制模板,保持原有的 fetch 机制
html_content = template_path.read_text(encoding='utf-8')
dashboard_path = output_dir / "dashboard.html"
dashboard_path.write_text(html_content, encoding='utf-8')
return dashboard_path
def _generate_simple_html(projects: list, output_dir: Path) -> Path:
"""创建简版 HTML(当模板不存在时,使用 fetch 加载 JSON)"""
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Star Tracker</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 40px; }}
h1 {{ color: #58a6ff; }}
.project {{ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 12px; }}
a {{ color: #58a6ff; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<h1>⭐ GitHub Star Tracker</h1>
<p>加载中...</p>
<div id="app"></div>
<script>
async function loadProjects() {{
try {{
const response = await fetch('projects.json');
const projects = await response.json();
renderProjects(projects);
}} catch (error) {{
document.getElementById('app').innerHTML =
'<p style="color: #f85149;">加载失败: 请确保 projects.json 存在</p>';
}}
}}
function renderProjects(projects) {{
document.getElementById('app').innerHTML = projects.map(p => `
<div class="project">
<a href="${{p.html_url}}" target="_blank"><strong>${{p.full_name}}</strong></a><br>
<span style="color: #8b949e;">${{p.description || '暂无描述'}}</span><br>
<small>⭐ ${{p.stargazers_count}} | ⑂ ${{p.forks_count}} | ${{p.language || '-'}}</small>
</div>
`).join('');
}}
loadProjects();
</script>
</body>
</html>'''
dashboard_path = output_dir / "dashboard.html"
dashboard_path.write_text(html, encoding='utf-8')
return dashboard_path
def generate_simple_html(projects: list, output_path: str) -> Path:
"""
创建简版 HTML
Args:
projects: 项目数据列表
output_path: 输出文件路径
Returns:
Path: 输出文件的 Path 对象
"""
return _generate_simple_html(projects, Path(output_path).parent)
def embed_data_in_template(template_path: str, projects: list) -> str:
"""
返回 HTML 模板内容(纯前端加载 JSON,不内嵌数据)
注意:此函数已废弃,现在使用纯前端 fetch 方式加载 JSON 文件
保留此函数是为了向后兼容,直接返回原始模板内容
Args:
template_path: 模板文件路径
projects: 项目数据列表(不再使用,保留参数以兼容旧调用)
Returns:
str: 原始模板内容
"""
template_file = Path(template_path)
if not template_file.exists():
raise FileNotFoundError(f"模板文件不存在: {template_path}")
# 直接返回原始模板内容,不进行任何修改
return template_file.read_text(encoding='utf-8')
#!/usr/bin/env python3
"""
GitHub Star 分类动态生成器
功能:
1. 首次运行:根据 JSON 数据中的 topics 频率自动生成分类
2. 后续运行:增量更新(保留现有分类,只添加新高频 topics)
使用:
python generate_categories.py # 使用默认路径
python generate_categories.py --json path.json # 指定 JSON 文件
python generate_categories.py --force # 强制重新生成
"""
import json
import yaml
import argparse
from pathlib import Path
from collections import Counter
from typing import Dict, List, Optional, Tuple
from datetime import datetime
class CategoryGenerator:
"""动态分类生成器"""
# 默认路径配置
DEFAULT_JSON_PATH = Path(__file__).parent.parent / "output" / "original_latest.json"
DEFAULT_CATEGORIES_PATH = Path(__file__).parent.parent / "output" / "categories.yaml"
TEMPLATE_CATEGORIES_PATH = Path(__file__).parent.parent / "assets" / "categories.yaml"
# 生成分类的最小频次阈值
MIN_FREQUENCY = 5
# 高频 topics 的分类映射(将相似 topics 合并为一个分类)
TOPIC_GROUPS = {
"ai-agents": ["ai", "agent", "agents", "ai-agent", "ai-agents", "agentic", "autonomous", "agentic-ai"],
"claude-code": ["claude-code", "claude", "mcp", "openclaw", "clawdbot", "moltbot", "superpowers", "claude-skills", "claude-desktop"],
"llm": ["llm", "llms", "rag", "ollama", "deepseek", "gemini", "anthropic", "openai", "gpt", "chatgpt", "codex", "qwen"],
"chinese-platform": ["wechat", "weixin", "bilibili", "douyin", "tiktok", "xiaohongshu", "jianying", "lark", "feishu"],
"devtools": ["typescript", "python", "javascript", "electron", "tauri", "swiftui", "cli", "vscode", "docker", "rust", "golang", "swift"],
"workflow": ["automation", "workflow", "n8n", "webhook", "integration"],
"productivity": ["obsidian", "productivity", "note", "notes", "markdown", "task", "todo"],
"content-processing": ["ocr", "pdf", "tts", "speech", "video", "download", "transcript", "whisper"],
"network": ["proxy", "vpn", "shadowsocks", "v2ray", "gfw", "clash"],
"crawler": ["crawler", "scraper", "spider", "web-scraping", "scraping", "playwright", "puppeteer"],
"rss": ["rss", "rsshub", "feed", "newsletter"],
}
# 分类图标和颜色映射
CATEGORY_STYLES = {
"ai-agents": {"icon": "🤖", "color": "#22c55e", "name": "AI Agents"},
"claude-code": {"icon": "🐛", "color": "#8b5cf6", "name": "Claude Code"},
"llm": {"icon": "🧠", "color": "#3b82f6", "name": "LLM/大模型"},
"chinese-platform": {"icon": "🇨🇳", "color": "#ef4444", "name": "中国平台"},
"devtools": {"icon": "🔧", "color": "#f59e0b", "name": "开发工具"},
"workflow": {"icon": "🔄", "color": "#06b6d4", "name": "工作流自动化"},
"productivity": {"icon": "⚡", "color": "#eab308", "name": "效率工具"},
"content-processing": {"icon": "📄", "color": "#6366f1", "name": "内容处理"},
"network": {"icon": "🌐", "color": "#64748b", "name": "网络工具"},
"crawler": {"icon": "🕷️", "color": "#84cc16", "name": "爬虫/抓取"},
"rss": {"icon": "📡", "color": "#f97316", "name": "RSS/订阅"},
}
def __init__(self, json_path: Optional[Path] = None, categories_path: Optional[Path] = None):
self.json_path = json_path or self.DEFAULT_JSON_PATH
self.categories_path = categories_path or self.DEFAULT_CATEGORIES_PATH
def load_json_data(self) -> Optional[Dict]:
"""加载 JSON 数据"""
if not self.json_path.exists():
print(f"❌ JSON 文件不存在: {self.json_path}")
return None
with open(self.json_path, 'r', encoding='utf-8') as f:
return json.load(f)
def load_existing_categories(self) -> List[Dict]:
"""加载现有分类(如果存在)"""
if self.categories_path.exists():
with open(self.categories_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if data and 'categories' in data:
return data['categories']
return []
def count_topics(self, data: Dict) -> Counter:
"""统计所有 topics 的频次"""
topics_counter = Counter()
repos = data.get('repos', [])
for repo in repos:
for topic in repo.get('topics', []):
topics_counter[topic.lower()] += 1
return topics_counter
def find_matching_group(self, topic: str) -> Optional[str]:
"""找到 topic 所属的分组"""
topic_lower = topic.lower()
for group_id, topics in self.TOPIC_GROUPS.items():
if topic_lower in topics:
return group_id
return None
def generate_categories_from_topics(self, topics_counter: Counter, existing_categories: List[Dict]) -> Tuple[List[Dict], List[str]]:
"""根据 topics 频率生成分类
Returns:
(新分类列表, 新增的分类ID列表)
"""
# 获取现有分类的 ID
existing_ids = {cat['id'] for cat in existing_categories}
existing_keywords = set()
for cat in existing_categories:
existing_keywords.update(k.lower() for k in cat.get('keywords', []))
# 统计每个分组的总频次
group_scores = Counter()
ungrouped_high_freq = []
for topic, freq in topics_counter.items():
if freq < self.MIN_FREQUENCY:
continue
group = self.find_matching_group(topic)
if group:
group_scores[group] += freq
else:
# 未分组的高频 topic
ungrouped_high_freq.append((topic, freq))
# 生成新分类
new_categories = []
added_ids = []
# 1. 添加高分分组(如果不在现有分类中)
for group_id, score in group_scores.most_common():
if group_id not in existing_ids and score >= self.MIN_FREQUENCY:
style = self.CATEGORY_STYLES.get(group_id, {
"icon": "📁",
"color": "#6b7280",
"name": group_id.replace("-", " ").title()
})
keywords = self.TOPIC_GROUPS.get(group_id, [group_id])
new_categories.append({
"id": group_id,
"name": style["name"],
"icon": style["icon"],
"color": style["color"],
"keywords": keywords,
"generated": True,
"generated_at": datetime.now().isoformat(),
"frequency": score
})
added_ids.append(group_id)
# 2. 添加未分组的高频 topics 作为独立分类(频次 >= 10)
for topic, freq in sorted(ungrouped_high_freq, key=lambda x: -x[1]):
if freq >= 10 and topic not in existing_keywords:
# 清理 topic 名称作为分类 ID
cat_id = topic.lower().replace("_", "-").replace(" ", "-")
if cat_id not in existing_ids and cat_id not in added_ids:
new_categories.append({
"id": cat_id,
"name": topic.replace("-", " ").replace("_", " ").title(),
"icon": "📁",
"color": "#6b7280",
"keywords": [topic],
"generated": True,
"generated_at": datetime.now().isoformat(),
"frequency": freq
})
added_ids.append(cat_id)
return new_categories, added_ids
def merge_categories(self, existing: List[Dict], new: List[Dict]) -> List[Dict]:
"""合并现有分类和新分类"""
# 按 frequency 排序(新分类),现有分类保持原顺序
existing_ids = {cat['id'] for cat in existing}
new_sorted = sorted(new, key=lambda x: x.get('frequency', 0), reverse=True)
# 清理内部字段(不保存到文件)
for cat in new_sorted:
cat.pop('frequency', None)
return existing + new_sorted
def save_categories(self, categories: List[Dict]) -> None:
"""保存分类到 YAML 文件"""
# 确保目录存在
self.categories_path.parent.mkdir(parents=True, exist_ok=True)
data = {
"categories": categories,
"_meta": {
"generated_at": datetime.now().isoformat(),
"source": "generate_categories.py",
"description": "基于 GitHub Star 数据动态生成的分类"
}
}
with open(self.categories_path, 'w', encoding='utf-8') as f:
yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
def update(self, force: bool = False) -> Dict:
"""执行分类更新
Args:
force: 是否强制重新生成(忽略现有分类)
Returns:
更新结果统计
"""
# 1. 加载 JSON 数据
data = self.load_json_data()
if not data:
return {"success": False, "error": "无法加载 JSON 数据"}
total_repos = data.get('total_count', 0)
print(f"📊 加载了 {total_repos} 个 Star 项目")
# 2. 统计 topics
topics_counter = self.count_topics(data)
unique_topics = len(topics_counter)
print(f"📈 发现 {unique_topics} 个不同的 topics")
# 3. 加载现有分类
existing = [] if force else self.load_existing_categories()
print(f"📁 现有分类: {len(existing)} 个")
# 4. 生成新分类
new_categories, added_ids = self.generate_categories_from_topics(topics_counter, existing)
print(f"✨ 新增分类: {len(new_categories)} 个")
if added_ids:
print(f" → {', '.join(added_ids)}")
# 5. 合并并保存
merged = self.merge_categories(existing, new_categories)
self.save_categories(merged)
print(f"💾 已保存到: {self.categories_path}")
# 6. 输出高频 topics 统计
print("\n📊 Top 20 Topics:")
for topic, count in topics_counter.most_common(20):
group = self.find_matching_group(topic)
group_str = f"→ {group}" if group else ""
print(f" {topic}: {count} {group_str}")
return {
"success": True,
"total_repos": total_repos,
"unique_topics": unique_topics,
"existing_categories": len(existing),
"new_categories": len(new_categories),
"total_categories": len(merged),
"added_ids": added_ids
}
def main():
parser = argparse.ArgumentParser(description="GitHub Star 分类动态生成器")
parser.add_argument(
"--json", "-j",
type=Path,
help="指定 JSON 数据文件路径"
)
parser.add_argument(
"--output", "-o",
type=Path,
help="指定输出分类文件路径"
)
parser.add_argument(
"--force", "-f",
action="store_true",
help="强制重新生成(忽略现有分类)"
)
args = parser.parse_args()
generator = CategoryGenerator(
json_path=args.json,
categories_path=args.output
)
result = generator.update(force=args.force)
if not result["success"]:
print(f"\n❌ 更新失败: {result.get('error')}")
return 1
print(f"\n✅ 更新完成!")
print(f" 总分类数: {result['total_categories']}")
return 0
if __name__ == "__main__":
exit(main())
#!/usr/bin/env python3
"""
GitHub Star 追踪器 - 主入口
用于追踪 GitHub Star 项目的更新、版本变化和活跃度
"""
import os
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime
from star_tracker import StarTracker
import dashboard_generator
# 加载 .env 配置文件(从 assets/ 目录)
try:
from dotenv import load_dotenv
# 从 assets/ 目录加载 .env 文件
env_path = Path(__file__).parent.parent / "assets" / ".env"
load_dotenv(dotenv_path=str(env_path))
except ImportError:
pass # 如果没有安装 python-dotenv,跳过环境变量加载
def init_command(tracker: StarTracker, args):
"""初始化:首次同步并保存原始数据"""
print(f"--- 正在获取 {args.user} 的 Star 列表(上限: {args.limit})---")
repos = tracker.get_starred_repos(args.user, limit=args.limit)
print(f"找到 {len(repos)} 个 Star 项目")
# 对 description 为 null 的项目重新抓取元数据
repos = tracker.retry_null_descriptions(repos)
# 记录数据质量日志
tracker.log_null_descriptions(repos, source="init")
# 保存快照
snapshot_file = tracker.save_latest(repos)
print(f"✓ 原始数据已保存: {snapshot_file}")
# 生成项目摘要(可选)
if args.summarize and len(repos) > 0:
print("\n--- 正在生成项目摘要 ---")
report_path = Path("star_summary.md")
with open(report_path, "w", encoding="utf-8") as f:
f.write(f"# GitHub Star 项目摘要\n\n")
f.write(f"**用户**: {args.user}\n")
f.write(f"**生成时间**: {tracker.get_readme.__self__}\n\n")
for i, repo in enumerate(repos[:20], 1):
print(f"[{i}/{min(20, len(repos))}] {repo['full_name']}")
readme = tracker.get_readme(repo["full_name"])
summary = tracker.summarize_with_ai(repo, readme)
f.write(f"## {i}. [{repo['full_name']}]({repo['html_url']})\n\n")
f.write(f"{summary}\n\n")
print(f"✓ 摘要已保存: {report_path}")
def check_command(tracker: StarTracker, args):
"""检查更新"""
print(f"--- 正在检查 {args.user} 的 Star 更新 ---")
# 获取当前状态
current_repos = tracker.get_starred_repos(args.user, limit=args.limit or 200)
print(f"当前 Star 数量: {len(current_repos)}")
# 加载历史数据
snapshot = tracker.load_latest()
if not snapshot:
print("❌ 未找到历史数据,请先运行: --init")
return
snapshot_time = snapshot.get("timestamp", "未知")[:16].replace("T", " ")
print(f"上次保存: {snapshot_time}")
# 分析变化
old_map = {r["full_name"]: r for r in snapshot["repos"]}
current_map = {r["full_name"]: r for r in current_repos}
new_repos = [r for fn, r in current_map.items() if fn not in old_map]
removed_repos = [fn for fn in old_map if fn not in current_map]
updated_repos = []
for fn, repo in current_map.items():
if fn in old_map and repo.get("updated_at") != old_map[fn].get("updated_at"):
updated_repos.append(repo)
# === 新增:star 变化触发元数据刷新 ===
repos_needing_refresh = []
# 新增 star 的项目:刷新元数据以确保 description/language 等字段完整
if new_repos:
print(f"\n🔄 新增 Star 检测到,正在刷新元数据...")
for repo in new_repos:
full_name = repo.get("full_name")
if full_name:
fresh_data = tracker.refresh_repo_metadata(full_name)
if fresh_data:
# 用最新 API 数据更新 current_map 和 current_repos 中的条目
repo["description"] = fresh_data.get("description")
repo["language"] = fresh_data.get("language")
repo["topics"] = fresh_data.get("topics", [])
repo["homepage"] = fresh_data.get("homepage")
repo["stargazers_count"] = fresh_data.get("stargazers_count", 0)
repo["forks_count"] = fresh_data.get("forks_count", 0)
current_map[full_name] = repo
repos_needing_refresh.append(full_name)
print(f" ✓ 已刷新 {len(new_repos)} 个新增项目的元数据")
# 取消 star 的项目:也记录到日志(不再刷新,因为已不在 star 列表中)
if removed_repos:
print(f"\n📝 已记录 {len(removed_repos)} 个取消 Star 的项目")
# === 新增:description 为 null 时自动重试 ===
current_repos = tracker.retry_null_descriptions(current_repos)
# 更新 current_map 以反映重试后的结果
current_map = {r["full_name"]: r for r in current_repos}
# === 新增:数据质量日志 ===
tracker.log_null_descriptions(current_repos, source="check_incremental")
# 保存新数据(包含刷新后的元数据)
tracker.save_latest(current_repos)
# 输出结果
print("\n" + "=" * 50)
if new_repos:
print(f"\n🆕 新增 Star ({len(new_repos)} 个):")
for repo in new_repos[:10]:
desc = repo.get('description') or '(无描述)'
print(f" • {repo['full_name']} - {desc[:50]}")
if len(new_repos) > 10:
print(f" ... 还有 {len(new_repos) - 10} 个")
if removed_repos:
print(f"\n🗑️ 取消 Star ({len(removed_repos)} 个):")
for name in removed_repos[:10]:
print(f" • {name}")
if updated_repos:
print(f"\n🔄 有更新的项目 ({len(updated_repos)} 个):")
for repo in updated_repos[:10]:
updated = repo.get("updated_at", "")[:10]
print(f" • {repo['full_name']} ({updated})")
if len(updated_repos) > 10:
print(f" ... 还有 {len(updated_repos) - 10} 个")
if not new_repos and not updated_repos:
print("\n✓ 没有检测到新项目或更新")
print("=" * 50)
def report_command(tracker: StarTracker, args):
"""生成完整报告"""
print(f"--- 正在生成 {args.user} 的更新报告 ---")
days = args.report_days or 7
report = tracker.generate_report(args.user, days=days)
output_path = Path(args.output or "star_report.md")
with open(output_path, "w", encoding="utf-8") as f:
f.write(report)
print(f"✓ 报告已保存: {output_path}")
print(f" 追踪时间范围: 最近 {days} 天")
def export_command(tracker: StarTracker, args):
"""导出 JSON 数据用于 HTML Dashboard"""
print(f"--- 正在导出 {args.user} 的 Star 数据 ---")
repos = tracker.get_starred_repos(args.user, limit=args.limit or 9999)
print(f"获取到 {len(repos)} 个项目")
# 获取每个项目的详细信息
export_data = []
for i, repo in enumerate(repos, 1):
print(f"[{i}/{len(repos)}] {repo['full_name']}", end='\r')
# 获取最新 release 信息
release = tracker.get_latest_release(repo['full_name'])
has_new_release = False
if release:
# 检查 release 是否是最近的(7天内)
try:
release_date = datetime.fromisoformat(release.get('published_at', '').replace('Z', '+00:00'))
days_since = (datetime.now(release_date.tzinfo) - release_date).days
has_new_release = days_since <= 7
except:
pass
# 获取项目健康度
health = tracker.get_repo_health(repo['full_name'])
export_data.append({
'id': repo['id'],
'full_name': repo['full_name'],
'name': repo['name'],
'owner': repo['owner']['login'],
'description': repo.get('description', ''),
'html_url': repo['html_url'],
'language': repo.get('language'),
'topics': repo.get('topics', []),
'stargazers_count': repo.get('stargazers_count', 0),
'forks_count': repo.get('forks_count', 0),
'open_issues_count': repo.get('open_issues_count', 0),
'created_at': repo.get('created_at'),
'updated_at': repo.get('updated_at'),
'pushed_at': repo.get('pushed_at'),
'starred_at': repo.get('starred_at'), # 用户何时 Star 的
'avatar_url': repo['owner'].get('avatar_url'),
'has_new_release': has_new_release,
'value': assess_value(repo, health),
'category': repo.get('category'), # 分类信息
'matched_keywords': repo.get('matched_keywords', []), # 匹配的关键词
'reason': repo.get('notes', '')
})
# 使用 dashboard_generator 生成 HTML 和数据文件
result = dashboard_generator.generate_dashboard(export_data, "output")
print(f"\n✓ 数据已导出: {result['data_js_path']}")
print(f" Dashboard: {result['dashboard_path']}")
print(f" 项目数量: {result['project_count']}")
print(f" 可以直接双击 output/dashboard.html 查看可视化面板")
def export_config_command(tracker: StarTracker, args):
"""导出配置到文件"""
config = tracker.export_config(args.user, include_repos=True)
output_path = Path("config-export.json")
with open(output_path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"✓ 配置已导出: {output_path}")
print(f" 包含价值关键词、忽略列表、追踪仓库等设置")
def import_config_command(tracker: StarTracker, args):
"""导入配置文件"""
config_path = Path(args.import_config)
if not config_path.exists():
print(f"❌ 配置文件不存在: {config_path}")
return
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
if not tracker.import_config(config):
print(f"❌ 配置格式无效: {config.get('error', '未知错误')}")
return
# 应用导入的配置
print(f"✓ 已导入配置: {config_path}")
print(f" 价值关键词: {', '.join(config.get('value_keywords', []))}")
print(f" ��星阈值: {config.get('min_stars', 0)}")
if "ignore_repos" in config:
print(f" 忽略列表: {', '.join(config.get('ignore_repos', []))}")
def version_diff_command(tracker: StarTracker, args):
"""查看版本差异"""
import json
# 首先需要获取两个版本号
print("\n请提供两个版本号进行比较:")
print("格式: owner/repo 按标签号 (如 v1.0.0)")
try:
old_version = input("旧版本: ").strip()
new_version = input("新版本: ").strip()
if not old_version or not new_version:
print("❌ 请提供完整的版本号")
return
# 解析仓库
parts = old_version.split("/", 1)
if len(parts) != 2:
print("❌ 版本格式错误,应为 owner/repo")
return
owner, repo = parts
# 获取版本差异
diff_data = tracker.get_version_diff(f"{owner}/{repo}", old_version, new_version)
print(f"\n### 版本差异: {owner}/{repo}")
print(f" 旧版本: {old_version}")
print(f" 新版本: {new_version}")
print(f" Commits 数量: {diff_data['commits_count']}")
if diff_data['commits']:
print("\n最近的 Commits:")
for commit in diff_data['commits']:
date = commit.get('date', '')[:10]
msg = commit.get('message', '')[:60]
print(f" [{date}] {msg[:80] if len(msg) > 80 else msg}")
print(f"\n对比链接: {diff_data['compare_url']}")
except Exception as e:
print(f"❌ 错误: {e}")
def star_command(tracker: StarTracker, args):
"""为仓库添加 Star"""
if not args.star:
print("❌ 请指定仓库 (--star owner/repo)")
return
success, msg = tracker.star_repo(args.star)
print(msg)
def unstar_command(tracker: StarTracker, args):
"""取消仓库的 Star"""
if not args.unstar:
print("❌ 请指定仓库 (--unstar owner/repo)")
return
success, msg = tracker.unstar_repo(args.unstar)
print(msg)
def cleanup_command(tracker: StarTracker, args):
"""清理长期未更新的仓库"""
print(f"\n检查 {args.user} 的 Star 项目(阈值: {args.cleanup_days} 天)...")
count, repos = tracker.cleanup_stale_repos(args.user, args.cleanup_days, dry_run=not args.execute)
if count == 0:
print("\n✓ 没有需要清理的项目")
return
print(f"\n找到 {count} 个长期未更新的项目")
if args.execute:
print("\n⚠️ 即将取消这些项目的 Star...")
confirm = input("确认执行? (y/N): ")
if confirm.lower() != 'y':
print("已取消")
return
success, failed, _ = tracker.batch_unstar(repos, dry_run=False)
print(f"\n✓ 完成: 成功 {success} 个, 失败 {failed} 个")
else:
print("\n💡 使用 --execute 参数实际执行清理")
print(f" python scripts/main.py --cleanup --cleanup-days {args.cleanup_days} --execute")
def batch_unstar_command(tracker: StarTracker, args):
"""批量取消 Star"""
import json
file_path = args.batch_unstar
if not Path(file_path).exists():
print(f"❌ 文件不存在: {file_path}")
return
with open(file_path, "r", encoding="utf-8") as f:
repos = json.load(f)
if isinstance(repos, dict):
repos = repos.get("repos", repos.get("tracked_repos", []))
if not repos:
print("❌ 仓库列表为空")
return
print(f"\n将从 Star 列表中移除 {len(repos)} 个仓库:")
for r in repos[:10]:
print(f" - {r}")
if len(repos) > 10:
print(f" ... 还有 {len(repos) - 10} 个")
print("\n💡 使用 --execute 参数实际执行(当前是预览模式)")
confirm = input("确认执行? (y/N): ")
if confirm.lower() != 'y':
print("已取消")
return
success, failed, _ = tracker.batch_unstar(repos, dry_run=False)
print(f"\n✓ 完成: 成功 {success} 个, 失败 {failed} 个")
def batch_star_command(tracker: StarTracker, args):
"""批量添加 Star"""
import json
file_path = args.batch_star
if not Path(file_path).exists():
print(f"❌ 文件不存在: {file_path}")
return
with open(file_path, "r", encoding="utf-8") as f:
repos = json.load(f)
if isinstance(repos, dict):
repos = repos.get("repos", repos.get("recommendations", []))
if not repos:
print("❌ 仓库列表为空")
return
print(f"\n将为 {len(repos)} 个仓库添加 Star:")
for r in repos[:10]:
print(f" - {r}")
if len(repos) > 10:
print(f" ... 还有 {len(repos) - 10} 个")
print("\n💡 使用 --execute 参数实际执行(当前是预览模式)")
confirm = input("确认执行? (y/N): ")
if confirm.lower() != 'y':
print("已取消")
return
success, failed, _ = tracker.batch_star(repos, dry_run=False)
print(f"\n✓ 完成: 成功 {success} 个, 失败 {failed} 个")
def assess_value(repo: dict, health: dict) -> str:
"""评估项目对用户的价值"""
# 这里可以根据自定义规则评估
stars = repo.get('stargazers_count', 0)
language = repo.get('language') or ''
# 高价值判断规则(可自定义)
high_value_keywords = ['claude', 'ai', 'llm', 'agent', 'python']
description = (repo.get('description') or '').lower()
if stars > 5000 or any(kw in description for kw in high_value_keywords):
return 'high'
elif stars > 1000 or 'Python' in language:
return 'medium'
return 'low'
def main():
parser = argparse.ArgumentParser(
description="GitHub Star 追踪器 - 监控你 Star 的项目更新",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
# 首次使用,初始化并保存快照
python scripts/main.py --init --user yourname --limit 50
# 检查更新(对比上次快照)
python scripts/main.py --check --user yourname
# 生成完整报告
python scripts/main.py --report --user yourname --days 7
# 导出 JSON 数据供 HTML Dashboard 使用
python scripts/main.py --export --user yourname
# 快速启动 Dashboard
python scripts/main.py --export --user yourname && open dashboard.html
# 导出配置
python scripts/main.py --export-config --user yourname
# 导入配置
python scripts/main.py --import-config config.json
# 查看版本差异
python scripts/main.py --version-diff --repo owner/repo
# 为单个仓库添加 Star
python scripts/main.py --star owner/repo
# 取消单个仓库的 Star
python scripts/main.py --unstar owner/repo
# 批量清理长期未更新的仓库(先预览)
python scripts/main.py --cleanup --days 90
# 批量清理(实际执行,需要 --execute)
python scripts/main.py --cleanup --days 90 --execute
# 批量取消 Star(从配置文件)
python scripts/main.py --batch-unstar repos.json
# 带项目摘要的初始化
python scripts/main.py --init --user yourname --summarize
""",
)
parser.add_argument("--user", help="GitHub 用户名(不提供则自动从 Token 获取)")
parser.add_argument("--init", action="store_true", help="初始化模式:首次同步并保存快照")
parser.add_argument("--check", action="store_true", help="检查模式:对比快照检测更新")
parser.add_argument("--report", action="store_true", help="报告模式:生成完整更新报告")
parser.add_argument("--export", action="store_true", help="导出模式:生成 projects.json 供 HTML Dashboard 使用")
parser.add_argument("--export-config", action="store_true", help="导出配置:将当前配置保存到文件")
parser.add_argument("--import-config", help="导入配置:从文件导入配置")
parser.add_argument("--version-diff", action="store_true", help="版本差异:查看两个版本之间的 commits")
parser.add_argument("--star", help="为指定仓库添加 Star (owner/repo)")
parser.add_argument("--unstar", help="取消指定仓库的 Star (owner/repo)")
parser.add_argument("--cleanup", action="store_true", help="清理长期未更新的仓库")
parser.add_argument("--cleanup-days", type=int, default=90, help="清理阈值天数 (默认: 90)")
parser.add_argument("--execute", action="store_true", help="实际执行清理操作(默认只预览)")
parser.add_argument("--batch-unstar", help="批量取消 Star:从 JSON 文件读取仓库列表")
parser.add_argument("--batch-star", help="批量添加 Star:从 JSON 文件读取仓库列表")
parser.add_argument("--limit", type=int, default=9999, help="处理的 Star 数量上限 (默认: 9999,即获取全部)")
parser.add_argument("--report-days", type=int, default=7, help="报告时间范围(天)(默认: 7)")
parser.add_argument("--summarize", action="store_true", help="是否使用 AI 生成项目摘要")
parser.add_argument("--no-ai", action="store_true", help="强制不使用 AI,使用基础摘要替代")
parser.add_argument("--output", "-o", help="报告输出文件路径")
args = parser.parse_args()
# 初始化追踪器
github_token = os.getenv("GITHUB_PAT")
tracker = StarTracker(github_token)
# 如果没有提供用户名,尝试从 Token 自动获取
username = args.user
if not username:
username = tracker.get_authenticated_user()
if not username:
print("❌ 错误: 未提供用户名且无法从 Token 获取")
print("💡 解决方法:")
print(" 1. 设置 GITHUB_PAT 环境变量")
print(" 2. 或使用 --user 参数指定用户名")
return 1
print(f"✓ 自动获取用户名: {username}")
args.user = username
# 检查操作模式
if not any([args.init, args.check, args.report, args.export, args.export_config,
args.import_config, args.version_diff, args.star, args.unstar,
args.cleanup, args.batch_unstar, args.batch_star]):
parser.print_help()
print("\n❌ 错误: 请指定操作模式")
return 1
# 执行对应命令
try:
if args.init:
init_command(tracker, args)
elif args.check:
check_command(tracker, args)
elif args.report:
report_command(tracker, args)
elif args.export:
export_command(tracker, args)
elif args.export_config:
export_config_command(tracker, args)
elif args.import_config:
import_config_command(tracker, args)
elif args.version_diff:
version_diff_command(tracker, args)
elif args.star:
star_command(tracker, args)
elif args.unstar:
unstar_command(tracker, args)
elif args.cleanup:
cleanup_command(tracker, args)
elif args.batch_unstar:
batch_unstar_command(tracker, args)
elif args.batch_star:
batch_star_command(tracker, args)
return 0
except KeyboardInterrupt:
print("\n\n⚠️ 操作已取消")
return 130
except Exception as e:
print(f"\n❌ 错误: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
快速导出 data.js - 从现有 JSON 数据和分类配置生成 HTML Dashboard 数据
不需要重新从 GitHub API 获取数据,直接使用本地缓存。
"""
import json
import yaml
from pathlib import Path
from typing import Dict, List, Optional
def load_json_data(json_path: Path) -> Dict:
"""加载 JSON 数据"""
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
def load_categories(categories_path: Path) -> List[Dict]:
"""加载分类配置"""
if not categories_path.exists():
return []
with open(categories_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
return data.get('categories', []) if data else []
def match_categories(repo: Dict, categories: List[Dict], min_score: int = 3) -> List[Dict]:
"""为仓库匹配多个分类(多标签模式)
Args:
repo: 仓库数据
categories: 分类列表
min_score: 最低匹配分数阈值
Returns:
匹配的分类列表(按分数排序)
"""
if not categories:
return []
# 收集仓库的可匹配信息
language = (repo.get('language') or '').lower()
description = (repo.get('description') or '').lower()
topics = [t.lower() for t in repo.get('topics', [])]
name = repo.get('name', '').lower()
full_name = repo.get('full_name', '').lower()
repo_text = f"{language} {description} {' '.join(topics)} {name} {full_name}"
# 检查每个分类的匹配度
matched = []
for cat in categories:
score = 0
keywords = cat.get('keywords', [])
# 统计匹配的关键词数量
matched_keywords = 0
for kw in keywords:
if kw.lower() in repo_text:
matched_keywords += 1
# 根据匹配的关键词数量计算分数
if matched_keywords > 0:
score = matched_keywords * 3 # 每个匹配的关键词得3分
# 语言精确匹配加分
if language and any(kw.lower() == language for kw in keywords):
score += 2
# topic 精确匹配加分
for topic in topics:
if topic in [kw.lower() for kw in keywords]:
score += 2
if score >= min_score:
matched.append((cat, score))
# 按分数排序,返回分类列表
matched.sort(key=lambda x: -x[1])
return [cat for cat, score in matched]
def assess_value(repo: dict) -> str:
"""评估项目价值"""
stars = repo.get('stargazers_count', 0)
language = repo.get('language') or ''
description = (repo.get('description') or '').lower()
high_value_keywords = ['claude', 'ai', 'llm', 'agent', 'python', 'mcp', 'openclaw']
if stars > 5000 or any(kw in description for kw in high_value_keywords):
return 'high'
elif stars > 1000 or 'Python' in language:
return 'medium'
return 'low'
def generate_data_js(output_path: Path, projects: List[Dict], categories: List[Dict]) -> None:
"""生成 data.js 文件,包含项目和分类数据"""
# 准备分类数据(转换为 id -> category 的映射)
categories_map = {}
for cat in categories:
cat_id = cat.get('id')
categories_map[cat_id] = {
'id': cat_id,
'name': cat.get('name', cat_id),
'icon': cat.get('icon', '📁'),
'color': cat.get('color', '#6b7280')
}
# 生成项目数据
projects_str = json.dumps(projects, ensure_ascii=False)
projects_str = projects_str.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
# 生成分类数据
categories_str = json.dumps(categories_map, ensure_ascii=False)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(f"const PROJECTS_DATA = {projects_str};\n\n")
f.write(f"const CATEGORIES_DATA = {categories_str};\n")
def quick_export(
json_path: Path = None,
categories_path: Path = None,
output_path: Path = None
) -> Dict:
"""快速导出 data.js"""
# 默认路径
base_dir = Path(__file__).parent.parent
json_path = json_path or base_dir / "output" / "original_latest.json"
categories_path = categories_path or base_dir / "output" / "categories.yaml"
output_path = output_path or base_dir / "output" / "data.js"
# 加载数据
data = load_json_data(json_path)
categories = load_categories(categories_path)
print(f"📊 加载了 {data.get('total_count', 0)} 个项目")
print(f"📁 加载了 {len(categories)} 个分类")
# 处理每个项目
export_data = []
category_counts = {}
for repo in data.get('repos', []):
# 匹配多个分类
matched_categories = match_categories(repo, categories)
# 统计每个分类的项目数
for cat in matched_categories:
cat_id = cat.get('id', 'uncategorized')
category_counts[cat_id] = category_counts.get(cat_id, 0) + 1
# 主分类(第一个匹配的分类)用于向后兼容
primary_cat = matched_categories[0] if matched_categories else None
# 所有分类 ID 列表
category_ids = [cat.get('id') for cat in matched_categories]
export_data.append({
'id': repo.get('id'),
'full_name': repo.get('full_name'),
'name': repo.get('name'),
'owner': repo.get('owner'),
'description': repo.get('description', ''),
'html_url': repo.get('html_url'),
'language': repo.get('language'),
'topics': repo.get('topics', []),
'stargazers_count': repo.get('stargazers_count', 0),
'forks_count': repo.get('forks_count', 0),
'open_issues_count': repo.get('open_issues_count', 0),
'created_at': repo.get('created_at'),
'updated_at': repo.get('updated_at'),
'pushed_at': repo.get('pushed_at'),
'starred_at': repo.get('starred_at'),
'avatar_url': f"https://avatars.githubusercontent.com/u/{repo.get('owner', '')}" if repo.get('owner') else None,
'value': assess_value(repo),
# 多分类支持
'categories': category_ids, # 所有匹配的分类 ID
'category': primary_cat.get('id') if primary_cat else None, # 主分类(向后兼容)
'category_name': primary_cat.get('name') if primary_cat else None,
'category_icon': primary_cat.get('icon') if primary_cat else None,
'category_color': primary_cat.get('color') if primary_cat else None,
})
# 生成 data.js
generate_data_js(output_path, export_data, categories)
print(f"\n📊 分类统计:")
for cat_id, count in sorted(category_counts.items(), key=lambda x: -x[1]):
cat_name = next((c.get('name', cat_id) for c in categories if c.get('id') == cat_id), cat_id)
print(f" {cat_name}: {count}")
uncategorized = len(data.get('repos', [])) - sum(category_counts.values())
if uncategorized > 0:
print(f" 未分类: {uncategorized}")
print(f"\n✅ 已生成: {output_path}")
print(f" 项目数: {len(export_data)}")
return {
"success": True,
"output_path": str(output_path),
"project_count": len(export_data),
"category_counts": category_counts
}
if __name__ == "__main__":
quick_export()