MoreRSS

site iconSeven | 小柒修改

Java 程序员,热衷自动化。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Seven | 小柒的 RSS 预览

用 Happy 在手机上控制 Claude Code:从安装到自建服务器

2026-05-14 00:00:00

<p>VibeCoding 大半年,最大的感受是:<strong>慢</strong>。一个需求跑大半天,高峰期 API 限速更是雪上加霜。你想走开干点别的,又怕 AI 卡在权限弹窗上等你审批——走也不是,守也不是。</p><p>实践久了之后会发现: VibeCoding 需要的不是持续盯盘,而是一种<strong>间歇性、片段化的持续注意力</strong>。<br>AI 自己跑着就行,你只在关键节点出现——审批权限、纠正方向、拍板决策。<strong>你不是监控器,你是把关人。</strong></p><p><span class="exturl" data-url="aHR0cHM6Ly9oYXBweS5lbmdpbmVlcmluZy8=">Happy Coder<i class="fa fa-external-link-alt"></i></span> 恰好解决了两个问题:</p><ul><li><strong>“守”</strong>——手机随时查看进度、审批、发指令,不用守在电脑前。</li><li><strong>“等”</strong>——通勤路上掏出手机接着推需求,碎片时间变生产力。</li></ul><p>这篇文章分享一下我从零开始用 Happy 的全过程,覆盖日常使用、后台常驻、开机自启、以及自建服务器。</p><span id="more"></span><h2 id="快速上手:五分钟连上手机"><a href="#快速上手:五分钟连上手机" class="headerlink" title="快速上手:五分钟连上手机"></a>快速上手:五分钟连上手机</h2><h3 id="安装-happy-cli"><a href="#安装-happy-cli" class="headerlink" title="安装 happy-cli"></a>安装 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL3Nsb3B1cy9oYXBweQ==">happy-cli<i class="fa fa-external-link-alt"></i></span></h3><p>前提是你已经装好了 <span class="exturl" data-url="aHR0cHM6Ly9kb2NzLmFudGhyb3BpYy5jb20vZW4vZG9jcy9jbGF1ZGUtY29kZQ==">Claude Code<i class="fa fa-external-link-alt"></i></span>(<code>claude</code> 命令可用)。然后一行命令搞定:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g happy</span><br></pre></td></tr></table></figure><h3 id="下载手机端-APP"><a href="#下载手机端-APP" class="headerlink" title="下载手机端 APP"></a>下载手机端 APP</h3><p>iOS 去 App Store,Android 去 <span class="exturl" data-url="aHR0cHM6Ly9wbGF5Lmdvb2dsZS5jb20vc3RvcmUvYXBwcy9kZXRhaWxzP2lkPWNvbS5leDNuZHIuaGFwcHk=">Google Play<i class="fa fa-external-link-alt"></i></span>,搜 “Happy Coder” 就能找到。APP 免费,下载安装即可。</p><h3 id="扫码连接"><a href="#扫码连接" class="headerlink" title="扫码连接"></a>扫码连接</h3><p>在电脑端运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">happy auth login</span><br></pre></td></tr></table></figure><p>终端里会显示一个二维码。打开手机上的 Happy Coder APP,扫码完成配对。认证信息存储在本地 <code>~/.happy/</code> 目录下,私钥不会离开你的设备。</p><h3 id="启动第一个会话"><a href="#启动第一个会话" class="headerlink" title="启动第一个会话"></a>启动第一个会话</h3><p>直接运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">happy</span><br></pre></td></tr></table></figure><p>和直接运行 <code>claude</code> 的区别在于,<code>happy</code> 启动的会话可以被手机端实时看到和控制。你在电脑终端输入的内容会同步显示在手机上,反过来也一样。手机端接管会话后,想切回电脑操作,在终端按任意键就行,<strong>切换是无缝的,会话不会中断</strong>。</p><p>顺便提一句,Happy 不只支持 Claude,还支持 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL29wZW5haS9jb2RleA==">Codex<i class="fa fa-external-link-alt"></i></span>、<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2dvb2dsZS1nZW1pbmkvZ2VtaW5pLWNsaQ==">Gemini<i class="fa fa-external-link-alt"></i></span> 等:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">happy codex # 启动 Codex 会话</span><br><span class="line">happy gemini # 启动 Gemini CLI 会话</span><br></pre></td></tr></table></figure><h2 id="后台常驻:让手机随时发起会话"><a href="#后台常驻:让手机随时发起会话" class="headerlink" title="后台常驻:让手机随时发起会话"></a>后台常驻:让手机随时发起会话</h2><p>上面 <code>happy</code> 命令需要手动启动,关掉终端会话就断了。如果我想随时随地从手机发起会话,电脑上得有个常驻后台的服务。这就是 <strong>daemon(守护进程)</strong> 的作用。</p><p>daemon 在后台持续运行,手机 APP 可以随时通过它创建新的 Claude Code 会话。创建会话时 APP 会让你选择工作目录,Claude 就在那个目录下跑,读取该项目的 <code>CLAUDE.md</code> 和上下文。</p><h3 id="基本操作"><a href="#基本操作" class="headerlink" title="基本操作"></a>基本操作</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">happy daemon start # 启动守护进程</span><br><span class="line">happy daemon stop # 停止守护进程</span><br><span class="line">happy daemon status # 查看运行状态</span><br><span class="line">happy daemon list # 列出活跃会话</span><br><span class="line">happy daemon logs # 查看日志文件路径</span><br></pre></td></tr></table></figure><h2 id="开机自启:Linux-systemd-配置"><a href="#开机自启:Linux-systemd-配置" class="headerlink" title="开机自启:Linux systemd 配置"></a>开机自启:Linux systemd 配置</h2><p>可能会高频使用 happy, 暂时设置了开机自启。<br>因为我的主力机是 Linux 系统,开机自启使用 <span class="exturl" data-url="aHR0cHM6Ly93d3cuZnJlZWRlc2t0b3Aub3JnL3dpa2kvU29mdHdhcmUvc3lzdGVtZC8=">systemd<i class="fa fa-external-link-alt"></i></span> 用户服务来实现。</p><p>创建服务文件 <code>~/.config/systemd/user/happy-daemon.service</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Happy Coder Daemon</span><br><span class="line"><span class="attr">After</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"><span class="attr">Wants</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">ExecStart</span>=/path/to/happy daemon start-sync</span><br><span class="line"><span class="attr">WorkingDirectory</span>=/your/workspace</span><br><span class="line"><span class="attr">Restart</span>=always</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">10</span></span><br><span class="line"><span class="attr">Environment</span>=PATH=/home/xxx/.local/bin:/usr/local/bin:/usr/bin:/bin</span><br><span class="line"><span class="attr">Environment</span>=HOME=/home/xxx</span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=default.target</span><br></pre></td></tr></table></figure><p>这里面有几个<strong>关键细节</strong>,逐一说清楚。</p><h3 id="start-sync-而不是-start"><a href="#start-sync-而不是-start" class="headerlink" title="start-sync 而不是 start"></a><code>start-sync</code> 而不是 <code>start</code></h3><p><code>happy daemon start</code> 会 spawn 一个子进程然后父进程退出,systemd 检测到主进程退出就认为服务结束了。而 <code>start-sync</code> 是前台阻塞模式,systemd 可以正确跟踪进程状态。用错了的话,daemon 会不断被 systemd 重启又退出,陷入死循环。</p><h3 id="Restart-always"><a href="#Restart-always" class="headerlink" title="Restart=always"></a><code>Restart=always</code></h3><p>不要用 <code>Restart=on-failure</code>。daemon 收到正常关机信号时退出码是 0,<code>on-failure</code> 不会重启正常退出的进程。我们需要的是无论什么原因退出都自动重启。</p><h3 id="Environment-不要包含-CLAUDECODE-变量"><a href="#Environment-不要包含-CLAUDECODE-变量" class="headerlink" title="Environment 不要包含 CLAUDECODE 变量"></a>Environment 不要包含 <code>CLAUDECODE</code> 变量</h3><p>如果 daemon 的环境里有 <code>CLAUDECODE</code> 或 <code>CLAUDE_CODE_ENTRYPOINT</code> 这类变量,从手机发起的会话会<strong>直接失败</strong>。症状是:手机上能看到会话创建成功,但一发消息就挂。排查这类问题,这里是第一个要检查的。</p><h3 id="loginctl-enable-linger"><a href="#loginctl-enable-linger" class="headerlink" title="loginctl enable-linger"></a><code>loginctl enable-linger</code></h3><p>systemd 用户服务默认只在用户登录时运行。执行 <code>loginctl enable-linger</code> 后,即使没有登录,用户服务也能启动。</p><h3 id="启用服务"><a href="#启用服务" class="headerlink" title="启用服务"></a>启用服务</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">loginctl enable-linger $(whoami)</span><br><span class="line">systemctl --user daemon-reload</span><br><span class="line">systemctl --user enable --now happy-daemon.service</span><br></pre></td></tr></table></figure><p>验证:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl --user status happy-daemon.service</span><br></pre></td></tr></table></figure><p>看到 <code>active (running)</code> 就对了。可以故意杀掉进程测试自动恢复:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">kill $(pgrep -f &quot;happy daemon start-sync&quot;)</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">等约 10 秒</span></span><br><span class="line">systemctl --user status happy-daemon.service</span><br></pre></td></tr></table></figure><p>应该看到它又自动重启了。</p><h2 id="自建-Happy-Server"><a href="#自建-Happy-Server" class="headerlink" title="自建 Happy Server"></a>自建 Happy Server</h2><p>Happy 默认使用官方中继服务器。所有通信端到端加密,服务器只能看到加密后的数据块,无法读取代码内容。用官方服务器在安全性上没问题。<br>但如果有数据合规要求、想在内网部署、或者纯粹想自己掌控基础设施,自建也很简单。</p><p><em>主要是官网真的太太太太太太太太太太太太太太太太太太太太太太太太太太太太太太太太慢了……………….</em></p><h3 id="Docker-部署(推荐)"><a href="#Docker-部署(推荐)" class="headerlink" title="Docker 部署(推荐)"></a>Docker 部署(推荐)</h3><p>Happy Server 独立模式内置了 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2VsZWN0cmljLXNxbC9wZ2xpdGU=">PGlite<i class="fa fa-external-link-alt"></i></span>(嵌入式 <span class="exturl" data-url="aHR0cHM6Ly93d3cucG9zdGdyZXNxbC5vcmcv">PostgreSQL<i class="fa fa-external-link-alt"></i></span>),<strong>不需要额外安装 Postgres、Redis 或 S3</strong>,一个容器搞定:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">构建镜像(在仓库根目录执行)</span></span><br><span class="line">docker build -t happy-server -f Dockerfile .</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">运行</span></span><br><span class="line">docker run -d \</span><br><span class="line"> --name happy-server \</span><br><span class="line"> -p 3005:3005 \</span><br><span class="line"> -e HANDY_MASTER_SECRET=your-secret-here \</span><br><span class="line"> -e PUBLIC_URL=https://happy.example.com \</span><br><span class="line"> -v happy-data:/data \</span><br><span class="line"> --restart unless-stopped \</span><br><span class="line"> happy-server</span><br></pre></td></tr></table></figure><p>几个关键说明:</p><ul><li><strong><code>HANDY_MASTER_SECRET</code> 是必须设置的</strong>,用于认证和加密。务必生成一个足够长的随机字符串</li><li><code>PUBLIC_URL</code> 设为你的公网可访问的 HTTPS 地址,影响文件链接的生成</li><li>数据持久化在 Docker volume <code>happy-data</code> 中,容器重启不丢数据</li><li><strong>服务器本身不处理 TLS</strong>,需要配合反向代理</li></ul><p>健康检查:访问 <code>GET /health</code>,返回 <code>{&quot;status&quot;:&quot;ok&quot;}</code> 说明服务正常。</p><h3 id="环境变量一览"><a href="#环境变量一览" class="headerlink" title="环境变量一览"></a>环境变量一览</h3><p><strong>必须设置:</strong></p><table><thead><tr><th>变量</th><th>说明</th></tr></thead><tbody><tr><td><code>HANDY_MASTER_SECRET</code></td><td>认证和加密的主密钥,必须设置</td></tr></tbody></table><p><strong>可选(独立部署):</strong></p><table><thead><tr><th>变量</th><th>默认值</th><th>说明</th></tr></thead><tbody><tr><td><code>PORT</code></td><td><code>3005</code></td><td>服务监听端口</td></tr><tr><td><code>PUBLIC_URL</code></td><td><code>http://localhost:3005</code></td><td>公网访问 URL</td></tr><tr><td><code>DATA_DIR</code></td><td><code>/data</code></td><td>数据存储目录</td></tr><tr><td><code>PGLITE_DIR</code></td><td><code>/data/pglite</code></td><td>PGlite 数据库目录</td></tr><tr><td><code>METRICS_ENABLED</code></td><td><code>false</code></td><td>设为 <code>true</code> 开启 Prometheus 指标</td></tr><tr><td><code>METRICS_PORT</code></td><td><code>9090</code></td><td>指标服务端口</td></tr></tbody></table><p><strong>可选(扩展部署):</strong></p><table><thead><tr><th>变量</th><th>说明</th></tr></thead><tbody><tr><td><code>DATABASE_URL</code></td><td>外部 PostgreSQL 连接地址(替代 PGlite)</td></tr><tr><td><code>REDIS_URL</code></td><td>Redis 地址(多副本扩展时使用)</td></tr><tr><td><code>S3_HOST</code> 等</td><td>S3&#x2F;MinIO 对象存储(替代本地文件存储)</td></tr></tbody></table><p>个人使用的话,设一个 <code>HANDY_MASTER_SECRET</code> 就够了。</p><h3 id="TLS-配置"><a href="#TLS-配置" class="headerlink" title="TLS 配置"></a>TLS 配置</h3><p>生产环境用 HTTPS,最省事的方式是用 <span class="exturl" data-url="aHR0cHM6Ly9jYWRkeXNlcnZlci5jb20v">Caddy<i class="fa fa-external-link-alt"></i></span> 做反向代理,它会<strong>自动申请和续期 <span class="exturl" data-url="aHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcv">Let’s Encrypt<i class="fa fa-external-link-alt"></i></span> 证书</strong>:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">happy.example.com &#123;</span><br><span class="line"> reverse_proxy localhost:3005</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两行配置,Caddy 帮你搞定证书。用 Nginx 的话需要自己配 certbot,稍微麻烦一点。</p><h2 id="CLI-指向自建服务器"><a href="#CLI-指向自建服务器" class="headerlink" title="CLI 指向自建服务器"></a>CLI 指向自建服务器</h2><p>自建服务器部署好之后,通过 <code>HAPPY_SERVER_URL</code> 环境变量让 happy-cli 连接你的服务器:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">export HAPPY_SERVER_URL=https://happy.example.com</span><br><span class="line">happy</span><br></pre></td></tr></table></figure><p>或者写到 shell 配置文件里一劳永逸。加到 <code>~/.zshrc</code> 或 <code>~/.bashrc</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">export HAPPY_SERVER_URL=https://happy.example.com</span><br></pre></td></tr></table></figure><p>如果前面用 systemd 管理 daemon,别忘了在 service 文件的 <code>Environment</code> 里也加上:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">Environment</span>=HAPPY_SERVER_URL=https://happy.example.com</span><br></pre></td></tr></table></figure><p>改完之后 <code>systemctl --user restart happy-daemon.service</code> 让配置生效。</p><h2 id="APP-指向自建服务器"><a href="#APP-指向自建服务器" class="headerlink" title="APP 指向自建服务器"></a>APP 指向自建服务器</h2><p>Happy APP 内置了服务器配置页面,切换很方便:</p><ol><li>打开 APP,进入<strong>设置</strong></li><li>找到 <strong>Server Configuration</strong>(服务器配置)</li><li>输入自建服务器地址,比如 <code>https://happy.example.com</code></li><li>APP 会自动验证:访问这个 URL,检查是否返回 “Welcome to Happy Server!”</li><li>验证通过后保存,之后所有连接走你的自建服务器</li><li>想切回官方服务器?同一个页面重置即可</li></ol><p>切换过程不需要重新登录。</p><hr><p>Happy Coder 解决的问题很小也很具体:让你不用坐在电脑前也能用 Claude Code。但就是这个便利,改变了我跟 Claude 协作的方式。通勤路上想到什么,掏出手机就能接着聊;周末出门在外,Claude 在家里的机器上跑着任务,手机上随时查看进度。</p><p>开源、免费、端到端加密,自托管完全可选。如果你也在用 Claude Code,不妨试试。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL3Nsb3B1cy9oYXBweQ==">slopus&#x2F;happy - GitHub<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9oYXBweS5lbmdpbmVlcmluZy9kb2NzL2ZhcS8=">Happy Engineering - Documentation<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9naXN0LmdpdGh1Yi5jb20vbGlhbWRhcm1vZHkvNGFiYTA4M2MyNmNjYjFiM2IwZjEwNjhlYzE4NWVmNjY=">How to build a 24&#x2F;7 personal AI agent with Claude Code - Liam Darmody<i class="fa fa-external-link-alt"></i></span></li></ul>

