skills.idiotfan.wang 是我给 Claude Code / Codex CLI / DSH 准备的技能包分发站,目前挂着 21 个 skill。这篇文章记一下它是怎么搭的——不复杂,但有几个值得说的小细节。

为什么会有这个站

我有一堆 skill,散落在 Gitea 的私有仓库里:大梦工资计算、腾讯文档、微信读书、GitNexus、各种图像/音乐生成…… 目录结构是标准的 Anthropic skill 格式——每个技能一个目录,里面一个 SKILL.md(front matter 里写 name / description / version),大一点的再带 scripts/references/

仓库自己用没问题,但有几个痛点:

  • 换台机器就要重新 clone 私有仓库,还要配认证,麻烦
  • 想给朋友用,总不能把 Gitea 账号密码给他
  • 技能更新了,别人拿到的还是旧版

所以我要的其实很简单:一个看得见、能下载、能校验的静态站——首页列出所有技能,点一下就能下载打包好的 tar.gz,附 sha256 校验和。

站点长什么样

纯手写静态页,零框架,就一个 index.html 内嵌 CSS + 一小段原生 JS:

  • GitHub 暗色风格,卡片网格展示技能(emoji 图标、名称、版本徽标、两行描述、体积、适用目标)
  • 顶部搜索框 + 分类筛选:全部 / 3D / 办公 / 图像 / 大梦 / 媒体 / 开发 / 系统 / 网络
  • 统计条:21 个技能 · 384 KB 打包体积 · 更新于 xx UTC

数据全部由一个 Python 脚本生成,没有任何运行时后端。

同步机制:cron 轮询,不用 webhook

一开始我也考虑过 Gitea Actions 或者 webhook,但最后选了最简单可靠的方式——VPS 上挂 cron

*/15 * * * * /opt/skill-server/rebuild.sh >> /var/log/skill-rebuild.log 2>&1

rebuild.sh 干两件事:拉代码、跑构建:

1
2
3
4
5
6
7
#!/bin/bash
# 从 Gitea 拉取最新 skills 源码 → 重新打包 → 更新静态站
set -e
TOKEN=$(cat /root/.gitea-skill-token)
cd /opt/skill-server/repo
git pull -q "https://idiotfan:$TOKEN@git.idiotfan.wang/idiotfan/skills.git" main 2>/dev/null || true
python3 /opt/skill-server/build.py

几个设计点:

  • Token 存在 /root/.gitea-skill-token 文件里,脚本里不写死密码,换 token 只改一个文件
  • git pull 失败也继续跑(|| true),旧版本照常发布,不会把站搞挂
  • 15 分钟一次的轮询对个人站完全够用,还省掉了 webhook 的运维负担

build.py:一站式的构建脚本

构建脚本在 /opt/skill-server/build.py(12KB,无第三方依赖,只用标准库)。它依次做四件事:

1. 解析 SKILL.md front matter

每个技能目录读 SKILL.md 的头部 YAML,提取 name / version / description / whenToUse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def read_frontmatter(skill_dir):
    name = os.path.basename(skill_dir)
    version, description, when = "1.0.0", "", None
    md = os.path.join(skill_dir, "SKILL.md")
    if os.path.exists(md):
        with open(md, encoding="utf-8", errors="replace") as f:
            head = f.read(4096)
        if head.startswith("---"):
            fm = head.split("---", 2)[1]
            for line in fm.splitlines():
                m = re.match(r"^(\w[\w-]*):\s*(.*)$", line)
                ...

版本号直接从仓库里的 SKILL.md 来,所以升级技能 = 改 front matter 里的 version 再 push,站上自动出现新版本。

2. 打包 + 同步

  • 每个技能目录打成 {name}-{version}.tar.gz,丢到 /var/www/skills/bundles/
  • 原始目录整个复制到 /var/www/skills/skills/{name}/nginx 开 autoindex,目录列表直接当详情页用,连详情页都不用写

3. 生成 index.json

每个技能记录 name / version / description / whenToUse / url / sha256 / size / targets,其中 sha256 是打包文件的校验和——下载后可以核对完整性,这是"一键安装"的信任基础。

4. 生成 index.html

Python 里直接拼 HTML 字符串(内嵌 CSS),加上 emoji 图标映射表和分类映射表:

1
2
ICONS = {"dameng-salary": "💰", "tencent-docs": "📝", ...}
CATEGORY = {"dameng-salary": "大梦", "tencent-docs": "办公", ...}

标题里的"更新于"时间戳用 datetime.now(timezone.utc) 生成,所以你能在站上看到 UTC 时间。

容易被忽略的安全细节

技能包里有不少敏感内容:API key、cookies、访问凭证。发布前做了两道保险:

1
2
3
4
5
EXCLUDE_FILES = {"key.txt", "cookies.txt", ".DS_Store"}   # 直接不发布
REDACT = [
    (r'DEFAULT_KEY = "[0-9a-f]{16,}\.[A-Za-z0-9]{10,}"',
     'DEFAULT_KEY = ""  # 服务器版不含内置 key,请设置环境变量 GLM_API_KEY'),
]
  • EXCLUDE_FILES:打包和同步时直接跳过敏感文件
  • REDACT:对 .py / .sh / .js / .md 做正则脱敏,把硬编码的密钥替换成空串和提示

提醒:代码里的密钥永远是隐患。脱敏是兜底,最好还是在源仓库里就别提交密钥。

访问控制:nginx basic auth

站点整套用 nginx 服务,外面套了一层 Basic Auth:

1
2
3
4
5
6
server {
    server_name skills.idiotfan.wang;
    auth_basic "idiotfan skills (需认证)";
    auth_basic_user_file /etc/nginx/.htpasswd-skills;
    ...
}

访问时浏览器会弹认证框,realm 就是"idiotfan skills (需认证)"。自己用或者给朋友发个账号都行,不用把 Gitea 权限开出去。

顺带一提:博客也是同一个套路

这套"cron 轮询 + git pull + 构建脚本"的思路我博客也在用:

*/5 * * * * /opt/blog-server/build.sh >> /var/log/blog-build.log 2>&1
1
2
3
4
5
6
7
#!/bin/bash
# 从 Gitea 拉取博客源码 → Hugo 构建 → 发布到 /var/www/blog
set -e
TOKEN=$(cat /root/.gitea-blog-token)
cd /opt/blog-server/site
git pull -q "https://idiotfan:$TOKEN@git.idiotfan.wang/idiotfan/blog.git" main 2>/dev/null || true
hugo --minify --destination /var/www/blog

所以博客"写完 push、几分钟内自动上线"就是这么来的——没有 webhook、没有 Actions runner,就一条 cron。对个人自建站来说,越简单的机制越不容易坏。

小结

整个站就三个组成部分:

组件作用
Gitea 私有仓库技能源码的唯一事实来源
/opt/skill-server/(cron + 脚本)拉取、脱敏、打包、生成页面
nginx + /var/www/skills静态服务 + 认证 + autoindex

下一步可能给站上加"更新日志"或者按安装目标(claude/codex/dsh)过滤,不过现在的版本已经够用了。自建服务的乐趣大概就在这:每个零件都看得见摸得着,坏了也修得快