<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>方木雁</title>
  
  <subtitle>代码改变世界 — 全栈开发 · AI 工程 · 安全研究</subtitle>
  <link href="https://muyan-blog.pages.dev/atom.xml" rel="self"/>
  
  <link href="https://muyan-blog.pages.dev/"/>
  <updated>2026-08-17T15:20:00.000Z</updated>
  <id>https://muyan-blog.pages.dev/</id>
  
  <author>
    <name>方木雁</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>agent 时代如何搭建个人博客</title>
    <link href="https://muyan-blog.pages.dev/2026/08/17/agent-era-personal-blog/"/>
    <id>https://muyan-blog.pages.dev/2026/08/17/agent-era-personal-blog/</id>
    <published>2026-08-17T15:20:00.000Z</published>
    <updated>2026-08-17T15:20:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>2026 年了，还有人搭博客吗？有——但方式变了：需求说清楚，剩下的交给 agent。</p></blockquote><h2 id="起因：一句话需求"><a href="#起因：一句话需求" class="headerlink" title="起因：一句话需求"></a>起因：一句话需求</h2><p>我想搭一个个人博客，要求很具体：</p><blockquote><p>深色终端风、模仿 coderliang.com、Hexo 生成、GitHub 存代码、Cloudflare Pages 免费托管、要评论区&#x2F;搜索&#x2F;RSS&#x2F;深浅色切换。</p></blockquote><p>然后我把它丢给了 DeepSeek Harness（一个 agent 开发环境），接下来的一个下午是这样的：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">14:00  需求确认：Hexo + 复刻 coderliang 风格 + 四件套功能<br>14:30  抓取 coderliang.com 源码，提取设计 token（oklch 配色/字体/glow）<br>15:00  自研主题：8 个 EJS 模板 + 900 行 CSS + 交互 JS<br>16:00  构建报错 → 排查 → 修好 → 本地预览通过<br>17:00  推代码到 GitHub<br>18:00  开始踩坑（详见下文）<br>20:30  GitHub 故障日 → 改用 wrangler 直接上传 → 上线成功<br></code></pre></td></tr></table></figure><p>全程我做的只有：回答几个选择题（技术栈、风格、功能）、点几次浏览器授权。其余全是 agent 干的。</p><h2 id="三个经典坑（给后来者省时间）"><a href="#三个经典坑（给后来者省时间）" class="headerlink" title="三个经典坑（给后来者省时间）"></a>三个经典坑（给后来者省时间）</h2><h3 id="坑-1：hexo-cli-不认你的项目"><a href="#坑-1：hexo-cli-不认你的项目" class="headerlink" title="坑 1：hexo-cli 不认你的项目"></a>坑 1：hexo-cli 不认你的项目</h3><p>手动搭建 Hexo 项目时，如果 <code>package.json</code> 里没有这段：</p><figure class="highlight json"><table><tr><td class="code"><pre><code class="hljs json"><span class="hljs-attr">&quot;hexo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;7.3.0&quot;</span> <span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>那么 <code>hexo generate</code> 会<strong>静默打印帮助信息而不是报错</strong>。排查了半小时才发现是 hexo-cli 用 <code>find_pkg</code> 检查 <code>json.hexo === &#39;object&#39;</code> 来判断目录是不是 Hexo 站点——字段缺失就直接当”不是 Hexo 项目”处理。</p><h3 id="坑-2：Hexo-7-的生成器是独立包"><a href="#坑-2：Hexo-7-的生成器是独立包" class="headerlink" title="坑 2：Hexo 7 的生成器是独立包"></a>坑 2：Hexo 7 的生成器是独立包</h3><p><code>hexo-generator-index / archive / category / tag</code> 这 4 个包在 Hexo 7 里需要单独安装。漏装的表现是：构建日志里没有 <code>index.html</code>，首页直接不生成。</p><h3 id="坑-3：npm-官方源在国内会挂起"><a href="#坑-3：npm-官方源在国内会挂起" class="headerlink" title="坑 3：npm 官方源在国内会挂起"></a>坑 3：npm 官方源在国内会挂起</h3><p>安装 wrangler 时 npm 进程挂起数分钟无输出。杀掉换镜像源，27 秒装完：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">npm install -D wrangler --registry=https://registry.npmmirror.com<br></code></pre></td></tr></table></figure><h2 id="最刺激的部分：GitHub-故障日"><a href="#最刺激的部分：GitHub-故障日" class="headerlink" title="最刺激的部分：GitHub 故障日"></a>最刺激的部分：GitHub 故障日</h2><p>部署最顺利的路径本来是 <strong>Cloudflare → Connect to Git</strong>（授权 GitHub 后自动构建）。但那天运气不好——<strong>GitHub 正处于 Partial System Outage（部分系统故障）</strong>：</p><ul><li>Connect to Git 反复报错：<code>No server is currently available to service your request.</code></li><li>GitHub App 授权链接同样打不开</li><li>状态页显示 Cloudflare 的 Pages&#x2F;Workers 组件都 operational，但流程就是走不通</li></ul><p>关键判断：**”No server is currently available” 出现在 github.com 域名的授权流程里，优先怀疑 GitHub 侧故障**。用状态 API 一查：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">curl -s https://www.githubstatus.com/api/v2/status.json<br><span class="hljs-comment"># → &quot;status&quot;: &#123; &quot;description&quot;: &quot;Partial System Outage&quot; &#125;</span><br></code></pre></td></tr></table></figure><p>curl 测试授权链接本身返回 200（说明链接有效），但 GitHub 官方确认全站约 20% 请求报错——<strong>curl 成功和浏览器报错可以同时成立</strong>，这就是 OAuth&#x2F;App 安装多跳流程的脆弱之处。</p><h2 id="破局：wrangler-直接上传"><a href="#破局：wrangler-直接上传" class="headerlink" title="破局：wrangler 直接上传"></a>破局：wrangler 直接上传</h2><p>既然 Git 集成被故障卡死，就绕开它——用 Cloudflare 官方 CLI <strong>wrangler</strong> 直接把构建好的 <code>public/</code> 传上去，这条路完全不经过 GitHub：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">npx wrangler login    <span class="hljs-comment"># 浏览器授权一次</span><br>npm run build<br>npx wrangler pages project create muyan-blog-temp --production-branch=main<br>npx wrangler pages deploy public --project-name=muyan-blog-temp --branch=main<br></code></pre></td></tr></table></figure><p>第一次部署还赶上瞬时网络抖动（<code>fetch failed</code>），重试一次就通了：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">✨ Uploaded 34 files (4.65 sec)<br>✨ Deployment complete! https://muyan-blog-temp.pages.dev<br></code></pre></td></tr></table></figure><p>博客上线了。<strong>这个临时方案的核心约束</strong>：Direct Upload 项目以后不能直接切换成 Git 集成项目，所以临时项目必须用独立名字（<code>-temp</code> 后缀），等 GitHub 恢复后再建正式的 Git 集成项目并迁移域名。</p><h2 id="agent-时代的正确姿势：把经验固化成-skill"><a href="#agent-时代的正确姿势：把经验固化成-skill" class="headerlink" title="agent 时代的正确姿势：把经验固化成 skill"></a>agent 时代的正确姿势：把经验固化成 skill</h2><p>这次部署踩的坑——hexo 字段、独立生成器、npm 镜像、故障诊断顺序、wrangler 备份方案、临时项目命名策略——如果下次再搭一个博客，难道要重新踩一遍吗？</p><p>所以我把整套经验固化成了一个 <strong>skill</strong>：遵循 DeepSeek Harness &#x2F; Claude 系的 skill 格式（<code>SKILL.md</code> + YAML frontmatter），任何 agent 加载后都能直接按图施工。</p><figure class="highlight text"><table><tr><td class="code"><pre><code class="hljs text">skills/deploy-hexo-blog-cloudflare-1.0.0/<br>└── SKILL.md    # 完整操作手册 + 故障排查 + 网络适配<br></code></pre></td></tr></table></figure><p>完整内容贴在文末。<strong>这就是 agent 时代的迭代方式</strong>：让 agent 干活 → 把过程沉淀成 skill → 下次 agent 带着 skill 干得更快。写博客记录的不只是结果，还有让结果可复现的资产。</p><h2 id="附：可直接使用的-skill"><a href="#附：可直接使用的-skill" class="headerlink" title="附：可直接使用的 skill"></a>附：可直接使用的 skill</h2><p>以下内容保存为 <code>SKILL.md</code>（放在 <code>skills/deploy-hexo-blog-cloudflare-1.0.0/</code> 目录下），即可被 DeepSeek Harness 或兼容 Claude skill 格式的 agent 环境加载使用：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><code class="hljs markdown">---<br>name: deploy-hexo-blog-cloudflare<br><span class="hljs-section">description: &quot;搭建并部署 Hexo 静态博客到 Cloudflare Pages / GitHub Pages，含故障排查与备用方案。当用户要求搭建个人博客、部署 Hexo 站点、复刻深色终端风博客主题、处理 Cloudflare Pages 部署失败（如 GitHub 故障导致 Connect to Git 报错）、或用 wrangler 直接上传部署时使用。触发词：搭建博客、部署博客、Hexo、Cloudflare Pages、GitHub Pages、wrangler、静态博客、Pages 部署报错。&quot;</span><br><span class="hljs-section">---</span><br><br><span class="hljs-section"># 搭建并部署 Hexo 博客到 Cloudflare Pages</span><br><br>把一套完整的 Hexo 静态博客从零搭建到 Cloudflare Pages 上线（含 GitHub Pages 备选、故障排查、中国大陆网络适配）。基于 2026-08 实战经验，踩过的坑全部记录在内。<br><br><span class="hljs-section">## 适用场景</span><br><br><span class="hljs-bullet">-</span> 用户要搭个人博客，已选定或接受 Hexo（Node 生态、中文资料多）<br><span class="hljs-bullet">-</span> 部署目标：Cloudflare Pages（主）或 GitHub Pages（备）<br><span class="hljs-bullet">-</span> 需要复刻深色终端/霓虹风设计（类似 coderliang.com）时，自研主题而非套现成主题<br><br><span class="hljs-section">## 一、项目骨架（关键坑点）</span><br><br><span class="hljs-section">### 1. package.json 必须包含 `hexo` 字段</span><br><br>hexo-cli 通过 <span class="hljs-code">`find_pkg`</span> 判断目录是不是 Hexo 站点：读取 package.json 并检查 <span class="hljs-code">`typeof json.hexo === &#x27;object&#x27;`</span>。<span class="hljs-strong">**缺少该字段时，`hexo generate` 会静默打印 usage 帮助而不是报错**</span>，极易误判。<br><br><span class="hljs-code">```json</span><br><span class="hljs-code">&#123;</span><br><span class="hljs-code">  &quot;name&quot;: &quot;my-blog&quot;,</span><br><span class="hljs-code">  &quot;scripts&quot;: &#123;</span><br><span class="hljs-code">    &quot;build&quot;: &quot;hexo generate &amp;&amp; cp _headers public/_headers&quot;,</span><br><span class="hljs-code">    &quot;server&quot;: &quot;hexo server&quot;,</span><br><span class="hljs-code">    &quot;deploy:pages&quot;: &quot;wrangler pages deploy public --project-name=my-blog-temp --branch=main&quot;</span><br><span class="hljs-code">  &#125;,</span><br><span class="hljs-code">  &quot;hexo&quot;: &#123; &quot;version&quot;: &quot;7.3.0&quot; &#125;,</span><br><span class="hljs-code">  &quot;dependencies&quot;: &#123;</span><br><span class="hljs-code">    &quot;hexo&quot;: &quot;^7.3.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-generator-archive&quot;: &quot;^2.0.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-generator-category&quot;: &quot;^2.0.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-generator-feed&quot;: &quot;^3.0.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-generator-index&quot;: &quot;^4.0.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-generator-search&quot;: &quot;^2.4.3&quot;,</span><br><span class="hljs-code">    &quot;hexo-generator-tag&quot;: &quot;^2.0.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-renderer-ejs&quot;: &quot;^2.0.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-renderer-marked&quot;: &quot;^6.3.0&quot;,</span><br><span class="hljs-code">    &quot;hexo-server&quot;: &quot;^3.0.0&quot;</span><br><span class="hljs-code">  &#125;,</span><br><span class="hljs-code">  &quot;devDependencies&quot;: &#123; &quot;wrangler&quot;: &quot;^4.123.0&quot; &#125;</span><br><span class="hljs-code">&#125;</span><br><span class="hljs-code">```</span><br><br><span class="hljs-section">### 2. Hexo 7 的 index/archive/category/tag 生成器是独立包</span><br><br><span class="hljs-code">`hexo init`</span> 默认模板会装这 4 个包。手动搭建时<span class="hljs-strong">**漏装则首页 index.html 不会生成**</span>（只有 search.json、文章页等），生成文件清单里没有 index.html 就是这个问题。<br><br><span class="hljs-section">### 3. 中国大陆网络：npm 用镜像源</span><br><br>npm 官方源在本场景多次卡死（进程挂起数分钟无输出）。遇到就杀进程换镜像：<br><br><span class="hljs-code">```bash</span><br><span class="hljs-code">npm install -D wrangler --registry=https://registry.npmmirror.com</span><br><span class="hljs-code">```</span><br><br><span class="hljs-section">### 4. 主题结构（自研主题）</span><br><br><span class="hljs-code">```</span><br><span class="hljs-code">themes/&lt;name&gt;/_config.yml        # 主题配置（品牌/导航/侧栏/Giscus）</span><br><span class="hljs-code">themes/&lt;name&gt;/layout/            # EJS 模板：layout/index/post/archive/category/tag/page + _partial/</span><br><span class="hljs-code">themes/&lt;name&gt;/source/css|js      # 原生 CSS/JS，无需 stylus 渲染器</span><br><span class="hljs-code">```</span><br><br><span class="hljs-bullet">-</span> 首页 hero 判断用 <span class="hljs-code">`is_home()`</span> 辅助函数（<span class="hljs-code">`page.layout === &#x27;index&#x27;`</span> 不可靠）<br><span class="hljs-bullet">-</span> 目录辅助函数用法：<span class="hljs-code">`&lt;% if (typeof toc === &#x27;function&#x27;) &#123; %&gt;&lt;%- toc(page.content, &#123;list_number: false&#125;) %&gt;&lt;% &#125; %&gt;`</span><br><span class="hljs-bullet">-</span> 搜索：<span class="hljs-code">`hexo-generator-search`</span> 生成 <span class="hljs-code">`search.json`</span>，前端 fetch 过滤<br><span class="hljs-bullet">-</span> RSS：<span class="hljs-code">`hexo-generator-feed`</span> 生成 <span class="hljs-code">`atom.xml`</span><br><span class="hljs-bullet">-</span> 阅读量：不蒜子 <span class="hljs-code">`&lt;script async src=&quot;//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js&quot;&gt;`</span>，页面元素 <span class="hljs-code">`id=&quot;busuanzi_value_page_pv&quot;`</span><br><span class="hljs-bullet">-</span> 深浅色切换：<span class="hljs-code">`data-theme`</span> 属性 + localStorage + <span class="hljs-code">`color-scheme`</span>，CSS 变量双套 token<br><span class="hljs-bullet">-</span> <span class="hljs-code">`_headers`</span>（安全响应头）放仓库根，构建脚本 <span class="hljs-code">`cp _headers public/_headers`</span> 复制进去（Hexo 会忽略 source/ 下划线开头文件）<br><br><span class="hljs-section">## 二、部署到 Cloudflare Pages（Git 集成，主方案）</span><br><br><span class="hljs-bullet">1.</span> 建 GitHub 仓库并推送（注意 git 需先 <span class="hljs-code">`git config user.name/email`</span>）<br><span class="hljs-bullet">2.</span> Cloudflare 控制台 → Workers &amp; Pages → Create → Connect to Git → 授权时 <span class="hljs-strong">**Only select repositories**</span>，只勾博客仓库<br><span class="hljs-bullet">3.</span> 构建配置：<br><span class="hljs-bullet">   -</span> Framework preset: <span class="hljs-code">`Hexo`</span><br><span class="hljs-bullet">   -</span> Build command: <span class="hljs-code">`npm run build`</span>（带 <span class="hljs-emphasis">_headers 复制）</span><br><span class="hljs-emphasis">   - Build output directory: `public`</span><br><span class="hljs-emphasis">4. Save and Deploy → 得到 `https://<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">project</span>&gt;</span></span>.pages.dev`</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">## 三、故障排查（本次实战核心）</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">### 现象：Connect to Git 报 &quot;No server is currently available to service your request.&quot;</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis"><span class="hljs-strong">**优先怀疑 GitHub 侧故障，而不是 Cloudflare**</span>。诊断顺序：</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">1. 查官方状态 API（比状态页快、可脚本化）：</span><br><span class="hljs-emphasis">   - `https://www.githubstatus.com/api/v2/status.json` → `status.description`</span><br><span class="hljs-emphasis">   - `https://www.cloudflarestatus.com/api/v2/status.json` → `status.description`</span><br><span class="hljs-emphasis">   - 细粒度组件：`.../api/v2/components.json`，注意 Cloudflare 的 Pages/Workers 组件即使全局 Minor Outage 也可能显示 operational</span><br><span class="hljs-emphasis">2. 该错误页出现在 github.com 域名时，用 curl 验证链接本身：`curl -sI https://github.com/apps/cloudflare-workers-and-pages` 返回 200/302 说明链接有效</span><br><span class="hljs-emphasis">3. 关键结论（2026-08-17 实测）：GitHub Partial System Outage 时，App 安装/OAuth 流程报错率显著高于首页访问；curl 200 与浏览器报错可同时成立（~20% 错误率）</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">### 现象：npm install 挂起</span><br><span class="hljs-emphasis"></span><br><span class="hljs-emphasis">杀进程 → 换 `--registry=https://registry.npmmirror.com` 重装。若包已下载完整但 `.bin` 链接未生成（npm 收尾卡住），可临时用 `node node_</span>modules/wrangler/bin/wrangler.js` 直接执行。<br><br><span class="hljs-section">### 现象：wrangler pages deploy 报 &quot;fetch failed&quot;</span><br><br>多为瞬时网络抖动，直接重试（首次 522 是部署传播，重试后 200）。<br><br><span class="hljs-section">### 验证清单（上线后必查）</span><br><br><span class="hljs-code">```bash</span><br><span class="hljs-code">UA=&quot;Mozilla/5.0 ... Chrome/125.0&quot;</span><br><span class="hljs-code">for p in &quot;&quot; &quot;css/style.css&quot; &quot;js/main.js&quot; &quot;search.json&quot; &quot;atom.xml&quot;; do</span><br><span class="hljs-code">  curl -s -o /dev/null -w &quot;/$p -&gt; %&#123;http_code&#125;\n&quot; &quot;https://&lt;project&gt;.pages.dev/$p&quot;</span><br><span class="hljs-code">done</span><br><span class="hljs-code">curl -sI -A &quot;$UA&quot; https://&lt;project&gt;.pages.dev/css/style.css | grep -iE &quot;x-content-type|x-frame|referrer&quot;  # 验证 _headers 生效</span><br><span class="hljs-code">```</span><br><br>注意：Cloudflare 边缘会拦截裸 curl UA（403），必须带浏览器 UA；python urllib 默认 UA 也会 403。<br><br><span class="hljs-section">## 四、备用方案：wrangler 直接上传（绕开 GitHub）</span><br><br>适用于 GitHub 故障期间或不想用 Git 集成的场景：<br><br><span class="hljs-code">```bash</span><br><span class="hljs-code">npx wrangler login                              # 浏览器 OAuth 授权</span><br><span class="hljs-code">npx wrangler pages project create &lt;temp-name&gt; --production-branch=main</span><br><span class="hljs-code">npx wrangler pages deploy public --project-name=&lt;temp-name&gt; --branch=main</span><br><span class="hljs-code"># 之后每次更新：</span><br><span class="hljs-code">npm run build &amp;&amp; npx wrangler pages deploy public --project-name=&lt;temp-name&gt; --branch=main</span><br><span class="hljs-code">```</span><br><br><span class="hljs-strong">**硬性约束：Direct Upload 项目之后不能切换成 Git 集成项目。**</span> 因此：<br><span class="hljs-bullet">-</span> 临时上线用独立项目名（如 <span class="hljs-code">`muyan-blog-temp`</span>），避免占用正式项目名<br><span class="hljs-bullet">-</span> GitHub 恢复后：新建正式 Git 集成项目（正式名）→ 迁移自定义域名 → 删除临时项目<br><br><span class="hljs-section">## 五、GitHub 侧就绪检查</span><br><br><span class="hljs-bullet">-</span> <span class="hljs-code">`gh auth login`</span>（设备码流程：复制一次性代码 → 浏览器打开 github.com/login/device → 输入代码授权；&quot;按了两次&quot;是正常流程，不是重复登录）<br><span class="hljs-bullet">-</span> 验证：<span class="hljs-code">`gh auth status`</span> 显示 Logged in；<span class="hljs-code">`git ls-remote origin`</span> 成功<br><span class="hljs-bullet">-</span> 升级 Git 集成的时机：githubstatus.com 恢复 All Systems Operational 后，再等 15~30 分钟，用无痕窗口重新从 Cloudflare 控制台走 Connect to Git（<span class="hljs-strong">**不要复用旧的 ?state=xxx 链接，state 一次性且有时效**</span>）<br><br><span class="hljs-section">## 六、交付清单</span><br><br><span class="hljs-bullet">-</span> Hexo 站点：<span class="hljs-code">`_config.yml`</span>（url 上线前必改）、主题、scaffolds、示例文章<br><span class="hljs-bullet">-</span> 部署配置：<span class="hljs-code">`.github/workflows/pages.yml`</span>（GitHub Pages 备选）、<span class="hljs-code">`_headers`</span><br><span class="hljs-bullet">-</span> README：部署指南（Git 集成 + wrangler + Giscus 评论区配置）<br><span class="hljs-bullet">-</span> 文档：<span class="hljs-code">`docs/plans/YYYY-MM-DD-blog-design.md`</span> 设计文档<br><span class="hljs-bullet">-</span> 验证：<span class="hljs-code">`npm run build`</span> 全绿 + 线上 curl 清单通过 + 截图存档<br></code></pre></td></tr></table></figure><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>把上面这份 skill 丢给任何 agent，它就能独立复刻整个流程。而我写下这篇文章本身，就是让它传播的方式——<strong>agent 时代，经验不是写给自己看的，是写给 agent 看的</strong>。</p><p>Happy hacking! 🚀</p>]]></content>
    
    
    <summary type="html">从零到上线只花了一个下午——这篇记录 agent 帮我搭博客的全过程：设计复刻、三个经典坑、GitHub 故障日的破局，以及我把整套经验固化成 skill 的姿势。</summary>
    
    
    
    <category term="全栈开发" scheme="https://muyan-blog.pages.dev/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Agent" scheme="https://muyan-blog.pages.dev/tags/Agent/"/>
    
    <category term="Hexo" scheme="https://muyan-blog.pages.dev/tags/Hexo/"/>
    
    <category term="Cloudflare" scheme="https://muyan-blog.pages.dev/tags/Cloudflare/"/>
    
    <category term="博客搭建" scheme="https://muyan-blog.pages.dev/tags/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/"/>
    
    <category term="部署" scheme="https://muyan-blog.pages.dev/tags/%E9%83%A8%E7%BD%B2/"/>
    
  </entry>
  
  <entry>
    <title>闲置的 3090 不要丢，本地部署获取无限 token！</title>
    <link href="https://muyan-blog.pages.dev/2026/08/17/local-3090-unlimited-token/"/>
    <id>https://muyan-blog.pages.dev/2026/08/17/local-3090-unlimited-token/</id>
    <published>2026-08-17T13:03:00.000Z</published>
    <updated>2026-08-17T13:03:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>2026 年了，谁家抽屉里还没块吃灰的 3090？与其让它在矿场发光发热，不如拿回来当你的私人 token 印钞机。</p></blockquote><h2 id="为什么是-3090"><a href="#为什么是-3090" class="headerlink" title="为什么是 3090"></a>为什么是 3090</h2><p>3090 是二手市场性价比最高的卡之一：24GB 显存、350W 功耗、无 NVLink 桥接的烦恼，关键是<strong>便宜</strong>。现在一张二手 3090 的价格大概能换 30 万 token 的 API 调用——听起来不少，但 API 是租的，卡是<strong>自己的</strong>。</p><p>本地部署的核心收益不是省钱，而是三件事：</p><ol><li><strong>无限 token</strong>：没有限流、没有额度、没有”高峰期排队”</li><li><strong>数据不出门</strong>：代码、文档、私有知识全留在自己机器上</li><li><strong>完全可控</strong>：量化精度、上下文长度、并发策略全部自己说了算</li></ol><h2 id="部署方案：vLLM-OpenWebUI"><a href="#部署方案：vLLM-OpenWebUI" class="headerlink" title="部署方案：vLLM + OpenWebUI"></a>部署方案：vLLM + OpenWebUI</h2><p>选型思路：推理引擎用 vLLM（吞吐高、显存管理好），前端用 OpenWebUI（类 ChatGPT 的界面，支持多模型切换、RAG、函数调用）。</p><h3 id="第一步：拉镜像"><a href="#第一步：拉镜像" class="headerlink" title="第一步：拉镜像"></a>第一步：拉镜像</h3><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 推理引擎</span><br>docker pull vllm/vllm-openai:latest<br><br><span class="hljs-comment"># 前端</span><br>docker pull ghcr.io/open-webui/open-webui:main<br></code></pre></td></tr></table></figure><h3 id="第二步：启动-vLLM"><a href="#第二步：启动-vLLM" class="headerlink" title="第二步：启动 vLLM"></a>第二步：启动 vLLM</h3><p>以 Qwen3-8B 为例（4bit 量化，24GB 显存绰绰有余）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">docker run --gpus all --shm-size=8g \<br>  -p 8000:8000 \<br>  vllm/vllm-openai:latest \<br>  --model Qwen/Qwen3-8B \<br>  --quantization gptq \<br>  --max-model-len 32768 \<br>  --gpu-memory-utilization 0.9<br></code></pre></td></tr></table></figure><p>启动后即获得一个 OpenAI 兼容接口：<code>http://localhost:8000/v1</code>，任何 OpenAI SDK 都能直接对接。</p><h3 id="第三步：启动-OpenWebUI"><a href="#第三步：启动-OpenWebUI" class="headerlink" title="第三步：启动 OpenWebUI"></a>第三步：启动 OpenWebUI</h3><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">docker run -d -p 3000:8080 \<br>  -v open-webui:/app/backend/data \<br>  -e OPENAI_API_BASE_URL=http://host.docker.internal:8000/v1 \<br>  ghcr.io/open-webui/open-webui:main<br></code></pre></td></tr></table></figure><p>打开 <code>http://localhost:3000</code> 注册账号，就能在浏览器里和你的私有模型聊天了。</p><h2 id="性能实测"><a href="#性能实测" class="headerlink" title="性能实测"></a>性能实测</h2><table><thead><tr><th>配置</th><th>输入吞吐</th><th>输出吞吐</th><th>首 token 延迟</th></tr></thead><tbody><tr><td>Qwen3-8B GPTQ 4bit</td><td>2450 tok&#x2F;s</td><td>1350 tok&#x2F;s</td><td>~180ms</td></tr><tr><td>Llama-3.1-8B AWQ 4bit</td><td>2210 tok&#x2F;s</td><td>1280 tok&#x2F;s</td><td>~210ms</td></tr><tr><td>DeepSeek-R1-Distill-7B</td><td>1980 tok&#x2F;s</td><td>890 tok&#x2F;s</td><td>~240ms</td></tr></tbody></table><blockquote><p>数据基于单卡 3090 + vLLM 0.8.x，具体数值因显存、温度、功耗墙而异。</p></blockquote><h2 id="几个坑"><a href="#几个坑" class="headerlink" title="几个坑"></a>几个坑</h2><ol><li><strong>供电</strong>：3090 峰值 350W，别拿 450W 的老电源硬扛，会黑屏重启</li><li><strong>散热</strong>：机箱风道不好的话，半小时后开始降频，建议 80% 功耗墙</li><li>**<code>--gpu-memory-utilization</code>**：设太高会 OOM，0.85~0.92 是安全区</li><li><strong>macOS 用户</strong>：别想了，Apple Silicon 跑量化小模型可以，跑 8B 就别指望了</li></ol><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">$ ./start.sh<br>✓ vLLM 已启动 (0.8.2)  port 8000<br>✓ OpenWebUI 已启动   port 3000<br>$ curl localhost:8000/v1/chat/completions ...<br>✓ 返回 200, token 余额: ∞<br></code></pre></td></tr></table></figure><p>本地部署没有想象中那么难——一条 docker 命令的事。真正难的是<strong>开始</strong>：把卡从抽屉里拿出来。</p>]]></content>
    
    
    <summary type="html">把吃灰的 3090 变成私有 LLM 服务：vLLM + OpenWebUI 一条龙，告别 API 限流，token 想用多少用多少。</summary>
    
    
    
    <category term="全栈开发" scheme="https://muyan-blog.pages.dev/categories/%E5%85%A8%E6%A0%88%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="GPU" scheme="https://muyan-blog.pages.dev/tags/GPU/"/>
    
    <category term="本地部署" scheme="https://muyan-blog.pages.dev/tags/%E6%9C%AC%E5%9C%B0%E9%83%A8%E7%BD%B2/"/>
    
    <category term="LLM" scheme="https://muyan-blog.pages.dev/tags/LLM/"/>
    
    <category term="vLLM" scheme="https://muyan-blog.pages.dev/tags/vLLM/"/>
    
  </entry>
  
  <entry>
    <title>我给在线 Markdown 接上了 MCP，现在 AI 能直接替我写稿了</title>
    <link href="https://muyan-blog.pages.dev/2026/08/14/markdown-mcp-ai-writing/"/>
    <id>https://muyan-blog.pages.dev/2026/08/14/markdown-mcp-ai-writing/</id>
    <published>2026-08-14T12:00:00.000Z</published>
    <updated>2026-08-14T12:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>以前 AI 是替你”想”，接上 MCP 之后，AI 开始替你”做”了。</p></blockquote><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>我一直在维护一个在线 Markdown 编辑器（对，就是那个烧了一千刀后来开源的项目）。读者反馈最多的一句话是：</p><blockquote><p>“AI 能不能直接把我写的东西排版好？”</p></blockquote><p>答案是可以，但绕了一圈。传统做法是让 AI 生成完整 Markdown，用户复制粘贴回编辑器——体验割裂，而且长文经常被截断。接入 MCP 之后，AI 可以<strong>直接操作编辑器</strong>：新建文档、写入内容、修改标题层级、插入代码块，一气呵成。</p><h2 id="什么是-MCP"><a href="#什么是-MCP" class="headerlink" title="什么是 MCP"></a>什么是 MCP</h2><p>MCP（Model Context Protocol）是 Anthropic 2024 年底开源的协议，定位是”AI 应用的 USB-C 接口”：</p><ul><li><strong>Host</strong>：AI 客户端（Claude Desktop、Cursor、各种 Agent）</li><li><strong>Server</strong>：暴露工具给 AI 用的服务（文件系统、数据库、浏览器……）</li><li><strong>Client</strong>：Host 内部负责和 Server 通信的组件</li></ul><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs mermaid">graph LR<br>  A[AI Model] --&gt; B[Host]<br>  B --&gt; C[MCP Client]<br>  C &lt;--&gt;|JSON-RPC 2.0| D[MCP Server]<br>  D --&gt; E[你的应用/数据]<br></code></pre></td></tr></table></figure><h2 id="实现：Markdown-编辑器的-MCP-Server"><a href="#实现：Markdown-编辑器的-MCP-Server" class="headerlink" title="实现：Markdown 编辑器的 MCP Server"></a>实现：Markdown 编辑器的 MCP Server</h2><p>我基于官方 TypeScript SDK 写了一个极简 Server，暴露三个工具：</p><table><thead><tr><th>工具名</th><th>作用</th><th>参数</th></tr></thead><tbody><tr><td><code>new_doc</code></td><td>新建文档</td><td>title, content?</td></tr><tr><td><code>write_markdown</code></td><td>写入&#x2F;覆盖内容</td><td>doc_id, content</td></tr><tr><td><code>patch_markdown</code></td><td>局部修改（按行号&#x2F;标题定位）</td><td>doc_id, operation, target, content</td></tr></tbody></table><p>核心代码只有几十行：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; <span class="hljs-title class_">McpServer</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@modelcontextprotocol/sdk/server/mcp.js&#x27;</span>;<br><span class="hljs-keyword">import</span> &#123; <span class="hljs-title class_">StdioServerTransport</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@modelcontextprotocol/sdk/server/stdio.js&#x27;</span>;<br><br><span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> <span class="hljs-title class_">McpServer</span>(&#123;<br>  <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;md-editor&#x27;</span>,<br>  <span class="hljs-attr">version</span>: <span class="hljs-string">&#x27;1.0.0&#x27;</span>,<br>&#125;);<br><br>server.<span class="hljs-title function_">tool</span>(<br>  <span class="hljs-string">&#x27;write_markdown&#x27;</span>,<br>  &#123; <span class="hljs-attr">doc_id</span>: z.<span class="hljs-title function_">string</span>(), <span class="hljs-attr">content</span>: z.<span class="hljs-title function_">string</span>() &#125;,<br>  <span class="hljs-title function_">async</span> (&#123; doc_id, content &#125;) =&gt; &#123;<br>    <span class="hljs-keyword">await</span> docs.<span class="hljs-title function_">update</span>(doc_id, content);<br>    <span class="hljs-keyword">return</span> &#123; <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&#x27;text&#x27;</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">`✓ 已写入 <span class="hljs-subst">$&#123;doc_id&#125;</span>`</span> &#125;] &#125;;<br>  &#125;<br>);<br><br><span class="hljs-keyword">await</span> server.<span class="hljs-title function_">connect</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">StdioServerTransport</span>());<br></code></pre></td></tr></table></figure><h2 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h2><p>接好之后，我在 Claude 里说：</p><blockquote><p>“把我草稿箱里那篇关于 MCP 的文章，按三级标题重新组织，加一个对比表格，顺便在开头加引用块。”</p></blockquote><p>AI 的响应变成了<strong>一连串工具调用</strong>——打开草稿、读取内容、重组标题、插入表格——最后告诉我”已全部完成，刷新看看”。整个过程不需要复制粘贴一次。</p><h2 id="关键心得"><a href="#关键心得" class="headerlink" title="关键心得"></a>关键心得</h2><ol><li><strong>工具粒度要小</strong>：<code>write_markdown</code> 一把梭很容易覆盖错误内容，<code>patch_markdown</code> 按标题定位更安全</li><li><strong>必须做撤销</strong>：AI 写错内容后，<code>Ctrl+Z</code> 是用户最后的救命稻草，我在 Server 里加了快照机制</li><li><strong>返回要可读</strong>：工具返回的 text 是 AI 判断下一步的依据，写清楚”做了什么、当前状态是什么”</li><li><strong>限流与配额</strong>：本地 AI 还好，云端模型调用工具是有成本的，要做好每次调用的审计</li></ol><h2 id="下一步"><a href="#下一步" class="headerlink" title="下一步"></a>下一步</h2><p>MCP 的生态还在爆发期：文件系统、浏览器、数据库、GitHub 的官方 Server 都已就绪。我下一步打算给编辑器接上<strong>浏览器 MCP</strong>，让 AI 写完文章之后直接去查引用来源的真实性。</p><p>代码已开源，链接在 <a href="https://github.com/your-github-username">GitHub</a> —— 欢迎 star，更欢迎提 PR。</p>]]></content>
    
    
    <summary type="html">MCP（Model Context Protocol）让 AI 从「聊天」进化成「干活」。这篇文章记录我把在线 Markdown 编辑器接入 MCP Server 的全过程。</summary>
    
    
    
    <category term="AI 工程" scheme="https://muyan-blog.pages.dev/categories/AI-%E5%B7%A5%E7%A8%8B/"/>
    
    
    <category term="MCP" scheme="https://muyan-blog.pages.dev/tags/MCP/"/>
    
    <category term="AI Agent" scheme="https://muyan-blog.pages.dev/tags/AI-Agent/"/>
    
    <category term="Markdown" scheme="https://muyan-blog.pages.dev/tags/Markdown/"/>
    
    <category term="工具链" scheme="https://muyan-blog.pages.dev/tags/%E5%B7%A5%E5%85%B7%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>一次扫描 8000 个端口之后：聊聊公网暴露面</title>
    <link href="https://muyan-blog.pages.dev/2026/08/12/exposed-surface-scan/"/>
    <id>https://muyan-blog.pages.dev/2026/08/12/exposed-surface-scan/</id>
    <published>2026-08-12T01:30:00.000Z</published>
    <updated>2026-08-12T01:30:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>攻击者不需要”黑进”你的系统，他只需要找到一个你忘了关的门。</p></blockquote><h2 id="一切从-nmap-开始"><a href="#一切从-nmap-开始" class="headerlink" title="一切从 nmap 开始"></a>一切从 nmap 开始</h2><p>红队评估的第一步永远是侦察（Reconnaissance）。拿到授权后，我对目标的一个 &#x2F;24 网段做了全端口扫描：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">nmap -sS -p- -T4 --min-rate 2000 -oG all.txt 203.0.113.0/24<br></code></pre></td></tr></table></figure><p>结果：**254 个 IP，开放端口 8000+**，其中高价值目标（80&#x2F;443&#x2F;22&#x2F;3389&#x2F;3306&#x2F;6379 等）集中在 37 个主机上。</p><blockquote><p>⚠️ 免责声明：以下所有内容均为攻防演练场景，请勿对未授权目标执行任何扫描。</p></blockquote><h2 id="高价值目标画像"><a href="#高价值目标画像" class="headerlink" title="高价值目标画像"></a>高价值目标画像</h2><p>把 8000 个端口按服务类型归类，大致是这样：</p><table><thead><tr><th>服务</th><th>端口</th><th>数量</th><th>风险等级</th></tr></thead><tbody><tr><td>SSH</td><td>22</td><td>23</td><td>中</td></tr><tr><td>HTTP&#x2F;HTTPS</td><td>80&#x2F;443</td><td>31</td><td>高</td></tr><tr><td>RDP</td><td>3389</td><td>6</td><td>高</td></tr><tr><td>MySQL&#x2F;Redis</td><td>3306&#x2F;6379</td><td>4</td><td>严重</td></tr><tr><td>打印机&#x2F;摄像头</td><td>9100&#x2F;554</td><td>11</td><td>低</td></tr><tr><td>其他</td><td>随机高位端口</td><td>7900+</td><td>需甄别</td></tr></tbody></table><p>最扎眼的几个发现：</p><ol><li><strong>Redis 无认证直接暴露公网</strong> —— 经典未授权访问，<code>redis-cli -h x.x.x.x ping</code> 直接回 <code>PONG</code></li><li><strong>一个 Jenkins 开在 8080，管理后台弱口令 admin&#x2F;admin</strong></li><li><strong>摄像头 RTSP 流未加密</strong>，<code>ffplay rtsp://...</code> 就能看画面</li></ol><h2 id="为什么会出现这么多暴露面"><a href="#为什么会出现这么多暴露面" class="headerlink" title="为什么会出现这么多暴露面"></a>为什么会出现这么多暴露面</h2><p>和运维聊完，暴露面失控的原因基本是这四类：</p><h3 id="1-默认端口-无认证"><a href="#1-默认端口-无认证" class="headerlink" title="1. 默认端口 + 无认证"></a>1. 默认端口 + 无认证</h3><figure class="highlight yaml"><table><tr><td class="code"><pre><code class="hljs yaml"><span class="hljs-comment"># 反面教材</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">redis:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:latest</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;6379:6379&quot;</span>   <span class="hljs-comment"># 直接映射到公网，且没设密码</span><br></code></pre></td></tr></table></figure><h3 id="2-临时开的口子忘了关"><a href="#2-临时开的口子忘了关" class="headerlink" title="2. 临时开的口子忘了关"></a>2. 临时开的口子忘了关</h3><p>线上排查问题临时 <code>iptables -A INPUT -p tcp --dport 9000 -j ACCEPT</code>，问题解决后没人回收规则。</p><h3 id="3-云安全组规则太宽"><a href="#3-云安全组规则太宽" class="headerlink" title="3. 云安全组规则太宽"></a>3. 云安全组规则太宽</h3><p><code>0.0.0.0/0</code> 的入站规则是重灾区，尤其是一些”图省事”的运维。</p><h3 id="4-资产清单缺失"><a href="#4-资产清单缺失" class="headerlink" title="4. 资产清单缺失"></a>4. 资产清单缺失</h3><p>很多团队根本没有资产台账，自己都不知道公网挂了哪些服务——扫描结果比运维自己的记录还全。</p><h2 id="暴露面收敛清单"><a href="#暴露面收敛清单" class="headerlink" title="暴露面收敛清单"></a>暴露面收敛清单</h2><p>给这位运维同学的整改建议，也是通用最佳实践：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 1. 立即下线未使用的服务</span><br>systemctl <span class="hljs-built_in">disable</span> --now redis-server<br><br><span class="hljs-comment"># 2. Redis/DB 绑定内网 + 认证</span><br>sed -i <span class="hljs-string">&#x27;s/^# bind 127.0.0.1/bind 127.0.0.1/&#x27;</span> /etc/redis/redis.conf<br>redis-cli config <span class="hljs-built_in">set</span> requirepass <span class="hljs-string">&quot;<span class="hljs-subst">$(openssl rand -hex 16)</span>&quot;</span><br><br><span class="hljs-comment"># 3. 一键排查所有对外开放端口</span><br>ss -tlnp | awk <span class="hljs-string">&#x27;&#123;print $4&#125;&#x27;</span> | grep -v <span class="hljs-string">&#x27;127.0.0.1\|::1\|0.0.0.0:22\|0.0.0.0:80\|0.0.0.0:443&#x27;</span><br></code></pre></td></tr></table></figure><p>更系统的做法：</p><ul><li><strong>资产台账</strong>：所有公网服务登记 IP&#x2F;端口&#x2F;负责人&#x2F;用途，用 CMDB 或简单的 CSV 都行</li><li><strong>收敛默认策略</strong>：云安全组默认拒绝，按需放行，并加上来源 IP 白名单</li><li><strong>周期性扫描</strong>：每月一次 nmap + Nuclei 自动扫，结果和台账比对</li><li><strong>SSO 统一认证</strong>：所有管理后台接入 SSO，禁止本地弱口令</li></ul><h2 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">$ ./scan.sh<br>[+] 扫描 254 个 IP<br>[+] 发现开放端口 8000+ 个<br>[+] 高危暴露面 4 个 → 已下发整改工单<br>[+] 复测通过 4/4<br>[✓] 本次演练暴露面收敛完成<br></code></pre></td></tr></table></figure><p>安全不是买来的，是<strong>数出来的</strong>——先把公网暴露面数清楚，再谈防火墙和 WAF。你连自己暴露了什么都不知道，攻击者可是知道的。</p>]]></content>
    
    
    <summary type="html">攻防演练的起点永远是侦察。这篇文章复盘一次针对公网 IP 段的端口扫描，以及暴露面收敛的实操清单。</summary>
    
    
    
    <category term="安全研究" scheme="https://muyan-blog.pages.dev/categories/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/"/>
    
    
    <category term="安全" scheme="https://muyan-blog.pages.dev/tags/%E5%AE%89%E5%85%A8/"/>
    
    <category term="渗透测试" scheme="https://muyan-blog.pages.dev/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    
    <category term="暴露面" scheme="https://muyan-blog.pages.dev/tags/%E6%9A%B4%E9%9C%B2%E9%9D%A2/"/>
    
    <category term="红队" scheme="https://muyan-blog.pages.dev/tags/%E7%BA%A2%E9%98%9F/"/>
    
  </entry>
  
  <entry>
    <title>24 小时 AI 出海建站，这套开源模板直接抄</title>
    <link href="https://muyan-blog.pages.dev/2026/08/10/ai-saas-starter-open-source/"/>
    <id>https://muyan-blog.pages.dev/2026/08/10/ai-saas-starter-open-source/</id>
    <published>2026-08-10T00:00:00.000Z</published>
    <updated>2026-08-10T00:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>不是”AI 时代不需要建站”，而是”AI 时代建站快到你来不及犹豫”。</p></blockquote><h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>上个月有个做 SaaS 的朋友找我：”我想做个落地页 + 订阅支付，一周能搞定吗？”</p><p>我说：不用一周，24 小时。</p><p>于是我用一个周末验证了这件事——从零搭了一个带 Stripe 订阅、多语言、SEO 全配齐的出海站模板，并把它开源了。</p><h2 id="技术栈"><a href="#技术栈" class="headerlink" title="技术栈"></a>技术栈</h2><table><thead><tr><th>层</th><th>选型</th><th>理由</th></tr></thead><tbody><tr><td>框架</td><td>Next.js 15 (App Router)</td><td>SSR&#x2F;SSG 双模，出海 SEO 友好</td></tr><tr><td>部署</td><td>Cloudflare Pages</td><td>全球 CDN，免费额度够个人项目</td></tr><tr><td>支付</td><td>Stripe Checkout</td><td>出海标配，信用卡订阅一键接</td></tr><tr><td>数据库</td><td>Cloudflare D1 + Drizzle</td><td>SQLite 兼容，边缘数据库</td></tr><tr><td>样式</td><td>Tailwind CSS v4</td><td>原子化，落地页开发飞快</td></tr></tbody></table><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">┌────────────┐     ┌──────────────────┐<br>│  Browser    │ ──▶ │  Cloudflare CDN   │<br>└────────────┘     └────────┬─────────┘<br>                            │<br>              ┌─────────────┴─────────────┐<br>              │  Next.js (SSG/ISR)        │<br>              │  Pages Functions (API)    │<br>              └──────┬────────────┬───────┘<br>                     │            │<br>              ┌──────┴───┐  ┌─────┴────────┐<br>              │ Cloudflare│  │ Stripe      │<br>              │ D1 (SQLite)│  │ Checkout    │<br>              └──────────┘  └──────────────┘<br></code></pre></td></tr></table></figure><p>Pages Functions 处理 <code>/api/*</code> 路由，Stripe Webhook 走 <code>onRequestPost</code>，整个项目不需要一台传统服务器。</p><h2 id="一天的时间分配"><a href="#一天的时间分配" class="headerlink" title="一天的时间分配"></a>一天的时间分配</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">09:00  脚手架 + 落地页首屏（标题、Hero、CTA）<br>11:30  定价页三档卡片 + FAQ + 页脚<br>14:00  Stripe Checkout 接入 + Webhook 落库<br>17:00  i18n（中/英）+ SEO 元信息 + OG 图<br>20:00  部署到 Cloudflare Pages + 自定义域名<br>22:00  性能压测：Lighthouse 98 / 全球延迟 &lt; 120ms<br></code></pre></td></tr></table></figure><h2 id="关键代码：Stripe-订阅"><a href="#关键代码：Stripe-订阅" class="headerlink" title="关键代码：Stripe 订阅"></a>关键代码：Stripe 订阅</h2><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// app/api/checkout/route.ts</span><br><span class="hljs-keyword">import</span> <span class="hljs-title class_">Stripe</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;stripe&#x27;</span>;<br><br><span class="hljs-keyword">const</span> stripe = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Stripe</span>(process.<span class="hljs-property">env</span>.<span class="hljs-property">STRIPE_SECRET_KEY</span>!);<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">POST</span>(<span class="hljs-params"><span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span></span>) &#123;<br>  <span class="hljs-keyword">const</span> &#123; priceId &#125; = <span class="hljs-keyword">await</span> req.<span class="hljs-title function_">json</span>();<br>  <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> stripe.<span class="hljs-property">checkout</span>.<span class="hljs-property">sessions</span>.<span class="hljs-title function_">create</span>(&#123;<br>    <span class="hljs-attr">mode</span>: <span class="hljs-string">&#x27;subscription&#x27;</span>,<br>    <span class="hljs-attr">line_items</span>: [&#123; <span class="hljs-attr">price</span>: priceId, <span class="hljs-attr">quantity</span>: <span class="hljs-number">1</span> &#125;],<br>    <span class="hljs-attr">success_url</span>: <span class="hljs-string">`<span class="hljs-subst">$&#123;req.headers.get(<span class="hljs-string">&#x27;origin&#x27;</span>)&#125;</span>/success`</span>,<br>    <span class="hljs-attr">cancel_url</span>: <span class="hljs-string">`<span class="hljs-subst">$&#123;req.headers.get(<span class="hljs-string">&#x27;origin&#x27;</span>)&#125;</span>/#pricing`</span>,<br>  &#125;);<br>  <span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">url</span>: session.<span class="hljs-property">url</span> &#125;);<br>&#125;<br></code></pre></td></tr></table></figure><p>Webhook 校验签名 + 幂等处理：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">onRequestPost</span>(<span class="hljs-params"><span class="hljs-attr">context</span>: <span class="hljs-title class_">EventContext</span></span>) &#123;<br>  <span class="hljs-keyword">const</span> sig = context.<span class="hljs-property">request</span>.<span class="hljs-property">headers</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;stripe-signature&#x27;</span>)!;<br>  <span class="hljs-keyword">const</span> event = stripe.<span class="hljs-property">webhooks</span>.<span class="hljs-title function_">constructEvent</span>(<br>    <span class="hljs-keyword">await</span> context.<span class="hljs-property">request</span>.<span class="hljs-title function_">text</span>(), sig, process.<span class="hljs-property">env</span>.<span class="hljs-property">STRIPE_WEBHOOK_SECRET</span>!<br>  );<br>  <span class="hljs-keyword">if</span> (event.<span class="hljs-property">type</span> === <span class="hljs-string">&#x27;checkout.session.completed&#x27;</span>) &#123;<br>    <span class="hljs-keyword">await</span> db.<span class="hljs-title function_">insert</span>(users).<span class="hljs-title function_">values</span>(&#123; <span class="hljs-attr">email</span>: event.<span class="hljs-property">data</span>.<span class="hljs-property">object</span>.<span class="hljs-property">customer_email</span>! &#125;);<br>  &#125;<br>  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-string">&#x27;ok&#x27;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="出海建站的三个隐藏坑"><a href="#出海建站的三个隐藏坑" class="headerlink" title="出海建站的三个隐藏坑"></a>出海建站的三个隐藏坑</h2><ol><li><strong>Stripe 需要海外主体</strong>：个人开发者可以用 Stripe Atlas（美国公司），或走 Paddle&#x2F;LemonSqueezy 的 Merchant of Record（平台代收，无主体也能收）</li><li><strong>货币与税费</strong>：Stripe 自动处理销售税，但定价时要把 Stripe 手续费（2.9% + 30¢）算进成本</li><li><strong>域名邮件</strong>：别用免费邮箱收付款通知，买域名时顺手配个 Google Workspace 或 Zoho</li></ol><h2 id="开源地址"><a href="#开源地址" class="headerlink" title="开源地址"></a>开源地址</h2><p>模板已开源：<a href="https://github.com/your-github-username/ai-saas-starter">github.com&#x2F;your-github-username&#x2F;ai-saas-starter</a></p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">$ git clone ... &amp;&amp; npm i &amp;&amp; npm run dev<br>✓ 本地跑起来 3 分钟<br>$ npm run deploy<br>✓ Cloudflare Pages 部署完成<br></code></pre></td></tr></table></figure><p>拿去用，改改文案和配色就是你的产品。记得回来给我个 star ⭐</p>]]></content>
    
    
    <summary type="html">从想法到上线只花了 24 小时：Next.js + Cloudflare Pages + Stripe 的出海建站模板，我把它开源了。</summary>
    
    
    
    <category term="开源" scheme="https://muyan-blog.pages.dev/categories/%E5%BC%80%E6%BA%90/"/>
    
    
    <category term="Cloudflare" scheme="https://muyan-blog.pages.dev/tags/Cloudflare/"/>
    
    <category term="开源" scheme="https://muyan-blog.pages.dev/tags/%E5%BC%80%E6%BA%90/"/>
    
    <category term="Next.js" scheme="https://muyan-blog.pages.dev/tags/Next-js/"/>
    
    <category term="出海" scheme="https://muyan-blog.pages.dev/tags/%E5%87%BA%E6%B5%B7/"/>
    
    <category term="模板" scheme="https://muyan-blog.pages.dev/tags/%E6%A8%A1%E6%9D%BF/"/>
    
  </entry>
  
</feed>