人生周报 - 202602

2026-01-21 00:00:00

<h3 id="资源"><a href="#资源" class="headerlink" title="资源"></a>资源</h3><ul><li><span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2FudGhyb3BpY3Mvc2tpbGxz">Anthropic 官方 Skills 示例<i class="fa fa-external-link-alt"></i></span> Skills 示例代码仓库, 可以学习创建自己的 Skills 武器库。</li></ul><h3 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h3><ul><li>逐渐深入使用 ClaudeCode,开始接触 Skills,Hooks,Permissions,好工具的设计真是巧妙,流程简约且高度可定制。</li><li>尝试用 ClaudeCode + Github SpecKit,想从零到一写一个翻译工具。效果层面目前还不好说,核心翻译能力还没写。时间层面,提效感觉不明显,specify,clarify,plan,task,analyze,implement 一套流程下来,一个小需求得花费大半天。</li><li>Anthropic 发布了 code-simplifier 工具,<strong>代码优化效果极好</strong>,可以在不改变业务逻辑的前提下优化代码,感觉可以集成到 CICD 帮忙做代码 review 了。</li><li>ClaudeCode 的潜力真是无穷无尽,写确定性的代码效果很好(写整个项目经验不足还不好评价)。帮我把日报汇总成年终总结,几乎一个字都不用改。后面准备尝试用 NotionMCP 帮我做开源服务指南自动化发布的最后一公里,感觉能行。</li><li>01&#x2F;16 卸载了抖音,头脑清净了许多。卸载抖音的当天上午,上班路上写完了两周的周报草稿。</li><li><span class="exturl" data-url="aHR0cHM6Ly93d3cuYmlnbW9kZWwuY24vZ2xtLWNvZGluZz9pYz1ST0VFQUFXTFhP">GLM Coding Plan<i class="fa fa-external-link-alt"></i></span> 5h 实践窗口居然有 2亿 token, 根本用不完。用 nginx 反代给同事接入,反馈也不错。</li></ul><h3 id="妙想"><a href="#妙想" class="headerlink" title="妙想"></a>妙想</h3><ul><li>我之前的笔记 + 知识库是用 markdown 格式 + VSCode 编辑器 + Git 版本控制 + Github 同步来实现的,后面找机会让 ClaudeCode 把我历史知识库整理发部成公共知识库,好东西就是要分享出去。</li><li>计划了 100 年的周复盘一直没有启动,想试一下让 ClaudeCode 学习我历史博客的写作风格,然后帮我写周复盘。</li></ul>

