<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>服务器 on idiotfan</title><link>https://idiotfan.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/</link><description>Recent content in 服务器 on idiotfan</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Fri, 14 Aug 2026 11:50:00 +0800</lastBuildDate><atom:link href="https://idiotfan.wang/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/index.xml" rel="self" type="application/rss+xml"/><item><title>我是怎么搭自己的 Skills 同步站的</title><link>https://idiotfan.wang/posts/my-skills-sync-site/</link><pubDate>Fri, 14 Aug 2026 11:50:00 +0800</pubDate><guid>https://idiotfan.wang/posts/my-skills-sync-site/</guid><description>从 Gitea 私有仓库到 skills.idiotfan.wang：cron 定时拉取 + Python 构建脚本 + nginx 认证，把 21 个技能包变成一键安装的分发站。</description><content:encoded><![CDATA[<p><a href="https://skills.idiotfan.wang">skills.idiotfan.wang</a> 是我给 Claude Code / Codex CLI / DSH 准备的技能包分发站，目前挂着 21 个 skill。这篇文章记一下它是怎么搭的——不复杂，但有几个值得说的小细节。</p>
<h2 id="为什么会有这个站">为什么会有这个站</h2>
<p>我有一堆 skill，散落在 Gitea 的私有仓库里：大梦工资计算、腾讯文档、微信读书、GitNexus、各种图像/音乐生成…… 目录结构是标准的 Anthropic skill 格式——每个技能一个目录，里面一个 <code>SKILL.md</code>（front matter 里写 <code>name</code> / <code>description</code> / <code>version</code>），大一点的再带 <code>scripts/</code>、<code>references/</code>。</p>
<p>仓库自己用没问题，但有几个痛点：</p>
<ul>
<li><strong>换台机器就要重新 clone 私有仓库</strong>，还要配认证，麻烦</li>
<li><strong>想给朋友用</strong>，总不能把 Gitea 账号密码给他</li>
<li>技能更新了，别人拿到的还是旧版</li>
</ul>
<p>所以我要的其实很简单：一个<strong>看得见、能下载、能校验</strong>的静态站——首页列出所有技能，点一下就能下载打包好的 tar.gz，附 sha256 校验和。</p>
<h2 id="站点长什么样">站点长什么样</h2>
<p>纯手写静态页，零框架，就一个 <code>index.html</code> 内嵌 CSS + 一小段原生 JS：</p>
<ul>
<li>GitHub 暗色风格，卡片网格展示技能（emoji 图标、名称、版本徽标、两行描述、体积、适用目标）</li>
<li>顶部搜索框 + 分类筛选：全部 / 3D / 办公 / 图像 / 大梦 / 媒体 / 开发 / 系统 / 网络</li>
<li>统计条：<code>21 个技能 · 384 KB 打包体积 · 更新于 xx UTC</code></li>
</ul>
<p>数据全部由一个 Python 脚本生成，没有任何运行时后端。</p>
<h2 id="同步机制cron-轮询不用-webhook">同步机制：cron 轮询，不用 webhook</h2>
<p>一开始我也考虑过 Gitea Actions 或者 webhook，但最后选了最简单可靠的方式——<strong>VPS 上挂 cron</strong>：</p>
<pre tabindex="0"><code class="language-cron" data-lang="cron">*/15 * * * * /opt/skill-server/rebuild.sh &gt;&gt; /var/log/skill-rebuild.log 2&gt;&amp;1
</code></pre><p><code>rebuild.sh</code> 干两件事：拉代码、跑构建：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># 从 Gitea 拉取最新 skills 源码 → 重新打包 → 更新静态站</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -e
</span></span><span class="line"><span class="cl"><span class="nv">TOKEN</span><span class="o">=</span><span class="k">$(</span>cat /root/.gitea-skill-token<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /opt/skill-server/repo
</span></span><span class="line"><span class="cl">git pull -q <span class="s2">&#34;https://idiotfan:</span><span class="nv">$TOKEN</span><span class="s2">@git.idiotfan.wang/idiotfan/skills.git&#34;</span> main 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">python3 /opt/skill-server/build.py
</span></span></code></pre></td></tr></table>
</div>
</div><p>几个设计点：</p>
<ul>
<li><strong>Token 存在 <code>/root/.gitea-skill-token</code> 文件里</strong>，脚本里不写死密码，换 token 只改一个文件</li>
<li><code>git pull</code> 失败也继续跑（<code>|| true</code>），旧版本照常发布，不会把站搞挂</li>
<li>15 分钟一次的轮询对个人站完全够用，还省掉了 webhook 的运维负担</li>
</ul>
<h2 id="buildpy一站式的构建脚本">build.py：一站式的构建脚本</h2>
<p>构建脚本在 <code>/opt/skill-server/build.py</code>（12KB，无第三方依赖，只用标准库）。它依次做四件事：</p>
<h3 id="1-解析-skillmd-front-matter">1. 解析 SKILL.md front matter</h3>
<p>每个技能目录读 <code>SKILL.md</code> 的头部 YAML，提取 <code>name</code> / <code>version</code> / <code>description</code> / <code>whenToUse</code>：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">read_frontmatter</span><span class="p">(</span><span class="n">skill_dir</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">skill_dir</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">version</span><span class="p">,</span> <span class="n">description</span><span class="p">,</span> <span class="n">when</span> <span class="o">=</span> <span class="s2">&#34;1.0.0&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="n">md</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">skill_dir</span><span class="p">,</span> <span class="s2">&#34;SKILL.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">md</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">md</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="s2">&#34;replace&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">head</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="mi">4096</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">head</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;---&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">fm</span> <span class="o">=</span> <span class="n">head</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;---&#34;</span><span class="p">,</span> <span class="mi">2</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">fm</span><span class="o">.</span><span class="n">splitlines</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                <span class="n">m</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="k">match</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^(\w[\w-]*):\s*(.*)$&#34;</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="o">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>版本号直接从仓库里的 <code>SKILL.md</code> 来，所以<strong>升级技能 = 改 front matter 里的 version 再 push</strong>，站上自动出现新版本。</p>
<h3 id="2-打包--同步">2. 打包 + 同步</h3>
<ul>
<li>每个技能目录打成 <code>{name}-{version}.tar.gz</code>，丢到 <code>/var/www/skills/bundles/</code></li>
<li>原始目录整个复制到 <code>/var/www/skills/skills/{name}/</code>，<strong>nginx 开 autoindex，目录列表直接当详情页用</strong>，连详情页都不用写</li>
</ul>
<h3 id="3-生成-indexjson">3. 生成 index.json</h3>
<p>每个技能记录 <code>name / version / description / whenToUse / url / sha256 / size / targets</code>，其中 <strong>sha256 是打包文件的校验和</strong>——下载后可以核对完整性，这是&quot;一键安装&quot;的信任基础。</p>
<h3 id="4-生成-indexhtml">4. 生成 index.html</h3>
<p>Python 里直接拼 HTML 字符串（内嵌 CSS），加上 emoji 图标映射表和分类映射表：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ICONS</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;dameng-salary&#34;</span><span class="p">:</span> <span class="s2">&#34;💰&#34;</span><span class="p">,</span> <span class="s2">&#34;tencent-docs&#34;</span><span class="p">:</span> <span class="s2">&#34;📝&#34;</span><span class="p">,</span> <span class="o">...</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">CATEGORY</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;dameng-salary&#34;</span><span class="p">:</span> <span class="s2">&#34;大梦&#34;</span><span class="p">,</span> <span class="s2">&#34;tencent-docs&#34;</span><span class="p">:</span> <span class="s2">&#34;办公&#34;</span><span class="p">,</span> <span class="o">...</span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>标题里的&quot;更新于&quot;时间戳用 <code>datetime.now(timezone.utc)</code> 生成，所以你能在站上看到 UTC 时间。</p>
<h2 id="容易被忽略的安全细节">容易被忽略的安全细节</h2>
<p>技能包里有不少敏感内容：API key、cookies、访问凭证。发布前做了两道保险：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">EXCLUDE_FILES</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;key.txt&#34;</span><span class="p">,</span> <span class="s2">&#34;cookies.txt&#34;</span><span class="p">,</span> <span class="s2">&#34;.DS_Store&#34;</span><span class="p">}</span>   <span class="c1"># 直接不发布</span>
</span></span><span class="line"><span class="cl"><span class="n">REDACT</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="sa">r</span><span class="s1">&#39;DEFAULT_KEY = &#34;[0-9a-f]{16,}\.[A-Za-z0-9]{10,}&#34;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="s1">&#39;DEFAULT_KEY = &#34;&#34;  # 服务器版不含内置 key，请设置环境变量 GLM_API_KEY&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>EXCLUDE_FILES</code>：打包和同步时直接跳过敏感文件</li>
<li><code>REDACT</code>：对 <code>.py</code> / <code>.sh</code> / <code>.js</code> / <code>.md</code> 做正则脱敏，把硬编码的密钥替换成空串和提示</li>
</ul>
<blockquote>
<p>提醒：<strong>代码里的密钥永远是隐患</strong>。脱敏是兜底，最好还是在源仓库里就别提交密钥。</p>
</blockquote>
<h2 id="访问控制nginx-basic-auth">访问控制：nginx basic auth</h2>
<p>站点整套用 nginx 服务，外面套了一层 Basic Auth：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">skills.idiotfan.wang</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">auth_basic</span> <span class="s">&#34;idiotfan</span> <span class="s">skills</span> <span class="s">(需认证)&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">auth_basic_user_file</span> <span class="s">/etc/nginx/.htpasswd-skills</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">...</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>访问时浏览器会弹认证框，realm 就是&quot;idiotfan skills (需认证)&quot;。自己用或者给朋友发个账号都行，不用把 Gitea 权限开出去。</p>
<h2 id="顺带一提博客也是同一个套路">顺带一提：博客也是同一个套路</h2>
<p>这套&quot;cron 轮询 + git pull + 构建脚本&quot;的思路我博客也在用：</p>
<pre tabindex="0"><code class="language-cron" data-lang="cron">*/5 * * * * /opt/blog-server/build.sh &gt;&gt; /var/log/blog-build.log 2&gt;&amp;1
</code></pre><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># 从 Gitea 拉取博客源码 → Hugo 构建 → 发布到 /var/www/blog</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -e
</span></span><span class="line"><span class="cl"><span class="nv">TOKEN</span><span class="o">=</span><span class="k">$(</span>cat /root/.gitea-blog-token<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /opt/blog-server/site
</span></span><span class="line"><span class="cl">git pull -q <span class="s2">&#34;https://idiotfan:</span><span class="nv">$TOKEN</span><span class="s2">@git.idiotfan.wang/idiotfan/blog.git&#34;</span> main 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">hugo --minify --destination /var/www/blog
</span></span></code></pre></td></tr></table>
</div>
</div><p>所以博客&quot;写完 push、几分钟内自动上线&quot;就是这么来的——<strong>没有 webhook、没有 Actions runner，就一条 cron</strong>。对个人自建站来说，越简单的机制越不容易坏。</p>
<h2 id="小结">小结</h2>
<p>整个站就三个组成部分：</p>
<table>
	<thead>
			<tr>
					<th>组件</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Gitea 私有仓库</td>
					<td>技能源码的唯一事实来源</td>
			</tr>
			<tr>
					<td><code>/opt/skill-server/</code>（cron + 脚本）</td>
					<td>拉取、脱敏、打包、生成页面</td>
			</tr>
			<tr>
					<td>nginx + /var/www/skills</td>
					<td>静态服务 + 认证 + autoindex</td>
			</tr>
	</tbody>
</table>
<p>下一步可能给站上加&quot;更新日志&quot;或者按安装目标（claude/codex/dsh）过滤，不过现在的版本已经够用了。自建服务的乐趣大概就在这：<strong>每个零件都看得见摸得着，坏了也修得快</strong>。</p>
]]></content:encoded></item><item><title>我的自建服务清单</title><link>https://idiotfan.wang/posts/my-selfhosted-services/</link><pubDate>Fri, 14 Aug 2026 10:20:00 +0800</pubDate><guid>https://idiotfan.wang/posts/my-selfhosted-services/</guid><description>一台 1.6G 内存的阿里云 VPS 上跑着的全套自建服务：博客、Gitea、Skills 仓库、服务门户与可能实验室 HR。</description><content:encoded><![CDATA[<p>这台 1.6G 内存的阿里云 VPS 上跑着我的全套自建服务：</p>
<table>
	<thead>
			<tr>
					<th>服务</th>
					<th>地址</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>博客（本站）</strong></td>
					<td><a href="https://idiotfan.wang">idiotfan.wang</a></td>
					<td>Hugo + PaperMod 静态站</td>
			</tr>
			<tr>
					<td><strong>Skills 仓库</strong></td>
					<td><a href="https://skills.idiotfan.wang">skills.idiotfan.wang</a></td>
					<td>我的 21 个技能包分发站</td>
			</tr>
			<tr>
					<td><strong>Gitea</strong></td>
					<td><a href="https://git.idiotfan.wang">git.idiotfan.wang</a></td>
					<td>自托管 git（博客 + skills 源码）</td>
			</tr>
			<tr>
					<td><strong>服务门户</strong></td>
					<td><a href="https://hello.idiotfan.wang">hello.idiotfan.wang</a></td>
					<td>所有服务一处直达</td>
			</tr>
			<tr>
					<td><strong>可能实验室 HR</strong></td>
					<td><a href="https://maybelab.idiotfan.wang">maybelab.idiotfan.wang</a></td>
					<td>水龙头系统</td>
			</tr>
	</tbody>
</table>
<p>博客的发布方式：本地 Markdown → <code>git push</code> → VPS 自动构建，5 分钟内上线。</p>
]]></content:encoded></item></channel></rss>