社区插件爬虫系统 (不会审查)
社区插件爬虫系统 (不会审查)
背景需求
你帮我写一个github仓库,其工作流固定每日4点自动执行一次 (后期可能会改成一日多次)。工作流会执行/调用脚本执行以下工作:
- 自动搜索 github 中的带 any-menu 标签的项目
- (可选: 增量搜索, 旧项的失效检查)
- (可选: 预留一个函数,用于手动更新某个仓库。例如某个仓库已失效,或某个仓库增加了,则可以在非自动更新时间马上更新这个仓库。场景:该功能用于手动快速更新和删除仓库)
- (可选:黑名单某些满足条件的项目。场景:某些项目有木马,或误识别,则手动拉黑)
- 更新时把这些项目自动更新到 community-plugins.json。然后客户端们就可以直接访问该文件来获取社区插件列表了。
- (可选) 更新和缓存时把插件描述 (github仓库描述) 也一起存起来,star 数也可以一起更新起来。
(可选) 你可以帮我检查这套流程是否是合理的设计,有无疏漏和可改进点。用 js 还是 py 语言写会比较好。
(暂时不做,但预留追加该功能的空间) 后期可能会定期扫描是否有带毒的社区插件并预警管理人员,查看是否将其归入黑名单。
(其他疑问) obsidian 的社区插件注释是如何做到,当你的插件更新、表单文件变化后,他会马上知道并在插件市场预览时直接显示最新的版本号。
可选参考的一些例子:
- Obsidian 插件(分官方上架版(可从官方仓库清单文件json中看到)、未上架版(可从issue中看到),还有一个是 github 上所有的 Obsidian 版本,我目前主要关注第三项)
- 机器人社区插件
- ComfyUI 社区插件
- 其他补充……
答
by deepseek-v4-pro
我会为你设计一个完整的 GitHub 仓库方案,并附上核心代码、工作流配置、可选增强的实现思路,以及对你提出的几个问题的解答。
一、仓库整体结构建议
any-menu-community-plugins/
├── .github/
│ └── workflows/
│ └── update-plugins.yml # 定时+手动触发的工作流
├── scripts/
│ ├── update_plugins.py # 核心更新脚本
│ ├── add_repo.py # 手动添加/更新单个仓库
│ ├── blacklist.py # 黑名单管理脚本
│ └── utils.py # 公共函数(GitHub API 封装等)
├── data/
│ ├── community-plugins.json # 最终输出的插件列表(会被提交回仓库)
│ ├── plugin-cache.json # 缓存插件详细信息(描述、star数等)
│ ├── blacklist.json # 黑名单列表
│ └── last_run.json # 增量搜索状态(上次检查时间/ETag)
├── .gitignore
└── README.md二、定时工作流配置(每日4点 + 手动触发 + 后期可改频率)
文件:.github/workflows/update-plugins.yml
name: Update Community Plugins
on:
schedule:
# 每天 UTC 4:00 执行(北京时间约12:00,可改为你想要时区的4点)
- cron: '0 4 * * *'
workflow_dispatch: # 允许手动触发
inputs:
repo_full_name:
description: '手动更新某个仓库 (格式 owner/name)'
required: false
action_type:
description: '操作类型'
required: false
default: 'full_update'
type: choice
options:
- full_update
- add_repo
- remove_repo
- add_blacklist
jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write # 需要写权限以提交变更
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install requests
- name: Run update script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.repo_full_name }}" != "" ]]; then
python scripts/add_repo.py "${{ github.event.inputs.repo_full_name }}" "${{ github.event.inputs.action_type }}"
else
python scripts/update_plugins.py
fi
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/
git diff --staged --quiet || git commit -m "chore: auto-update community plugins list"
git push后期想改成一日多次,只需修改 cron 字段即可,比如 0 */8 * * * 代表每8小时一次。
三、核心更新脚本(Python 实现)
3.1 主脚本 scripts/update_plugins.py
#!/usr/bin/env python3
import os
import json
import time
from datetime import datetime, timezone
import requests
from utils import search_github_repos, check_rate_limit, load_json, save_json
## 配置
TOPIC = "any-menu"
OUTPUT_FILE = "data/community-plugins.json"
CACHE_FILE = "data/plugin-cache.json"
BLACKLIST_FILE = "data/blacklist.json"
STATE_FILE = "data/last_run.json"
HEADERS = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
"X-GitHub-Api-Version": "2022-11-28"
}
def fetch_all_repos():
"""全量搜索带 any-menu 标签的仓库"""
repos = search_github_repos(f"topic:{TOPIC} fork:true", HEADERS) # 包含 fork 吗?看需求
return repos
def filter_blacklist(repos, blacklist):
"""根据黑名单过滤仓库"""
blacklist_ids = {b['full_name'].lower() for b in blacklist}
return [r for r in repos if r['full_name'].lower() not in blacklist_ids]
def enrich_repo(repo, cache):
"""补充描述、star数等,并更新缓存"""
full_name = repo['full_name']
# 从 GitHub API 数据中直接提取
entry = {
"id": full_name,
"name": repo["name"],
"full_name": full_name,
"description": repo["description"] or "",
"stars": repo["stargazers_count"],
"url": repo["html_url"],
"topics": repo.get("topics", []),
"updated_at": repo["updated_at"],
"default_branch": repo["default_branch"]
}
# 如果需要更多字段,可以再请求单个仓库 API
cache[full_name] = {
"description": entry["description"],
"stars": entry["stars"],
"updated_at": entry["updated_at"]
}
return entry
def update_plugins():
# 检查 API 频率
check_rate_limit(HEADERS)
# 加载缓存和黑名单
cache = load_json(CACHE_FILE) or {}
blacklist = load_json(BLACKLIST_FILE) or []
print("正在搜索 GitHub 仓库...")
repos = fetch_all_repos()
print(f"找到 {len(repos)} 个带 {TOPIC} 标签的仓库")
# 过滤黑名单
valid_repos = filter_blacklist(repos, blacklist)
print(f"黑名单过滤后剩余 {len(valid_repos)} 个仓库")
# 构建最终列表并更新缓存
plugins = []
for repo in valid_repos:
entry = enrich_repo(repo, cache)
plugins.append(entry)
# 按 star 数降序排列(可选)
plugins.sort(key=lambda x: x["stars"], reverse=True)
# 保存结果
save_json(OUTPUT_FILE, plugins)
save_json(CACHE_FILE, cache)
# 记录更新时间
save_json(STATE_FILE, {"last_run": datetime.now(timezone.utc).isoformat()})
print(f"更新完成,共 {len(plugins)} 个插件写入 {OUTPUT_FILE}")
if __name__ == "__main__":
update_plugins()3.2 工具函数 scripts/utils.py
import requests
import json
import time
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent / "data"
def search_github_repos(query, headers, per_page=100):
"""封装 GitHub Search API 分页获取全部结果"""
repos = []
url = f"https://api.github.com/search/repositories?q={query}&sort=stars&order=desc&per_page={per_page}"
while url:
resp = requests.get(url, headers=headers)
if resp.status_code == 403 and 'rate limit' in resp.text:
reset_time = int(resp.headers.get('X-RateLimit-Reset', time.time()+60))
sleep_time = max(reset_time - time.time(), 10)
print(f"遇到限速,等待 {sleep_time:.0f} 秒...")
time.sleep(sleep_time)
continue
resp.raise_for_status()
data = resp.json()
repos.extend(data.get('items', []))
url = resp.links.get('next', {}).get('url')
return repos
def check_rate_limit(headers):
resp = requests.get("https://api.github.com/rate_limit", headers=headers)
resp.raise_for_status()
core = resp.json()['resources']['core']
print(f"API 剩余次数: {core['remaining']}/{core['limit']},重置时间: {core['reset']}")
if core['remaining'] < 10:
wait = max(core['reset'] - time.time(), 10)
print(f"接近限速,等待 {wait:.0f} 秒...")
time.sleep(wait)
def load_json(filepath):
path = Path(filepath)
if path.exists():
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
return None
def save_json(filepath, data):
path = Path(filepath)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)四、可选增强功能的具体实现
4.1 增量搜索(减少 API 调用)
利用 If-Modified-Since 或 If-None-Match 和 etag 对搜索 API 做条件请求。
但 GitHub Search API 不支持条件请求,所以推荐保存全量列表和最后更新时间:
- 每次运行获取当前全量仓库列表。
- 与上次
community-plugins.json比较,找出新增和移除的仓库。 - 仅对新仓库调用单个仓库 API 补全详细信息(如果要获取比搜索接口更多的字段)。
- 移除的仓库标记为“可能失效”,可做二次验证(例如 HEAD 请求仓库页面检查是否 404)。
在 update_plugins.py 中加入简单比较逻辑即可。
4.2 失效检查
对于已存在的插件列表,随机或定期抽检每个仓库是否仍然存在 / 是否还包含 any-menu 标签:
def check_repo_status(full_name, headers):
url = f"https://api.github.com/repos/{full_name}"
resp = requests.get(url, headers=headers)
if resp.status_code == 404:
return False
# 还可以再检查 topics
data = resp.json()
return TOPIC in data.get('topics', [])可设置一个标志 verified_at,若超过 N 天未验证则后台抽检,发现失效则移除。
4.3 手动更新/删除某个仓库(workflow_dispatch 输入)
通过 add_repo.py 脚本实现:
## scripts/add_repo.py
import sys
import json
from utils import load_json, save_json, HEADERS, TOPIC
import requests
def add_or_update(repo_full_name):
url = f"https://api.github.com/repos/{repo_full_name}"
resp = requests.get(url, headers=HEADERS)
if resp.status_code != 200:
print(f"仓库 {repo_full_name} 不存在或无法访问")
return
repo = resp.json()
if TOPIC not in repo.get("topics", []):
print(f"仓库未包含 {TOPIC} 标签,不会添加")
return
plugins = load_json("data/community-plugins.json") or []
# 替换已有条目或新增
new_entry = {
"id": repo["full_name"],
"name": repo["name"],
"full_name": repo["full_name"],
"description": repo.get("description", ""),
"stars": repo["stargazers_count"],
"url": repo["html_url"],
"topics": repo["topics"],
"updated_at": repo["updated_at"],
"default_branch": repo["default_branch"]
}
found = False
for i, p in enumerate(plugins):
if p["full_name"].lower() == repo_full_name.lower():
plugins[i] = new_entry
found = True
break
if not found:
plugins.append(new_entry)
save_json("data/community-plugins.json", plugins)
print(f"已成功更新/添加 {repo_full_name}")
def remove_repo(repo_full_name):
plugins = load_json("data/community-plugins.json") or []
plugins = [p for p in plugins if p["full_name"].lower() != repo_full_name.lower()]
save_json("data/community-plugins.json", plugins)
print(f"已移除 {repo_full_name}")
if __name__ == "__main__":
action = sys.argv[2] if len(sys.argv) > 2 else "add_repo"
repo = sys.argv[1]
if action == "remove_repo":
remove_repo(repo)
else:
add_or_update(repo)工作流里已按 workflow_dispatch 输入调用该脚本,非常方便。
4.4 黑名单管理
data/blacklist.json 格式示例:
[
{
"full_name": "malware-user/repo-name",
"reason": "木马",
"added_at": "2026-06-28T12:00:00Z"
}
]提供对应的 blacklist.py 脚本用于添加/移除,调用方式类似 add_repo.py。
过滤逻辑在上方 update_plugins.py 中已实现。
4.5 插件描述和 star 数缓存
已在 enrich_repo() 中实现,plugin-cache.json 存储历史描述和 star 数,可用于后续趋势分析或避免每次请求单个仓库。
五、设计评估与改进建议
5.1 整体评估
合理性:
- 以 GitHub 仓库 + Actions 定时任务 + 静态 JSON 文件作为数据源,是最轻量、最易维护的社区插件分发方案。
- 使用 GitHub Topics (
any-menu) 作为插件登记入口,开发者只需给自己仓库打上标签即可被收录,零门槛。 - 客户端直接请求
community-plugins.jsonraw 文件,无需服务器,天然具备 CDN 缓存,成本极低。
潜在疏漏与改进点:
版本过滤问题
目前你只是搜到了所有打标签的仓库,但没有区分插件的“版本”或“是否正式发布”。建议增加:- 仅收录含有
any-menu标签 且 有 Release 的仓库(或至少验证主分支存在)。 - 在 JSON 中记录
latest_release_tag和release_url,方便客户端下载指定版本。
- 仅收录含有
API 限速
搜索 API 限制较严(认证用户 30 次/分钟)。如果any-menu仓库数量暴增,可能一次全量更新就会撞限。
解决方案:- 使用条件请求(ETag)缓存已获取数据,仅在数据变化时才更新。
- 如果仓库非常多,可改用定时全量搜索 + 持续监听
star/fork事件的 Webhook(需要额外服务)作为补充。
安全性
恶意仓库可能伪造描述、包含木马。
解决:- 黑名单机制已设计,但需考虑自动扫描:比如用脚本检查
package.json(如果是 Node 项目)或二进制文件是否异常。 - 在 JSON 中增加
verified字段,默认 false,需人工审核后置 true。
- 黑名单机制已设计,但需考虑自动扫描:比如用脚本检查
失效检测频率
建议不仅依赖定时全量更新,还应每天抽查 N 个最老验证的仓库,确保移除失效项,保持列表新鲜。数据一致性
workflow 直接 push 回仓库会产生大量提交,建议:- 若 JSON 内容无变化,则跳过 commit。
- 或者将 JSON 文件输出到 GitHub Pages 分支,而不是不断在主分支提交。
多客户端并发访问
直接访问 raw 文件没问题,但若需要高级过滤(如按分类、最新更新),可在 JSON 中提供字段,由客户端筛选。
5.2 语言选择:Python vs JavaScript
Python
- 数据处理简洁,
requests库稳定,适合写脚本。 - GitHub Actions 原生支持 Python,无需额外安装 Node 依赖。
- 社区大量类似脚本用 Python 编写,参考例子多。
- 缺点:速度较 Node 稍慢,但对这个场景完全无影响。
- 数据处理简洁,
JavaScript/TypeScript
- 如果用
@octokit/rest可以更方便地处理 GitHub API,自带限速处理、分页。 - 如果需要和前端客户端共享类型定义,TS 更有优势。
- 但需要
package.json、安装依赖,体积略大。
- 如果用
建议:优先使用 Python。原因:
- 你的需求属于纯后端脚本,无前端共享类型需求。
- Python 脚本在 Actions 中更轻量,依赖仅
requests,甚至可只用标准库urllib。 - 后续若需要做数据分析(如 star 趋势),Python 生态更成熟。
六、关于 Obsidian 插件市场“实时更新版本号”的机制
Obsidian 社区插件市场并非直接扫描 GitHub 仓库,而是采用插件提交 PR + 审核 + 自动构建发布的流程,大致机制如下:
插件描述清单
每个插件仓库根目录都有一个manifest.json(或manifest-beta.json),其中包含:{ "id": "my-plugin", "version": "1.2.3", ... }插件注册
开发者 forkobsidianmd/obsidian-releases仓库,在community-plugins.json中添加自己的插件条目(包含 id、repo、分支等),并提 PR。审核通过合并后,插件即被市场收录。版本同步
Obsidian 客户端会定期(或启动时)拉取obsidian-releases/community-plugins.json文件。
文件内每个插件条目包含repo字段,不直接存版本号。
客户端需要获取版本号时,会 直接向该插件的 GitHub raw 地址请求manifest.json。
例如:https://raw.githubusercontent.com/user/repo/HEAD/manifest.json
这个请求会返回仓库主分支最新的manifest.json。因此,一旦开发者更新了manifest.json中的 version 并推送,客户端立即能看到新版本号(因为 raw.githubusercontent.com 通常有 CDN 缓存,但延迟在几分钟内)。更实时的机制
有些插件市场(如 Obsidian)还依赖obsidian-releases仓库的 GitHub Actions,定期检查插件仓库的manifest.json是否有变化,更新到官方清单中,以便提供离线版本信息。但核心原理仍是:客户端按需拉取各插件仓库的最新 manifest。
你的 any-menu 场景可实现类似机制:
- 可以在
community-plugins.json中只存repo和branch。 - 客户端运行时,逐个请求
raw.githubusercontent.com/{repo}/{branch}/your-plugin-config.json来获取最新版本、描述等。 - 但这样会增加大量请求,建议折中:你的自动脚本在每次更新时,顺便抓取每个仓库的主配置文件(如
any-menu.json),把关键字段(版本、描述)填充到community-plugins.json中,减少客户端请求。
如果你希望客户端能“准实时”感知版本变化而不必等待定时任务,可以采用以下轻量级方案:
- 设置一个 GitHub Actions 触发器,监听
push事件(但监听的是你的插件仓库),当插件作者推送到主分支时,通过 Repository Dispatch 事件触发你的更新 workflow 增量更新该仓库的信息。这需要插件作者配合配置,但可实现接近实时的效果。
七、总结
以上设计了一个完整、可扩展的自动化社区插件清单方案:
- ✅ 每日4点(可改)全量更新
- ✅ 支持手动添加/删除仓库
- ✅ 黑名单过滤
- ✅ 缓存描述和 star 数
- ✅ 使用 Python 脚本,结构清晰
- ✅ 工作流自动 commit 回仓库,客户端直接读取 JSON
- ⚠️ 建议增加 Release 检测、失效验证、审核机制等安全优化
你可以基于这套框架快速启动,后续根据社区规模逐步加入增量搜索、监测失效等高级功能。