人生周报 - 202601

2026-01-20 00:00:00

<ul><li>购买了 <span class="exturl" data-url="aHR0cHM6Ly93d3cuYmlnbW9kZWwuY24vZ2xtLWNvZGluZz9pYz1ST0VFQUFXTFhP">GLM Coding Plan<i class="fa fa-external-link-alt"></i></span>,可以随意玩 ClaudeCode 了。虽然模型智能不如国外,好在量大管饱,入门 ClaudeCode 是不错的。</li><li>新买的丐版 MacMini 内存和硬盘都不多,ClaudeCode 开发过程中会用到一些开发环境(Go 语言, Podman, PostGreSQL),不想浪费 MacMini 的空间。索性就把之前淘汰下来的 Manjaro Linux 24h 开机,当作服务器用。<strong>Tmux 会话保持 + ClaudeCode 终端沟通 + VSCode 远程开发</strong>,整个流程非常顺滑。</li><li>有一个叫 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL3Nsb3B1cy9oYXBweQ==">happy<i class="fa fa-external-link-alt"></i></span> 的工具,可以让你远程控制 ClaudeCode 工作,感兴趣的话可以试一下。</li><li>测试了几个 AI 编程工具(GeminiCLI,GithubCopilot),最终还是觉得 ClaudeCode 更好用。其实真没必要折腾各种免费或者白嫖的工具,<strong>免费的往往也是最贵的,时间、精力、机会这些方面的成本不是金钱能够衡量的</strong>。</li><li>之前帮同事写过一个号码认证自动拨测脚本,这次又有需求,借助 AI 编程工具把脚本升级到了 GUI。纯 VibeCoding,调测时间远大于开发时间,且 AI 经常会在实现一个新需求的时候搞出来一个旧功能的 bug。后面可能会多尝试一下 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2NvZGVydmlzb3IvbGVhbi1zcGVj">LeanSpec<i class="fa fa-external-link-alt"></i></span> 或者 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9zcGVjLWtpdA==">SpecKit<i class="fa fa-external-link-alt"></i></span> 这样的 SDD 框架。</li></ul>

善用 MVP 思维,5 天时间零成本搭建小报童排行榜

2024-07-20 21:03:00

<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>你好,我是小柒。<br>Java 程序员,热衷自动化。</p><p>前段时间花了 5 天左右,借助 Notion + CloudFlare Workers + Github Actions 零成本搭建了一个支持一键分销的<span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS8=">小报童排行榜<i class="fa fa-external-link-alt"></i></span>: <span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS8=">https://xiaobot.osguider.com<i class="fa fa-external-link-alt"></i></span>。在这里复盘一下过程,希望能够对你有所启发。</p><p><strong>善用 MVP 思维,用最小的成本最快地实现最核心的功能,尽早触达用户,尽早获取反馈,不断迭代,不断升级。</strong></p><h3 id="什么是小报童排行榜?"><a href="#什么是小报童排行榜?" class="headerlink" title="什么是小报童排行榜?"></a>什么是小报童排行榜?</h3><!-- TODO --><p>小报童排行榜是一个罗列了(几乎)所有小报童专栏的导航网页。可以帮助你<strong>一键分销所有小报童专栏</strong>。</p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/983d596c02f675ddcd74cd7261d5f2ef.png" alt="小报童排行榜-最近更新专栏"></p><span id="more"></span><h3 id="为什么要做小报童排行榜?"><a href="#为什么要做小报童排行榜?" class="headerlink" title="为什么要做小报童排行榜?"></a>为什么要做小报童排行榜?</h3><ul><li>主要是因为小报童官方没有公开的专栏索引列表,但这个痛点确实是在的。既然有需求未被实现,机会不就来了?</li><li>实现足够简单,成本足够低,做这个游刃有余。</li><li>折腾副业,没自己的产品始终感觉没底气。所以,小报童排行榜可以是我的产品支撑,也可以是我向外链接的突破口。</li><li>能给我带来收益:哪怕是最基础的小报童分销,也能带来一些收益。且不说后面依据这个产品的放大升级。</li></ul><h3 id="小报童排行榜给我带来了什么?"><a href="#小报童排行榜给我带来了什么?" class="headerlink" title="小报童排行榜给我带来了什么?"></a>小报童排行榜给我带来了什么?</h3><ul><li>我能做成一件事的信心</li><li>和我一起做事的朋友</li><li>借助小报童排行榜,链接到了更多朋友</li><li>副业生态的起点</li><li>一点点小钱</li></ul><h3 id="一些有趣的统计数据"><a href="#一些有趣的统计数据" class="headerlink" title="一些有趣的统计数据"></a>一些有趣的统计数据</h3><ul><li>迄今为止我收录了 308 位作者的 403 个小报童专栏。其中:<ul><li>有 26 位作者的 38 个专栏订阅数量超过 3000,占比 9%。<strong>只要订阅数量超过 3000 就能跻身前 10%。</strong></li><li>有 50 位作者的 72 个专栏订阅数量在 1000 和 3000 之间,占比 18%。<strong>只要订阅数量超过 1000 就能跻身前 30%。</strong></li></ul><!-- - 有 249 位作者的 293 个专栏订阅数量低于 1000,占比 72%。**你是否,能扛得住“泯然众人矣”的结果呢?** --><ul><li>有 97 位作者的 105 个专栏超过 1 年没有更新,算是已完结的状态。<strong>有人入场,有人离场。机会、永远都在那里等你,剩下的、就是你能否主动抓住它了。</strong></li><li>15 亿国人,你的竞争对手只有 308 位。<strong>其实只要开始,就已经是成功了。</strong></li></ul></li><li>有很多数据不太好看的专栏(订阅数量为 0),其中绝大部分是订阅制而非买断式。因为小报童展示的订阅数&#x3D;当前仍在订阅状态的用户数,当用户订阅到期不再续订时,这个数量就会减少。所以<strong>单从数据好看的角度考虑,买断式的专栏会更好一点。</strong></li><li>上线至今,小报童排行榜分销佣金收入 371.61 元,给朋友们定制一键分销系统(扣除返现后)收入 210 元。对于在互联网上流浪多年的我和 Huazi,这个<strong>正反馈极佳</strong>。</li></ul><p><strong>我把手上已有的小报童专栏排名数据整理成了一个 Excel 表格,如果你想要自己做一些数据分析,可以通过 <code>osguider</code> 微信我领取。</strong></p><h3 id="整体实现思路"><a href="#整体实现思路" class="headerlink" title="整体实现思路"></a>整体实现思路</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">sequenceDiagram</span><br><span class="line"> actor 小报童专栏作者</span><br><span class="line"> participant 金数据表单</span><br><span class="line"> participant paperDB AS Notion专栏数据库</span><br><span class="line"></span><br><span class="line"> 小报童专栏作者-&gt;&gt;+金数据表单: 填写表单请求专栏收录</span><br><span class="line"></span><br><span class="line"> 金数据表单-&gt;&gt;paperDB: 推送专栏信息</span><br><span class="line"> CloudFlareWorkers定时任务-&gt;&gt;+paperDB: 定时扫描专栏信息</span><br><span class="line"> paperDB--&gt;&gt;-CloudFlareWorkers定时任务: 小报童专栏信息</span><br><span class="line"> CloudFlareWorkers定时任务-&gt;&gt;+小报童: 爬取小报童专栏详细信息</span><br><span class="line"> 小报童--&gt;&gt;-CloudFlareWorkers定时任务: 小报童专栏详细信息</span><br><span class="line"> CloudFlareWorkers定时任务-&gt;&gt;paperDB: 小报童专栏详细信息写入数据库</span><br><span class="line"></span><br><span class="line"> GithubActions-&gt;&gt;+paperDB: 定时获取专栏详细信息列表</span><br><span class="line"> paperDB--&gt;&gt;-GithubActions: 专栏详细信息列表</span><br><span class="line"> GithubActions-&gt;&gt;eleventy模板引擎: 生成专栏信息列表数据文件</span><br><span class="line"> eleventy模板引擎-&gt;&gt;小报童排行榜: 编译静态文件</span><br></pre></td></tr></table></figure><h2 id="搭建一个小报童排行榜,可以有多简单?"><a href="#搭建一个小报童排行榜,可以有多简单?" class="headerlink" title="搭建一个小报童排行榜,可以有多简单?"></a>搭建一个小报童排行榜,可以有多简单?</h2><p>按照我以往程序员的思维,如果想要做一个小报童排行榜,至少要经历以下步骤:</p><ul><li>需求分析</li><li>概要设计</li><li>数据库设计</li><li>爬虫&amp;业务代码编写</li><li>前端代码编写</li><li>部署上线</li></ul><p>带着完美主义的思维,最后可能是因为设计配色不满意、可能是因为前端代码没写好,也可能是爬虫做不出来……从而不了了之,前功尽弃。</p><p>这件事情,真的需要这么复杂吗?</p><h2 id="需求分析"><a href="#需求分析" class="headerlink" title="需求分析"></a>需求分析</h2><p>需求分析需要考虑两个层面:我想做什么?我能做什么?其中:</p><p>我想做的事情很简单:把所有小报童专栏汇聚到一个网页,供用户查阅和订阅。<br>我能做什么主要取决于我能从小报童官方拿到什么样的信息。经过简单的分析,我可以从小报童官方获取到以下信息:</p><ul><li>专栏名称</li><li>专栏作者</li><li>专栏介绍</li><li>专栏订阅数量</li><li>专栏内容数量</li><li>专栏价格</li><li>专栏分销比例</li><li>专栏分销链接</li><li>专栏创建时间</li><li>专栏更新时间</li></ul><p>小报童排行榜的核心功能是<strong>列举出所有的小报童专栏信息</strong>,Notion 数据库可以完美实现这个功能!说干就干!</p><h2 id="数据库搭建"><a href="#数据库搭建" class="headerlink" title="数据库搭建"></a>数据库搭建</h2><p>按照可以从小报童官方获取到的信息搭建 Notion 数据库,这个过程就像是给 Excel 添加多个列字段,非常简单。<br><em>这里不再赘述 Notion 的使用,如果你对 Notion 感兴趣,可以通过这几个链接学习。<span class="exturl" data-url="aHR0cHM6Ly9ub3Rpb25jaGluYS5jby8=">中文用户指南<i class="fa fa-external-link-alt"></i></span> | <span class="exturl" data-url="aHR0cHM6Ly93d3cubm90aW9uLnNvL2hlbHAvbm90aW9uLWFjYWRlbXkvY291cnNlLzEwMS1pbnRyb2R1Y3Rpb24=">Notion Academy<i class="fa fa-external-link-alt"></i></span> | <span class="exturl" data-url="aHR0cHM6Ly93d3cubm90aW9uLnNvL2hlbHA=">Notion Help Center<i class="fa fa-external-link-alt"></i></span></em></p><p>一条 Notion 数据库记录长这个样子:</p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/4841724753dee7cd8a82acb71eee58da.png" alt="notion数据库记录-生财有术"></p><p>汇总的列表长这个样子:</p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/cc68f75e8dc407de4f8ae4c88e325c3c.png" alt="notion database-table-小报童专栏"></p><hr><p>Notion 极其强大!支持任意的页面嵌套,支持把任意页面发布为公网可以访问的网页,甚至可以给同一个数据库定制不同的视图!<br>那么依据这个功能,我们实际上已经完成了最简单的 MVP:<strong>手动收集小报童专栏信息,并通过 Notion 展示。</strong></p><p>小报童排行榜的第一个 MVP 长这个样子:</p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/c2c0108d7600038a3505d227d6382db7.png" alt="小报童排行榜 Notion Site"></p><h2 id="数据爬取"><a href="#数据爬取" class="headerlink" title="数据爬取"></a>数据爬取</h2><p>作为一个程序员、作为一名 RPA 教练,手动录数据多少有点说不过去了吧?</p><p>我们来看几个小报童专栏分销链接:</p><ul><li>生财有术项目精选:<span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm5ldC9wL3NoZW5nY2FpeW91c2h1P3JlZmVyPTZmNGVjYzJiLTcwZTktNGY1OC04MmRlLTcxZTViMWYzNTdlZQ==">https://xiaobot.net/p/shengcaiyoushu?refer=6f4ecc2b-70e9-4f58-82de-71e5b1f357ee<i class="fa fa-external-link-alt"></i></span></li><li>IP合伙人·八年成事(百问百答):<span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm5ldC9wL0lQMTA/cmVmZXI9NmY0ZWNjMmItNzBlOS00ZjU4LTgyZGUtNzFlNWIxZjM1N2Vl">https://xiaobot.net/p/IP10?refer=6f4ecc2b-70e9-4f58-82de-71e5b1f357ee<i class="fa fa-external-link-alt"></i></span></li></ul><p>稍加比较就能发现,分销链接由以下几个部分组成:</p><ul><li>域名:<code>https://xiaobot.net/</code></li><li>小报童专栏 ID: <code>shengcaiyoushu</code> &#x2F; <code>IP10</code></li><li>分销标识:<code>refer=6f4ecc2b-70e9-4f58-82de-71e5b1f357ee</code></li></ul><p>其中:域名是不变的,分销标识只跟发起分销的用户有关系,所以在收集专栏信息的时候,我们只需要收集小报童专栏 ID 即可。这一个步骤,现在确实是手动收集和录入的。<br>但在这之后,我们可以通过拼接域名和小报童专栏 ID 得到指定小报童专栏的页面地址。访问专栏页面,可以获取到我们需要的几乎所有信息,通过 RPA&#x2F;写代码 手段轻松获取(这里我的方案是通过 CloudFlare Workers 调用小报童接口获取数据,再使用 Notion API 写入数据库)。<br><em>请适当控制 RPA 运行频率,保护服务器负载。</em></p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/d4ed64c257add9f132fdd3660b89494f.png" alt="小报童专栏主页-生财有术项目精选"></p><p>到这里,内容的自动爬取和更新也搞定了。</p><h2 id="页面编写-自动发布"><a href="#页面编写-自动发布" class="headerlink" title="页面编写 &amp; 自动发布"></a>页面编写 &amp; 自动发布</h2><p>Notion 也有缺点。毕竟是海外的产品,国内访问速度非常不稳定,根据我自己的测试,Notion Site 在极端情况下需要 10-20 秒才能加载出来,这个速度用户肯定是无法忍受的。在我的认知体系内,写一个静态页面是比较好的解决方案。</p><p>在看过我自己写的丑得不能再丑的 HTML Table 之后,我的设计师伙伴 @Huazi 终于还是忍不住亲自上场了。他负责页面样式,我负责内容填充,完美配合。</p><p>技术选型大概是这样的:</p><ul><li>页面样式:HTML + CSS + JavaScript + Tailwind CSS</li><li>内容渲染:通过 JavaScript 脚本调用 Notion API 获取数据,再使用 eleventy 把数据渲染到 HTML。</li><li>自动化:通过 Github Actions 每天定时执行内容渲染逻辑,然后自动发布网页到服务器。</li></ul><p>至此,第二版小报童排行榜问世:<span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS8=">小报童排行榜<i class="fa fa-external-link-alt"></i></span>。</p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/983d596c02f675ddcd74cd7261d5f2ef.png" alt="小报童排行榜-最近更新专栏"></p><h2 id="赋能"><a href="#赋能" class="headerlink" title="赋能"></a>赋能</h2><p>前面的小报童排行榜只能帮助我自己分销所有小报童专栏,分销佣金也只能我自己拿,怎么让别人愿意帮我推广小报童排行榜呢?最简单的方法当然是让利!<br>**通过给别人定制一键分销网页,页面里所有小报童专栏全部改成别人的分销链接。**这样别人也可以通过自己的一键分销网页分销小报童专栏获取收益,我收取一定的技术维护费用,各取所需,各享其利。</p><p>实现也很简单,增加一个 Notion 数据库记录分销商信息:</p><p><img data-src="https://picgo-daily.oss-cn-guangzhou.aliyuncs.com/picgo-daily/2024/07c5dcc18be8db9e951c425a720aba23.png" alt="Notion数据库-分销商"></p><p>通过 Cloudflare Workers 读取 Notion 分销商数据库信息,然后通过 eleventy 渲染子页面并发布即可。这里有几个一键分销页面,可供参考:</p><ul><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9vc2d1aWRlci8=">https://xiaobot.osguider.com/osguider/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9IZXlIdWF6aS8=">https://xiaobot.osguider.com/HeyHuazi/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS83NjcxODQwMC8=">https://xiaobot.osguider.com/76718400/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS93aHl2aW5jZW50Lw==">https://xiaobot.osguider.com/whyvincent/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9IZWxwMDAwMDAwLw==">https://xiaobot.osguider.com/Help000000/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9meG05OTk2MC8=">https://xiaobot.osguider.com/fxm99960/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9MU0wwMjExMDIv">https://xiaobot.osguider.com/LSL021102/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS95emhtMTExLw==">https://xiaobot.osguider.com/yzhm111/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9iYW9qaWVfeG1nLw==">https://xiaobot.osguider.com/baojie_xmg/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9saXVsaXV6YWlzaC8=">https://xiaobot.osguider.com/liuliuzaish/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS95aTI1MzkwLw==">https://xiaobot.osguider.com/yi25390/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS9IWDE3NDcwMjM5NzEv">https://xiaobot.osguider.com/HX1747023971/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS81OTU2ODMwNzkv">https://xiaobot.osguider.com/595683079/<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS8xNzM4NDA3NjEwLw==">https://xiaobot.osguider.com/1738407610/<i class="fa fa-external-link-alt"></i></span></li></ul><h2 id="未来的计划"><a href="#未来的计划" class="headerlink" title="未来的计划"></a>未来的计划</h2><ul><li>表单提交自动收录:小报童专栏作者填写金数据表单,cloudflare workers 自动爬取专栏信息并收录到 Notion 数据库。</li><li>表单提交自动生成一键分销链接:想要构建一键分销系统的朋友填写金数据表单,cloudflare workers 自动构建一键分销页面。</li><li>知识星球绑定销售:做一个知识星球强化个人 IP,小报童排行榜作为赠品而不是单独销售。这样可以把认可产品的朋友带入到我的 IP 圈,逐渐影响,让他们认可我这个人,进而创造更多合作的机会。</li></ul><h2 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h2><p>当然,现在的小报童排行榜并不完美。<br>比如现在还不支持分页,比如现在只能用 <code>Ctrl + F</code> 做检索,比如不支持用户自定义要分销的专栏列表。<br>如果你也觉得有些不爽的地方,欢迎找我聊聊,万分感激。</p><h2 id="致谢"><a href="#致谢" class="headerlink" title="致谢"></a>致谢</h2><p>感谢我的队友 <span class="exturl" data-url="aHR0cHM6Ly9odWF6aS5zcGFjZS8=">@Huazi<i class="fa fa-external-link-alt"></i></span>,没有他,小报童排行榜不会如此美观,不会如此受人欢迎。</p><h2 id="关于我"><a href="#关于我" class="headerlink" title="关于我"></a>关于我</h2><p>重新认识一下。<br>你好,我是小柒,微信 <code>osguider</code>。<br>Java 程序员,热衷自动化。生财有术 RPA 航海教练(23 年 12 月)。</p><hr><p><span class="exturl" data-url="aHR0cHM6Ly94aWFvYm90Lm9zZ3VpZGVyLmNvbS8=">小报童排行榜<i class="fa fa-external-link-alt"></i></span> 作者,支持一键分销所有小报童专栏,也可以给你<strong>定制专属分销链接</strong>,欢迎体验。</p><hr><p><span class="exturl" data-url="aHR0cHM6Ly9vc2d1aWRlci5jb20v">开源服务指南<i class="fa fa-external-link-alt"></i></span>主理人,依托 Notion 搭建了完整的自动化工作流和图文生成体系,这个后面也会写复盘。</p><p>目前实现了:</p><ul><li>每天自动从 Github 爬取开源项目信息</li><li>自动调用 ChatGPT 开源项目简介</li><li>手动审核 ChatGPT 生成的简介并标记审核状态</li><li>借助 Pipedream 和 Notion 把已审核的开源项目自动汇总成 Markdown 格式的文章</li><li>借助 mdnice 把 Markdown 文章自动转为微信公众号图文</li><li>手动分发(等后面抽出时间这里可以用 Automa 做自动分发)</li></ul><p>了解开源服务指南:</p><ul><li><span class="exturl" data-url="aHR0cHM6Ly9vc2d1aWRlci5jb20v">官网<i class="fa fa-external-link-alt"></i></span></li><li>微信公众号:开源服务指南</li></ul><hr><p>做过一个不起眼的导航网站:<span class="exturl" data-url="aHR0cHM6Ly9rYW5qaWFuLmRpcWlnYW4uY24v">看见导航<i class="fa fa-external-link-alt"></i></span> 收藏了很多我见过且觉得值得推荐的网页和工具,可能会对你有所帮助。</p><hr><p>写过一写 Java 开发工具的使用经验:<span class="exturl" data-url="aHR0cHM6Ly9pZGVhLmRpcWlnYW4uY24v">Intellij IDEA 最佳实践<i class="fa fa-external-link-alt"></i></span></p><hr><p>也欢迎访问我的个人博客:<a href="https://blog.diqigan.cn/">Seven’s blog</a></p><hr>

给 eleventy(11ty) 添加 sitemap.xml 和 robots.txt

2024-06-14 10:09:55

<h2 id="配置过程"><a href="#配置过程" class="headerlink" title="配置过程"></a>配置过程</h2><ol><li>添加数据文件 <code>_data/site.json</code>,写入以下内容,定义站点信息和 sitemap 中的一些默认值:</li></ol> <figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line"> <span class="attr">&quot;baseUrl&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://xiaobot.osguider.com&quot;</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;robots&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/robots.txt&quot;</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;sitemap&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"> <span class="attr">&quot;path&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sitemap.xml&quot;</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;changefreq&quot;</span><span class="punctuation">:</span> <span class="string">&quot;daily&quot;</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">&quot;priority&quot;</span><span class="punctuation">:</span> <span class="number">0.5</span></span><br><span class="line"> <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ol><li>添加 sitemap 模板文件,写入以下内容:</li></ol> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">permalink: &quot;&#123;&#123; site.sitemap.path &#125;&#125;&quot;</span><br><span class="line">eleventyExcludeFromCollections: true</span><br><span class="line">---</span><br><span class="line">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</span><br><span class="line"></span><br><span class="line">&lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&gt;</span><br><span class="line"> &#123;% for page in collections.all %&#125;</span><br><span class="line"> &#123;% unless page.data.sitemap.ignore %&#125;</span><br><span class="line"> &lt;url&gt;</span><br><span class="line"> &lt;loc&gt;&#123;&#123; site.baseUrl &#125;&#125;&#123;&#123; page.url | url &#125;&#125;&lt;/loc&gt;</span><br><span class="line"> &lt;lastmod&gt;&#123;&#123; page.date | date: &#x27;%Y-%m-%dT%H:%M:%S.%LZ&#x27; &#125;&#125;&lt;/lastmod&gt;</span><br><span class="line"> &lt;changefreq&gt;&#123;&#123; site.sitemap.changefreq &#125;&#125;&lt;/changefreq&gt;</span><br><span class="line"> &lt;priority&gt;&#123;&#123; page.data.sitemap.priority | default: site.sitemap.priority | default: 0.5 &#125;&#125;&lt;/priority&gt;</span><br><span class="line"> &lt;/url&gt;</span><br><span class="line"> &#123;% endunless %&#125;</span><br><span class="line"> &#123;% endfor %&#125;</span><br><span class="line">&lt;/urlset&gt;</span><br></pre></td></tr></table></figure><ol><li><p>【可选】配置不同页面的 sitemap 表现:</p><ul><li>如果不希望某些页面在被包含在 sitemap 文件中,在页面元数据中添加 <code>sitemap.ignore: true</code> 即可;</li><li>可以对不同的页面设置不同的 sitemap 优先级,在页面元数据中添加 <code>sitemap.priority: 0.5</code>,取值范围 0-1;</li><li>对于分页数据,要设置 <code>pagination.addAllPagesToCollections: true</code> 才会在 <code>sitemap.xml</code> 文件中包含每一个分页页面。</li></ul></li><li><p>添加模板文件 <code>src/robots.txt</code>,写入以下内容:</p></li></ol> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">eleventyComputed:</span><br><span class="line"> permalink: &quot;&#123;&#123; site.robots &#125;&#125;&quot;</span><br><span class="line">eleventyExcludeFromCollections: true</span><br><span class="line">---</span><br><span class="line">Sitemap: &#123;&#123; site.baseUrl &#125;&#125; &#123;&#123; site.sitemap.path &#125;&#125;</span><br><span class="line"></span><br><span class="line">User-agent: *</span><br><span class="line">Disallow:</span><br></pre></td></tr></table></figure><p>重新编译,over!</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tLzExdHkvZWxldmVudHkvaXNzdWVzLzI0OA==">How to create sitemap.xml<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2NlcGgvY2VwaC5pby9pc3N1ZXMvMTM2">Sitemap xml<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9zaG9waWZ5LmdpdGh1Yi5pby9saXF1aWQv">liquid<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly93d3cuc2l0ZW1hcHMub3JnL3Byb3RvY29sLmh0bWw=">sitemap format<i class="fa fa-external-link-alt"></i></span></li></ul>

开源服务指南博客文章自动生成

2024-04-21 18:09:55

<ol><li><p><span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2ZlYXR1cmVzL2FjdGlvbnM=">GitHub Actions<i class="fa fa-external-link-alt"></i></span> 可以添加运行参数。我只需要设置 filepath 和 content 两个参数,然后配合 shell 脚本就可以自动生成和提交博文到 GitHub 仓库,进而触发自动构建和发布。</p></li><li><p>GitHub Actions 提供了 <span class="exturl" data-url="aHR0cHM6Ly9kb2NzLmdpdGh1Yi5jb20vZW4vcmVzdC9hY3Rpb25zL3dvcmtmbG93cz9hcGlWZXJzaW9uPTIwMjItMTEtMjg=">REST API<i class="fa fa-external-link-alt"></i></span> 来触发前面的工作流,这样我就可以通过 HTTP 请求来自动生成和发布博文。</p></li><li><p>开源服务指南数据库现在是建立在 Notion 上的,Notion 也提供了 REST API 的交互方式。所以我只需要定时扫描 Notion 数据库,获取状态刚变更为 “已发布” 的博文,提取文章内容,通过第 2 步中提到的 REST API 来触发第 1 步中提到的 GitHub Actions 即可自动生成和发布博文。这里我使用了 <span class="exturl" data-url="aHR0cHM6Ly93b3JrZXJzLmNsb3VkZmxhcmUuY29tLw==">Cloudflare Workers<i class="fa fa-external-link-alt"></i></span> 实现。</p></li><li><p>怎么监测 Notion 数据库文章状态变动呢?想要监测状态“变动”,我们需要知道变动前的状态和变动后的状态,进而需要有数据库缓存变动前的状态,能做,但麻烦。所幸,<span class="exturl" data-url="aHR0cHM6Ly9waXBlZHJlYW0uY29tLw==">pipedream<i class="fa fa-external-link-alt"></i></span> 帮我们做好了这个事情。它能够<span class="exturl" data-url="aHR0cHM6Ly9waXBlZHJlYW0uY29tL2FwcHMvbm90aW9uL3RyaWdnZXJzL3VwZGF0ZWQtcGFnZQ==">监测 Notion 数据库变动<i class="fa fa-external-link-alt"></i></span>,并且触发工作流执行。</p></li></ol><span id="more"></span><hr><p>所以,最后的工作流程就是:</p><ul><li>Pipedream 监测开源服务指南 Notion 文章数据库变动,提取状态为“已完成”的文章,把文章 id 通过 HTTP 请求发送给 Cloudflare Workers;</li><li>Cloudflare Workers 根据文章 id 查询文章内容,把文章路径和文章内容作为参数,发送请求给 Github Actions;</li><li>Github Actions 把文章内容写入文章路径,提交文章源文件到 Github 仓库;</li><li>Github Actions 监听代码提交,持续集成和发版;</li></ul><p>嗯,云服务挺好用。</p><p>置于为什么不直接用 Pipedream 提取参数触发 Github Actions 工作流,个人主观意愿影响比较多:Pipedream 代码编写体验略差,稳定性欠佳,所以在逐步往 Cloudflare Workers 迁移。这个回头细讲。</p><hr><p>附录:</p><ul><li><span class="exturl" data-url="aHR0cHM6Ly9kb2NzLmdpdGh1Yi5jb20vZW4vYWN0aW9ucw==">Github Actions 文档<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXJzLmNsb3VkZmxhcmUuY29tL3dvcmtlcnMv">Cloudflare Workers 文档<i class="fa fa-external-link-alt"></i></span></li><li><span class="exturl" data-url="aHR0cHM6Ly9waXBlZHJlYW0uY29tL2RvY3Mv">Pipedream 文档<i class="fa fa-external-link-alt"></i></span></li><li>第 1 步中提到的 Github Actions 代码:</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Create</span> <span class="string">Post</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line"> <span class="attr">workflow_dispatch:</span></span><br><span class="line"> <span class="attr">inputs:</span></span><br><span class="line"> <span class="attr">path:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">&#x27;File path&#x27;</span></span><br><span class="line"> <span class="attr">required:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">content:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">&#x27;File content&#x27;</span></span><br><span class="line"> <span class="attr">required:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line"> <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line"> <span class="attr">create-file:</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">repository</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">Post</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> cat &lt;&lt; EOF &gt; ./content/post/$&#123;&#123; github.event.inputs.path &#125;&#125;</span></span><br><span class="line"><span class="string"> $&#123;&#123; github.event.inputs.content &#125;&#125;</span></span><br><span class="line"><span class="string"> EOF</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Commit</span> <span class="string">and</span> <span class="string">push</span> <span class="string">changes</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> git config user.name &quot;username&quot;</span></span><br><span class="line"><span class="string"> git config user.email &quot;[email protected]&quot;</span></span><br><span class="line"><span class="string"> git add ./content/post/$&#123;&#123; github.event.inputs.path &#125;&#125;</span></span><br><span class="line"><span class="string"> git commit -m &quot;add post: $&#123;&#123; github.event.inputs.path &#125;&#125;&quot;</span></span><br><span class="line"><span class="string"> git push</span></span><br></pre></td></tr></table></figure><ul><li>第 2 步中提到的 HTTP 请求:</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> filePath = <span class="string">&#x27;daily/daily-01.md&#x27;</span>;</span><br><span class="line"><span class="keyword">const</span> fileContent = <span class="string">&#x27;Hello World&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> owner = <span class="string">&quot;osguider&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> repo = <span class="string">&quot;blog&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> workflow_file = <span class="string">&quot;create-post.yml&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> url = <span class="string">`https://api.github.com/repos/<span class="subst">$&#123;owner&#125;</span>/<span class="subst">$&#123;repo&#125;</span>/actions/workflows/<span class="subst">$&#123;workflow_file&#125;</span>/dispatches`</span>;</span><br><span class="line"><span class="keyword">const</span> headers = &#123;</span><br><span class="line"> <span class="string">&#x27;Authorization&#x27;</span>: <span class="string">`Bearer <span class="subst">$&#123;process.env.GITHUB_REPO_PAT_BLOG&#125;</span>`</span>,</span><br><span class="line"> <span class="string">&#x27;Accept&#x27;</span>: <span class="string">&#x27;application/vnd.github.v3+json&#x27;</span>,</span><br><span class="line"> <span class="string">&#x27;Content-Type&#x27;</span>: <span class="string">&#x27;application/json&#x27;</span></span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> data = &#123;</span><br><span class="line"> <span class="string">&#x27;ref&#x27;</span>: <span class="string">&#x27;main&#x27;</span>,</span><br><span class="line"> <span class="string">&#x27;inputs&#x27;</span>: &#123;</span><br><span class="line"> <span class="attr">path</span>: filePath;</span><br><span class="line"> <span class="attr">content</span>: fileContent;</span><br><span class="line"> &#125;,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">axios.<span class="title function_">post</span>(url,</span><br><span class="line"> data,</span><br><span class="line"> &#123; <span class="attr">headers</span>: headers &#125;)</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function">(<span class="params">response</span>) =&gt;</span> &#123;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;GitHub Action dispatched successfully!&#x27;</span>);</span><br><span class="line"> &#125;)</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function">(<span class="params">error</span>) =&gt;</span> &#123;</span><br><span class="line"> <span class="comment">// TODO 错误通知</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Failed to dispatch GitHub Action: <span class="subst">$&#123;error&#125;</span>`</span>);</span><br><span class="line"> &#125;);</span><br></pre></td></tr></table></figure>