<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://wuli-git.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://wuli-git.github.io/" rel="alternate" type="text/html" /><updated>2026-05-19T12:30:33+08:00</updated><id>https://wuli-git.github.io/feed.xml</id><title type="html">fox小栖共创博客</title><subtitle>wuli 和 cc00mi 共创的技术、工具与探索笔记
</subtitle><author><name>wuli</name><email>1328433750@qq.com</email></author><entry><title type="html">Uni-app 项目如何用 GitHub CI/CD 自动部署到 GitHub Pages：完整 SOP、底层原理与避坑总结</title><link href="https://wuli-git.github.io/2026/05/17/Uni-app%E9%A1%B9%E7%9B%AE%E5%A6%82%E4%BD%95%E7%94%A8GitHub-CICD%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%88%B0GitHub-Pages-%E5%AE%8C%E6%95%B4-SOP-%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%B8%8E%E9%81%BF%E5%9D%91%E6%80%BB%E7%BB%93.html" rel="alternate" type="text/html" title="Uni-app 项目如何用 GitHub CI/CD 自动部署到 GitHub Pages：完整 SOP、底层原理与避坑总结" /><published>2026-05-17T00:00:00+08:00</published><updated>2026-05-17T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/05/17/Uni-app%E9%A1%B9%E7%9B%AE%E5%A6%82%E4%BD%95%E7%94%A8GitHub-CICD%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%88%B0GitHub-Pages%EF%BC%9A%E5%AE%8C%E6%95%B4-SOP%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%B8%8E%E9%81%BF%E5%9D%91%E6%80%BB%E7%BB%93</id><content type="html" xml:base="https://wuli-git.github.io/2026/05/17/Uni-app%E9%A1%B9%E7%9B%AE%E5%A6%82%E4%BD%95%E7%94%A8GitHub-CICD%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E5%88%B0GitHub-Pages-%E5%AE%8C%E6%95%B4-SOP-%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%B8%8E%E9%81%BF%E5%9D%91%E6%80%BB%E7%BB%93.html"><![CDATA[<h2 id="一写在前面">一、写在前面</h2>

<p>如果你已经看过上一篇“静态网页如何通过 GitHub 自动构建并发布”的文章，那么这一篇可以看作它的续篇。</p>

<p>上一篇解决的是更基础的一层问题：</p>

<ul>
  <li>如何把纯静态网页接入 GitHub</li>
  <li>如何使用 GitHub Actions 自动构建</li>
  <li>如何把构建产物发布到 GitHub Pages</li>
</ul>

<p>那一层的核心关注点是：</p>

<ul>
  <li>构建有没有跑通</li>
  <li>静态资源路径对不对</li>
  <li>页面能不能正常打开</li>
</ul>

<p>而这一篇开始进入更复杂但也更真实的项目场景：</p>

<ul>
  <li>项目不再只是简单静态页面</li>
  <li>而是 <code class="language-plaintext highlighter-rouge">uni-app</code> 这类需要工程化构建的前端项目</li>
  <li>页面运行后还依赖外部 API</li>
  <li>最终还要处理浏览器跨域、代理、图片 CDN 可达性、运行时拓扑等问题</li>
</ul>

<p>所以如果说上一篇是在回答：</p>

<blockquote>
  <p>“静态页面如何通过 GitHub 自动化部署？”</p>
</blockquote>

<p>那么这一篇回答的就是：</p>

<blockquote>
  <p>“当项目从纯静态页，演进到带工程化构建和运行时接口依赖的前端应用后，CI/CD 体系要如何随之升级？”</p>
</blockquote>

<p>这篇文章沉淀的是一次真实的项目改造过程：把一个原本更偏 <code class="language-plaintext highlighter-rouge">HBuilderX / uni-app</code> 开发流的壁纸项目，改造成：</p>

<ul>
  <li>前端通过 GitHub Actions 自动构建</li>
  <li>构建产物自动发布到 GitHub Pages</li>
  <li>运行期接口通过独立后端代理解决 CORS</li>
  <li>图片资源通过代理兜底解决客户端 CDN 连接失败</li>
</ul>

<p>本次最终上线结果是：</p>

<ul>
  <li>GitHub 仓库：<code class="language-plaintext highlighter-rouge">wuli-git/wallpaper</code></li>
  <li>GitHub Pages 前端：<code class="language-plaintext highlighter-rouge">https://wuli-git.github.io/wallpaper/</code></li>
  <li>Railway 代理：<code class="language-plaintext highlighter-rouge">https://wallpaper-production-9537.up.railway.app</code></li>
  <li>前端 API Base：<code class="language-plaintext highlighter-rouge">https://wallpaper-production-9537.up.railway.app/api/bizhi</code></li>
</ul>

<p>这类项目的难点，不在“把静态文件传到网上”，而在于：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">uni-app</code> 工程能不能在 CI 环境中稳定构建出 H5 产物</li>
  <li>构建出来的 H5 是否能正确适配 GitHub Pages 的子路径</li>
  <li>页面运行时依赖的接口，是否允许浏览器跨域访问</li>
  <li>页面拿到数据后，图片资源是否也能被用户浏览器正常加载</li>
</ol>

<p>如果只理解成“把页面部署出去”，最后通常会出现一个典型症状：</p>

<ul>
  <li>页面外壳能打开</li>
  <li>路由能跳</li>
  <li>但数据是空的</li>
  <li>或者数据出来了，图片一片空白</li>
  <li>看起来像“部署成功了”，实际上业务并没有跑通</li>
</ul>

<p>本文会把这个问题拆透。</p>

<hr />

<h2 id="二适用场景">二、适用场景</h2>

<p>这套 SOP 适合下面这类项目：</p>

<ul>
  <li>使用 <code class="language-plaintext highlighter-rouge">uni-app</code> 开发</li>
  <li>需要发布 H5 版本</li>
  <li>代码托管在 GitHub</li>
  <li>想通过 GitHub Actions 做自动构建与自动部署</li>
  <li>最终站点托管在 GitHub Pages</li>
  <li>项目运行时依赖外部 API</li>
  <li>上游 API 不完全受自己控制</li>
</ul>

<p>如果你的项目只是纯静态 HTML/CSS/JS，没有接口依赖，也没有 <code class="language-plaintext highlighter-rouge">uni-app</code> 这层构建链，流程会简单很多。本文后面会专门讲差异。</p>

<hr />

<h2 id="三最终架构图">三、最终架构图</h2>

<p>最终落地后的架构是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>开发者 push 到 GitHub main 分支
        ↓
GitHub Actions 拉取代码
        ↓
安装依赖并构建 uni-app H5 产物
        ↓
上传 Pages artifact
        ↓
GitHub Pages 发布静态前端
        ↓
用户访问 https://wuli-git.github.io/wallpaper/
        ↓
前端页面请求 Railway 上的 Node 代理
        ↓
Node 代理转发到上游壁纸接口
        ↓
代理补齐 CORS 响应头并自动附加 access-key
        ↓
代理把 CDN 图片地址改写成自己的图片代理地址
        ↓
浏览器拿到数据与图片并渲染页面
</code></pre></div></div>

<p>也就是说，最终不是“一个服务”，而是两个部分协同：</p>

<ul>
  <li>静态前端：GitHub Pages</li>
  <li>动态代理：Railway 上的 Node 服务</li>
</ul>

<hr />

<h2 id="四完整-sop">四、完整 SOP</h2>

<h3 id="41-整理-uni-app-工程为可-ci-构建结构">4.1 整理 uni-app 工程为可 CI 构建结构</h3>

<p>首先要解决的是：原项目必须能在命令行里构建，而不是只能在 HBuilderX 图形界面里点按钮。</p>

<p>核心处理包括：</p>

<ol>
  <li>补齐 <code class="language-plaintext highlighter-rouge">package.json</code></li>
  <li>补齐 <code class="language-plaintext highlighter-rouge">vite.config.js</code></li>
  <li>把源码整理到 <code class="language-plaintext highlighter-rouge">src/</code> 目录</li>
  <li>确保 <code class="language-plaintext highlighter-rouge">manifest.json</code>、<code class="language-plaintext highlighter-rouge">pages.json</code>、<code class="language-plaintext highlighter-rouge">main.js</code>、<code class="language-plaintext highlighter-rouge">App.vue</code> 等位于 <code class="language-plaintext highlighter-rouge">src/</code></li>
  <li>把 <code class="language-plaintext highlighter-rouge">api/</code>、<code class="language-plaintext highlighter-rouge">components/</code>、<code class="language-plaintext highlighter-rouge">utils/</code>、<code class="language-plaintext highlighter-rouge">commom/</code>、<code class="language-plaintext highlighter-rouge">static/</code>、<code class="language-plaintext highlighter-rouge">uni.scss</code> 等运行所需资源同步到 <code class="language-plaintext highlighter-rouge">src/</code></li>
  <li>安装构建依赖，比如 <code class="language-plaintext highlighter-rouge">sass</code></li>
</ol>

<p>本项目最终采用的构建脚本是：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"dev:h5"</span><span class="p">:</span><span class="w"> </span><span class="s2">"uni"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"build:h5"</span><span class="p">:</span><span class="w"> </span><span class="s2">"uni build"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>为什么这一步必须做</strong></p>

<p>因为 GitHub Actions 本质上只会执行脚本，它不会打开 HBuilderX 帮你点“发行到 H5”。</p>

<p>如果项目不能命令行构建，那么就谈不上真正的 CI/CD。</p>

<hr />

<h3 id="42-锁定一组兼容的-dcloud--vue--vite-依赖">4.2 锁定一组兼容的 DCloud / Vue / Vite 依赖</h3>

<p>这次迁移里最容易误判的一类错误，是把缺失依赖当成“少装一个包”来补。</p>

<p>例如依次遇到：</p>

<ul>
  <li>缺 <code class="language-plaintext highlighter-rouge">@dcloudio/uni-cli-shared</code></li>
  <li>缺 <code class="language-plaintext highlighter-rouge">@dcloudio/uni-cli-i18n</code></li>
  <li>缺 <code class="language-plaintext highlighter-rouge">webpack</code></li>
  <li>缺 <code class="language-plaintext highlighter-rouge">semver</code></li>
</ul>

<p>如果每报一个就手动 <code class="language-plaintext highlighter-rouge">npm install</code> 一个，最后很容易把依赖树补歪。</p>

<p>正确做法是：<strong>把 DCloud 相关包按同一代版本锁成一组</strong>。</p>

<p>本项目最终使用的核心依赖类似：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@dcloudio/uni-app"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.0.0-5000720260410001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@dcloudio/uni-h5"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.0.0-5000720260410001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@dcloudio/uni-components"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.0.0-5000720260410001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@dcloudio/uni-i18n"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.0.0-5000720260410001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.4.21"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@dcloudio/vite-plugin-uni"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.0.0-5000720260410001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@vitejs/plugin-vue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5.2.4"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@vue/compiler-sfc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3.4.21"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"sass"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.77.8"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vite"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5.2.8"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>注意：不要使用不存在的包或不存在的版本，例如：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">@dcloudio/cli</code></li>
  <li><code class="language-plaintext highlighter-rouge">@dcloudio/uni-app@^2.0.2</code></li>
  <li><code class="language-plaintext highlighter-rouge">@dcloudio/vite-plugin-uni@^4.61.2026051901</code></li>
</ul>

<p>这些会直接导致 <code class="language-plaintext highlighter-rouge">E404</code> 或 <code class="language-plaintext highlighter-rouge">ETARGET</code>。</p>

<hr />

<h3 id="43-处理-github-pages-的子路径问题">4.3 处理 GitHub Pages 的子路径问题</h3>

<p>GitHub Pages 的项目页通常不是部署在根路径，而是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://用户名.github.io/仓库名/
</code></pre></div></div>

<p>本项目实际地址是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wuli-git.github.io/wallpaper/
</code></pre></div></div>

<p>所以 <code class="language-plaintext highlighter-rouge">vite.config.js</code> 里必须配置：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">vite</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">uniPlugin</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@dcloudio/vite-plugin-uni</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">uni</span> <span class="o">=</span> <span class="nx">uniPlugin</span><span class="p">.</span><span class="k">default</span> <span class="o">||</span> <span class="nx">uniPlugin</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
  <span class="na">base</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/wallpaper/</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span><span class="nx">uni</span><span class="p">()],</span>
<span class="p">});</span>
</code></pre></div></div>

<p>这里有两个关键点：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">base: "/wallpaper/"</code> 用来适配 GitHub Pages 子路径</li>
  <li><code class="language-plaintext highlighter-rouge">const uni = uniPlugin.default || uniPlugin</code> 用来兼容插件导出形态，避免 <code class="language-plaintext highlighter-rouge">uni is not a function</code></li>
</ul>

<p><strong>为什么这一步必须做</strong></p>

<p>如果不写 <code class="language-plaintext highlighter-rouge">base</code>，构建产物里的资源路径默认会按根路径处理，例如：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/assets/index.js
</code></pre></div></div>

<p>而 GitHub Pages 项目页实际需要的是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/wallpaper/assets/index.js
</code></pre></div></div>

<p>不修这个，页面可能会出现：</p>

<ul>
  <li>CSS 丢失</li>
  <li>JS 404</li>
  <li>路由资源错位</li>
  <li>页面空白</li>
</ul>

<hr />

<h3 id="44-配置-github-actions-自动构建与部署">4.4 配置 GitHub Actions 自动构建与部署</h3>

<p>推荐使用 GitHub 官方 Pages workflow，而不是老式的“把 dist 推到 gh-pages 分支”的做法。</p>

<p>本项目最终的 workflow 重点如下：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to GitHub Pages</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
  <span class="na">pages</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">concurrency</span><span class="pi">:</span>
  <span class="na">group</span><span class="pi">:</span> <span class="s">pages</span>
  <span class="na">cancel-in-progress</span><span class="pi">:</span> <span class="no">true</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Node.js</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">node-version</span><span class="pi">:</span> <span class="m">18</span>
          <span class="na">cache</span><span class="pi">:</span> <span class="s">npm</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install dependencies</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">npm ci</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build H5</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">VITE_API_BASE_URL</span><span class="pi">:</span> <span class="s">$</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">npm run build:h5</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Pages</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/configure-pages@v5</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Detect build output path</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">detect</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [ -d "dist/build/h5" ]; then</span>
            <span class="s">echo "path=dist/build/h5" &gt;&gt; "$GITHUB_OUTPUT"</span>
          <span class="s">elif [ -d "unpackage/dist/build/h5" ]; then</span>
            <span class="s">echo "path=unpackage/dist/build/h5" &gt;&gt; "$GITHUB_OUTPUT"</span>
          <span class="s">elif [ -d "dist" ]; then</span>
            <span class="s">echo "path=dist" &gt;&gt; "$GITHUB_OUTPUT"</span>
          <span class="s">else</span>
            <span class="s">echo "Build output not found"</span>
            <span class="s">ls -la</span>
            <span class="s">ls -la dist || true</span>
            <span class="s">ls -la unpackage || true</span>
            <span class="s">exit 1</span>
          <span class="s">fi</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create Pages fallback</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">cp "$/index.html" "$/404.html"</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload artifact</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-pages-artifact@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">$</span>

  <span class="na">deploy</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">github-pages</span>
      <span class="na">url</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to GitHub Pages</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">deployment</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/deploy-pages@v4</span>
</code></pre></div></div>

<p>这里比最小示例多做了两件事：</p>

<ol>
  <li><strong>自动检测构建产物目录</strong>
    <ul>
      <li>不同 uni-app / Vite 组合可能输出到 <code class="language-plaintext highlighter-rouge">dist/build/h5</code> 或 <code class="language-plaintext highlighter-rouge">unpackage/dist/build/h5</code></li>
      <li>workflow 里自动判断，避免产物路径写死后上传失败</li>
    </ul>
  </li>
  <li><strong>生成 <code class="language-plaintext highlighter-rouge">404.html</code></strong>
    <ul>
      <li>GitHub Pages 刷新 hash / history 路由时可能需要 fallback</li>
      <li>复制一份 <code class="language-plaintext highlighter-rouge">index.html</code> 为 <code class="language-plaintext highlighter-rouge">404.html</code> 可以提升路由容错</li>
    </ul>
  </li>
</ol>

<h3 id="github-仓库还要做的配置">GitHub 仓库还要做的配置</h3>

<p>进入：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Settings -&gt; Pages
</code></pre></div></div>

<p>把 Source 设置为：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GitHub Actions
</code></pre></div></div>

<p>如果出现环境保护规则导致部署被拒绝，还要进入：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Settings -&gt; Environments -&gt; github-pages
</code></pre></div></div>

<p>把 Deployment branches and tags 调整为允许 <code class="language-plaintext highlighter-rouge">main</code> 分支部署，或者取消不必要的分支限制。</p>

<hr />

<h3 id="45-修复原项目中的模板与编码问题">4.5 修复原项目中的模板与编码问题</h3>

<p>真实项目迁移到 CI 时，常见并且最费时间的问题不是“部署”，而是“代码本身不能在严格构建环境里通过”。</p>

<p>这次实践里主要踩到的坑有：</p>

<ul>
  <li>Vue 模板存在坏掉的闭合标签</li>
  <li>属性之间缺少空格</li>
  <li>字符串被错误编码后破坏模板结构</li>
  <li>中文乱码导致引号和标签异常</li>
  <li>某些页面在原运行环境里能“凑合跑”，但在 Vite 严格解析时直接报错</li>
</ul>

<p>典型报错长这样：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[plugin:vite:vue] Whitespace was expected.
</code></pre></div></div>

<p>所以实际过程里，需要逐个修：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/pages/index/index.vue</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/pages/user/user.vue</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/pages/notice/detail.vue</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/components/theme-item/theme-item.vue</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/pages/preview/preview.vue</code></li>
  <li><code class="language-plaintext highlighter-rouge">src/pages/search/search.vue</code></li>
</ul>

<h3 id="这类问题的本质">这类问题的本质</h3>

<p>不是 GitHub Pages 有问题，也不是 CI 有问题，而是：</p>

<ul>
  <li>本地某些工具链容错更高</li>
  <li>但标准构建器更严格</li>
</ul>

<p>CI 的价值之一，就是把这类“隐性坏代码”逼出来。</p>

<hr />

<h3 id="46-页面能打开但没有壁纸定位运行时接口问题">4.6 页面能打开但没有壁纸：定位运行时接口问题</h3>

<p>这一步是最关键的认知分水岭。</p>

<p>部署成功后，页面出现了，但壁纸数据没有出来。<br />
这时很多人会误判为：</p>

<ul>
  <li>GitHub Pages 不支持 uni-app</li>
  <li>GitHub Actions 没构建完整</li>
  <li>资源路径还有问题</li>
</ul>

<p>但真实原因可能完全不同。</p>

<p>本项目里，前端请求逻辑原本写死为：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">BASE_URL</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://tea.qingnian8.com/api/bizhi</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<p>也就是说，页面加载后会在浏览器里直接请求这个上游接口。</p>

<p>经排查发现：</p>

<ul>
  <li>这个接口本身有数据</li>
  <li>用 <code class="language-plaintext highlighter-rouge">curl</code>、服务端脚本请求是成功的</li>
  <li>但浏览器跨域请求会被拦截</li>
</ul>

<p>根因是接口响应头缺少：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin</code></li>
  <li><code class="language-plaintext highlighter-rouge">Access-Control-Allow-Headers</code></li>
  <li><code class="language-plaintext highlighter-rouge">Access-Control-Allow-Methods</code></li>
</ul>

<p>即：<strong>CORS 不通过</strong>。</p>

<hr />

<h3 id="47-为什么静态站点需要额外后端代理">4.7 为什么静态站点需要额外后端代理</h3>

<p>既然上游接口不支持浏览器跨域，那就必须在中间加一层自己的服务。</p>

<p>最终采用的方案是：</p>

<ul>
  <li>前端继续部署到 GitHub Pages</li>
  <li>代理后端部署到 Railway</li>
</ul>

<p>代理的职责很简单：</p>

<ol>
  <li>接收来自浏览器的请求</li>
  <li>转发给 <code class="language-plaintext highlighter-rouge">https://tea.qingnian8.com/api/bizhi</code></li>
  <li>把上游返回的数据回传</li>
  <li>在响应中补上 CORS 头</li>
  <li>自动附加上游接口需要的 <code class="language-plaintext highlighter-rouge">access-key</code></li>
  <li>必要时改写图片 URL，让图片也走代理</li>
</ol>

<p>这层代理不是“可选优化”，而是浏览器环境下的必要组成部分。</p>

<hr />

<h3 id="48-代理服务的实现方式">4.8 代理服务的实现方式</h3>

<p>最终代理使用一个最小 Node 服务实现，目录单独放在：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>proxy-server/
</code></pre></div></div>

<p>原因是：</p>

<ul>
  <li>前端仓库根目录是 <code class="language-plaintext highlighter-rouge">uni-app</code></li>
  <li>代理是纯 Node 服务</li>
  <li>把两者拆开，方便 Railway 只部署代理子目录</li>
</ul>

<h3 id="代理服务的核心能力">代理服务的核心能力</h3>

<ul>
  <li>支持 <code class="language-plaintext highlighter-rouge">OPTIONS</code> 预检</li>
  <li>按请求头动态回写允许的 <code class="language-plaintext highlighter-rouge">Origin</code></li>
  <li>转发查询参数</li>
  <li>转发请求体</li>
  <li>自动补 <code class="language-plaintext highlighter-rouge">access-key</code></li>
  <li>过滤 hop-by-hop headers</li>
  <li>提供 <code class="language-plaintext highlighter-rouge">/</code> 和 <code class="language-plaintext highlighter-rouge">/health</code> 健康检查</li>
  <li>提供 <code class="language-plaintext highlighter-rouge">/proxy-image?url=...</code> 图片代理</li>
  <li>把上游 JSON 里的 <code class="language-plaintext highlighter-rouge">https://cdn.qingnian8.com/...</code> 图片地址改写为代理图片地址</li>
</ul>

<p>这样前端最终请求的是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/api/bizhi/classify?select=true
</code></pre></div></div>

<p>而不是直接请求上游：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://tea.qingnian8.com/api/bizhi/classify?select=true
</code></pre></div></div>

<p>图片最终请求的是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/proxy-image?url=https%3A%2F%2Fcdn.qingnian8.com%2F...
</code></pre></div></div>

<p>而不是浏览器直接请求：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://cdn.qingnian8.com/...
</code></pre></div></div>

<hr />

<h3 id="49-railway-部署代理">4.9 Railway 部署代理</h3>

<p>部署步骤如下：</p>

<ol>
  <li>登录 Railway</li>
  <li>选择从 GitHub 仓库部署</li>
  <li>选择仓库 <code class="language-plaintext highlighter-rouge">wuli-git/wallpaper</code></li>
  <li>把 <code class="language-plaintext highlighter-rouge">Root Directory</code> 设成：</li>
</ol>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>proxy-server
</code></pre></div></div>

<ol>
  <li>配置环境变量：</li>
</ol>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ALLOWED_ORIGIN=https://wuli-git.github.io
UPSTREAM_ACCESS_KEY=&lt;你的上游 access-key&gt;
TARGET_ORIGIN=https://tea.qingnian8.com
TARGET_PREFIX=/api/bizhi
PROXY_PREFIX=/api/bizhi
IMAGE_PROXY_PREFIX=/proxy-image
</code></pre></div></div>

<ol>
  <li>生成公网域名，本项目为：</li>
</ol>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app
</code></pre></div></div>

<p>注意：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">railway.internal</code> 这种地址不能给浏览器用</li>
  <li>必须使用 Railway 生成的公网域名</li>
  <li><code class="language-plaintext highlighter-rouge">UPSTREAM_ACCESS_KEY</code> 不建议写进公开文章或前端构建产物，应放在 Railway 环境变量里</li>
</ul>

<hr />

<h3 id="410-回填-github-actions-构建变量">4.10 回填 GitHub Actions 构建变量</h3>

<p>为了让前端在 H5 构建时自动使用代理地址，需要在 GitHub 仓库里增加变量：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Settings -&gt; Secrets and variables -&gt; Actions -&gt; Variables
</code></pre></div></div>

<p>新增：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VITE_API_BASE_URL=https://wallpaper-production-9537.up.railway.app/api/bizhi
</code></pre></div></div>

<p>之后重新运行 GitHub Pages workflow。</p>

<p>前端请求逻辑会优先读取：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_API_BASE_URL</span>
</code></pre></div></div>

<p>从而在 H5 环境中走代理，而不是直连上游。</p>

<p>本项目里请求层还做了一个关键修复：H5 下手动把 GET 参数拼到 URL 上，避免 <code class="language-plaintext highlighter-rouge">uni.request</code> 在 H5 构建后出现列表接口参数没有正确带上的情况。</p>

<p>核心思想类似：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">BASE_URL</span> <span class="o">=</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_API_BASE_URL</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">https://tea.qingnian8.com/api/bizhi</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">appendQuery</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">data</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">();</span>
  <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">data</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">key</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">data</span><span class="p">[</span><span class="nx">key</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">value</span> <span class="o">!==</span> <span class="kc">undefined</span> <span class="o">&amp;&amp;</span> <span class="nx">value</span> <span class="o">!==</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">value</span> <span class="o">!==</span> <span class="dl">""</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">params</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">params</span><span class="p">.</span><span class="nx">toString</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">query</span><span class="p">)</span> <span class="k">return</span> <span class="nx">url</span><span class="p">;</span>
  <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">}${</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">"</span><span class="s2">?</span><span class="dl">"</span><span class="p">)</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">&amp;</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">?</span><span class="dl">"</span><span class="p">}${</span><span class="nx">query</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="五排错分支从报错现象定位根因">五、排错分支：从报错现象定位根因</h2>

<p>这一节是本次部署最值得沉淀的部分。</p>

<p>不要把所有错误混成一句“部署失败”。更好的方式是像走树一样排查：先判断错误发生在哪一层，再进入对应分支。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>部署 / 上线异常
├─ A. npm install / npm ci 阶段失败
│  ├─ A1. E404：包不存在
│  ├─ A2. ETARGET：版本不存在
│  └─ A3. 缺模块：依赖代际混乱
├─ B. npm run build:h5 阶段失败
│  ├─ B1. uni is not a function
│  ├─ B2. Could not resolve ./main.js
│  ├─ B3. @ 路径找不到
│  ├─ B4. Sass 变量不存在
│  └─ B5. Vue 模板解析失败
├─ C. GitHub Pages 部署阶段失败
│  ├─ C1. Pages source 配错
│  ├─ C2. artifact 路径不对
│  └─ C3. 环境保护规则拒绝 main
├─ D. 页面能打开，但没有接口数据
│  ├─ D1. 前端仍直连上游
│  ├─ D2. 上游 CORS 不通过
│  └─ D3. 构建变量没有注入
├─ E. 首页有数据，但分类列表没有数据
│  └─ E1. H5 GET 参数没有稳定序列化
├─ F. 数据有了，但图片不显示
│  └─ F1. CDN 图片连接被关闭
└─ G. Railway 代理访问异常
   ├─ G1. 根路径返回 404
   ├─ G2. Root Directory 配错
   └─ G3. 环境变量缺失
</code></pre></div></div>

<hr />

<h3 id="分支-a依赖安装失败">分支 A：依赖安装失败</h3>

<h4 id="a1dcloudiocli-报-e404">A1：<code class="language-plaintext highlighter-rouge">@dcloudio/cli</code> 报 <code class="language-plaintext highlighter-rouge">E404</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm error code E404
npm error 404 Not Found - GET https://registry.npmjs.org/@dcloudio%2fcli
npm error 404 '@dcloudio/cli@...' is not in this registry.
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>如果错误里出现 <code class="language-plaintext highlighter-rouge">@dcloudio/cli</code>，基本可以判断是包名用错。</p>

<p><strong>根因</strong></p>

<p><code class="language-plaintext highlighter-rouge">@dcloudio/cli</code> 并不是这个项目需要安装的 npm 包。uni-app Vite 项目应该通过 <code class="language-plaintext highlighter-rouge">@dcloudio/vite-plugin-uni</code> 和相关 <code class="language-plaintext highlighter-rouge">@dcloudio/uni-*</code> 包构建。</p>

<p><strong>修复方案</strong></p>

<ul>
  <li>不要执行 <code class="language-plaintext highlighter-rouge">npx @dcloudio/cli</code></li>
  <li>不要在 <code class="language-plaintext highlighter-rouge">package.json</code> 里添加 <code class="language-plaintext highlighter-rouge">@dcloudio/cli</code></li>
  <li>使用 <code class="language-plaintext highlighter-rouge">npm run build:h5</code> 调用本项目脚本</li>
</ul>

<hr />

<h4 id="a2dcloud-包报-etarget">A2：DCloud 包报 <code class="language-plaintext highlighter-rouge">ETARGET</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm error code ETARGET
npm error notarget No matching version found for @dcloudio/vite-plugin-uni@^4.61.2026051901.
</code></pre></div></div>

<p>或者：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm error notarget No matching version found for @dcloudio/uni-app@^2.0.2.
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>如果错误里出现 <code class="language-plaintext highlighter-rouge">No matching version found</code>，说明你请求的版本在 npm registry 里不存在。</p>

<p><strong>根因</strong></p>

<p>版本号是猜出来的，或者把不同代际的包混在了一起。</p>

<p><strong>修复方案</strong></p>

<p>统一锁定一组真实存在且互相兼容的版本，例如：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@dcloudio/uni-app               3.0.0-5000720260410001
@dcloudio/uni-h5                3.0.0-5000720260410001
@dcloudio/uni-components        3.0.0-5000720260410001
@dcloudio/uni-i18n              3.0.0-5000720260410001
@dcloudio/vite-plugin-uni       3.0.0-5000720260410001
vue                             3.4.21
vite                            5.2.8
</code></pre></div></div>

<hr />

<h4 id="a3不断缺-dcloudiouni-cli-shareddcloudiouni-cli-i18nwebpacksemver">A3：不断缺 <code class="language-plaintext highlighter-rouge">@dcloudio/uni-cli-shared</code>、<code class="language-plaintext highlighter-rouge">@dcloudio/uni-cli-i18n</code>、<code class="language-plaintext highlighter-rouge">webpack</code>、<code class="language-plaintext highlighter-rouge">semver</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error: Cannot find module '@dcloudio/uni-cli-shared'
Error: Cannot find module '@dcloudio/uni-cli-i18n'
Error: Cannot find module 'webpack'
Error: Cannot find module 'semver'
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>如果每装一个包又缺下一个包，说明不是“少一个依赖”，而是依赖树整体不匹配。</p>

<p><strong>根因</strong></p>

<p>DCloud 相关包版本混用，导致插件内部引用的配套包没有被正确安装。</p>

<p><strong>修复方案</strong></p>

<ul>
  <li>停止一个个补包</li>
  <li>清理错误依赖组合</li>
  <li>重新统一 DCloud 版本</li>
  <li>重新生成 <code class="language-plaintext highlighter-rouge">package-lock.json</code></li>
  <li>本地用 <code class="language-plaintext highlighter-rouge">npm install</code> 验证，CI 用 <code class="language-plaintext highlighter-rouge">npm ci</code> 固化安装</li>
</ul>

<hr />

<h3 id="分支-b构建失败">分支 B：构建失败</h3>

<h4 id="b1uni-is-not-a-function">B1：<code class="language-plaintext highlighter-rouge">uni is not a function</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TypeError: uni is not a function
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>错误出现在 <code class="language-plaintext highlighter-rouge">vite.config.js</code> 里的 <code class="language-plaintext highlighter-rouge">plugins: [uni()]</code> 附近。</p>

<p><strong>根因</strong></p>

<p><code class="language-plaintext highlighter-rouge">@dcloudio/vite-plugin-uni</code> 的导出形态在不同环境下可能表现为默认导出或对象导出。</p>

<p><strong>修复方案</strong></p>

<p>使用兼容写法：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">uniPlugin</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@dcloudio/vite-plugin-uni</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">uni</span> <span class="o">=</span> <span class="nx">uniPlugin</span><span class="p">.</span><span class="k">default</span> <span class="o">||</span> <span class="nx">uniPlugin</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h4 id="b2could-not-resolve-mainjs-from-indexhtml">B2：<code class="language-plaintext highlighter-rouge">Could not resolve "./main.js" from "index.html"</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Could not resolve "./main.js" from "index.html"
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>项目已经整理成 <code class="language-plaintext highlighter-rouge">src/</code> 结构，但 <code class="language-plaintext highlighter-rouge">index.html</code> 还在引用旧路径。</p>

<p><strong>根因</strong></p>

<p>入口文件从根目录移动到了 <code class="language-plaintext highlighter-rouge">src/main.js</code>，但 HTML 没同步。</p>

<p><strong>修复方案</strong></p>

<p>把 <code class="language-plaintext highlighter-rouge">index.html</code> 入口改成：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span> <span class="na">src=</span><span class="s">"/src/main.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<hr />

<h4 id="b3apiapisjs组件或工具文件找不到">B3：<code class="language-plaintext highlighter-rouge">@/api/apis.js</code>、组件或工具文件找不到</h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to resolve import "@/api/apis.js"
</code></pre></div></div>

<p>或某些组件、工具、静态资源路径找不到。</p>

<p><strong>判断方式</strong></p>

<p>错误里出现 <code class="language-plaintext highlighter-rouge">@/xxx</code>，说明构建器正在从 <code class="language-plaintext highlighter-rouge">src/</code> 里解析路径。</p>

<p><strong>根因</strong></p>

<p>源码迁移到 <code class="language-plaintext highlighter-rouge">src/</code> 后，原本根目录下的 <code class="language-plaintext highlighter-rouge">api/</code>、<code class="language-plaintext highlighter-rouge">components/</code>、<code class="language-plaintext highlighter-rouge">utils/</code>、<code class="language-plaintext highlighter-rouge">commom/</code> 等目录没有一起进入 <code class="language-plaintext highlighter-rouge">src/</code>。</p>

<p><strong>修复方案</strong></p>

<p>把运行时需要的目录同步到 <code class="language-plaintext highlighter-rouge">src/</code>：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/api
src/components
src/utils
src/commom
src/static
src/uni.scss
</code></pre></div></div>

<hr />

<h4 id="b4sass-报-brand-theme-color-未定义">B4：Sass 报 <code class="language-plaintext highlighter-rouge">$brand-theme-color</code> 未定义</h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Undefined variable.
$brand-theme-color
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>样式编译阶段失败，变量来自项目全局 <code class="language-plaintext highlighter-rouge">uni.scss</code>。</p>

<p><strong>根因</strong></p>

<p><code class="language-plaintext highlighter-rouge">uni.scss</code> 没有放到 uni-app 构建器预期的位置，导致全局变量没有注入。</p>

<p><strong>修复方案</strong></p>

<p>确保 <code class="language-plaintext highlighter-rouge">uni.scss</code> 存在于 <code class="language-plaintext highlighter-rouge">src/uni.scss</code>，并且里面包含项目所需的全局主题变量。</p>

<hr />

<h4 id="b5vue-模板报-whitespace-was-expected">B5：Vue 模板报 <code class="language-plaintext highlighter-rouge">Whitespace was expected</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[plugin:vite:vue] Whitespace was expected.
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>错误通常会指向某个 <code class="language-plaintext highlighter-rouge">.vue</code> 文件的 template 区域。</p>

<p><strong>根因</strong></p>

<p>模板里存在格式损坏，例如：</p>

<ul>
  <li>标签没闭合</li>
  <li>属性之间缺少空格</li>
  <li>中文乱码破坏了引号</li>
  <li>拷贝过程中混入异常字符</li>
</ul>

<p><strong>修复方案</strong></p>

<p>逐个打开报错文件，按标准 Vue 模板语法修复。不要只盯 CI，因为这类问题本质是源码模板损坏。</p>

<hr />

<h3 id="分支-cgithub-pages-部署失败">分支 C：GitHub Pages 部署失败</h3>

<h4 id="c1pages-source-没有设置为-github-actions">C1：Pages Source 没有设置为 GitHub Actions</h4>

<p><strong>现象</strong></p>

<p>workflow 成功或部分成功，但 Pages 站点没有更新。</p>

<p><strong>判断方式</strong></p>

<p>进入仓库：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Settings -&gt; Pages
</code></pre></div></div>

<p>检查 Source。</p>

<p><strong>根因</strong></p>

<p>Pages 还在使用旧的分支发布模式，而不是 GitHub Actions artifact 发布。</p>

<p><strong>修复方案</strong></p>

<p>把 Source 设置为：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GitHub Actions
</code></pre></div></div>

<hr />

<h4 id="c2上传-artifact-路径不对">C2：上传 artifact 路径不对</h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Build output not found
</code></pre></div></div>

<p>或 Pages 部署后是空站点。</p>

<p><strong>判断方式</strong></p>

<p>检查构建产物实际在哪个目录：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dist/build/h5
unpackage/dist/build/h5
dist
</code></pre></div></div>

<p><strong>根因</strong></p>

<p>不同构建链的 H5 输出目录不完全一致，workflow 写死了错误目录。</p>

<p><strong>修复方案</strong></p>

<p>在 workflow 里做输出目录检测，找到真实存在的目录后再上传。</p>

<hr />

<h4 id="c3main-不允许部署到-github-pages">C3：<code class="language-plaintext highlighter-rouge">main</code> 不允许部署到 <code class="language-plaintext highlighter-rouge">github-pages</code></h4>

<p><strong>现象</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Branch "main" is not allowed to deploy to github-pages due to environment protection rules.
The deployment was rejected or didn't satisfy other protection rules.
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>错误出现在 <code class="language-plaintext highlighter-rouge">Deploy to GitHub Pages</code> 步骤。</p>

<p><strong>根因</strong></p>

<p>GitHub Environment 对 <code class="language-plaintext highlighter-rouge">github-pages</code> 设置了部署分支保护，但 <code class="language-plaintext highlighter-rouge">main</code> 没被允许。</p>

<p><strong>修复方案</strong></p>

<p>进入：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Settings -&gt; Environments -&gt; github-pages
</code></pre></div></div>

<p>在 Deployment branches and tags 里允许 <code class="language-plaintext highlighter-rouge">main</code>，或者取消不必要的限制。</p>

<hr />

<h3 id="分支-d页面能打开但没有接口数据">分支 D：页面能打开，但没有接口数据</h3>

<h4 id="d1前端仍然直连上游接口">D1：前端仍然直连上游接口</h4>

<p><strong>现象</strong></p>

<p>页面打开，但 Network 里请求的是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://tea.qingnian8.com/api/bizhi/...
</code></pre></div></div>

<p>而不是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/api/bizhi/...
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>打开浏览器 DevTools 的 Network 面板，看请求域名。</p>

<p><strong>根因</strong></p>

<p><code class="language-plaintext highlighter-rouge">VITE_API_BASE_URL</code> 没有在构建时注入，或者前端请求层没有读取它。</p>

<p><strong>修复方案</strong></p>

<p>GitHub Actions 构建步骤中增加：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">env</span><span class="pi">:</span>
  <span class="na">VITE_API_BASE_URL</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<p>前端请求层读取：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">BASE_URL</span> <span class="o">=</span> <span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">VITE_API_BASE_URL</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">https://tea.qingnian8.com/api/bizhi</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h4 id="d2上游接口-cors-不通过">D2：上游接口 CORS 不通过</h4>

<p><strong>现象</strong></p>

<p>浏览器控制台出现 CORS 错误，或者接口请求状态看似失败。</p>

<p><strong>判断方式</strong></p>

<p>同一个接口：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">curl</code> 请求成功</li>
  <li>服务端脚本请求成功</li>
  <li>浏览器请求失败</li>
</ul>

<p><strong>根因</strong></p>

<p>上游接口没有返回浏览器需要的 CORS 响应头。</p>

<p><strong>修复方案</strong></p>

<p>加自己的 Node 代理：</p>

<ul>
  <li>浏览器请求 Railway 代理</li>
  <li>Railway 代理请求上游</li>
  <li>Railway 代理给浏览器补 CORS 头</li>
</ul>

<hr />

<h4 id="d3构建变量改了但线上仍然没生效">D3：构建变量改了，但线上仍然没生效</h4>

<p><strong>现象</strong></p>

<p>GitHub Variables 已经配置了 <code class="language-plaintext highlighter-rouge">VITE_API_BASE_URL</code>，但线上 JS 仍然请求旧地址。</p>

<p><strong>判断方式</strong></p>

<p>检查 GitHub Actions 是否在变量配置后重新运行过。</p>

<p><strong>根因</strong></p>

<p>Vite 的环境变量是在构建时注入的，不是页面运行时动态读取 GitHub 设置。</p>

<p><strong>修复方案</strong></p>

<p>重新运行 GitHub Pages workflow，或者重新 push 一次触发构建。</p>

<hr />

<h3 id="分支-e首页有数据但分类列表没有数据">分支 E：首页有数据，但分类列表没有数据</h3>

<h4 id="e1h5-get-参数没有稳定序列化">E1：H5 GET 参数没有稳定序列化</h4>

<p><strong>现象</strong></p>

<p>首页分类能出来，但进入：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wuli-git.github.io/wallpaper/#/pages/classlist/classlist?id=...&amp;name=...
</code></pre></div></div>

<p>页面列表为空。</p>

<p><strong>判断方式</strong></p>

<p>直接访问代理接口时有数据，但前端页面没有：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/api/bizhi/wallList?classid=...&amp;pageNum=1&amp;pageSize=...
</code></pre></div></div>

<p><strong>根因</strong></p>

<p>H5 环境下 <code class="language-plaintext highlighter-rouge">uni.request</code> 对 GET <code class="language-plaintext highlighter-rouge">data</code> 的处理不够稳定，参数没有按预期出现在 URL 查询串里。</p>

<p><strong>修复方案</strong></p>

<p>在请求封装层里手动序列化 GET 参数：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">method</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">url</span> <span class="o">=</span> <span class="nx">appendQuery</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
  <span class="nx">data</span> <span class="o">=</span> <span class="p">{};</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h3 id="分支-f数据有了但图片不显示">分支 F：数据有了，但图片不显示</h3>

<h4 id="f1cdnqingnian8com-图片请求-err_connection_closed">F1：<code class="language-plaintext highlighter-rouge">cdn.qingnian8.com</code> 图片请求 <code class="language-plaintext highlighter-rouge">ERR_CONNECTION_CLOSED</code></h4>

<p><strong>现象</strong></p>

<p>接口数据已经返回，页面结构也渲染了，但图片不显示。控制台出现：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET https://cdn.qingnian8.com/public/xxmBizhi/... net::ERR_CONNECTION_CLOSED
</code></pre></div></div>

<p><strong>判断方式</strong></p>

<p>Network 里 API 请求成功，但图片请求失败，失败域名集中在 <code class="language-plaintext highlighter-rouge">cdn.qingnian8.com</code>。</p>

<p><strong>根因</strong></p>

<p>这已经不是 API CORS 问题，而是用户浏览器直接访问图片 CDN 时连接被关闭。页面拿到了图片 URL，但浏览器加载不到图片。</p>

<p><strong>修复方案</strong></p>

<p>继续复用 Railway 代理，增加图片代理能力：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/proxy-image?url=https%3A%2F%2Fcdn.qingnian8.com%2F...
</code></pre></div></div>

<p>同时在代理返回 API JSON 时，把里面的 CDN 图片地址改写成代理图片地址：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://cdn.qingnian8.com/...
</code></pre></div></div>

<p>改为：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/proxy-image?url=...
</code></pre></div></div>

<p>这样浏览器不再直接请求 <code class="language-plaintext highlighter-rouge">cdn.qingnian8.com</code>，而是请求自己的 Railway 代理。</p>

<hr />

<h3 id="分支-grailway-代理访问异常">分支 G：Railway 代理访问异常</h3>

<h4 id="g1打开-railway-根路径返回-404">G1：打开 Railway 根路径返回 404</h4>

<p><strong>现象</strong></p>

<p>访问：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/
</code></pre></div></div>

<p>返回 404 或 <code class="language-plaintext highlighter-rouge">Proxy route not found</code>。</p>

<p><strong>判断方式</strong></p>

<p>访问 API 路径：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/api/bizhi/classify?select=true
</code></pre></div></div>

<p>如果 API 路径能返回数据，根路径 404 不一定是问题。</p>

<p><strong>根因</strong></p>

<p>代理服务原本只设计给 <code class="language-plaintext highlighter-rouge">/api/bizhi/...</code> 使用，根路径没有业务意义。</p>

<p><strong>修复方案</strong></p>

<p>为了方便排查，可以给代理增加：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/
/health
</code></pre></div></div>

<p>返回：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>这样后续判断 Railway 是否活着会更直观。</p>

<hr />

<h4 id="g2railway-部署后请求不到代理接口">G2：Railway 部署后请求不到代理接口</h4>

<p><strong>现象</strong></p>

<p>Railway 服务启动了，但 <code class="language-plaintext highlighter-rouge">/api/bizhi/...</code> 不通。</p>

<p><strong>判断方式</strong></p>

<p>检查 Railway 服务设置里的 Root Directory。</p>

<p><strong>根因</strong></p>

<p>仓库根目录是前端项目，代理实际在 <code class="language-plaintext highlighter-rouge">proxy-server/</code>。如果 Railway 从仓库根目录启动，会找不到正确的 <code class="language-plaintext highlighter-rouge">package.json</code> 或启动脚本。</p>

<p><strong>修复方案</strong></p>

<p>Railway 的 Root Directory 设置为：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>proxy-server
</code></pre></div></div>

<hr />

<h4 id="g3代理接口-502-或上游鉴权失败">G3：代理接口 502 或上游鉴权失败</h4>

<p><strong>现象</strong></p>

<p>代理地址能访问，但返回 502，或者上游返回鉴权错误。</p>

<p><strong>判断方式</strong></p>

<p>检查 Railway Variables。</p>

<p><strong>根因</strong></p>

<p>缺少上游所需的 <code class="language-plaintext highlighter-rouge">access-key</code>，或者目标地址环境变量写错。</p>

<p><strong>修复方案</strong></p>

<p>Railway 至少配置：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UPSTREAM_ACCESS_KEY=&lt;你的上游 access-key&gt;
TARGET_ORIGIN=https://tea.qingnian8.com
TARGET_PREFIX=/api/bizhi
PROXY_PREFIX=/api/bizhi
</code></pre></div></div>

<hr />

<h2 id="六为什么这和普通静态页面部署完全不同">六、为什么这和普通静态页面部署完全不同</h2>

<p>很多人第一次做这类项目时，最大误区是：</p>

<blockquote>
  <p>“GitHub Pages 不就是上传静态文件吗？为什么这么复杂？”</p>
</blockquote>

<p>问题在于，<strong>uni-app H5 项目并不等于纯静态页面项目</strong>。</p>

<p>它和纯静态页至少有 4 个本质差异。</p>

<h3 id="61-差异一它先是工程然后才是静态产物">6.1 差异一：它先是“工程”，然后才是“静态产物”</h3>

<p>纯静态页通常是：</p>

<ul>
  <li>直接写好的 HTML/CSS/JS</li>
  <li>上传即可</li>
</ul>

<p>而 <code class="language-plaintext highlighter-rouge">uni-app</code> 项目是：</p>

<ul>
  <li>源码</li>
  <li>需要构建器转译</li>
  <li>需要生成 H5 产物</li>
</ul>

<p>也就是说，部署前必须先完成：</p>

<ul>
  <li>Vue 编译</li>
  <li>路由与资源处理</li>
  <li>SCSS 编译</li>
  <li>平台适配</li>
</ul>

<p>所以它天然需要 CI 构建链。</p>

<h3 id="62-差异二github-pages-是子路径不是根域部署">6.2 差异二：GitHub Pages 是子路径，不是根域部署</h3>

<p>纯静态页很多时候直接部署在根路径，或者资源路径手写容易控制。</p>

<p>但 <code class="language-plaintext highlighter-rouge">uni-app + Vite + GitHub Pages</code> 里，如果不明确写：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">base</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/wallpaper/</span><span class="dl">"</span>
</code></pre></div></div>

<p>资源路径就会错。</p>

<p>这在本地开发环境不明显，但线上必暴露。</p>

<h3 id="63-差异三页面本身是静态的但业务数据是动态的">6.3 差异三：页面本身是静态的，但业务数据是动态的</h3>

<p>这是最关键的一点。</p>

<p>GitHub Pages 只能托管静态文件：</p>

<ul>
  <li>HTML</li>
  <li>CSS</li>
  <li>JS</li>
  <li>图片</li>
</ul>

<p>它不会替你提供：</p>

<ul>
  <li>运行时接口</li>
  <li>服务端渲染</li>
  <li>鉴权中转</li>
  <li>跨域豁免</li>
</ul>

<p>如果前端代码运行时还要调用第三方接口，那么：</p>

<ul>
  <li>页面能部署成功</li>
  <li>不代表业务能跑通</li>
</ul>

<p>这也是为什么“页面打开了但没有数据”会成为最常见的假成功状态。</p>

<h3 id="64-差异四浏览器安全模型会接管运行时">6.4 差异四：浏览器安全模型会接管运行时</h3>

<p>本地工具、服务端脚本、curl 可以请求成功，不代表浏览器也能请求成功。</p>

<p>浏览器还会额外检查：</p>

<ul>
  <li>CORS</li>
  <li>预检请求</li>
  <li>自定义请求头是否合法</li>
  <li>目标域名是否允许该来源访问</li>
  <li>图片 CDN 是否可从用户浏览器直接访问</li>
</ul>

<p>所以一旦页面部署到 GitHub Pages，真正的运行时环境就变成了：</p>

<ul>
  <li>用户浏览器</li>
  <li>公网源站</li>
  <li>标准 Web 安全策略</li>
</ul>

<p>这和在 HBuilderX 预览、小程序端运行，是两套不同的约束模型。</p>

<hr />

<h2 id="七技术底层分析">七、技术底层分析</h2>

<h3 id="71-github-pages-的本质">7.1 GitHub Pages 的本质</h3>

<p>GitHub Pages 本质上是一个静态文件托管平台。</p>

<p>它做的事情只有两件：</p>

<ol>
  <li>接收你构建出来的产物</li>
  <li>把这些产物通过 CDN / 静态站点形式提供给浏览器访问</li>
</ol>

<p>它<strong>不执行 Node 后端逻辑</strong>，也<strong>不提供动态 API</strong>。</p>

<p>所以：</p>

<ul>
  <li>适合放前端</li>
  <li>不适合直接承载需要服务端逻辑的业务</li>
</ul>

<h3 id="72-github-actions-的本质">7.2 GitHub Actions 的本质</h3>

<p>GitHub Actions 是构建流水线，不是线上运行环境。</p>

<p>它负责：</p>

<ul>
  <li>checkout 代码</li>
  <li>安装依赖</li>
  <li>运行构建脚本</li>
  <li>上传构建产物</li>
  <li>触发 Pages 发布</li>
</ul>

<p>但它不会在页面上线后继续给你提供运行时 API。</p>

<p>也就是说：</p>

<ul>
  <li>CI 解决的是“怎么产出文件”</li>
  <li>不是“怎么处理运行中的接口调用”</li>
</ul>

<h3 id="73-cors-的本质">7.3 CORS 的本质</h3>

<p>CORS 是浏览器的安全机制。</p>

<p>当页面从：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wuli-git.github.io
</code></pre></div></div>

<p>去请求：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://tea.qingnian8.com
</code></pre></div></div>

<p>浏览器会认为这是跨源请求。<br />
即便目标接口真实有数据，只要响应头不声明允许这个来源访问，浏览器就会拦截。</p>

<p>换句话说：</p>

<ul>
  <li>服务端“能响应”</li>
  <li>不等于浏览器“允许前端读取”</li>
</ul>

<p>这也是为什么 curl 测试成功，但页面仍然空白。</p>

<h3 id="74-为什么代理能解决问题">7.4 为什么代理能解决问题</h3>

<p>加代理后，浏览器请求目标变成：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app
</code></pre></div></div>

<p>代理服务自己再去请求：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://tea.qingnian8.com
</code></pre></div></div>

<p>此时：</p>

<ul>
  <li>浏览器只和你的代理打交道</li>
  <li>代理返回你自己定义的 CORS 头</li>
  <li>上游接口变成服务器之间的通信</li>
  <li>上游 access-key 不再需要暴露给浏览器</li>
</ul>

<p>于是浏览器的跨域限制就被正确处理掉了。</p>

<p>这不是“绕过浏览器”，而是符合 Web 架构约束的标准做法。</p>

<h3 id="75-为什么图片也可能需要代理">7.5 为什么图片也可能需要代理</h3>

<p>很多人以为接口能返回数据就结束了。</p>

<p>但壁纸项目还有第二层运行时依赖：图片 CDN。</p>

<p>接口返回的数据里包含图片地址，例如：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://cdn.qingnian8.com/public/xxmBizhi/...
</code></pre></div></div>

<p>如果用户浏览器访问这个 CDN 时出现：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>net::ERR_CONNECTION_CLOSED
</code></pre></div></div>

<p>那么页面依旧会空白或只有占位结构。</p>

<p>所以这次最终代理不只代理 API，还代理图片：</p>

<ul>
  <li>API 代理解决数据和 CORS</li>
  <li>图片代理解决 CDN 客户端不可达</li>
</ul>

<p>这也是这次实践里最容易漏掉的一层。</p>

<hr />

<h2 id="八这次实践里最值得复用的经验">八、这次实践里最值得复用的经验</h2>

<h3 id="81-不要把页面能打开误认为部署完成">8.1 不要把“页面能打开”误认为“部署完成”</h3>

<p>真正的验收标准应该至少包括：</p>

<ol>
  <li>页面能打开</li>
  <li>静态资源无 404</li>
  <li>API 请求成功</li>
  <li>数据能正常渲染</li>
  <li>图片能正常加载</li>
  <li>关键交互可用</li>
</ol>

<p>只做到第 1 步，不叫业务上线。</p>

<h3 id="82-先解决可构建再解决可运行">8.2 先解决“可构建”，再解决“可运行”</h3>

<p>迁移这类项目时，不要一开始就纠结线上域名、代理、UI 空白。</p>

<p>正确节奏是：</p>

<ol>
  <li>先让项目能在 CI 里稳定 build</li>
  <li>再让 Pages 正确托管构建产物</li>
  <li>再让 API 通过代理拿到数据</li>
  <li>最后验证图片、路由、交互这些运行时细节</li>
</ol>

<p>否则会把“构建错误”和“运行时错误”混在一起，排查非常痛苦。</p>

<h3 id="83-对依赖接口的静态站代理通常不是补丁而是架构组成部分">8.3 对依赖接口的静态站，代理通常不是补丁，而是架构组成部分</h3>

<p>如果业务依赖第三方接口，而你又不能控制对方 CORS，那么：</p>

<ul>
  <li>代理层不是临时权宜之计</li>
  <li>而是正式架构的一部分</li>
</ul>

<p>从设计阶段就应该考虑进去。</p>

<h3 id="84-不要把密钥塞进公开前端">8.4 不要把密钥塞进公开前端</h3>

<p>静态前端的所有 JS 最终都会被浏览器下载。</p>

<p>所以：</p>

<ul>
  <li>GitHub Pages 上的前端代码不能真正保密</li>
  <li>access-key 这类配置应该尽量放在代理服务端</li>
  <li>Railway Variables 比写死在前端源码里更合适</li>
</ul>

<hr />

<h2 id="九最终可复用模板">九、最终可复用模板</h2>

<p>如果以后再做类似项目，可以直接复用这一套模板。</p>

<h3 id="前端侧">前端侧</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">uni-app</code> H5 构建</li>
  <li><code class="language-plaintext highlighter-rouge">vite.config.js</code> 配置 Pages 子路径</li>
  <li>GitHub Actions 构建并发布到 Pages</li>
  <li>用 <code class="language-plaintext highlighter-rouge">VITE_API_BASE_URL</code> 注入代理地址</li>
  <li>GET 请求参数在 H5 下手动序列化</li>
</ul>

<h3 id="后端侧">后端侧</h3>

<ul>
  <li>单独一个最小 Node 代理</li>
  <li>独立目录部署到 Railway / Render / Zeabur</li>
  <li>代理负责：
    <ul>
      <li>转发请求</li>
      <li>补 CORS</li>
      <li>管理上游密钥</li>
      <li>改写图片 CDN 地址</li>
      <li>提供健康检查</li>
    </ul>
  </li>
</ul>

<h3 id="配置侧">配置侧</h3>

<ul>
  <li>GitHub 仓库管理前端构建变量</li>
  <li>Railway 管理代理服务环境变量</li>
  <li>GitHub Pages Source 使用 GitHub Actions</li>
  <li><code class="language-plaintext highlighter-rouge">github-pages</code> 环境允许 <code class="language-plaintext highlighter-rouge">main</code> 分支部署</li>
</ul>

<hr />

<h2 id="十最终验收清单">十、最终验收清单</h2>

<p>上线后不要只看页面有没有打开，建议按下面顺序验收：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. GitHub Actions 是否成功
2. Pages 地址是否能打开
3. 控制台是否没有 JS / CSS 资源 404
4. Network 里的 API 域名是否为 Railway 代理
5. /api/bizhi/classify?select=true 是否返回数据
6. 分类页 /wallList 是否带上 classid 等查询参数
7. 图片 URL 是否已经变成 /proxy-image?url=...
8. 页面是否能看到真实壁纸图片
9. 刷新页面和复制链接打开是否正常
</code></pre></div></div>

<p>本项目最终验收地址：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wuli-git.github.io/wallpaper/
</code></pre></div></div>

<p>分类页示例：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wuli-git.github.io/wallpaper/#/pages/classlist/classlist?id=6524a48f6523417a8a8b825d&amp;name=可爱萌宠
</code></pre></div></div>

<p>代理健康检查：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://wallpaper-production-9537.up.railway.app/health
</code></pre></div></div>

<hr />

<h2 id="十一结语">十一、结语</h2>

<p>把 <code class="language-plaintext highlighter-rouge">uni-app</code> 项目部署到 GitHub Pages，看似只是一个“前端上线”问题，实际上它横跨了三层：</p>

<ul>
  <li>工程构建层</li>
  <li>静态托管层</li>
  <li>运行时接口层</li>
</ul>

<p>真正难的不是把文件传上去，而是理解：</p>

<ul>
  <li>哪些问题属于构建阶段</li>
  <li>哪些问题属于托管阶段</li>
  <li>哪些问题属于浏览器运行时安全模型</li>
</ul>

<p>一旦把这三层分开，整个问题就会清晰很多。</p>

<p>这次实践最终得出的结论可以浓缩成一句话：</p>

<blockquote>
  <p>Uni-app 项目部署到 GitHub Pages，真正的分界线不在“能不能发版”，而在“页面上线后是否还能拿到真实业务数据与图片资源”。</p>
</blockquote>

<p>如果只是纯静态站，GitHub Pages 足够。<br />
如果页面依赖运行时接口，那就必须把代理、CORS、图片资源可达性和部署拓扑一起设计进去。</p>

<p>从系列化的视角看：</p>

<ul>
  <li>上一篇，解决“纯静态网页如何通过 GitHub 自动构建与发布”</li>
  <li>这一篇，解决“工程化前端项目如何通过 GitHub CI/CD 稳定上线”</li>
  <li>下一步，则可以继续扩展到更复杂的项目形态，例如：
    <ul>
      <li>带后端服务的前后端分离项目</li>
      <li>带数据库的全栈应用</li>
      <li>带多环境区分的项目</li>
      <li>带灰度、回滚、监控的生产级交付流程</li>
    </ul>
  </li>
</ul>

<p>如果说静态网页部署是 GitHub CI/CD 的入门层，那么 <code class="language-plaintext highlighter-rouge">uni-app + Pages + Railway 代理</code> 这类项目就是一个非常典型的中阶过渡层：</p>

<ul>
  <li>它已经不再只是“传文件”</li>
  <li>但又还没有重到必须完整引入 Kubernetes、容器编排、复杂后端集群</li>
</ul>

<hr />

<h2 id="十二从-uni-app-再往后更复杂项目如何使用-cicd-部署">十二、从 uni-app 再往后：更复杂项目如何使用 CI/CD 部署</h2>

<p>如果把“静态网页自动部署”看成入门，把“<code class="language-plaintext highlighter-rouge">uni-app + GitHub Pages + 代理</code>”看成进阶，那么再往后，CI/CD 体系通常会继续演进到更复杂的项目类型。</p>

<p>这时重点不再只是“能不能构建并发布”，而会逐渐转向：</p>

<ul>
  <li>如何管理多个服务</li>
  <li>如何管理多个环境</li>
  <li>如何保证发布稳定性</li>
  <li>如何实现回滚、监控和灰度</li>
</ul>

<h3 id="121-前后端分离项目">12.1 前后端分离项目</h3>

<p>典型形态：</p>

<ul>
  <li>前端：React、Vue、Next.js 前端站点</li>
  <li>后端：Node.js、Java、Go、Python API 服务</li>
  <li>数据库：MySQL、PostgreSQL、Redis</li>
</ul>

<p>这类项目的 CI/CD 一般会拆成两条流水线：</p>

<ol>
  <li>
    <p>前端流水线<br />
负责安装依赖、跑测试、打包前端产物、发布到静态托管平台或前端运行平台。</p>
  </li>
  <li>
    <p>后端流水线<br />
负责安装依赖、跑单元测试、构建服务、发布到云主机、容器平台或 Serverless 平台。</p>
  </li>
</ol>

<p>相比 uni-app 这篇文章里的架构，升级点在于：</p>

<ul>
  <li>不再只有“前端 + 轻代理”</li>
  <li>而是“前端 + 正式后端 + 数据库”</li>
</ul>

<p>CI/CD 的关键关注点也会变成：</p>

<ul>
  <li>接口环境变量管理</li>
  <li>数据库连接配置</li>
  <li>前后端版本兼容</li>
  <li>后端发布成功后再切换前端</li>
</ul>

<h3 id="122-monorepo--多服务项目">12.2 Monorepo / 多服务项目</h3>

<p>当项目进一步复杂化，经常会进入 monorepo 形态，例如：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">apps/web</code></li>
  <li><code class="language-plaintext highlighter-rouge">apps/admin</code></li>
  <li><code class="language-plaintext highlighter-rouge">apps/api</code></li>
  <li><code class="language-plaintext highlighter-rouge">packages/ui</code></li>
  <li><code class="language-plaintext highlighter-rouge">packages/shared</code></li>
</ul>

<p>这类项目的 CI/CD 难点在于：</p>

<ul>
  <li>哪个服务变了，就只构建哪个服务</li>
  <li>公共包变了，哪些下游要一起重建</li>
  <li>如何避免每次提交都全量部署所有应用</li>
</ul>

<p>这时常见做法是：</p>

<ul>
  <li>按目录拆分 workflow</li>
  <li>做受影响范围检测</li>
  <li>为不同应用配置独立部署目标</li>
</ul>

<p>也就是说，流水线从“单项目直线型”开始走向“多项目选择性执行”。</p>

<h3 id="123-含数据库迁移的全栈项目">12.3 含数据库迁移的全栈项目</h3>

<p>一旦项目带数据库，并且数据结构会变化，CI/CD 就不再只是代码上线问题，还会变成数据演进问题。</p>

<p>此时典型链路会变成：</p>

<ol>
  <li>代码测试通过</li>
  <li>构建镜像或构建服务产物</li>
  <li>执行数据库迁移</li>
  <li>发布后端服务</li>
  <li>发布前端</li>
  <li>做健康检查</li>
</ol>

<p>这类项目最怕的不是构建失败，而是：</p>

<ul>
  <li>新代码已上线，但数据库未迁移</li>
  <li>数据库已迁移，但服务未成功启动</li>
  <li>某一步失败后，没有回滚策略</li>
</ul>

<p>所以这里的 CI/CD 会更强调：</p>

<ul>
  <li>迁移顺序控制</li>
  <li>发布前后健康检查</li>
  <li>失败回滚</li>
  <li>数据兼容窗口设计</li>
</ul>

<h3 id="124-生产级项目灰度回滚监控">12.4 生产级项目：灰度、回滚、监控</h3>

<p>再往上走，真正生产级的 CI/CD 通常不会停留在“push 即上线”。</p>

<p>它会继续增加：</p>

<ul>
  <li>
    <p>多环境<br />
例如 <code class="language-plaintext highlighter-rouge">dev / test / staging / production</code></p>
  </li>
  <li>
    <p>灰度发布<br />
例如先放 5% 流量，再逐步扩大</p>
  </li>
  <li>
    <p>自动回滚<br />
例如健康检查失败后自动回退到上一版本</p>
  </li>
  <li>
    <p>监控告警<br />
例如发布后自动观察错误率、延迟、接口可用性</p>
  </li>
  <li>
    <p>审批流<br />
例如生产环境发布前必须人工确认</p>
  </li>
</ul>

<p>这时，CI/CD 的本质就从“自动部署工具”进一步升级成“交付治理系统”。</p>

<hr />

<h2 id="参考资料">参考资料</h2>

<ul>
  <li>GitHub Pages 自定义工作流：<br />
https://docs.github.com/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages</li>
  <li>GitHub Actions 变量参考：<br />
https://docs.github.com/en/actions/reference/variables-reference</li>
  <li>GitHub Actions 仓库变量 API：<br />
https://docs.github.com/en/rest/actions/variables</li>
  <li>Railway Variables：<br />
https://docs.railway.com/variables</li>
  <li>Railway CLI / Deploying：<br />
https://docs.railway.com/cli/deploying</li>
</ul>]]></content><author><name>cc00mi</name></author><category term="tips  deploy CI/CD" /><summary type="html"><![CDATA[一、写在前面 如果你已经看过上一篇“静态网页如何通过 GitHub 自动构建并发布”的文章，那么这一篇可以看作它的续篇。 上一篇解决的是更基础的一层问题： 如何把纯静态网页接入 GitHub 如何使用 GitHub Actions 自动构建 如何把构建产物发布到 GitHub Pages 那一层的核心关注点是： 构建有没有跑通 静态资源路径对不对 页面能不能正常打开 而这一篇开始进入更复杂但也更真实的项目场景： 项目不再只是简单静态页面 而是 uni-app 这类需要工程化构建的前端项目 页面运行后还依赖外部 API 最终还要处理浏览器跨域、代理、图片 CDN 可达性、运行时拓扑等问题 所以如果说上一篇是在回答： “静态页面如何通过 GitHub 自动化部署？” 那么这一篇回答的就是： “当项目从纯静态页，演进到带工程化构建和运行时接口依赖的前端应用后，CI/CD 体系要如何随之升级？” 这篇文章沉淀的是一次真实的项目改造过程：把一个原本更偏 HBuilderX / uni-app 开发流的壁纸项目，改造成： 前端通过 GitHub Actions 自动构建 构建产物自动发布到 GitHub Pages 运行期接口通过独立后端代理解决 CORS 图片资源通过代理兜底解决客户端 CDN 连接失败 本次最终上线结果是： GitHub 仓库：wuli-git/wallpaper GitHub Pages 前端：https://wuli-git.github.io/wallpaper/ Railway 代理：https://wallpaper-production-9537.up.railway.app 前端 API Base：https://wallpaper-production-9537.up.railway.app/api/bizhi 这类项目的难点，不在“把静态文件传到网上”，而在于： uni-app 工程能不能在 CI 环境中稳定构建出 H5 产物 构建出来的 H5 是否能正确适配 GitHub Pages 的子路径 页面运行时依赖的接口，是否允许浏览器跨域访问 页面拿到数据后，图片资源是否也能被用户浏览器正常加载 如果只理解成“把页面部署出去”，最后通常会出现一个典型症状： 页面外壳能打开 路由能跳 但数据是空的 或者数据出来了，图片一片空白 看起来像“部署成功了”，实际上业务并没有跑通 本文会把这个问题拆透。 二、适用场景 这套 SOP 适合下面这类项目： 使用 uni-app 开发 需要发布 H5 版本 代码托管在 GitHub 想通过 GitHub Actions 做自动构建与自动部署 最终站点托管在 GitHub Pages 项目运行时依赖外部 API 上游 API 不完全受自己控制 如果你的项目只是纯静态 HTML/CSS/JS，没有接口依赖，也没有 uni-app 这层构建链，流程会简单很多。本文后面会专门讲差异。 三、最终架构图 最终落地后的架构是： 开发者 push 到 GitHub main 分支 ↓ GitHub Actions 拉取代码 ↓ 安装依赖并构建 uni-app H5 产物 ↓ 上传 Pages artifact ↓ GitHub Pages 发布静态前端 ↓ 用户访问 https://wuli-git.github.io/wallpaper/ ↓ 前端页面请求 Railway 上的 Node 代理 ↓ Node 代理转发到上游壁纸接口 ↓ 代理补齐 CORS 响应头并自动附加 access-key ↓ 代理把 CDN 图片地址改写成自己的图片代理地址 ↓ 浏览器拿到数据与图片并渲染页面 也就是说，最终不是“一个服务”，而是两个部分协同： 静态前端：GitHub Pages 动态代理：Railway 上的 Node 服务 四、完整 SOP 4.1 整理 uni-app 工程为可 CI 构建结构 首先要解决的是：原项目必须能在命令行里构建，而不是只能在 HBuilderX 图形界面里点按钮。 核心处理包括： 补齐 package.json 补齐 vite.config.js 把源码整理到 src/ 目录 确保 manifest.json、pages.json、main.js、App.vue 等位于 src/ 把 api/、components/、utils/、commom/、static/、uni.scss 等运行所需资源同步到 src/ 安装构建依赖，比如 sass 本项目最终采用的构建脚本是： { "scripts": { "dev:h5": "uni", "build:h5": "uni build" } } 为什么这一步必须做 因为 GitHub Actions 本质上只会执行脚本，它不会打开 HBuilderX 帮你点“发行到 H5”。 如果项目不能命令行构建，那么就谈不上真正的 CI/CD。 4.2 锁定一组兼容的 DCloud / Vue / Vite 依赖 这次迁移里最容易误判的一类错误，是把缺失依赖当成“少装一个包”来补。 例如依次遇到： 缺 @dcloudio/uni-cli-shared 缺 @dcloudio/uni-cli-i18n 缺 webpack 缺 semver 如果每报一个就手动 npm install 一个，最后很容易把依赖树补歪。 正确做法是：把 DCloud 相关包按同一代版本锁成一组。 本项目最终使用的核心依赖类似： { "dependencies": { "@dcloudio/uni-app": "3.0.0-5000720260410001", "@dcloudio/uni-h5": "3.0.0-5000720260410001", "@dcloudio/uni-components": "3.0.0-5000720260410001", "@dcloudio/uni-i18n": "3.0.0-5000720260410001", "vue": "3.4.21" }, "devDependencies": { "@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001", "@vitejs/plugin-vue": "5.2.4", "@vue/compiler-sfc": "3.4.21", "sass": "^1.77.8", "vite": "5.2.8" } } 注意：不要使用不存在的包或不存在的版本，例如： @dcloudio/cli @dcloudio/uni-app@^2.0.2 @dcloudio/vite-plugin-uni@^4.61.2026051901 这些会直接导致 E404 或 ETARGET。 4.3 处理 GitHub Pages 的子路径问题 GitHub Pages 的项目页通常不是部署在根路径，而是： https://用户名.github.io/仓库名/ 本项目实际地址是： https://wuli-git.github.io/wallpaper/ 所以 vite.config.js 里必须配置： import { defineConfig } from "vite"; import uniPlugin from "@dcloudio/vite-plugin-uni"; const uni = uniPlugin.default || uniPlugin; export default defineConfig({ base: "/wallpaper/", plugins: [uni()], }); 这里有两个关键点： base: "/wallpaper/" 用来适配 GitHub Pages 子路径 const uni = uniPlugin.default || uniPlugin 用来兼容插件导出形态，避免 uni is not a function 为什么这一步必须做 如果不写 base，构建产物里的资源路径默认会按根路径处理，例如： /assets/index.js 而 GitHub Pages 项目页实际需要的是： /wallpaper/assets/index.js 不修这个，页面可能会出现： CSS 丢失 JS 404 路由资源错位 页面空白 4.4 配置 GitHub Actions 自动构建与部署 推荐使用 GitHub 官方 Pages workflow，而不是老式的“把 dist 推到 gh-pages 分支”的做法。 本项目最终的 workflow 重点如下： name: Deploy to GitHub Pages on: push: branches: - main workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 18 cache: npm - name: Install dependencies run: npm ci - name: Build H5 env: VITE_API_BASE_URL: $ run: npm run build:h5 - name: Setup Pages uses: actions/configure-pages@v5 - name: Detect build output path id: detect run: | if [ -d "dist/build/h5" ]; then echo "path=dist/build/h5" &gt;&gt; "$GITHUB_OUTPUT" elif [ -d "unpackage/dist/build/h5" ]; then echo "path=unpackage/dist/build/h5" &gt;&gt; "$GITHUB_OUTPUT" elif [ -d "dist" ]; then echo "path=dist" &gt;&gt; "$GITHUB_OUTPUT" else echo "Build output not found" ls -la ls -la dist || true ls -la unpackage || true exit 1 fi - name: Create Pages fallback run: cp "$/index.html" "$/404.html" - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: $ deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: $ steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 这里比最小示例多做了两件事： 自动检测构建产物目录 不同 uni-app / Vite 组合可能输出到 dist/build/h5 或 unpackage/dist/build/h5 workflow 里自动判断，避免产物路径写死后上传失败 生成 404.html GitHub Pages 刷新 hash / history 路由时可能需要 fallback 复制一份 index.html 为 404.html 可以提升路由容错 GitHub 仓库还要做的配置 进入： Settings -&gt; Pages 把 Source 设置为： GitHub Actions 如果出现环境保护规则导致部署被拒绝，还要进入： Settings -&gt; Environments -&gt; github-pages 把 Deployment branches and tags 调整为允许 main 分支部署，或者取消不必要的分支限制。 4.5 修复原项目中的模板与编码问题 真实项目迁移到 CI 时，常见并且最费时间的问题不是“部署”，而是“代码本身不能在严格构建环境里通过”。 这次实践里主要踩到的坑有： Vue 模板存在坏掉的闭合标签 属性之间缺少空格 字符串被错误编码后破坏模板结构 中文乱码导致引号和标签异常 某些页面在原运行环境里能“凑合跑”，但在 Vite 严格解析时直接报错 典型报错长这样： [plugin:vite:vue] Whitespace was expected. 所以实际过程里，需要逐个修： src/pages/index/index.vue src/pages/user/user.vue src/pages/notice/detail.vue src/components/theme-item/theme-item.vue src/pages/preview/preview.vue src/pages/search/search.vue 这类问题的本质 不是 GitHub Pages 有问题，也不是 CI 有问题，而是： 本地某些工具链容错更高 但标准构建器更严格 CI 的价值之一，就是把这类“隐性坏代码”逼出来。 4.6 页面能打开但没有壁纸：定位运行时接口问题 这一步是最关键的认知分水岭。 部署成功后，页面出现了，但壁纸数据没有出来。 这时很多人会误判为： GitHub Pages 不支持 uni-app GitHub Actions 没构建完整 资源路径还有问题 但真实原因可能完全不同。 本项目里，前端请求逻辑原本写死为： const BASE_URL = "https://tea.qingnian8.com/api/bizhi"; 也就是说，页面加载后会在浏览器里直接请求这个上游接口。 经排查发现： 这个接口本身有数据 用 curl、服务端脚本请求是成功的 但浏览器跨域请求会被拦截 根因是接口响应头缺少： Access-Control-Allow-Origin Access-Control-Allow-Headers Access-Control-Allow-Methods 即：CORS 不通过。 4.7 为什么静态站点需要额外后端代理 既然上游接口不支持浏览器跨域，那就必须在中间加一层自己的服务。 最终采用的方案是： 前端继续部署到 GitHub Pages 代理后端部署到 Railway 代理的职责很简单： 接收来自浏览器的请求 转发给 https://tea.qingnian8.com/api/bizhi 把上游返回的数据回传 在响应中补上 CORS 头 自动附加上游接口需要的 access-key 必要时改写图片 URL，让图片也走代理 这层代理不是“可选优化”，而是浏览器环境下的必要组成部分。 4.8 代理服务的实现方式 最终代理使用一个最小 Node 服务实现，目录单独放在： proxy-server/ 原因是： 前端仓库根目录是 uni-app 代理是纯 Node 服务 把两者拆开，方便 Railway 只部署代理子目录 代理服务的核心能力 支持 OPTIONS 预检 按请求头动态回写允许的 Origin 转发查询参数 转发请求体 自动补 access-key 过滤 hop-by-hop headers 提供 / 和 /health 健康检查 提供 /proxy-image?url=... 图片代理 把上游 JSON 里的 https://cdn.qingnian8.com/... 图片地址改写为代理图片地址 这样前端最终请求的是： https://wallpaper-production-9537.up.railway.app/api/bizhi/classify?select=true 而不是直接请求上游： https://tea.qingnian8.com/api/bizhi/classify?select=true 图片最终请求的是： https://wallpaper-production-9537.up.railway.app/proxy-image?url=https%3A%2F%2Fcdn.qingnian8.com%2F... 而不是浏览器直接请求： https://cdn.qingnian8.com/... 4.9 Railway 部署代理 部署步骤如下： 登录 Railway 选择从 GitHub 仓库部署 选择仓库 wuli-git/wallpaper 把 Root Directory 设成： proxy-server 配置环境变量： ALLOWED_ORIGIN=https://wuli-git.github.io UPSTREAM_ACCESS_KEY=&lt;你的上游 access-key&gt; TARGET_ORIGIN=https://tea.qingnian8.com TARGET_PREFIX=/api/bizhi PROXY_PREFIX=/api/bizhi IMAGE_PROXY_PREFIX=/proxy-image 生成公网域名，本项目为： https://wallpaper-production-9537.up.railway.app 注意： railway.internal 这种地址不能给浏览器用 必须使用 Railway 生成的公网域名 UPSTREAM_ACCESS_KEY 不建议写进公开文章或前端构建产物，应放在 Railway 环境变量里 4.10 回填 GitHub Actions 构建变量 为了让前端在 H5 构建时自动使用代理地址，需要在 GitHub 仓库里增加变量： Settings -&gt; Secrets and variables -&gt; Actions -&gt; Variables 新增： VITE_API_BASE_URL=https://wallpaper-production-9537.up.railway.app/api/bizhi 之后重新运行 GitHub Pages workflow。 前端请求逻辑会优先读取： import.meta.env.VITE_API_BASE_URL 从而在 H5 环境中走代理，而不是直连上游。 本项目里请求层还做了一个关键修复：H5 下手动把 GET 参数拼到 URL 上，避免 uni.request 在 H5 构建后出现列表接口参数没有正确带上的情况。 核心思想类似： const BASE_URL = import.meta.env.VITE_API_BASE_URL || "https://tea.qingnian8.com/api/bizhi"; function appendQuery(url, data = {}) { const params = new URLSearchParams(); Object.keys(data).forEach((key) =&gt; { const value = data[key]; if (value !== undefined &amp;&amp; value !== null &amp;&amp; value !== "") { params.append(key, value); } }); const query = params.toString(); if (!query) return url; return `${url}${url.includes("?") ? "&amp;" : "?"}${query}`; } 五、排错分支：从报错现象定位根因 这一节是本次部署最值得沉淀的部分。 不要把所有错误混成一句“部署失败”。更好的方式是像走树一样排查：先判断错误发生在哪一层，再进入对应分支。 部署 / 上线异常 ├─ A. npm install / npm ci 阶段失败 │ ├─ A1. E404：包不存在 │ ├─ A2. ETARGET：版本不存在 │ └─ A3. 缺模块：依赖代际混乱 ├─ B. npm run build:h5 阶段失败 │ ├─ B1. uni is not a function │ ├─ B2. Could not resolve ./main.js │ ├─ B3. @ 路径找不到 │ ├─ B4. Sass 变量不存在 │ └─ B5. Vue 模板解析失败 ├─ C. GitHub Pages 部署阶段失败 │ ├─ C1. Pages source 配错 │ ├─ C2. artifact 路径不对 │ └─ C3. 环境保护规则拒绝 main ├─ D. 页面能打开，但没有接口数据 │ ├─ D1. 前端仍直连上游 │ ├─ D2. 上游 CORS 不通过 │ └─ D3. 构建变量没有注入 ├─ E. 首页有数据，但分类列表没有数据 │ └─ E1. H5 GET 参数没有稳定序列化 ├─ F. 数据有了，但图片不显示 │ └─ F1. CDN 图片连接被关闭 └─ G. Railway 代理访问异常 ├─ G1. 根路径返回 404 ├─ G2. Root Directory 配错 └─ G3. 环境变量缺失 分支 A：依赖安装失败 A1：@dcloudio/cli 报 E404 现象 npm error code E404 npm error 404 Not Found - GET https://registry.npmjs.org/@dcloudio%2fcli npm error 404 '@dcloudio/cli@...' is not in this registry. 判断方式 如果错误里出现 @dcloudio/cli，基本可以判断是包名用错。 根因 @dcloudio/cli 并不是这个项目需要安装的 npm 包。uni-app Vite 项目应该通过 @dcloudio/vite-plugin-uni 和相关 @dcloudio/uni-* 包构建。 修复方案 不要执行 npx @dcloudio/cli 不要在 package.json 里添加 @dcloudio/cli 使用 npm run build:h5 调用本项目脚本 A2：DCloud 包报 ETARGET 现象 npm error code ETARGET npm error notarget No matching version found for @dcloudio/vite-plugin-uni@^4.61.2026051901. 或者： npm error notarget No matching version found for @dcloudio/uni-app@^2.0.2. 判断方式 如果错误里出现 No matching version found，说明你请求的版本在 npm registry 里不存在。 根因 版本号是猜出来的，或者把不同代际的包混在了一起。 修复方案 统一锁定一组真实存在且互相兼容的版本，例如： @dcloudio/uni-app 3.0.0-5000720260410001 @dcloudio/uni-h5 3.0.0-5000720260410001 @dcloudio/uni-components 3.0.0-5000720260410001 @dcloudio/uni-i18n 3.0.0-5000720260410001 @dcloudio/vite-plugin-uni 3.0.0-5000720260410001 vue 3.4.21 vite 5.2.8 A3：不断缺 @dcloudio/uni-cli-shared、@dcloudio/uni-cli-i18n、webpack、semver 现象 Error: Cannot find module '@dcloudio/uni-cli-shared' Error: Cannot find module '@dcloudio/uni-cli-i18n' Error: Cannot find module 'webpack' Error: Cannot find module 'semver' 判断方式 如果每装一个包又缺下一个包，说明不是“少一个依赖”，而是依赖树整体不匹配。 根因 DCloud 相关包版本混用，导致插件内部引用的配套包没有被正确安装。 修复方案 停止一个个补包 清理错误依赖组合 重新统一 DCloud 版本 重新生成 package-lock.json 本地用 npm install 验证，CI 用 npm ci 固化安装 分支 B：构建失败 B1：uni is not a function 现象 TypeError: uni is not a function 判断方式 错误出现在 vite.config.js 里的 plugins: [uni()] 附近。 根因 @dcloudio/vite-plugin-uni 的导出形态在不同环境下可能表现为默认导出或对象导出。 修复方案 使用兼容写法： import uniPlugin from "@dcloudio/vite-plugin-uni"; const uni = uniPlugin.default || uniPlugin; B2：Could not resolve "./main.js" from "index.html" 现象 Could not resolve "./main.js" from "index.html" 判断方式 项目已经整理成 src/ 结构，但 index.html 还在引用旧路径。 根因 入口文件从根目录移动到了 src/main.js，但 HTML 没同步。 修复方案 把 index.html 入口改成： &lt;script type="module" src="/src/main.js"&gt;&lt;/script&gt; B3：@/api/apis.js、组件或工具文件找不到 现象 Failed to resolve import "@/api/apis.js" 或某些组件、工具、静态资源路径找不到。 判断方式 错误里出现 @/xxx，说明构建器正在从 src/ 里解析路径。 根因 源码迁移到 src/ 后，原本根目录下的 api/、components/、utils/、commom/ 等目录没有一起进入 src/。 修复方案 把运行时需要的目录同步到 src/： src/api src/components src/utils src/commom src/static src/uni.scss B4：Sass 报 $brand-theme-color 未定义 现象 Undefined variable. $brand-theme-color 判断方式 样式编译阶段失败，变量来自项目全局 uni.scss。 根因 uni.scss 没有放到 uni-app 构建器预期的位置，导致全局变量没有注入。 修复方案 确保 uni.scss 存在于 src/uni.scss，并且里面包含项目所需的全局主题变量。 B5：Vue 模板报 Whitespace was expected 现象 [plugin:vite:vue] Whitespace was expected. 判断方式 错误通常会指向某个 .vue 文件的 template 区域。 根因 模板里存在格式损坏，例如： 标签没闭合 属性之间缺少空格 中文乱码破坏了引号 拷贝过程中混入异常字符 修复方案 逐个打开报错文件，按标准 Vue 模板语法修复。不要只盯 CI，因为这类问题本质是源码模板损坏。 分支 C：GitHub Pages 部署失败 C1：Pages Source 没有设置为 GitHub Actions 现象 workflow 成功或部分成功，但 Pages 站点没有更新。 判断方式 进入仓库： Settings -&gt; Pages 检查 Source。 根因 Pages 还在使用旧的分支发布模式，而不是 GitHub Actions artifact 发布。 修复方案 把 Source 设置为： GitHub Actions C2：上传 artifact 路径不对 现象 Build output not found 或 Pages 部署后是空站点。 判断方式 检查构建产物实际在哪个目录： dist/build/h5 unpackage/dist/build/h5 dist 根因 不同构建链的 H5 输出目录不完全一致，workflow 写死了错误目录。 修复方案 在 workflow 里做输出目录检测，找到真实存在的目录后再上传。 C3：main 不允许部署到 github-pages 现象 Branch "main" is not allowed to deploy to github-pages due to environment protection rules. The deployment was rejected or didn't satisfy other protection rules. 判断方式 错误出现在 Deploy to GitHub Pages 步骤。 根因 GitHub Environment 对 github-pages 设置了部署分支保护，但 main 没被允许。 修复方案 进入： Settings -&gt; Environments -&gt; github-pages 在 Deployment branches and tags 里允许 main，或者取消不必要的限制。 分支 D：页面能打开，但没有接口数据 D1：前端仍然直连上游接口 现象 页面打开，但 Network 里请求的是： https://tea.qingnian8.com/api/bizhi/... 而不是： https://wallpaper-production-9537.up.railway.app/api/bizhi/... 判断方式 打开浏览器 DevTools 的 Network 面板，看请求域名。 根因 VITE_API_BASE_URL 没有在构建时注入，或者前端请求层没有读取它。 修复方案 GitHub Actions 构建步骤中增加： env: VITE_API_BASE_URL: $ 前端请求层读取： const BASE_URL = import.meta.env.VITE_API_BASE_URL || "https://tea.qingnian8.com/api/bizhi"; D2：上游接口 CORS 不通过 现象 浏览器控制台出现 CORS 错误，或者接口请求状态看似失败。 判断方式 同一个接口： curl 请求成功 服务端脚本请求成功 浏览器请求失败 根因 上游接口没有返回浏览器需要的 CORS 响应头。 修复方案 加自己的 Node 代理： 浏览器请求 Railway 代理 Railway 代理请求上游 Railway 代理给浏览器补 CORS 头 D3：构建变量改了，但线上仍然没生效 现象 GitHub Variables 已经配置了 VITE_API_BASE_URL，但线上 JS 仍然请求旧地址。 判断方式 检查 GitHub Actions 是否在变量配置后重新运行过。 根因 Vite 的环境变量是在构建时注入的，不是页面运行时动态读取 GitHub 设置。 修复方案 重新运行 GitHub Pages workflow，或者重新 push 一次触发构建。 分支 E：首页有数据，但分类列表没有数据 E1：H5 GET 参数没有稳定序列化 现象 首页分类能出来，但进入： https://wuli-git.github.io/wallpaper/#/pages/classlist/classlist?id=...&amp;name=... 页面列表为空。 判断方式 直接访问代理接口时有数据，但前端页面没有： https://wallpaper-production-9537.up.railway.app/api/bizhi/wallList?classid=...&amp;pageNum=1&amp;pageSize=... 根因 H5 环境下 uni.request 对 GET data 的处理不够稳定，参数没有按预期出现在 URL 查询串里。 修复方案 在请求封装层里手动序列化 GET 参数： if (method.toUpperCase() === "GET") { url = appendQuery(url, data); data = {}; } 分支 F：数据有了，但图片不显示 F1：cdn.qingnian8.com 图片请求 ERR_CONNECTION_CLOSED 现象 接口数据已经返回，页面结构也渲染了，但图片不显示。控制台出现： GET https://cdn.qingnian8.com/public/xxmBizhi/... net::ERR_CONNECTION_CLOSED 判断方式 Network 里 API 请求成功，但图片请求失败，失败域名集中在 cdn.qingnian8.com。 根因 这已经不是 API CORS 问题，而是用户浏览器直接访问图片 CDN 时连接被关闭。页面拿到了图片 URL，但浏览器加载不到图片。 修复方案 继续复用 Railway 代理，增加图片代理能力： /proxy-image?url=https%3A%2F%2Fcdn.qingnian8.com%2F... 同时在代理返回 API JSON 时，把里面的 CDN 图片地址改写成代理图片地址： https://cdn.qingnian8.com/... 改为： https://wallpaper-production-9537.up.railway.app/proxy-image?url=... 这样浏览器不再直接请求 cdn.qingnian8.com，而是请求自己的 Railway 代理。 分支 G：Railway 代理访问异常 G1：打开 Railway 根路径返回 404 现象 访问： https://wallpaper-production-9537.up.railway.app/ 返回 404 或 Proxy route not found。 判断方式 访问 API 路径： https://wallpaper-production-9537.up.railway.app/api/bizhi/classify?select=true 如果 API 路径能返回数据，根路径 404 不一定是问题。 根因 代理服务原本只设计给 /api/bizhi/... 使用，根路径没有业务意义。 修复方案 为了方便排查，可以给代理增加： / /health 返回： { "ok": true } 这样后续判断 Railway 是否活着会更直观。 G2：Railway 部署后请求不到代理接口 现象 Railway 服务启动了，但 /api/bizhi/... 不通。 判断方式 检查 Railway 服务设置里的 Root Directory。 根因 仓库根目录是前端项目，代理实际在 proxy-server/。如果 Railway 从仓库根目录启动，会找不到正确的 package.json 或启动脚本。 修复方案 Railway 的 Root Directory 设置为： proxy-server G3：代理接口 502 或上游鉴权失败 现象 代理地址能访问，但返回 502，或者上游返回鉴权错误。 判断方式 检查 Railway Variables。 根因 缺少上游所需的 access-key，或者目标地址环境变量写错。 修复方案 Railway 至少配置： UPSTREAM_ACCESS_KEY=&lt;你的上游 access-key&gt; TARGET_ORIGIN=https://tea.qingnian8.com TARGET_PREFIX=/api/bizhi PROXY_PREFIX=/api/bizhi 六、为什么这和普通静态页面部署完全不同 很多人第一次做这类项目时，最大误区是： “GitHub Pages 不就是上传静态文件吗？为什么这么复杂？” 问题在于，uni-app H5 项目并不等于纯静态页面项目。 它和纯静态页至少有 4 个本质差异。 6.1 差异一：它先是“工程”，然后才是“静态产物” 纯静态页通常是： 直接写好的 HTML/CSS/JS 上传即可 而 uni-app 项目是： 源码 需要构建器转译 需要生成 H5 产物 也就是说，部署前必须先完成： Vue 编译 路由与资源处理 SCSS 编译 平台适配 所以它天然需要 CI 构建链。 6.2 差异二：GitHub Pages 是子路径，不是根域部署 纯静态页很多时候直接部署在根路径，或者资源路径手写容易控制。 但 uni-app + Vite + GitHub Pages 里，如果不明确写： base: "/wallpaper/" 资源路径就会错。 这在本地开发环境不明显，但线上必暴露。 6.3 差异三：页面本身是静态的，但业务数据是动态的 这是最关键的一点。 GitHub Pages 只能托管静态文件： HTML CSS JS 图片 它不会替你提供： 运行时接口 服务端渲染 鉴权中转 跨域豁免 如果前端代码运行时还要调用第三方接口，那么： 页面能部署成功 不代表业务能跑通 这也是为什么“页面打开了但没有数据”会成为最常见的假成功状态。 6.4 差异四：浏览器安全模型会接管运行时 本地工具、服务端脚本、curl 可以请求成功，不代表浏览器也能请求成功。 浏览器还会额外检查： CORS 预检请求 自定义请求头是否合法 目标域名是否允许该来源访问 图片 CDN 是否可从用户浏览器直接访问 所以一旦页面部署到 GitHub Pages，真正的运行时环境就变成了： 用户浏览器 公网源站 标准 Web 安全策略 这和在 HBuilderX 预览、小程序端运行，是两套不同的约束模型。 七、技术底层分析 7.1 GitHub Pages 的本质 GitHub Pages 本质上是一个静态文件托管平台。 它做的事情只有两件： 接收你构建出来的产物 把这些产物通过 CDN / 静态站点形式提供给浏览器访问 它不执行 Node 后端逻辑，也不提供动态 API。 所以： 适合放前端 不适合直接承载需要服务端逻辑的业务 7.2 GitHub Actions 的本质 GitHub Actions 是构建流水线，不是线上运行环境。 它负责： checkout 代码 安装依赖 运行构建脚本 上传构建产物 触发 Pages 发布 但它不会在页面上线后继续给你提供运行时 API。 也就是说： CI 解决的是“怎么产出文件” 不是“怎么处理运行中的接口调用” 7.3 CORS 的本质 CORS 是浏览器的安全机制。 当页面从： https://wuli-git.github.io 去请求： https://tea.qingnian8.com 浏览器会认为这是跨源请求。 即便目标接口真实有数据，只要响应头不声明允许这个来源访问，浏览器就会拦截。 换句话说： 服务端“能响应” 不等于浏览器“允许前端读取” 这也是为什么 curl 测试成功，但页面仍然空白。 7.4 为什么代理能解决问题 加代理后，浏览器请求目标变成： https://wallpaper-production-9537.up.railway.app 代理服务自己再去请求： https://tea.qingnian8.com 此时： 浏览器只和你的代理打交道 代理返回你自己定义的 CORS 头 上游接口变成服务器之间的通信 上游 access-key 不再需要暴露给浏览器 于是浏览器的跨域限制就被正确处理掉了。 这不是“绕过浏览器”，而是符合 Web 架构约束的标准做法。 7.5 为什么图片也可能需要代理 很多人以为接口能返回数据就结束了。 但壁纸项目还有第二层运行时依赖：图片 CDN。 接口返回的数据里包含图片地址，例如： https://cdn.qingnian8.com/public/xxmBizhi/... 如果用户浏览器访问这个 CDN 时出现： net::ERR_CONNECTION_CLOSED 那么页面依旧会空白或只有占位结构。 所以这次最终代理不只代理 API，还代理图片： API 代理解决数据和 CORS 图片代理解决 CDN 客户端不可达 这也是这次实践里最容易漏掉的一层。 八、这次实践里最值得复用的经验 8.1 不要把“页面能打开”误认为“部署完成” 真正的验收标准应该至少包括： 页面能打开 静态资源无 404 API 请求成功 数据能正常渲染 图片能正常加载 关键交互可用 只做到第 1 步，不叫业务上线。 8.2 先解决“可构建”，再解决“可运行” 迁移这类项目时，不要一开始就纠结线上域名、代理、UI 空白。 正确节奏是： 先让项目能在 CI 里稳定 build 再让 Pages 正确托管构建产物 再让 API 通过代理拿到数据 最后验证图片、路由、交互这些运行时细节 否则会把“构建错误”和“运行时错误”混在一起，排查非常痛苦。 8.3 对依赖接口的静态站，代理通常不是补丁，而是架构组成部分 如果业务依赖第三方接口，而你又不能控制对方 CORS，那么： 代理层不是临时权宜之计 而是正式架构的一部分 从设计阶段就应该考虑进去。 8.4 不要把密钥塞进公开前端 静态前端的所有 JS 最终都会被浏览器下载。 所以： GitHub Pages 上的前端代码不能真正保密 access-key 这类配置应该尽量放在代理服务端 Railway Variables 比写死在前端源码里更合适 九、最终可复用模板 如果以后再做类似项目，可以直接复用这一套模板。 前端侧 uni-app H5 构建 vite.config.js 配置 Pages 子路径 GitHub Actions 构建并发布到 Pages 用 VITE_API_BASE_URL 注入代理地址 GET 请求参数在 H5 下手动序列化 后端侧 单独一个最小 Node 代理 独立目录部署到 Railway / Render / Zeabur 代理负责： 转发请求 补 CORS 管理上游密钥 改写图片 CDN 地址 提供健康检查 配置侧 GitHub 仓库管理前端构建变量 Railway 管理代理服务环境变量 GitHub Pages Source 使用 GitHub Actions github-pages 环境允许 main 分支部署 十、最终验收清单 上线后不要只看页面有没有打开，建议按下面顺序验收： 1. GitHub Actions 是否成功 2. Pages 地址是否能打开 3. 控制台是否没有 JS / CSS 资源 404 4. Network 里的 API 域名是否为 Railway 代理 5. /api/bizhi/classify?select=true 是否返回数据 6. 分类页 /wallList 是否带上 classid 等查询参数 7. 图片 URL 是否已经变成 /proxy-image?url=... 8. 页面是否能看到真实壁纸图片 9. 刷新页面和复制链接打开是否正常 本项目最终验收地址： https://wuli-git.github.io/wallpaper/ 分类页示例： https://wuli-git.github.io/wallpaper/#/pages/classlist/classlist?id=6524a48f6523417a8a8b825d&amp;name=可爱萌宠 代理健康检查： https://wallpaper-production-9537.up.railway.app/health 十一、结语 把 uni-app 项目部署到 GitHub Pages，看似只是一个“前端上线”问题，实际上它横跨了三层： 工程构建层 静态托管层 运行时接口层 真正难的不是把文件传上去，而是理解： 哪些问题属于构建阶段 哪些问题属于托管阶段 哪些问题属于浏览器运行时安全模型 一旦把这三层分开，整个问题就会清晰很多。 这次实践最终得出的结论可以浓缩成一句话： Uni-app 项目部署到 GitHub Pages，真正的分界线不在“能不能发版”，而在“页面上线后是否还能拿到真实业务数据与图片资源”。 如果只是纯静态站，GitHub Pages 足够。 如果页面依赖运行时接口，那就必须把代理、CORS、图片资源可达性和部署拓扑一起设计进去。 从系列化的视角看： 上一篇，解决“纯静态网页如何通过 GitHub 自动构建与发布” 这一篇，解决“工程化前端项目如何通过 GitHub CI/CD 稳定上线” 下一步，则可以继续扩展到更复杂的项目形态，例如： 带后端服务的前后端分离项目 带数据库的全栈应用 带多环境区分的项目 带灰度、回滚、监控的生产级交付流程 如果说静态网页部署是 GitHub CI/CD 的入门层，那么 uni-app + Pages + Railway 代理 这类项目就是一个非常典型的中阶过渡层： 它已经不再只是“传文件” 但又还没有重到必须完整引入 Kubernetes、容器编排、复杂后端集群 十二、从 uni-app 再往后：更复杂项目如何使用 CI/CD 部署 如果把“静态网页自动部署”看成入门，把“uni-app + GitHub Pages + 代理”看成进阶，那么再往后，CI/CD 体系通常会继续演进到更复杂的项目类型。 这时重点不再只是“能不能构建并发布”，而会逐渐转向： 如何管理多个服务 如何管理多个环境 如何保证发布稳定性 如何实现回滚、监控和灰度 12.1 前后端分离项目 典型形态： 前端：React、Vue、Next.js 前端站点 后端：Node.js、Java、Go、Python API 服务 数据库：MySQL、PostgreSQL、Redis 这类项目的 CI/CD 一般会拆成两条流水线： 前端流水线 负责安装依赖、跑测试、打包前端产物、发布到静态托管平台或前端运行平台。 后端流水线 负责安装依赖、跑单元测试、构建服务、发布到云主机、容器平台或 Serverless 平台。 相比 uni-app 这篇文章里的架构，升级点在于： 不再只有“前端 + 轻代理” 而是“前端 + 正式后端 + 数据库” CI/CD 的关键关注点也会变成： 接口环境变量管理 数据库连接配置 前后端版本兼容 后端发布成功后再切换前端 12.2 Monorepo / 多服务项目 当项目进一步复杂化，经常会进入 monorepo 形态，例如： apps/web apps/admin apps/api packages/ui packages/shared 这类项目的 CI/CD 难点在于： 哪个服务变了，就只构建哪个服务 公共包变了，哪些下游要一起重建 如何避免每次提交都全量部署所有应用 这时常见做法是： 按目录拆分 workflow 做受影响范围检测 为不同应用配置独立部署目标 也就是说，流水线从“单项目直线型”开始走向“多项目选择性执行”。 12.3 含数据库迁移的全栈项目 一旦项目带数据库，并且数据结构会变化，CI/CD 就不再只是代码上线问题，还会变成数据演进问题。 此时典型链路会变成： 代码测试通过 构建镜像或构建服务产物 执行数据库迁移 发布后端服务 发布前端 做健康检查 这类项目最怕的不是构建失败，而是： 新代码已上线，但数据库未迁移 数据库已迁移，但服务未成功启动 某一步失败后，没有回滚策略 所以这里的 CI/CD 会更强调： 迁移顺序控制 发布前后健康检查 失败回滚 数据兼容窗口设计 12.4 生产级项目：灰度、回滚、监控 再往上走，真正生产级的 CI/CD 通常不会停留在“push 即上线”。 它会继续增加： 多环境 例如 dev / test / staging / production 灰度发布 例如先放 5% 流量，再逐步扩大 自动回滚 例如健康检查失败后自动回退到上一版本 监控告警 例如发布后自动观察错误率、延迟、接口可用性 审批流 例如生产环境发布前必须人工确认 这时，CI/CD 的本质就从“自动部署工具”进一步升级成“交付治理系统”。 参考资料 GitHub Pages 自定义工作流： https://docs.github.com/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages GitHub Actions 变量参考： https://docs.github.com/en/actions/reference/variables-reference GitHub Actions 仓库变量 API： https://docs.github.com/en/rest/actions/variables Railway Variables： https://docs.railway.com/variables Railway CLI / Deploying： https://docs.railway.com/cli/deploying]]></summary></entry><entry><title type="html">GitHub上利用CI/CD自动化部署上线静态网页</title><link href="https://wuli-git.github.io/2026/05/16/GitHub%E4%B8%8A%E5%88%A9%E7%94%A8CICD%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF.html" rel="alternate" type="text/html" title="GitHub上利用CI/CD自动化部署上线静态网页" /><published>2026-05-16T00:00:00+08:00</published><updated>2026-05-16T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/05/16/GitHub%E4%B8%8A%E5%88%A9%E7%94%A8CICD%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF</id><content type="html" xml:base="https://wuli-git.github.io/2026/05/16/GitHub%E4%B8%8A%E5%88%A9%E7%94%A8CICD%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2%E4%B8%8A%E7%BA%BF.html"><![CDATA[<h2 id="一什么是-cicd-流水线">一、什么是 CI/CD 流水线？</h2>

<p><strong>CI（Continuous Integration 持续集成）</strong></p>

<p>每次提交代码到 GitHub 时，自动执行：拉取代码 → 构建 → 测试 → 检查，保证代码质量。</p>

<p><strong>CD（Continuous Deployment 持续部署）</strong></p>

<p>构建测试通过后，<strong>自动将项目部署到线上环境</strong>，无需手动上传、手动发布。</p>

<p><strong>GitHub CI/CD 流水线 = 提交代码 → 自动构建 → 自动上线</strong></p>

<p>全程自动化，一次配置，永久生效。</p>

<h2 id="二-cicd-实现效果">二、 CI/CD 实现效果</h2>

<ul>
  <li>向 <code class="language-plaintext highlighter-rouge">master</code> 分支推送代码</li>
  <li><strong>GitHub Actions 自动触发流水线</strong></li>
  <li>自动将项目部署到 GitHub Pages</li>
  <li>全球可访问在线网站</li>
  <li><strong>无需服务器、无需花钱、0 成本上线</strong></li>
</ul>

<h2 id="三流水线配置">三、流水线配置</h2>

<ol>
  <li>配置文件路径（必须固定）：</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.github/workflows/deploy.yml
</code></pre></div></div>

<ol>
  <li>配置文件</li>
</ol>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 流水线名称（可自定义，随便写）</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">GitHub CI/CD 自动部署</span>

<span class="c1"># 触发条件：当代码推送到 master 分支时自动运行</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">master</span> <span class="pi">]</span>  <span class="c1"># 你的分支名：master / main 按需修改</span>

<span class="c1"># 执行任务</span>
<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build-and-deploy</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>  <span class="c1"># 运行环境：固定写 ubuntu-latest 即可</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="c1"># 步骤1：从 GitHub 拉取最新代码</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">拉取代码</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="c1"># 步骤2：自动部署到 GitHub Pages（上线）</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">部署到 GitHub Pages</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">peaceiris/actions-gh-pages@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">github_token</span><span class="pi">:</span> <span class="s">$</span>  <span class="c1"># GitHub 自动提供的密钥，不用改</span>
          <span class="na">publish_dir</span><span class="pi">:</span> <span class="s">./</span>  <span class="c1"># 要上线的文件夹：</span>
                           <span class="c1"># ./ = 普通静态网页项目</span>
                           <span class="c1"># ./dist = Vue/React 打包目录</span>
                           <span class="c1"># ./html = 网页目录（按需改）</span>
</code></pre></div></div>

<ol>
  <li>提交推送：</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add <span class="nb">.</span>
git commit <span class="nt">-m</span> <span class="s2">"add ci/cd"</span>
git push
</code></pre></div></div>

<ol>
  <li>
    <p>修改github gh-pages源分支</p>

    <ol>
      <li>
        <p>进入仓库 → <code class="language-plaintext highlighter-rouge">Settings</code> → 左侧找到 <code class="language-plaintext highlighter-rouge">Pages</code>。</p>
      </li>
      <li>
        <p>在 <code class="language-plaintext highlighter-rouge">Build and deployment</code>部分，检查 <code class="language-plaintext highlighter-rouge">Source</code>选项：必须选择 <code class="language-plaintext highlighter-rouge">Deploy from a branch</code>。下面的 <code class="language-plaintext highlighter-rouge">Branch</code> 必须选 <code class="language-plaintext highlighter-rouge">gh-pages</code>，文件夹选 <code class="language-plaintext highlighter-rouge">/ (root)</code>。</p>

        <p><img src="/assets/images/src-img/20260517CICD.png" alt="gh-pages源分支" /></p>
      </li>
    </ol>
  </li>
  <li>
    <p>查看 Actions → 显示绿色 ✅ 即为部署成功</p>
  </li>
  <li>
    <p>访问：<code class="language-plaintext highlighter-rouge">https://你的用户名.github.io/仓库名/</code></p>
  </li>
</ol>

<p>ps: 在仓库右侧设置 Website</p>

<ol>
  <li>进入仓库首页</li>
  <li>右侧找到 About → 点击设置齿轮 ⚙️</li>
  <li>在 Website 输入你的上线地址</li>
  <li>保存</li>
</ol>

<p>仓库首页右侧会显示可点击的网站链接，读者一键体验。</p>

<h2 id="四常见问题">四、常见问题</h2>

<ol>
  <li>部署成功但访问 404？</li>
</ol>

<ul>
  <li>GitHub Pages 生效延迟 1～3 分钟</li>
  <li>检查是否有 <code class="language-plaintext highlighter-rouge">index.html</code></li>
  <li>GitHub Pages 的内容是从 <code class="language-plaintext highlighter-rouge">gh-pages</code> 分支拉取的。如果流水线执行失败，或者根本没运行，<code class="language-plaintext highlighter-rouge">gh-pages</code> 分支里还是旧的（甚至是空的），访问自然是 404。去你的仓库页面，点 <code class="language-plaintext highlighter-rouge">Actions</code>，看最近一次流水线是否成功（绿色 ✅）。去仓库分支列表，看有没有 <code class="language-plaintext highlighter-rouge">gh-pages</code> 这个分支。如果没有，说明流水线从来没成功部署过。</li>
</ul>

<ol>
  <li>部署失败（红色 ×）？</li>
</ol>

<ul>
  <li>进入仓库 Settings → Actions → General</li>
  <li>找到 Workflow permissions → 选择 Read and write permissions</li>
  <li>重新运行流水线</li>
</ul>

<ol>
  <li>如何自动更新？</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push
</code></pre></div></div>

<p>自动构建、自动上线，无需任何操作。</p>]]></content><author><name>cc00mi</name></author><category term="tips	project" /><summary type="html"><![CDATA[一、什么是 CI/CD 流水线？ CI（Continuous Integration 持续集成） 每次提交代码到 GitHub 时，自动执行：拉取代码 → 构建 → 测试 → 检查，保证代码质量。 CD（Continuous Deployment 持续部署） 构建测试通过后，自动将项目部署到线上环境，无需手动上传、手动发布。 GitHub CI/CD 流水线 = 提交代码 → 自动构建 → 自动上线 全程自动化，一次配置，永久生效。 二、 CI/CD 实现效果 向 master 分支推送代码 GitHub Actions 自动触发流水线 自动将项目部署到 GitHub Pages 全球可访问在线网站 无需服务器、无需花钱、0 成本上线 三、流水线配置 配置文件路径（必须固定）： .github/workflows/deploy.yml 配置文件 # 流水线名称（可自定义，随便写） name: GitHub CI/CD 自动部署 # 触发条件：当代码推送到 master 分支时自动运行 on: push: branches: [ master ] # 你的分支名：master / main 按需修改 # 执行任务 jobs: build-and-deploy: runs-on: ubuntu-latest # 运行环境：固定写 ubuntu-latest 即可 steps: # 步骤1：从 GitHub 拉取最新代码 - name: 拉取代码 uses: actions/checkout@v4 # 步骤2：自动部署到 GitHub Pages（上线） - name: 部署到 GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: $ # GitHub 自动提供的密钥，不用改 publish_dir: ./ # 要上线的文件夹： # ./ = 普通静态网页项目 # ./dist = Vue/React 打包目录 # ./html = 网页目录（按需改） 提交推送： git add . git commit -m "add ci/cd" git push 修改github gh-pages源分支 进入仓库 → Settings → 左侧找到 Pages。 在 Build and deployment部分，检查 Source选项：必须选择 Deploy from a branch。下面的 Branch 必须选 gh-pages，文件夹选 / (root)。 查看 Actions → 显示绿色 ✅ 即为部署成功 访问：https://你的用户名.github.io/仓库名/ ps: 在仓库右侧设置 Website 进入仓库首页 右侧找到 About → 点击设置齿轮 ⚙️ 在 Website 输入你的上线地址 保存 仓库首页右侧会显示可点击的网站链接，读者一键体验。 四、常见问题 部署成功但访问 404？ GitHub Pages 生效延迟 1～3 分钟 检查是否有 index.html GitHub Pages 的内容是从 gh-pages 分支拉取的。如果流水线执行失败，或者根本没运行，gh-pages 分支里还是旧的（甚至是空的），访问自然是 404。去你的仓库页面，点 Actions，看最近一次流水线是否成功（绿色 ✅）。去仓库分支列表，看有没有 gh-pages 这个分支。如果没有，说明流水线从来没成功部署过。 部署失败（红色 ×）？ 进入仓库 Settings → Actions → General 找到 Workflow permissions → 选择 Read and write permissions 重新运行流水线 如何自动更新？ git push 自动构建、自动上线，无需任何操作。]]></summary></entry><entry><title type="html">MCP</title><link href="https://wuli-git.github.io/2026/05/16/MCP.html" rel="alternate" type="text/html" title="MCP" /><published>2026-05-16T00:00:00+08:00</published><updated>2026-05-16T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/05/16/MCP</id><content type="html" xml:base="https://wuli-git.github.io/2026/05/16/MCP.html"><![CDATA[<h1 id="mcp">MCP</h1>

<h2 id="一先搞懂mcp不是高深技术是ai的万能连接器">一、先搞懂：MCP不是高深技术，是AI的“万能连接器”</h2>

<p>咱们先抛掉官方定义（Model Context Protocol，模型上下文协议），用一个生活化的例子类比：</p>

<p>你有一部手机（相当于我们常用的AI，比如Claude、GPT），手机本身很强大，但它不能直接用U盘、鼠标、耳机——除非有一个“充电口”（也就是接口）。以前的手机接口五花八门，有的是Micro-USB，有的是Type-C，换个设备就用不了；而MCP，就相当于给所有AI统一了一个“Type-C接口”，不管你用的是哪款AI，不管你想连接电脑文件、本地笔记，还是服务器、硬件，只要通过这个接口，就能“即插即用”。</p>

<p>简单说：<strong>MCP的核心作用，就是让AI能轻松访问我们本地的东西、调用各种工具，而且不用反复适配，一次连接，所有AI都能用</strong>。</p>

<h2 id="二小白最关心没有mcpai会束手无策">二、小白最关心：没有MCP，AI会“束手无策”？</h2>

<p>很多人可能会说，我平时用AI写文案、查资料，不用MCP也好好的啊？那是因为你用的是AI的“基础功能”，如果想让AI帮你做更贴合自己的事，没有MCP就会很麻烦。</p>

<p>举两个最常见的场景，一看就懂：</p>

<p>❌ 没有MCP的困扰：你想让AI帮你整理电脑里的本地Excel台账（没上传到网上），AI会告诉你“对不起，我无法访问你的本地文件”；你想让AI帮你查自己电脑里的笔记，它也做不到——因为AI和你的本地文件“断联”了，没有一个统一的通道能让它们沟通。</p>

<p>✅ 有了MCP的便捷：只要装一个简单的MCP“连接器”（不用懂复杂代码，跟着教程点几下就行），AI就能直接读取你电脑里的Excel、PDF、笔记，甚至能帮你运行电脑里的命令、查看日志，相当于给AI开了一个“本地权限”，让它成为你的专属助手。</p>

<h2 id="三再通俗点mcp就是ai的本地通行证">三、再通俗点：MCP就是“AI的本地通行证”</h2>

<p>我们可以把整个过程想象成：</p>

<p>AI是一个很聪明的“外援”，但它被关在“网络世界”里，进不来你的电脑（本地设备）；而MCP，就是给这个外援办了一张“本地通行证”，还配了一个“翻译官”——让AI能看懂你电脑里的文件、能调用你电脑里的工具，也能把它的指令传递给你的设备。</p>

<p>而且这张“通行证”是“通用的”，不管你换哪个AI（Claude、Cursor还是其他AI工具），只要有这张通行证，都能自由进出你的本地设备，不用再给每个AI单独办“通行证”（也就是不用反复适配各种接口）。</p>

<h2 id="四mcp底层架构">四、MCP底层架构</h2>

<ol>
  <li>核心三角色：</li>
</ol>

<p>① MCP Client（客户端）：相当于“AI的接头人”，装在AI工具里（比如Claude、Cursor），负责和MCP Server沟通，传递AI的指令（比如“帮我读一下本地Excel”），再把结果回传给AI。</p>

<p>技术逻辑：客户端内置 MCP 协议解析模块，支持 STDIO（本地直连）、HTTP/SSE（远程连接）两种传输方式，无需用户手动开发，只需配置连接参数（如服务端路径、传输方式），即可与本地服务端建立双向通信，将 AI 的自然语言指令转化为服务端可识别的标准化指令（如调用<code class="language-plaintext highlighter-rouge">read_local_file</code>工具并传入文件路径）。</p>

<p>② MCP Server（服务端）：相当于“本地设备的守门人”，装在你的电脑/服务器上，负责接收AI的指令，调用本地能力（比如读取文件、运行命令），再把结果反馈给Client。</p>

<p>技术逻辑：基于 Python 异步 IO（async/await）实现，通过<code class="language-plaintext highlighter-rouge">Server</code>类初始化服务实例，用<code class="language-plaintext highlighter-rouge">@app.tool()</code>装饰器注册本地工具（如文件读取、命令执行），工具函数需定义标准化参数（如文件路径、命令内容）和返回格式，确保客户端可识别；启动服务后，默认通过 STDIO 或 HTTP/SSE 监听请求，接收客户端指令并调用本地能力。</p>

<p>对应实操：代码中<code class="language-plaintext highlighter-rouge">app = Server("my-file-server")</code>初始化服务，<code class="language-plaintext highlighter-rouge">read_local_file</code>函数注册为工具，<code class="language-plaintext highlighter-rouge">app.run()</code>启动监听，本质就是服务端的核心实现。</p>

<p>③ 传输层：相当于“Client和Server之间的通道”，负责传递数据，不用自己搭建，MCP默认提供两种简单方式，按需选择即可。</p>

<p>技术逻辑：MCP 默认使用 JSON 格式封装数据（指令类型、工具名称、参数、返回结果），无需用户手动处理序列化 / 反序列化；STDIO 传输通过本地进程间通信（无网络依赖），HTTP/SSE 通过 TCP 端口传输，两种方式均内置错误处理（如连接中断重连、指令解析失败反馈），保障数据传输稳定。</p>

<ol>
  <li>两种传输方式（入门者优先选第一种）：</li>
</ol>

<p>① STDIO（本地直连）：最常用、最安全，不用开端口，相当于AI和电脑“直接对话”，数据全程在本地，不经过外网，适合个人使用（比如用AI读本地文件）。</p>

<p>② HTTP/SSE（远程连接）：适合多设备共享，比如同一局域网内的多台电脑，都能通过这个通道连接同一个MCP Server，适合小团队使用。</p>

<p><strong>（以 “AI 读取本地文件” 为例）</strong></p>

<ol>
  <li>服务端部署：用户通过 Python 代码启动 MCP Server，注册 “本地文件读取” 工具，服务端进入监听状态；</li>
  <li>客户端连接：AI 工具（如 Cursor）的 MCP Client 配置连接参数（选择 STDIO 传输），与本地服务端建立连接；</li>
  <li>指令传递：用户向 AI 发送指令（“读取本地笔记.md”），AI 通过 Client 将指令转化为标准化请求（指定工具<code class="language-plaintext highlighter-rouge">read_local_file</code>、参数<code class="language-plaintext highlighter-rouge">path</code>），传递给服务端；</li>
  <li>本地执行：服务端解析请求，调用注册的工具函数，执行本地文件读取操作，获取文件内容；</li>
  <li>结果反馈：服务端将执行结果（文件内容）封装为标准化响应，通过传输层返回给 Client，Client 再将结果传递给 AI，AI 整理后反馈给用户。</li>
</ol>

<p>总结：底层逻辑就是“AI→Client→传输层→Server→本地设备”，再反向返回结果，流程很简单，入门者不用写代码，也能理解整个数据传递过程。</p>

<h4 id="关键技术点">关键技术点</h4>

<ul>
  <li>工具注册机制：通过装饰器<code class="language-plaintext highlighter-rouge">@app.tool()</code>实现，本质是将本地函数（如文件读取、命令执行）注册到服务端的工具列表，AI 可通过 MCP 协议自动识别工具的功能、参数，无需手动适配；</li>
  <li>异步通信：服务端基于异步 IO 实现，可同时处理多个客户端请求（如同时读取多个文件、执行多个命令），避免阻塞；</li>
  <li>跨系统兼容：通过<code class="language-plaintext highlighter-rouge">platform</code>模块判断系统（Windows/Mac/Linux），适配不同系统的本地调用逻辑（如 Windows 用 CMD、Mac 用 bash 执行命令），这也是代码中兼容多系统的核心。</li>
</ul>

<h2 id="五mcp应用场景">五、MCP应用场景</h2>

<p>不用觉得MCP是程序员的专属，普通人、入门技术者都能用到它的核心功能，除了之前分享的基础场景，补充4个更实用的拓展场景，兼顾易用性和技术入门需求，每个场景均附完整代码教程，可直接复制运行：</p>

<ol>
  <li>本地文件问答（纯小白也能⽤）：不用把PDF、Word、笔记上传到网上（担心隐私泄露），通过MCP，AI能直接读取你电脑里的文件，帮你总结重点、提炼内容，比如你存了一堆工作笔记，让AI帮你整理成汇报，不用自己逐字看。</li>
  <li>AI辅助写代码（入门技术者重点）：如果你是新手学编程，用Cursor、VSCode的AI插件，通过MCP，AI能直接读取你电脑里的项目代码，帮你找bug、补代码，甚至帮你运行调试，不用再手动复制粘贴代码给AI；进阶一点，还能让AI调用本地编译器，实时查看代码运行结果。</li>
  <li>办公自动化（小白/入门者通用）：让AI帮你整理电脑里的Excel数据、自动生成报表，甚至帮你调用本地邮件工具发邮件，省去大量重复操作，比如每月要统计的台账，AI通过MCP读取Excel，几分钟就能整理好；入门技术者还能简单配置，让AI定时执行这些操作（比如每天自动整理前一天的文件）。</li>
  <li>局域网/小型服务器管理（入门技术者适用）：如果家里有树莓派、小型服务器，通过MCP Server部署在服务器上，AI能远程调用服务器的命令，查看进程、监控日志，甚至简单控制硬件（比如控制树莓派连接的灯光），不用手动登录服务器操作，新手也能轻松上手。</li>
</ol>

<h2 id="六使用步骤基础-仅方便理解">六、使用步骤(基础 仅方便理解)</h2>

<p>所有教程均基于Python环境（最通用，新手易操作），先完成基础环境搭建，再对应学习具体应用场景，代码可直接复制，每一行均有详细注释，不用懂复杂编程逻辑，跟着步骤走就能成功运行。</p>

<h3 id="前置准备基础环境搭建">前置准备：基础环境搭建</h3>

<p>不管哪个应用场景，都需要先完成这2步，全程5分钟搞定：</p>

<ol>
  <li>安装Python（已安装的跳过）：</li>
</ol>

<p>去Python官网（https://www.python.org/）下载对应系统版本（Windows/Mac），安装时<strong>务必勾选“Add Python to PATH”（添加到环境变量）</strong>，安装完成后，打开CMD（Windows）/终端（Mac），输入“python –version”，能显示版本号即安装成功。</p>

<ol>
  <li>安装MCP SDK（核心工具）：</li>
</ol>

<p>在CMD/终端输入以下命令，复制粘贴即可，等待1-2分钟安装完成：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pip</span> <span class="n">install</span> <span class="n">mcp</span>  <span class="c1"># 核心MCP SDK，所有场景都需要
</span><span class="n">pip</span> <span class="n">install</span> <span class="n">openpyxl</span>  <span class="c1"># 读取Excel文件必备（办公自动化场景用）
</span><span class="n">pip</span> <span class="n">install</span> <span class="n">python</span><span class="o">-</span><span class="n">dotenv</span>  <span class="c1"># 配置环境（服务器管理场景用）
</span></code></pre></div></div>

<h3 id="应用教程1本地文件读取">应用教程1：本地文件读取</h3>

<p>场景：让AI读取本地TXT、Markdown文件，总结内容、提取重点，不用上传文件，保护隐私。</p>

<p>步骤1：编写MCP Server代码（可直接复制）</p>

<p>新建一个文本文件，复制下面代码，保存为“mcp_file_server.py”（保存路径建议放在桌面，方便操作）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 导入MCP核心模块
</span><span class="kn">from</span> <span class="nn">mcp.server</span> <span class="kn">import</span> <span class="n">Server</span>
<span class="kn">from</span> <span class="nn">mcp.types</span> <span class="kn">import</span> <span class="n">Tool</span>

<span class="c1"># 1. 初始化MCP服务，名称可随便起（比如my-file-server）
</span><span class="n">app</span> <span class="o">=</span> <span class="n">Server</span><span class="p">(</span><span class="s">"my-file-server"</span><span class="p">)</span>

<span class="c1"># 2. 注册“读取本地文件”的工具（AI会自动识别这个工具）
</span><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">tool</span><span class="p">()</span>  <span class="c1"># 装饰器，告诉MCP这是一个可被AI调用的工具
</span><span class="k">async</span> <span class="k">def</span> <span class="nf">read_local_file</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""
    读取本地文件的工具（AI会看到这个注释，知道该怎么用）
    :param path: 文件的完整路径（比如C:\Users\XXX\Desktop\笔记.txt）
    :return: 文件的内容（返回给AI，供AI分析）
    """</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># 打开文件，读取内容（encoding="utf-8"避免中文乱码）
</span>        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s">"r"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">content</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
        <span class="k">return</span> <span class="sa">f</span><span class="s">"文件读取成功，内容如下：</span><span class="se">\n</span><span class="si">{</span><span class="n">content</span><span class="si">}</span><span class="s">"</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="c1"># 捕获异常（比如路径错误、文件不存在），返回错误信息，避免服务崩溃
</span>        <span class="k">return</span> <span class="sa">f</span><span class="s">"文件读取失败，原因：</span><span class="si">{</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)</span><span class="si">}</span><span class="s">"</span>

<span class="c1"># 3. 启动MCP服务（启动后，AI才能连接）
</span><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"MCP文件服务启动中..."</span><span class="p">)</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>  <span class="c1"># 启动服务，默认使用STDIO传输方式（本地直连）
</span></code></pre></div></div>

<p>步骤2：启动MCP Server</p>

<ol>
  <li>打开CMD/终端，输入“cd 桌面”（切换到文件保存路径，若保存在其他路径，替换为对应路径）；</li>
  <li>输入“python mcp_file_server.py”，看到“MCP文件服务启动中…”即启动成功，不要关闭这个CMD/终端（关闭则服务停止）。</li>
</ol>

<p>步骤3：AI客户端对接+使用</p>

<ol>
  <li>打开支持MCP的AI工具（推荐Claude桌面端、Cursor）；</li>
  <li>找到“MCP连接”选项，选择“本地连接”，传输方式默认“stdio”，点击“连接”，提示“连接成功”；</li>
  <li>在AI对话框输入指令（替换成自己的文件路径）：</li>
</ol>

<p>“帮我读取本地路径为【C:\Users\XXX\Desktop\工作笔记.md】的文件，总结里面的核心重点，分点说明”</p>

<ol>
  <li>AI会通过MCP调用本地服务，读取文件并返回总结，全程无文件上传，隐私可控。</li>
</ol>

<p>注意：文件路径务必写对（Windows用反斜杠\，Mac用正斜杠/），不会找路径就右键文件→“属性”→“位置”，复制粘贴即可。</p>

<h3 id="应用教程2办公自动化读取excel入门技术者适用">应用教程2：办公自动化（读取Excel，入门技术者适用）</h3>

<p>场景：让AI读取本地Excel文件，统计数据、生成报表，省去手动整理的麻烦，适合职场人、学生。</p>

<p>步骤1：编写MCP Server代码（可直接复制）</p>

<p>新建文本文件，复制下面代码，保存为“mcp_excel_server.py”：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 导入所需模块（MCP核心+Excel读取模块）
</span><span class="kn">from</span> <span class="nn">mcp.server</span> <span class="kn">import</span> <span class="n">Server</span>
<span class="kn">from</span> <span class="nn">mcp.types</span> <span class="kn">import</span> <span class="n">Tool</span>
<span class="kn">from</span> <span class="nn">openpyxl</span> <span class="kn">import</span> <span class="n">load_workbook</span>  <span class="c1"># 读取Excel的模块（已提前安装）
</span>
<span class="c1"># 初始化MCP服务
</span><span class="n">app</span> <span class="o">=</span> <span class="n">Server</span><span class="p">(</span><span class="s">"my-excel-server"</span><span class="p">)</span>

<span class="c1"># 注册“读取Excel并统计数据”的工具
</span><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">tool</span><span class="p">()</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">read_excel_and_analysis</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">sheet_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"Sheet1"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""
    读取本地Excel文件，统计数据并简单分析（AI会识别这个工具的用法）
    :param path: Excel文件的完整路径（比如C:\Users\XXX\Desktop\月度台账.xlsx）
    :param sheet_name: Excel的工作表名称，默认是Sheet1（可根据自己的Excel修改）
    :return: 数据统计结果（返回给AI，供AI进一步整理）
    """</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># 加载Excel文件
</span>        <span class="n">workbook</span> <span class="o">=</span> <span class="n">load_workbook</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
        <span class="c1"># 选择工作表
</span>        <span class="n">sheet</span> <span class="o">=</span> <span class="n">workbook</span><span class="p">[</span><span class="n">sheet_name</span><span class="p">]</span>
        <span class="c1"># 获取Excel的总行数、总列数
</span>        <span class="n">row_count</span> <span class="o">=</span> <span class="n">sheet</span><span class="p">.</span><span class="n">max_row</span>
        <span class="n">col_count</span> <span class="o">=</span> <span class="n">sheet</span><span class="p">.</span><span class="n">max_column</span>
        <span class="c1"># 获取表头（第一行数据）
</span>        <span class="n">headers</span> <span class="o">=</span> <span class="p">[</span><span class="n">cell</span><span class="p">.</span><span class="n">value</span> <span class="k">for</span> <span class="n">cell</span> <span class="ow">in</span> <span class="n">sheet</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span>
        <span class="c1"># 获取前10行数据（避免数据过多，可根据需求修改）
</span>        <span class="n">data</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="nb">min</span><span class="p">(</span><span class="n">row_count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">11</span><span class="p">)):</span>
            <span class="n">row_data</span> <span class="o">=</span> <span class="p">[</span><span class="n">cell</span><span class="p">.</span><span class="n">value</span> <span class="k">for</span> <span class="n">cell</span> <span class="ow">in</span> <span class="n">sheet</span><span class="p">[</span><span class="n">row</span><span class="p">]]</span>
            <span class="n">data</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">headers</span><span class="p">,</span> <span class="n">row_data</span><span class="p">)))</span>
        <span class="c1"># 关闭Excel文件
</span>        <span class="n">workbook</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>
        <span class="c1"># 返回统计结果
</span>        <span class="k">return</span> <span class="sa">f</span><span class="s">"Excel读取成功！</span><span class="se">\n</span><span class="s">工作表：</span><span class="si">{</span><span class="n">sheet_name</span><span class="si">}</span><span class="se">\n</span><span class="s">总行数：</span><span class="si">{</span><span class="n">row_count</span><span class="si">}</span><span class="se">\n</span><span class="s">总列数：</span><span class="si">{</span><span class="n">col_count</span><span class="si">}</span><span class="se">\n</span><span class="s">表头：</span><span class="si">{</span><span class="n">headers</span><span class="si">}</span><span class="se">\n</span><span class="s">前10行数据：</span><span class="si">{</span><span class="n">data</span><span class="si">}</span><span class="se">\n</span><span class="s">请帮我统计数据并生成简洁报表（分点说明）"</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="s">"Excel读取失败，原因：</span><span class="si">{</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)</span><span class="si">}</span><span class="s">"</span>

<span class="c1"># 启动服务
</span><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"MCP Excel服务启动中..."</span><span class="p">)</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>
</code></pre></div></div>

<p>步骤2：启动服务+对接AI</p>

<ol>
  <li>打开CMD/终端，切换到文件保存路径（比如“cd 桌面”）；</li>
  <li>输入“python mcp_excel_server.py”，启动服务（不要关闭CMD/终端）；</li>
  <li>AI客户端对接MCP服务（和教程1步骤3一致，连接成功即可）。</li>
</ol>

<p>步骤3：使用AI整理Excel数据</p>

<p>在AI对话框输入指令（替换成自己的Excel路径和工作表名称）：</p>

<p>“帮我读取本地路径为【C:\Users\XXX\Desktop\2026年4月台账.xlsx】的Excel文件，工作表名称是【销售数据】，统计每个产品的销售额，生成简洁报表，标注最高销售额和最低销售额”</p>

<p>AI会调用MCP服务读取Excel数据，自动统计并生成报表，全程不用手动复制粘贴数据。</p>

<h3 id="补充常见问题排查">补充：常见问题排查</h3>

<ol>
  <li>启动服务时提示“no module named ‘mcp’”：说明MCP SDK没安装成功，重新在CMD/终端输入“pip install mcp”，确保安装完成。</li>
  <li>AI连接失败：检查MCP服务是否启动（CMD/终端是否处于运行状态），传输方式是否选择“stdio”，路径是否正确。</li>
  <li>读取文件/Excel失败：检查文件路径是否正确，文件是否存在，Excel文件是否处于打开状态（关闭Excel再尝试）。</li>
  <li>命令执行失败：检查命令是否符合自己的系统（Windows和Mac/Linux命令不同），避免执行高危命令。</li>
</ol>

<h2 id="七codexclaudecursor-集成-mcp-的技术实现">七、Codex、Claude、Cursor 集成 MCP 的技术实现</h2>

<p>三款工具均<strong>原生支持 MCP 协议</strong>，集成逻辑一致（客户端集成 + 连接配置），差异仅在于客户端适配细节和功能侧重，具体如下：</p>

<h3 id="1-cursor代码编辑专用-ai集成实现">1. Cursor（代码编辑专用 AI）集成实现</h3>

<p>聚焦 “编辑器与本地项目联动”，MCP 客户端集成在编辑器内核中，与代码编辑功能深度绑定；</p>

<ol>
  <li>内置 MCP Client 模块，默认支持 STDIO 传输（无需配置端口），启动后自动检测本地运行的 MCP Server；</li>
  <li>客户端与编辑器文件管理模块联动，可直接获取当前打开的项目路径，通过 MCP 调用服务端工具，实现 “代码读取、调试、运行” 一站式操作；</li>
  <li>适配逻辑：用户打开本地项目后，Cursor 的 MCP Client 自动关联项目路径，AI 指令（如 “查错”“补全”）通过 MCP 传递给服务端，服务端读取项目文件并返回结果，实时同步到编辑器界面。</li>
</ol>

<p>ps：仅支持与 “当前打开的本地项目” 联动，未打开项目时无法通过 MCP 读取本地文件，本质是客户端对项目路径的关联限制。</p>

<h3 id="2-claude通用-ai集成实现">2. Claude（通用 AI）集成实现</h3>

<p>聚焦 “多场景本地资源调用”，MCP 客户端集成在桌面端（网页端暂不支持），侧重灵活交互；</p>

<ol>
  <li>客户端支持 STDIO、HTTP/SSE 两种传输方式，可自动检测本地 MCP Server，无需手动输入服务端路径；</li>
  <li>内置指令解析模块，可将复杂自然语言指令（如 “批量读取 Python 文件并生成文档”）转化为标准化 MCP 请求，调用服务端多个工具（文件读取、命令执行）；</li>
  <li>权限控制：默认不限制命令执行权限，需用户在服务端代码中添加高危命令拦截（如前文代码中可新增<code class="language-plaintext highlighter-rouge">if command in ["rm", "del"]</code>判断），避免风险。</li>
</ol>

<h3 id="3-codex代码生成专用-ai集成实现">3. Codex（代码生成专用 AI）集成实现</h3>

<p>聚焦 “适配本地环境的代码生成”，MCP 客户端集成在客户端 / VS Code 插件中，侧重环境适配；</p>

<ol>
  <li>客户端支持手动配置服务端路径（HTTP/SSE 传输），或自动关联 STDIO 传输的本地服务端；</li>
  <li>与本地环境检测模块联动，通过 MCP 调用服务端执行<code class="language-plaintext highlighter-rouge">pip list</code>（查看依赖）、<code class="language-plaintext highlighter-rouge">python --version</code>（查看版本）等命令，获取本地环境信息，生成适配的代码；</li>
  <li>支持与本地版本控制工具（如 Git）联动，通过 MCP 调用 Git 命令，实现 “版本状态查看、提交” 等操作，与代码生成功能深度融合。</li>
</ol>

<p>ps：客户端需确保本地编译器路径已添加到环境变量，否则通过 MCP 调用编译器运行代码时会报错，本质是客户端对本地环境的依赖适配。</p>]]></content><author><name>cc00mi</name></author><category term="basics agent" /><summary type="html"><![CDATA[MCP 一、先搞懂：MCP不是高深技术，是AI的“万能连接器” 咱们先抛掉官方定义（Model Context Protocol，模型上下文协议），用一个生活化的例子类比： 你有一部手机（相当于我们常用的AI，比如Claude、GPT），手机本身很强大，但它不能直接用U盘、鼠标、耳机——除非有一个“充电口”（也就是接口）。以前的手机接口五花八门，有的是Micro-USB，有的是Type-C，换个设备就用不了；而MCP，就相当于给所有AI统一了一个“Type-C接口”，不管你用的是哪款AI，不管你想连接电脑文件、本地笔记，还是服务器、硬件，只要通过这个接口，就能“即插即用”。 简单说：MCP的核心作用，就是让AI能轻松访问我们本地的东西、调用各种工具，而且不用反复适配，一次连接，所有AI都能用。 二、小白最关心：没有MCP，AI会“束手无策”？ 很多人可能会说，我平时用AI写文案、查资料，不用MCP也好好的啊？那是因为你用的是AI的“基础功能”，如果想让AI帮你做更贴合自己的事，没有MCP就会很麻烦。 举两个最常见的场景，一看就懂： ❌ 没有MCP的困扰：你想让AI帮你整理电脑里的本地Excel台账（没上传到网上），AI会告诉你“对不起，我无法访问你的本地文件”；你想让AI帮你查自己电脑里的笔记，它也做不到——因为AI和你的本地文件“断联”了，没有一个统一的通道能让它们沟通。 ✅ 有了MCP的便捷：只要装一个简单的MCP“连接器”（不用懂复杂代码，跟着教程点几下就行），AI就能直接读取你电脑里的Excel、PDF、笔记，甚至能帮你运行电脑里的命令、查看日志，相当于给AI开了一个“本地权限”，让它成为你的专属助手。 三、再通俗点：MCP就是“AI的本地通行证” 我们可以把整个过程想象成： AI是一个很聪明的“外援”，但它被关在“网络世界”里，进不来你的电脑（本地设备）；而MCP，就是给这个外援办了一张“本地通行证”，还配了一个“翻译官”——让AI能看懂你电脑里的文件、能调用你电脑里的工具，也能把它的指令传递给你的设备。 而且这张“通行证”是“通用的”，不管你换哪个AI（Claude、Cursor还是其他AI工具），只要有这张通行证，都能自由进出你的本地设备，不用再给每个AI单独办“通行证”（也就是不用反复适配各种接口）。 四、MCP底层架构 核心三角色： ① MCP Client（客户端）：相当于“AI的接头人”，装在AI工具里（比如Claude、Cursor），负责和MCP Server沟通，传递AI的指令（比如“帮我读一下本地Excel”），再把结果回传给AI。 技术逻辑：客户端内置 MCP 协议解析模块，支持 STDIO（本地直连）、HTTP/SSE（远程连接）两种传输方式，无需用户手动开发，只需配置连接参数（如服务端路径、传输方式），即可与本地服务端建立双向通信，将 AI 的自然语言指令转化为服务端可识别的标准化指令（如调用read_local_file工具并传入文件路径）。 ② MCP Server（服务端）：相当于“本地设备的守门人”，装在你的电脑/服务器上，负责接收AI的指令，调用本地能力（比如读取文件、运行命令），再把结果反馈给Client。 技术逻辑：基于 Python 异步 IO（async/await）实现，通过Server类初始化服务实例，用@app.tool()装饰器注册本地工具（如文件读取、命令执行），工具函数需定义标准化参数（如文件路径、命令内容）和返回格式，确保客户端可识别；启动服务后，默认通过 STDIO 或 HTTP/SSE 监听请求，接收客户端指令并调用本地能力。 对应实操：代码中app = Server("my-file-server")初始化服务，read_local_file函数注册为工具，app.run()启动监听，本质就是服务端的核心实现。 ③ 传输层：相当于“Client和Server之间的通道”，负责传递数据，不用自己搭建，MCP默认提供两种简单方式，按需选择即可。 技术逻辑：MCP 默认使用 JSON 格式封装数据（指令类型、工具名称、参数、返回结果），无需用户手动处理序列化 / 反序列化；STDIO 传输通过本地进程间通信（无网络依赖），HTTP/SSE 通过 TCP 端口传输，两种方式均内置错误处理（如连接中断重连、指令解析失败反馈），保障数据传输稳定。 两种传输方式（入门者优先选第一种）： ① STDIO（本地直连）：最常用、最安全，不用开端口，相当于AI和电脑“直接对话”，数据全程在本地，不经过外网，适合个人使用（比如用AI读本地文件）。 ② HTTP/SSE（远程连接）：适合多设备共享，比如同一局域网内的多台电脑，都能通过这个通道连接同一个MCP Server，适合小团队使用。 （以 “AI 读取本地文件” 为例） 服务端部署：用户通过 Python 代码启动 MCP Server，注册 “本地文件读取” 工具，服务端进入监听状态； 客户端连接：AI 工具（如 Cursor）的 MCP Client 配置连接参数（选择 STDIO 传输），与本地服务端建立连接； 指令传递：用户向 AI 发送指令（“读取本地笔记.md”），AI 通过 Client 将指令转化为标准化请求（指定工具read_local_file、参数path），传递给服务端； 本地执行：服务端解析请求，调用注册的工具函数，执行本地文件读取操作，获取文件内容； 结果反馈：服务端将执行结果（文件内容）封装为标准化响应，通过传输层返回给 Client，Client 再将结果传递给 AI，AI 整理后反馈给用户。 总结：底层逻辑就是“AI→Client→传输层→Server→本地设备”，再反向返回结果，流程很简单，入门者不用写代码，也能理解整个数据传递过程。 关键技术点 工具注册机制：通过装饰器@app.tool()实现，本质是将本地函数（如文件读取、命令执行）注册到服务端的工具列表，AI 可通过 MCP 协议自动识别工具的功能、参数，无需手动适配； 异步通信：服务端基于异步 IO 实现，可同时处理多个客户端请求（如同时读取多个文件、执行多个命令），避免阻塞； 跨系统兼容：通过platform模块判断系统（Windows/Mac/Linux），适配不同系统的本地调用逻辑（如 Windows 用 CMD、Mac 用 bash 执行命令），这也是代码中兼容多系统的核心。 五、MCP应用场景 不用觉得MCP是程序员的专属，普通人、入门技术者都能用到它的核心功能，除了之前分享的基础场景，补充4个更实用的拓展场景，兼顾易用性和技术入门需求，每个场景均附完整代码教程，可直接复制运行： 本地文件问答（纯小白也能⽤）：不用把PDF、Word、笔记上传到网上（担心隐私泄露），通过MCP，AI能直接读取你电脑里的文件，帮你总结重点、提炼内容，比如你存了一堆工作笔记，让AI帮你整理成汇报，不用自己逐字看。 AI辅助写代码（入门技术者重点）：如果你是新手学编程，用Cursor、VSCode的AI插件，通过MCP，AI能直接读取你电脑里的项目代码，帮你找bug、补代码，甚至帮你运行调试，不用再手动复制粘贴代码给AI；进阶一点，还能让AI调用本地编译器，实时查看代码运行结果。 办公自动化（小白/入门者通用）：让AI帮你整理电脑里的Excel数据、自动生成报表，甚至帮你调用本地邮件工具发邮件，省去大量重复操作，比如每月要统计的台账，AI通过MCP读取Excel，几分钟就能整理好；入门技术者还能简单配置，让AI定时执行这些操作（比如每天自动整理前一天的文件）。 局域网/小型服务器管理（入门技术者适用）：如果家里有树莓派、小型服务器，通过MCP Server部署在服务器上，AI能远程调用服务器的命令，查看进程、监控日志，甚至简单控制硬件（比如控制树莓派连接的灯光），不用手动登录服务器操作，新手也能轻松上手。 六、使用步骤(基础 仅方便理解) 所有教程均基于Python环境（最通用，新手易操作），先完成基础环境搭建，再对应学习具体应用场景，代码可直接复制，每一行均有详细注释，不用懂复杂编程逻辑，跟着步骤走就能成功运行。 前置准备：基础环境搭建 不管哪个应用场景，都需要先完成这2步，全程5分钟搞定： 安装Python（已安装的跳过）： 去Python官网（https://www.python.org/）下载对应系统版本（Windows/Mac），安装时务必勾选“Add Python to PATH”（添加到环境变量），安装完成后，打开CMD（Windows）/终端（Mac），输入“python –version”，能显示版本号即安装成功。 安装MCP SDK（核心工具）： 在CMD/终端输入以下命令，复制粘贴即可，等待1-2分钟安装完成： pip install mcp # 核心MCP SDK，所有场景都需要 pip install openpyxl # 读取Excel文件必备（办公自动化场景用） pip install python-dotenv # 配置环境（服务器管理场景用） 应用教程1：本地文件读取 场景：让AI读取本地TXT、Markdown文件，总结内容、提取重点，不用上传文件，保护隐私。 步骤1：编写MCP Server代码（可直接复制） 新建一个文本文件，复制下面代码，保存为“mcp_file_server.py”（保存路径建议放在桌面，方便操作）： # 导入MCP核心模块 from mcp.server import Server from mcp.types import Tool # 1. 初始化MCP服务，名称可随便起（比如my-file-server） app = Server("my-file-server") # 2. 注册“读取本地文件”的工具（AI会自动识别这个工具） @app.tool() # 装饰器，告诉MCP这是一个可被AI调用的工具 async def read_local_file(path: str) -&gt; str: """ 读取本地文件的工具（AI会看到这个注释，知道该怎么用） :param path: 文件的完整路径（比如C:\Users\XXX\Desktop\笔记.txt） :return: 文件的内容（返回给AI，供AI分析） """ try: # 打开文件，读取内容（encoding="utf-8"避免中文乱码） with open(path, "r", encoding="utf-8") as f: content = f.read() return f"文件读取成功，内容如下：\n{content}" except Exception as e: # 捕获异常（比如路径错误、文件不存在），返回错误信息，避免服务崩溃 return f"文件读取失败，原因：{str(e)}" # 3. 启动MCP服务（启动后，AI才能连接） if __name__ == "__main__": print("MCP文件服务启动中...") app.run() # 启动服务，默认使用STDIO传输方式（本地直连） 步骤2：启动MCP Server 打开CMD/终端，输入“cd 桌面”（切换到文件保存路径，若保存在其他路径，替换为对应路径）； 输入“python mcp_file_server.py”，看到“MCP文件服务启动中…”即启动成功，不要关闭这个CMD/终端（关闭则服务停止）。 步骤3：AI客户端对接+使用 打开支持MCP的AI工具（推荐Claude桌面端、Cursor）； 找到“MCP连接”选项，选择“本地连接”，传输方式默认“stdio”，点击“连接”，提示“连接成功”； 在AI对话框输入指令（替换成自己的文件路径）： “帮我读取本地路径为【C:\Users\XXX\Desktop\工作笔记.md】的文件，总结里面的核心重点，分点说明” AI会通过MCP调用本地服务，读取文件并返回总结，全程无文件上传，隐私可控。 注意：文件路径务必写对（Windows用反斜杠\，Mac用正斜杠/），不会找路径就右键文件→“属性”→“位置”，复制粘贴即可。 应用教程2：办公自动化（读取Excel，入门技术者适用） 场景：让AI读取本地Excel文件，统计数据、生成报表，省去手动整理的麻烦，适合职场人、学生。 步骤1：编写MCP Server代码（可直接复制） 新建文本文件，复制下面代码，保存为“mcp_excel_server.py”： # 导入所需模块（MCP核心+Excel读取模块） from mcp.server import Server from mcp.types import Tool from openpyxl import load_workbook # 读取Excel的模块（已提前安装） # 初始化MCP服务 app = Server("my-excel-server") # 注册“读取Excel并统计数据”的工具 @app.tool() async def read_excel_and_analysis(path: str, sheet_name: str = "Sheet1") -&gt; str: """ 读取本地Excel文件，统计数据并简单分析（AI会识别这个工具的用法） :param path: Excel文件的完整路径（比如C:\Users\XXX\Desktop\月度台账.xlsx） :param sheet_name: Excel的工作表名称，默认是Sheet1（可根据自己的Excel修改） :return: 数据统计结果（返回给AI，供AI进一步整理） """ try: # 加载Excel文件 workbook = load_workbook(path) # 选择工作表 sheet = workbook[sheet_name] # 获取Excel的总行数、总列数 row_count = sheet.max_row col_count = sheet.max_column # 获取表头（第一行数据） headers = [cell.value for cell in sheet[1]] # 获取前10行数据（避免数据过多，可根据需求修改） data = [] for row in range(2, min(row_count + 1, 11)): row_data = [cell.value for cell in sheet[row]] data.append(dict(zip(headers, row_data))) # 关闭Excel文件 workbook.close() # 返回统计结果 return f"Excel读取成功！\n工作表：{sheet_name}\n总行数：{row_count}\n总列数：{col_count}\n表头：{headers}\n前10行数据：{data}\n请帮我统计数据并生成简洁报表（分点说明）" except Exception as e: return f"Excel读取失败，原因：{str(e)}" # 启动服务 if __name__ == "__main__": print("MCP Excel服务启动中...") app.run() 步骤2：启动服务+对接AI 打开CMD/终端，切换到文件保存路径（比如“cd 桌面”）； 输入“python mcp_excel_server.py”，启动服务（不要关闭CMD/终端）； AI客户端对接MCP服务（和教程1步骤3一致，连接成功即可）。 步骤3：使用AI整理Excel数据 在AI对话框输入指令（替换成自己的Excel路径和工作表名称）： “帮我读取本地路径为【C:\Users\XXX\Desktop\2026年4月台账.xlsx】的Excel文件，工作表名称是【销售数据】，统计每个产品的销售额，生成简洁报表，标注最高销售额和最低销售额” AI会调用MCP服务读取Excel数据，自动统计并生成报表，全程不用手动复制粘贴数据。 补充：常见问题排查 启动服务时提示“no module named ‘mcp’”：说明MCP SDK没安装成功，重新在CMD/终端输入“pip install mcp”，确保安装完成。 AI连接失败：检查MCP服务是否启动（CMD/终端是否处于运行状态），传输方式是否选择“stdio”，路径是否正确。 读取文件/Excel失败：检查文件路径是否正确，文件是否存在，Excel文件是否处于打开状态（关闭Excel再尝试）。 命令执行失败：检查命令是否符合自己的系统（Windows和Mac/Linux命令不同），避免执行高危命令。 七、Codex、Claude、Cursor 集成 MCP 的技术实现 三款工具均原生支持 MCP 协议，集成逻辑一致（客户端集成 + 连接配置），差异仅在于客户端适配细节和功能侧重，具体如下： 1. Cursor（代码编辑专用 AI）集成实现 聚焦 “编辑器与本地项目联动”，MCP 客户端集成在编辑器内核中，与代码编辑功能深度绑定； 内置 MCP Client 模块，默认支持 STDIO 传输（无需配置端口），启动后自动检测本地运行的 MCP Server； 客户端与编辑器文件管理模块联动，可直接获取当前打开的项目路径，通过 MCP 调用服务端工具，实现 “代码读取、调试、运行” 一站式操作； 适配逻辑：用户打开本地项目后，Cursor 的 MCP Client 自动关联项目路径，AI 指令（如 “查错”“补全”）通过 MCP 传递给服务端，服务端读取项目文件并返回结果，实时同步到编辑器界面。 ps：仅支持与 “当前打开的本地项目” 联动，未打开项目时无法通过 MCP 读取本地文件，本质是客户端对项目路径的关联限制。 2. Claude（通用 AI）集成实现 聚焦 “多场景本地资源调用”，MCP 客户端集成在桌面端（网页端暂不支持），侧重灵活交互； 客户端支持 STDIO、HTTP/SSE 两种传输方式，可自动检测本地 MCP Server，无需手动输入服务端路径； 内置指令解析模块，可将复杂自然语言指令（如 “批量读取 Python 文件并生成文档”）转化为标准化 MCP 请求，调用服务端多个工具（文件读取、命令执行）； 权限控制：默认不限制命令执行权限，需用户在服务端代码中添加高危命令拦截（如前文代码中可新增if command in ["rm", "del"]判断），避免风险。 3. Codex（代码生成专用 AI）集成实现 聚焦 “适配本地环境的代码生成”，MCP 客户端集成在客户端 / VS Code 插件中，侧重环境适配； 客户端支持手动配置服务端路径（HTTP/SSE 传输），或自动关联 STDIO 传输的本地服务端； 与本地环境检测模块联动，通过 MCP 调用服务端执行pip list（查看依赖）、python --version（查看版本）等命令，获取本地环境信息，生成适配的代码； 支持与本地版本控制工具（如 Git）联动，通过 MCP 调用 Git 命令，实现 “版本状态查看、提交” 等操作，与代码生成功能深度融合。 ps：客户端需确保本地编译器路径已添加到环境变量，否则通过 MCP 调用编译器运行代码时会报错，本质是客户端对本地环境的依赖适配。]]></summary></entry><entry><title type="html">流式输出</title><link href="https://wuli-git.github.io/2026/05/14/%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA.html" rel="alternate" type="text/html" title="流式输出" /><published>2026-05-14T00:00:00+08:00</published><updated>2026-05-14T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/05/14/%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA</id><content type="html" xml:base="https://wuli-git.github.io/2026/05/14/%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA.html"><![CDATA[<h1 id="流式输出">流式输出</h1>

<h2 id="一什么是流式输出">一、什么是流式输出？</h2>

<p>​	流式输出是一种边生成、边传输、边展示的数据处理模式，核心是将完整内容拆分为小数据块，生成一块推送一块，无需等待全部内容就绪，最典型的场景就是AI对话的逐字输出效果。它与批量输出（全部生成完一次性返回）的核心区别的是，流式输出<strong>首字节返回快、无需缓存全文、可随时中断</strong>，而批量输出<strong>延迟高、需缓存完整内容、无法中途终止</strong>。</p>

<p>​	类比来说，流式输出类似自来水边流边用，批量输出则类似等待餐品全部做好后再取用。</p>

<h2 id="二sse规范是什么">二、SSE规范是什么？</h2>

<p>​	SSE（服务端发送事件）是实现流式输出最常用、最适合入门的标准化方案，基于HTTP协议实现，无需复杂握手流程。其核心规范包括两部分：</p>

<p>1.，必须的响应头，需包含</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache  //禁止缓存
Connection: keep-alive  //保持长连接
</span></code></pre></div></div>

<ol>
  <li>
    <p>数据格式</p>

    <p>每条消息由<strong>一行或多行字段</strong>组成，<strong>以空行 \n\n 结束</strong>，标识单条消息结束</p>

    <p>4 个标准字段（仅这 4 个有效）</p>

    <p>1）<strong>data: 内容</strong>（必选，最常用）</p>

    <p>承载消息正文，<strong>UTF-8 文本</strong>，每行都以 <code class="language-plaintext highlighter-rouge">data:</code> 开头</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data: 第一行
data: 第二行
\n
</code></pre></div>    </div>

    <p>2）<strong>event: 事件名</strong>（可选）</p>

    <p>自定义事件类型，前端 <code class="language-plaintext highlighter-rouge">addEventListener</code> 监听，不写默认是 <code class="language-plaintext highlighter-rouge">message</code> 事件</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>event: update
data: {"status":"ok"}
\n
</code></pre></div>    </div>

    <p>3）<strong>id: 字符串</strong>（可选）</p>

    <p>消息唯一 ID，用于<strong>断点续传</strong>，客户端重连时自动带请求头：<code class="language-plaintext highlighter-rouge">Last-Event-ID: 上次的id</code></p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id: 123
data: hello
\n
</code></pre></div>    </div>

    <p>4）<strong>retry: 毫秒</strong>（可选）</p>

    <p>客户端断线后<strong>自动重连间隔</strong>，默认：浏览器自定（通常 3–5 秒）</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>retry: 3000
data: reconnect in 3s
\n
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="三实操">三、实操</h2>

<p>前后端如何实现SSE流式连接？完整流程分为四步：</p>

<ol>
  <li>
    <p>前端发起连接，通过浏览器原生EventSource对象，指定服务端接口地址（如示例地址http://127.0.0.1:5000/stream），自动发起HTTP请求并监听连接；</p>

    <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 前端</span>
<span class="kd">const</span> <span class="nx">es</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://127.0.0.1:5000/stream</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>后端响应并维持长连接，通过Flask框架编写接口，返回SSE标准响应头，利用yield关键字逐段生成并推送数据（yield与return的区别是，yield可暂停推送并保持连接，return会一次性返回并关闭连接）；</p>
  </li>
</ol>

<p>后端返回必须带这三个头，<strong>标志这是 SSE 长连接</strong>：</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">   Content-Type: text/event-stream
   Cache-Control: no-cache
   Connection: keep-alive
</span></code></pre></div></div>

<p>浏览器看到这个头，就知道：<strong>不要关闭连接，持续监听后端发的数据</strong></p>

<p>后端保持连接不断开，循环生成数据</p>

<p>Python 后端用 <code class="language-plaintext highlighter-rouge">yield</code> 逐段产出：</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="k">def</span> <span class="nf">sse_gen</span><span class="p">():</span>
       <span class="k">yield</span> <span class="s">"data: 第一段内容</span><span class="se">\n\n</span><span class="s">"</span>
       <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
       <span class="k">yield</span> <span class="s">"data: 第二段内容</span><span class="se">\n\n</span><span class="s">"</span>
</code></pre></div></div>

<p>后端按 SSE 规范发数据块</p>

<p>每条消息格式固定，必须以<strong>两个换行 <code class="language-plaintext highlighter-rouge">\n\n</code></strong> 结尾：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   data: 你好呀\n\n
</code></pre></div></div>

<p>浏览器收到 <code class="language-plaintext highlighter-rouge">\n\n</code> 就认为<strong>一条消息结束</strong>，触发前端事件。</p>

<ol>
  <li>数据推送与渲染，后端按SSE规范推送数据，前端通过监听EventSource的onmessage事件，实时接收并渲染数据，实现流式展示；4. 连接保活与重连，后端定时发送心跳信息，网络中断时浏览器会按retry配置自动重连，并携带上一次接收的消息ID实现断点续传。</li>
</ol>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="nx">es</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
       <span class="c1">// e.data 就是后端发的 data 内容</span>
       <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">box</span><span class="dl">"</span><span class="p">).</span><span class="nx">innerText</span> <span class="o">+=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
   <span class="p">};</span>
</code></pre></div></div>

<p>后端定时发 <code class="language-plaintext highlighter-rouge">: heartbeat\n\n</code> 心跳，防止网关断开长连接；如果网络断了，<strong>浏览器自动重连</strong>，不用前端写重连逻辑。</p>

<p>SSE 本质就是：前端 <code class="language-plaintext highlighter-rouge">EventSource</code> 发起普通 HTTP 请求→后端返回 SSE 专属头，<strong>保持长连接不关闭</strong>→后端按 <code class="language-plaintext highlighter-rouge">data:xxx\n\n</code> 格式<strong>逐段推数据</strong>→前端监听 <code class="language-plaintext highlighter-rouge">onmessage</code>，收到一段渲染一段</p>

<h2 id="四补充说明">四、补充说明</h2>

<p>​	流式输出的核心优势的是低延迟、省内存、体验好、抗超时，适用于AI对话、实时日志、大文件下载等场景；其与WebSocket的区别是，SSE是单向推送、基于HTTP、自动重连、操作简单，WebSocket是双向通信、基于独立协议、需手动重连、复杂度高。此外，提供了两种Python流式输出代码，分别是控制台本地模拟流式打字效果，以及Flask+SSE接口流式返回（含前端测试代码），可根据需求选择使用。</p>

<ol>
  <li>本地控制台流式输出</li>
</ol>

<p>原理：逐个字符输出，加延时，不一次性打印全部</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">sys</span>

<span class="k">def</span> <span class="nf">stream_output</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">delay</span><span class="o">=</span><span class="mf">0.05</span><span class="p">):</span>
    <span class="s">"""逐字符流式输出"""</span>
    <span class="k">for</span> <span class="n">char</span> <span class="ow">in</span> <span class="n">text</span><span class="p">:</span>
        <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">char</span><span class="p">)</span>   <span class="c1"># 逐个写入
</span>        <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span>       <span class="c1"># 强制刷新缓冲区
</span>        <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">delay</span><span class="p">)</span>
    <span class="k">print</span><span class="p">()</span>  <span class="c1"># 换行结束
</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">content</span> <span class="o">=</span> <span class="s">"你好，这是Python实现的流式输出效果，像AI逐字打字一样～"</span>
    <span class="n">stream_output</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
</code></pre></div></div>

<ol>
  <li>Flask + SSE 接口流式输出</li>
</ol>

<p><strong>服务端代码 server.py</strong></p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">Response</span>
<span class="kn">import</span> <span class="nn">time</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="c1"># 模拟大模型逐段生成文本
</span><span class="k">def</span> <span class="nf">generate_stream</span><span class="p">():</span>
    <span class="n">sentences</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s">"哈喽～"</span><span class="p">,</span>
        <span class="s">"我是流式输出接口"</span><span class="p">,</span>
        <span class="s">"采用SSE协议逐块推送数据"</span><span class="p">,</span>
        <span class="s">"不需要等待全部生成完"</span>
    <span class="p">]</span>
    <span class="k">for</span> <span class="n">sent</span> <span class="ow">in</span> <span class="n">sentences</span><span class="p">:</span>
        <span class="c1"># SSE 固定格式：data:xxx\n\n
</span>        <span class="k">yield</span> <span class="sa">f</span><span class="s">"data: </span><span class="si">{</span><span class="n">sent</span><span class="si">}</span><span class="se">\n\n</span><span class="s">"</span>
        <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.8</span><span class="p">)</span>

<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="s">'/stream'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">stream</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">Response</span><span class="p">(</span>
        <span class="n">generate_stream</span><span class="p">(),</span>
        <span class="n">mimetype</span><span class="o">=</span><span class="s">"text/event-stream"</span><span class="p">,</span>  <span class="c1"># 标识SSE流式
</span>        <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Cache-Control"</span><span class="p">:</span> <span class="s">"no-cache"</span><span class="p">,</span> <span class="s">"Connection"</span><span class="p">:</span> <span class="s">"keep-alive"</span><span class="p">}</span>
    <span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">app</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>前端测试 html</strong></p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html&gt;</span>
<span class="nt">&lt;body&gt;</span>
<span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"content"</span><span class="nt">&gt;&lt;/div&gt;</span>

<span class="nt">&lt;script&gt;</span>
<span class="kd">const</span> <span class="nx">evtSource</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://127.0.0.1:5000/stream</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">evtSource</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">content</span><span class="dl">"</span><span class="p">).</span><span class="nx">innerText</span> <span class="o">+=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
<span class="p">};</span>
<span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>]]></content><author><name>cc00mi</name></author><category term="streaming" /><category term="output、SSE、basics" /><summary type="html"><![CDATA[流式输出 一、什么是流式输出？ ​ 流式输出是一种边生成、边传输、边展示的数据处理模式，核心是将完整内容拆分为小数据块，生成一块推送一块，无需等待全部内容就绪，最典型的场景就是AI对话的逐字输出效果。它与批量输出（全部生成完一次性返回）的核心区别的是，流式输出首字节返回快、无需缓存全文、可随时中断，而批量输出延迟高、需缓存完整内容、无法中途终止。 ​ 类比来说，流式输出类似自来水边流边用，批量输出则类似等待餐品全部做好后再取用。 二、SSE规范是什么？ ​ SSE（服务端发送事件）是实现流式输出最常用、最适合入门的标准化方案，基于HTTP协议实现，无需复杂握手流程。其核心规范包括两部分： 1.，必须的响应头，需包含 Content-Type: text/event-stream; charset=utf-8 Cache-Control: no-cache //禁止缓存 Connection: keep-alive //保持长连接 数据格式 每条消息由一行或多行字段组成，以空行 \n\n 结束，标识单条消息结束 4 个标准字段（仅这 4 个有效） 1）data: 内容（必选，最常用） 承载消息正文，UTF-8 文本，每行都以 data: 开头 data: 第一行 data: 第二行 \n 2）event: 事件名（可选） 自定义事件类型，前端 addEventListener 监听，不写默认是 message 事件 event: update data: {"status":"ok"} \n 3）id: 字符串（可选） 消息唯一 ID，用于断点续传，客户端重连时自动带请求头：Last-Event-ID: 上次的id id: 123 data: hello \n 4）retry: 毫秒（可选） 客户端断线后自动重连间隔，默认：浏览器自定（通常 3–5 秒） retry: 3000 data: reconnect in 3s \n 三、实操 前后端如何实现SSE流式连接？完整流程分为四步： 前端发起连接，通过浏览器原生EventSource对象，指定服务端接口地址（如示例地址http://127.0.0.1:5000/stream），自动发起HTTP请求并监听连接； // 前端 const es = new EventSource("http://127.0.0.1:5000/stream"); 后端响应并维持长连接，通过Flask框架编写接口，返回SSE标准响应头，利用yield关键字逐段生成并推送数据（yield与return的区别是，yield可暂停推送并保持连接，return会一次性返回并关闭连接）； 后端返回必须带这三个头，标志这是 SSE 长连接： Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive 浏览器看到这个头，就知道：不要关闭连接，持续监听后端发的数据 后端保持连接不断开，循环生成数据 Python 后端用 yield 逐段产出： def sse_gen(): yield "data: 第一段内容\n\n" time.sleep(1) yield "data: 第二段内容\n\n" 后端按 SSE 规范发数据块 每条消息格式固定，必须以两个换行 \n\n 结尾： data: 你好呀\n\n 浏览器收到 \n\n 就认为一条消息结束，触发前端事件。 数据推送与渲染，后端按SSE规范推送数据，前端通过监听EventSource的onmessage事件，实时接收并渲染数据，实现流式展示；4. 连接保活与重连，后端定时发送心跳信息，网络中断时浏览器会按retry配置自动重连，并携带上一次接收的消息ID实现断点续传。 es.onmessage = function(e) { // e.data 就是后端发的 data 内容 document.getElementById("box").innerText += e.data; }; 后端定时发 : heartbeat\n\n 心跳，防止网关断开长连接；如果网络断了，浏览器自动重连，不用前端写重连逻辑。 SSE 本质就是：前端 EventSource 发起普通 HTTP 请求→后端返回 SSE 专属头，保持长连接不关闭→后端按 data:xxx\n\n 格式逐段推数据→前端监听 onmessage，收到一段渲染一段 四、补充说明 ​ 流式输出的核心优势的是低延迟、省内存、体验好、抗超时，适用于AI对话、实时日志、大文件下载等场景；其与WebSocket的区别是，SSE是单向推送、基于HTTP、自动重连、操作简单，WebSocket是双向通信、基于独立协议、需手动重连、复杂度高。此外，提供了两种Python流式输出代码，分别是控制台本地模拟流式打字效果，以及Flask+SSE接口流式返回（含前端测试代码），可根据需求选择使用。 本地控制台流式输出 原理：逐个字符输出，加延时，不一次性打印全部 import time import sys def stream_output(text, delay=0.05): """逐字符流式输出""" for char in text: sys.stdout.write(char) # 逐个写入 sys.stdout.flush() # 强制刷新缓冲区 time.sleep(delay) print() # 换行结束 if __name__ == "__main__": content = "你好，这是Python实现的流式输出效果，像AI逐字打字一样～" stream_output(content) Flask + SSE 接口流式输出 服务端代码 server.py from flask import Flask, Response import time app = Flask(__name__) # 模拟大模型逐段生成文本 def generate_stream(): sentences = [ "哈喽～", "我是流式输出接口", "采用SSE协议逐块推送数据", "不需要等待全部生成完" ] for sent in sentences: # SSE 固定格式：data:xxx\n\n yield f"data: {sent}\n\n" time.sleep(0.8) @app.route('/stream') def stream(): return Response( generate_stream(), mimetype="text/event-stream", # 标识SSE流式 headers={"Cache-Control": "no-cache", "Connection": "keep-alive"} ) if __name__ == "__main__": app.run(debug=True) 前端测试 html &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;body&gt; &lt;div id="content"&gt;&lt;/div&gt; &lt;script&gt; const evtSource = new EventSource("http://127.0.0.1:5000/stream"); evtSource.onmessage = function(e) { document.getElementById("content").innerText += e.data; }; &lt;/script&gt; &lt;/body&gt; &lt;/html&gt;]]></summary></entry><entry><title type="html">Codex做一个桌宠使用体验</title><link href="https://wuli-git.github.io/2026/05/13/codex%E4%BD%BF%E7%94%A8%E4%BD%93%E9%AA%8C%E4%B9%8BHatch_pet%E7%94%9F%E6%88%90.html" rel="alternate" type="text/html" title="Codex做一个桌宠使用体验" /><published>2026-05-13T00:00:00+08:00</published><updated>2026-05-13T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/05/13/codex%E4%BD%BF%E7%94%A8%E4%BD%93%E9%AA%8C%E4%B9%8BHatch_pet%E7%94%9F%E6%88%90</id><content type="html" xml:base="https://wuli-git.github.io/2026/05/13/codex%E4%BD%BF%E7%94%A8%E4%BD%93%E9%AA%8C%E4%B9%8BHatch_pet%E7%94%9F%E6%88%90.html"><![CDATA[<h1 id="基于hatch-pet的桌宠生成流程文档">基于Hatch Pet的桌宠生成流程文档</h1>

<h1 id="1-目标与交付产物">1 目标与交付产物</h1>

<h2 id="11-核心目标">1。1 核心目标</h2>

<p>依托hatch-pet流程，实现从角色设定到可用桌宠资源包的全流程标准化产出，确保交付件符合行业规范，可直接用于导入、分发或二次开发。</p>

<h2 id="12-最终交付产物">1.2 最终交付产物</h2>

<ul>
  <li>
    <p>pet.png：8列×9行动画图集，共计72格，包含桌宠所有动作帧</p>
  </li>
  <li>
    <p>pet.json：桌宠核心配置文件，定义动作逻辑、触发规则等</p>
  </li>
  <li>
    <p>QA预览图（Contact Sheet/检查图）：用于直观校验帧序列、风格一致性及透明通道等</p>
  </li>
  <li>
    <p>可发布桌宠目录：标准化目录结构，支持直接导入相关平台或对外分发</p>
  </li>
</ul>

<h1 id="2-前置准备">2. 前置准备</h1>

<h2 id="21-输入素材">2.1 输入素材</h2>

<ul>
  <li>
    <p>角色参考图：建议提供多角度视图，便于确保角色轮廓、比例一致性</p>
  </li>
  <li>
    <p>风格要求：明确界定视觉风格（像素风/非像素风）、Q版还原程度及整体色调规范</p>
  </li>
  <li>
    <p>品牌元素（可选）：需融入的品牌配色、LOGO、服饰符号等标志性元素</p>
  </li>
</ul>

<h2 id="22-动作要求">2.2 动作要求</h2>

<p>为保障桌宠交互体验，需至少定义以下基础动作组（符合桌宠常规使用场景）：</p>

<ul>
  <li>
    <p>idle（待机动作）：桌宠无交互时的默认循环动作</p>
  </li>
  <li>
    <p>walk（行走动作）：桌宠移动时的动画序列</p>
  </li>
  <li>
    <p>run（可选动作）：桌宠快速移动时的动画序列</p>
  </li>
  <li>
    <p>jump/fall（可选动作）：桌宠跳跃、下落时的动画序列</p>
  </li>
  <li>
    <p>sleep（休眠动作）：桌宠长时间无交互时的休眠动画</p>
  </li>
  <li>
    <p>interact（点击反馈动作）：用户点击桌宠时的响应动画</p>
  </li>
</ul>

<h2 id="23-技术约束">2.3 技术约束</h2>

<ul>
  <li>
    <p>图集规格：固定为8列×9行网格布局，总格数严格控制为72格</p>
  </li>
  <li>
    <p>空白帧处理：未使用的网格格位需以透明填充，避免影响整体显示效果</p>
  </li>
  <li>
    <p>背景要求：所有帧图像背景均为透明（RGBA格式），无任何底色残留</p>
  </li>
  <li>
    <p>命名规范：帧文件命名需包含动作类型、帧序列、版本号，确保命名统一、可追溯</p>
  </li>
</ul>

<h1 id="3-标准生成流程基于hatchpet">3. 标准生成流程（基于hatchpet）</h1>

<h2 id="步骤1需求冻结brief阶段">步骤1：需求冻结（Brief阶段）</h2>

<p>明确项目核心需求，完成需求文档固化，为后续流程提供明确依据。</p>

<ul>
  <li>
    <p>输入：角色定位说明、动作清单、视觉风格详细要求</p>
  </li>
  <li>
    <p>输出：建议生成pet_brief.md文档，固化需求细节，便于团队同步及后期追溯</p>
  </li>
  <li>
    <p>检查点：</p>

    <ul>
      <li>
        <p>角色视觉风格唯一且明确，无模糊表述</p>
      </li>
      <li>
        <p>动作数量及帧需求，符合72格图集的总量约束</p>
      </li>
      <li>
        <p>已明确界定“像素风”或“插画风”，无风格歧义</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="步骤2基础角色帧生成imagegen阶段">步骤2：基础角色帧生成（ImageGen阶段）</h2>

<p>通过hatch-pet集成的imagegen工具，生成角色关键帧及动作参考，奠定动画基础。</p>

<ul>
  <li>
    <p>输出：</p>

    <ul>
      <li>
        <p>按动作分类的关键帧草稿（建议按动作建立独立目录管理）</p>
      </li>
      <li>
        <p>每个动作的首尾帧，确保后续动画循环的连贯性</p>
      </li>
    </ul>
  </li>
  <li>
    <p>检查点：</p>

    <ul>
      <li>
        <p>角色轮廓、头身比例一致，无帧间漂移现象</p>
      </li>
      <li>
        <p>配色方案稳定，帧间无明显色偏，符合预设色调要求</p>
      </li>
      <li>
        <p>角色透视角度统一，避免出现“忽大忽小”的视觉偏差</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="步骤3动作补帧与图像清洗">步骤3：动作补帧与图像清洗</h2>

<p>基于关键帧，补充中间帧形成完整动画序列，并对图像进行优化处理，确保动画流畅、画面干净。</p>

<ul>
  <li>
    <p>输出：每个动作对应的连续帧PNG序列（透明背景，符合技术约束要求）</p>
  </li>
  <li>
    <p>检查点：</p>

    <ul>
      <li>
        <p>动作循环自然，首尾帧衔接流畅，无跳帧、卡顿现象</p>
      </li>
      <li>
        <p>角色重心平稳，动画过程中无明显抖动</p>
      </li>
      <li>
        <p>图像边缘干净，无白边、脏像素等冗余元素</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="步骤489图集组装">步骤4：8×9图集组装</h2>

<p>按照hatchpet规范，将所有动作帧有序装配到8列×9行的网格中，生成最终图集。</p>

<ul>
  <li>
    <p>输出：</p>

    <ul>
      <li>
        <p>pet.png（最终动画图集，符合技术约束）</p>
      </li>
      <li>
        <p>格位映射表：明确动作与图集行列、帧范围的对应关系，便于后续配置校对</p>
      </li>
    </ul>
  </li>
  <li>
    <p>检查点：</p>

    <ul>
      <li>
        <p>图集总格数不超过72格，无超出约束的情况</p>
      </li>
      <li>
        <p>未使用的空白格均为透明填充，无底色残留</p>
      </li>
      <li>
        <p>每格尺寸、锚点一致，确保动画播放时角色位置稳定</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="步骤5petjson配置生成与校对">步骤5：pet.json配置生成与校对</h2>

<p>配置桌宠的动作逻辑、播放速度、触发条件及循环规则，确保配置与图集匹配，满足交互需求。</p>

<ul>
  <li>
    <p>建议配置字段：</p>

    <ul>
      <li>
        <p>基础信息：name（桌宠名称）、author（作者）、version（版本号）</p>
      </li>
      <li>
        <p>图集配置：sprite（图集路径、网格尺寸等基础信息）</p>
      </li>
      <li>
        <p>动画配置：animations（动作对应的帧段范围、播放帧率fps、是否循环loop）</p>
      </li>
      <li>
        <p>行为配置：behaviors（空闲时随机动作、用户交互响应逻辑等）</p>
      </li>
    </ul>
  </li>
  <li>
    <p>检查点：</p>

    <ul>
      <li>
        <p>配置中的帧索引与图集格位映射完全一致，无错配情况</p>
      </li>
      <li>
        <p>动作播放时长合理（idle动作播放速度较慢、walk动作中等、interact动作较快）</p>
      </li>
      <li>
        <p>缺失动作需设置降级策略（建议 fallback 至idle动作，避免异常）</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="步骤6可视化qa校验关键环节">步骤6：可视化QA校验（关键环节）</h2>

<p>借助hatch-pet的QA流程，生成Contact Sheet（检查图），对桌宠资产进行全面验收，避免问题流入发布环节。</p>

<ul>
  <li>
    <p>必查项：</p>

    <ul>
      <li>
        <p>帧连续性：动画播放无闪烁、无错帧、无卡顿，循环流畅</p>
      </li>
      <li>
        <p>透明通道：所有帧背景透明，无任何底色残留</p>
      </li>
      <li>
        <p>网格对齐：所有帧均在网格范围内，无越界、偏移现象</p>
      </li>
      <li>
        <p>动作语义：动作表现与定义一致（如walk动作需呈现明显行走姿态）</p>
      </li>
      <li>
        <p>风格一致性：整套资产采用统一美术语言，无风格割裂现象</p>
      </li>
    </ul>
  </li>
</ul>

<h2 id="步骤7打包发布">步骤7：打包发布</h2>

<p>整理标准化发布目录，附带相关说明文档，确保交付物可直接用于导入或分发。</p>

<p>建议目录结构：</p>

<p><code class="language-plaintext highlighter-rouge">plain text
pet/
├─ pet.png          # 最终动画图集
├─ pet.json         # 桌宠配置文件
├─ preview.png      # QA预览图（检查图）
├─ README.md        # 桌宠说明、使用方法
└─ CHANGELOG.md     # 版本更新日志
</code></p>

<h1 id="4-帧位规划建议示例">4. 帧位规划建议（示例）</h1>

<p>为避免后期帧数量超出72格约束，建议提前制定帧位“预算表”，合理分配各动作的帧数量，示例如下：</p>

<table>
  <thead>
    <tr>
      <th>动作类型</th>
      <th>帧数量</th>
      <th>备注</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>idle（待机）</td>
      <td>12</td>
      <td>循环流畅，动作舒缓</td>
    </tr>
    <tr>
      <td>walk（行走）</td>
      <td>16</td>
      <td>步态自然，帧间衔接流畅</td>
    </tr>
    <tr>
      <td>run（奔跑）</td>
      <td>12</td>
      <td>可选，动作幅度大于行走</td>
    </tr>
    <tr>
      <td>sleep（休眠）</td>
      <td>8</td>
      <td>动作舒缓，符合休眠场景</td>
    </tr>
    <tr>
      <td>interact（点击反馈）</td>
      <td>12</td>
      <td>动作敏捷，反馈及时</td>
    </tr>
    <tr>
      <td>jump/fall（跳跃/下落）</td>
      <td>8</td>
      <td>可选，动作连贯</td>
    </tr>
    <tr>
      <td>预留帧</td>
      <td>4</td>
      <td>用于后期调整或补充动作</td>
    </tr>
    <tr>
      <td>合计</td>
      <td>72</td>
      <td>符合图集总格数约束</td>
    </tr>
  </tbody>
</table>

<h1 id="5-实操经验与问题解决">5. 实操经验与问题解决</h1>

<h2 id="51-核心问题及解决方案">5.1 核心问题及解决方案</h2>

<ul>
  <li>
    <p>配置相关问题：核心难点在于config.toml与auth.json的配置。建议获取API-KEY后，优先使用CC-switch一键配置（效率更高、出错率低）；若获取的API-KEY绑定分组无image2等生图模型权限，需更换具备生图模型调用权限的中转站。</p>
  </li>
  <li>
    <p>image工具配置问题：配置过程中易出现image内置工具不可用的报错，可通过将角色参考图上传至官方OPEN-AI平台生成图像，替代内置工具，解决报错问题。</p>
  </li>
  <li>
    <p>本地图片上传问题：遇到本地图片无法上传至OPEN-AI官网的情况，可采用轻量化生成方式规避，或开通本地文件上传权限，确保素材正常使用。</p>
  </li>
  <li>
    <p>总体体验下来,codex这个agent还是很强大的，就像一个人，可以为你做事情，调用你的电脑与大模型不同（是一个脑子，只能思考）</p>
  </li>
</ul>

<h2 id="效果">效果</h2>
<p><img src="image-3.png" alt="alt text" /></p>

<p>If you like TeXt, don’t forget to give me a star. :star2:</p>

<p><a href="https://github.com/kitian616/jekyll-TeXt-theme/"><img src="https://img.shields.io/github/stars/kitian616/jekyll-TeXt-theme.svg?label=Stars&amp;style=social" alt="Star This Project" /></a></p>]]></content><author><name>wuli</name><email>1328433750@qq.com</email></author><category term="TeXt" /><summary type="html"><![CDATA[基于Hatch Pet的桌宠生成流程文档 1 目标与交付产物 1。1 核心目标 依托hatch-pet流程，实现从角色设定到可用桌宠资源包的全流程标准化产出，确保交付件符合行业规范，可直接用于导入、分发或二次开发。 1.2 最终交付产物 pet.png：8列×9行动画图集，共计72格，包含桌宠所有动作帧 pet.json：桌宠核心配置文件，定义动作逻辑、触发规则等 QA预览图（Contact Sheet/检查图）：用于直观校验帧序列、风格一致性及透明通道等 可发布桌宠目录：标准化目录结构，支持直接导入相关平台或对外分发 2. 前置准备 2.1 输入素材 角色参考图：建议提供多角度视图，便于确保角色轮廓、比例一致性 风格要求：明确界定视觉风格（像素风/非像素风）、Q版还原程度及整体色调规范 品牌元素（可选）：需融入的品牌配色、LOGO、服饰符号等标志性元素 2.2 动作要求 为保障桌宠交互体验，需至少定义以下基础动作组（符合桌宠常规使用场景）： idle（待机动作）：桌宠无交互时的默认循环动作 walk（行走动作）：桌宠移动时的动画序列 run（可选动作）：桌宠快速移动时的动画序列 jump/fall（可选动作）：桌宠跳跃、下落时的动画序列 sleep（休眠动作）：桌宠长时间无交互时的休眠动画 interact（点击反馈动作）：用户点击桌宠时的响应动画 2.3 技术约束 图集规格：固定为8列×9行网格布局，总格数严格控制为72格 空白帧处理：未使用的网格格位需以透明填充，避免影响整体显示效果 背景要求：所有帧图像背景均为透明（RGBA格式），无任何底色残留 命名规范：帧文件命名需包含动作类型、帧序列、版本号，确保命名统一、可追溯 3. 标准生成流程（基于hatchpet） 步骤1：需求冻结（Brief阶段） 明确项目核心需求，完成需求文档固化，为后续流程提供明确依据。 输入：角色定位说明、动作清单、视觉风格详细要求 输出：建议生成pet_brief.md文档，固化需求细节，便于团队同步及后期追溯 检查点： 角色视觉风格唯一且明确，无模糊表述 动作数量及帧需求，符合72格图集的总量约束 已明确界定“像素风”或“插画风”，无风格歧义 步骤2：基础角色帧生成（ImageGen阶段） 通过hatch-pet集成的imagegen工具，生成角色关键帧及动作参考，奠定动画基础。 输出： 按动作分类的关键帧草稿（建议按动作建立独立目录管理） 每个动作的首尾帧，确保后续动画循环的连贯性 检查点： 角色轮廓、头身比例一致，无帧间漂移现象 配色方案稳定，帧间无明显色偏，符合预设色调要求 角色透视角度统一，避免出现“忽大忽小”的视觉偏差 步骤3：动作补帧与图像清洗 基于关键帧，补充中间帧形成完整动画序列，并对图像进行优化处理，确保动画流畅、画面干净。 输出：每个动作对应的连续帧PNG序列（透明背景，符合技术约束要求） 检查点： 动作循环自然，首尾帧衔接流畅，无跳帧、卡顿现象 角色重心平稳，动画过程中无明显抖动 图像边缘干净，无白边、脏像素等冗余元素 步骤4：8×9图集组装 按照hatchpet规范，将所有动作帧有序装配到8列×9行的网格中，生成最终图集。 输出： pet.png（最终动画图集，符合技术约束） 格位映射表：明确动作与图集行列、帧范围的对应关系，便于后续配置校对 检查点： 图集总格数不超过72格，无超出约束的情况 未使用的空白格均为透明填充，无底色残留 每格尺寸、锚点一致，确保动画播放时角色位置稳定 步骤5：pet.json配置生成与校对 配置桌宠的动作逻辑、播放速度、触发条件及循环规则，确保配置与图集匹配，满足交互需求。 建议配置字段： 基础信息：name（桌宠名称）、author（作者）、version（版本号） 图集配置：sprite（图集路径、网格尺寸等基础信息） 动画配置：animations（动作对应的帧段范围、播放帧率fps、是否循环loop） 行为配置：behaviors（空闲时随机动作、用户交互响应逻辑等） 检查点： 配置中的帧索引与图集格位映射完全一致，无错配情况 动作播放时长合理（idle动作播放速度较慢、walk动作中等、interact动作较快） 缺失动作需设置降级策略（建议 fallback 至idle动作，避免异常） 步骤6：可视化QA校验（关键环节） 借助hatch-pet的QA流程，生成Contact Sheet（检查图），对桌宠资产进行全面验收，避免问题流入发布环节。 必查项： 帧连续性：动画播放无闪烁、无错帧、无卡顿，循环流畅 透明通道：所有帧背景透明，无任何底色残留 网格对齐：所有帧均在网格范围内，无越界、偏移现象 动作语义：动作表现与定义一致（如walk动作需呈现明显行走姿态） 风格一致性：整套资产采用统一美术语言，无风格割裂现象 步骤7：打包发布 整理标准化发布目录，附带相关说明文档，确保交付物可直接用于导入或分发。 建议目录结构： plain text pet/ ├─ pet.png # 最终动画图集 ├─ pet.json # 桌宠配置文件 ├─ preview.png # QA预览图（检查图） ├─ README.md # 桌宠说明、使用方法 └─ CHANGELOG.md # 版本更新日志 4. 帧位规划建议（示例） 为避免后期帧数量超出72格约束，建议提前制定帧位“预算表”，合理分配各动作的帧数量，示例如下： 动作类型 帧数量 备注 idle（待机） 12 循环流畅，动作舒缓 walk（行走） 16 步态自然，帧间衔接流畅 run（奔跑） 12 可选，动作幅度大于行走 sleep（休眠） 8 动作舒缓，符合休眠场景 interact（点击反馈） 12 动作敏捷，反馈及时 jump/fall（跳跃/下落） 8 可选，动作连贯 预留帧 4 用于后期调整或补充动作 合计 72 符合图集总格数约束 5. 实操经验与问题解决 5.1 核心问题及解决方案 配置相关问题：核心难点在于config.toml与auth.json的配置。建议获取API-KEY后，优先使用CC-switch一键配置（效率更高、出错率低）；若获取的API-KEY绑定分组无image2等生图模型权限，需更换具备生图模型调用权限的中转站。 image工具配置问题：配置过程中易出现image内置工具不可用的报错，可通过将角色参考图上传至官方OPEN-AI平台生成图像，替代内置工具，解决报错问题。 本地图片上传问题：遇到本地图片无法上传至OPEN-AI官网的情况，可采用轻量化生成方式规避，或开通本地文件上传权限，确保素材正常使用。 总体体验下来,codex这个agent还是很强大的，就像一个人，可以为你做事情，调用你的电脑与大模型不同（是一个脑子，只能思考） 效果 If you like TeXt, don’t forget to give me a star. :star2:]]></summary></entry><entry><title type="html">把个人博客改造成共创博客：一次 Jekyll/TeXt 实战记录</title><link href="https://wuli-git.github.io/2026/05/13/%E6%8A%8A%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E6%94%B9%E9%80%A0%E6%88%90%E5%85%B1%E5%88%9B%E5%8D%9A%E5%AE%A2.html" rel="alternate" type="text/html" title="把个人博客改造成共创博客：一次 Jekyll/TeXt 实战记录" /><published>2026-05-13T00:00:00+08:00</published><updated>2026-05-13T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/05/13/%E6%8A%8A%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E6%94%B9%E9%80%A0%E6%88%90%E5%85%B1%E5%88%9B%E5%8D%9A%E5%AE%A2</id><content type="html" xml:base="https://wuli-git.github.io/2026/05/13/%E6%8A%8A%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2%E6%94%B9%E9%80%A0%E6%88%90%E5%85%B1%E5%88%9B%E5%8D%9A%E5%AE%A2.html"><![CDATA[<h1 id="把个人博客改造成共创博客一次-jekylltext-实战记录">把个人博客改造成共创博客：一次 Jekyll/TeXt 实战记录</h1>

<p>这篇文章记录一次很小、但很有意义的博客改造：把一个原本偏个人表达的 GitHub Pages 博客，调整成由 wuli 和 cc00mi 共创的博客。</p>

<p>目标不是大动干戈重写主题，而是在尽量尊重原有仓库结构的前提下，让博客具备“多作者身份”“文章署名”“关于我们说明”和“可持续发布流程”。</p>

<!--more-->

<h2 id="1-改造目标">1. 改造目标</h2>

<p>这次改造希望达成四件事：</p>

<ul>
  <li>博客整体身份从“个人博客”变成“共创博客”。</li>
  <li>文章可以区分作者，例如 <code class="language-plaintext highlighter-rouge">wuli</code> 或 <code class="language-plaintext highlighter-rouge">cc00mi</code>。</li>
  <li>关于页不再是主题默认介绍，而是展示共创者信息。</li>
  <li>后续发布文章时，只需要在 Front Matter 中声明作者即可。</li>
</ul>

<p>最终采用的是 Jekyll 原生能力和 TeXt 主题已经预留的作者机制。</p>

<h2 id="2-先确认博客技术栈">2. 先确认博客技术栈</h2>

<p>当前博客是一个 GitHub Pages + Jekyll 项目，主题结构接近 TeXt。</p>

<p>关键目录如下：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── _config.yml
├── _data
│   ├── authors.yml
│   └── navigation.yml
├── _includes
├── _layouts
├── _posts
└── about.md
</code></pre></div></div>

<p>其中最重要的是：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">_config.yml</code>：站点级配置，比如标题、描述、默认作者、文章默认布局。</li>
  <li><code class="language-plaintext highlighter-rouge">_data/authors.yml</code>：作者资料库。</li>
  <li><code class="language-plaintext highlighter-rouge">_posts</code>：文章目录。</li>
  <li><code class="language-plaintext highlighter-rouge">about.md</code>：关于页。</li>
</ul>

<p>TeXt 主题内部已经会读取文章 Front Matter 中的 <code class="language-plaintext highlighter-rouge">author</code> 字段，并通过 <code class="language-plaintext highlighter-rouge">site.data.authors[page.author]</code> 找到对应作者信息。</p>

<p>这意味着我们不需要重写复杂模板，只要把作者数据补齐即可。</p>

<h2 id="3-修正站点默认作者配置">3. 修正站点默认作者配置</h2>

<p>原配置中作者信息位置不完全符合主题预期。</p>

<p>主题通常会读取：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">author</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">wuli</span>
</code></pre></div></div>

<p>因此需要在 <code class="language-plaintext highlighter-rouge">_config.yml</code> 中确保站点作者字段是 <code class="language-plaintext highlighter-rouge">author:</code>，而不是任意自定义键名。</p>

<p>同时，把站点标题和描述调整为共创博客语义：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">title</span><span class="pi">:</span> <span class="s">fox小栖共创博客</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">&gt;</span>
  <span class="s">wuli 和 cc00mi 共创的技术、工具与探索笔记</span>

<span class="na">author</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">person</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">wuli</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s">https://wuli-git.github.io</span>
  <span class="na">avatar</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/assets/头像.png"</span>
  <span class="na">bio</span><span class="pi">:</span> <span class="s">做一个诗意，浪漫，自律的人.</span>
  <span class="na">email</span><span class="pi">:</span> <span class="s">1328433750@qq.com</span>
  <span class="na">github</span><span class="pi">:</span> <span class="s">wuli-git</span>
</code></pre></div></div>

<p>这里的 <code class="language-plaintext highlighter-rouge">author</code> 更像是站点默认作者，也会被页脚、默认文章信息等位置使用。</p>

<h2 id="4-增加作者资料库">4. 增加作者资料库</h2>

<p>接下来编辑 <code class="language-plaintext highlighter-rouge">_data/authors.yml</code>，加入两位作者。</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">wuli</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">person</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">wuli</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s">https://wuli-git.github.io</span>
  <span class="na">avatar</span><span class="pi">:</span> <span class="s">/assets/头像.png</span>
  <span class="na">bio</span><span class="pi">:</span> <span class="s">做一个诗意，浪漫，自律的人.</span>
  <span class="na">email</span><span class="pi">:</span> <span class="s">1328433750@qq.com</span>
  <span class="na">github</span><span class="pi">:</span> <span class="s">wuli-git</span>
  <span class="na">npm</span><span class="pi">:</span> <span class="s">http://106nixi213493.vicp.fun</span>

<span class="na">cc00mi</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">person</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">cc00mi</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s">https://github.com/cc00mi</span>
  <span class="na">avatar</span><span class="pi">:</span> <span class="s">/assets/cc00mi-avatar.png</span>
  <span class="na">bio</span><span class="pi">:</span> <span class="s">explore anything new!</span>
  <span class="na">github</span><span class="pi">:</span> <span class="s">cc00mi</span>
</code></pre></div></div>

<p>这里有一个实践细节：头像最好放在 <code class="language-plaintext highlighter-rouge">assets</code> 目录下，而不是放在 <code class="language-plaintext highlighter-rouge">_posts</code> 目录里。</p>

<p>例如：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>assets/cc00mi-avatar.png
</code></pre></div></div>

<p>这样引用路径更稳定，也更符合静态站点资源管理习惯。</p>

<h2 id="5-给文章设置默认作者">5. 给文章设置默认作者</h2>

<p>为了避免历史文章没有作者字段时显示异常，可以给所有文章配置默认作者。</p>

<p>在 <code class="language-plaintext highlighter-rouge">_config.yml</code> 的 <code class="language-plaintext highlighter-rouge">defaults</code> 中加入：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">defaults</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">posts</span>
    <span class="na">values</span><span class="pi">:</span>
      <span class="na">layout</span><span class="pi">:</span> <span class="s">article</span>
      <span class="na">author</span><span class="pi">:</span> <span class="s">wuli</span>
      <span class="na">sharing</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">license</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">aside</span><span class="pi">:</span>
        <span class="na">toc</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">show_edit_on_github</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">show_subscribe</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">pageview</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>这样，老文章即使没有写 <code class="language-plaintext highlighter-rouge">author</code>，也会默认归属到 <code class="language-plaintext highlighter-rouge">wuli</code>。</p>

<p>新文章如果是 cc00mi 发布，只需要显式写：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">文章标题</span>
<span class="na">author</span><span class="pi">:</span> <span class="s">cc00mi</span>
<span class="nn">---</span>
</code></pre></div></div>

<h2 id="6-重写关于页">6. 重写关于页</h2>

<p>原来的 <code class="language-plaintext highlighter-rouge">about.md</code> 还是主题默认说明，不适合作为共创博客的门面。</p>

<p>因此将其改成“关于我们”：</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 关于我们</span>

这里是 wuli 和 cc00mi 共创的个人博客：记录技术实践、工具体验、学习笔记，以及那些值得被认真探索的新东西。

<span class="gu">## 共创者</span>

<span class="gu">### wuli</span>

做一个诗意，浪漫，自律的人。

<span class="gu">### cc00mi</span>

explore anything new!
</code></pre></div></div>

<p>关于页承担的是“这个站点是谁和谁一起维护、为什么存在、会写什么”的说明作用。</p>

<p>这是共创博客非常重要的一步，因为它让访问者能立刻理解站点身份。</p>

<h2 id="7-发布新文章的方式">7. 发布新文章的方式</h2>

<p>以后发布文章，只需要在 <code class="language-plaintext highlighter-rouge">_posts</code> 目录中新建 Markdown 文件。</p>

<p>命名格式：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>YYYY-MM-DD-文章标题.md
</code></pre></div></div>

<p>例如：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026-05-14-把个人博客改造成共创博客.md
</code></pre></div></div>

<p>文章头部写：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">把个人博客改造成共创博客：一次 Jekyll/TeXt 实战记录</span>
<span class="na">tags</span><span class="pi">:</span> <span class="s">Jekyll GitHub-Pages Blog</span>
<span class="na">author</span><span class="pi">:</span> <span class="s">cc00mi</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>这里有一个非常重要的坑：文章日期不要写成未来日期。</p>

<p>Jekyll 默认不会发布未来日期的文章。也就是说，如果今天是 <code class="language-plaintext highlighter-rouge">2026-05-14</code>，而文件名写成：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026-05-18-xxx.md
</code></pre></div></div>

<p>那么文章在 GitHub 仓库里能看到，但博客页面不会显示。等到日期到达，或者开启 <code class="language-plaintext highlighter-rouge">future: true</code> 后才会被构建出来。</p>

<p>更推荐的做法是：当天发布就使用当天或过去日期。</p>

<h2 id="8-git-提交与推送">8. Git 提交与推送</h2>

<p>本地写完后，使用 Git 提交：</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cd</span><span class="w"> </span><span class="nx">F:\wuli00\wuli-git.github.io</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">status</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">_config.yml</span><span class="w"> </span><span class="nx">_data/authors.yml</span><span class="w"> </span><span class="nx">about.md</span><span class="w"> </span><span class="nx">assets/cc00mi-avatar.png</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">_posts/2026-05-14-</span><span class="err">把个人博客改造成共创博客</span><span class="o">.</span><span class="nf">md</span><span class="w">
</span><span class="nx">git</span><span class="w"> </span><span class="nx">commit</span><span class="w"> </span><span class="nt">-m</span><span class="w"> </span><span class="s2">"Add co-created blog setup post"</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">push</span><span class="w"> </span><span class="nx">origin</span><span class="w"> </span><span class="nx">master</span><span class="w">
</span></code></pre></div></div>

<p>推送后，GitHub Pages 会自动构建并发布。</p>

<p>一般几十秒到几分钟后就能在博客中看到新文章。</p>

<h2 id="9-这次改造后的效果">9. 这次改造后的效果</h2>

<p>完成后，博客具备了这些能力：</p>

<ul>
  <li>站点标题和描述体现共创属性。</li>
  <li>作者信息集中维护在 <code class="language-plaintext highlighter-rouge">_data/authors.yml</code>。</li>
  <li>文章可以通过 <code class="language-plaintext highlighter-rouge">author: cc00mi</code> 指定署名。</li>
  <li>历史文章默认归属 <code class="language-plaintext highlighter-rouge">wuli</code>。</li>
  <li>关于页展示两位共创者，而不是主题默认内容。</li>
  <li>后续新增作者时，只需要继续扩展 <code class="language-plaintext highlighter-rouge">_data/authors.yml</code>。</li>
</ul>

<p>这是一种比较轻量的改造方式：不破坏主题、不重写布局，但把博客从“一个人的空间”推进成了“可以共同生长的空间”。</p>

<h2 id="10-后续可以继续增强什么">10. 后续可以继续增强什么</h2>

<p>当前方案是单作者署名。如果未来需要更完整的共创能力，可以继续做这些增强：</p>

<ul>
  <li>支持一篇文章多个作者，例如 <code class="language-plaintext highlighter-rouge">authors: [wuli, cc00mi]</code>。</li>
  <li>新增作者列表页，例如 <code class="language-plaintext highlighter-rouge">/authors.html</code>。</li>
  <li>为每位作者生成独立归档页。</li>
  <li>在文章底部显示作者卡片。</li>
  <li>在导航栏加入“共创者”入口。</li>
</ul>

<p>但对当前阶段来说，最重要的是先把发布流程跑顺。</p>

<p>一个博客不必一开始就复杂。它可以先拥有清晰的身份、稳定的写作入口，然后在一次次发布里慢慢长出自己的形状。</p>]]></content><author><name>cc00mi</name></author><category term="Jekyll" /><category term="GitHub-Pages" /><category term="Blog" /><summary type="html"><![CDATA[把个人博客改造成共创博客：一次 Jekyll/TeXt 实战记录 这篇文章记录一次很小、但很有意义的博客改造：把一个原本偏个人表达的 GitHub Pages 博客，调整成由 wuli 和 cc00mi 共创的博客。 目标不是大动干戈重写主题，而是在尽量尊重原有仓库结构的前提下，让博客具备“多作者身份”“文章署名”“关于我们说明”和“可持续发布流程”。]]></summary></entry><entry><title type="html">Copilot_student_certify</title><link href="https://wuli-git.github.io/2026/04/07/copilot_student_certify.html" rel="alternate" type="text/html" title="Copilot_student_certify" /><published>2026-04-07T00:00:00+08:00</published><updated>2026-04-07T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/04/07/copilot_student_certify</id><content type="html" xml:base="https://wuli-git.github.io/2026/04/07/copilot_student_certify.html"><![CDATA[<h1 id="github-copilot">GitHub Copilot</h1>

<div style="text-align: center;">

![](https://cloud.tsinghua.edu.cn/f/9f1f76c21f1a4b99b11c/?dl=1)

</div>

<p>想要体验各种最新模型？</p>

<div style="text-align: center;">

![](https://cloud.tsinghua.edu.cn/f/5ebd11963c4a432db045/?dl=1)

</div>

<p>想要更集成的 AI coding 体验？</p>

<div style="text-align: center;">

![更方便的对话窗口&amp;代码补全](https://cloud.tsinghua.edu.cn/f/962377f286c848f7aab6/?dl=1)

</div>

<p>那就来看看<del>并白嫖</del><strong>GitHub Copilot</strong> 吧！</p>

<p>:::note</p>

<p>在通过 github 教育认证后，可以免费使用 10 刀每月的 copilot pro 订阅+4 刀每月的 github pro 订阅（为期两年，两年学生认证到期后可重新认证续期）</p>

<p>:::</p>

<h2 id="1-准备工作">1. 准备工作</h2>

<p>首先用<strong>学生邮箱</strong>注册一个 GitHub 账号（已经有 GitHub 号的同学可以添加学生邮箱，并设置成首选）</p>

<p>然后将个人主页按下图模板设置好：</p>

<div style="text-align: center;">

![公开档案](https://cloud.tsinghua.edu.cn/f/00c752a6701241b3a6f1/?dl=1)

</div>

<div style="text-align: center;">

![支付信息](https://cloud.tsinghua.edu.cn/f/fecb151fb46b40848469/?dl=1)

</div>

<p>然后需要设置双重验证，在手机上下载 Microsoft Authenticator（应用商店搜索即可）</p>

<p>在 GitHub 的 password and authentication 栏目中（就在 payment information 下面），在 Two-factor methods 下选择<strong>Authenticator app</strong>，按照网页要求进行设置即可</p>

<div style="text-align: center;">

![双重验证](https://cloud.tsinghua.edu.cn/f/3d4a25df739b4a66a4fc/?dl=1)

</div>

<p>:::warning</p>

<p>这一步会生成账户的恢复代码，注意留存好</p>

<p>:::</p>

<h2 id="2-学生认证">2. 学生认证</h2>

<p>:::note</p>

<p>新注册的 GitHub 账号可能会认证失败，稍等几天就好</p>

<p>:::</p>

<p>:::warning</p>

<p>认证过程中记得关掉代理，若在校外需挂学校 VPN，GitHub 会通过网络定位你是不是在校内</p>

<p>:::</p>

<p>来到 Billing and licensing 下的 Education benefits</p>

<div style="text-align: center;">

![入口](https://cloud.tsinghua.edu.cn/f/4d1e24f3f312493ebc54/?dl=1)

</div>

<p>进入后按指示操作即可</p>

<p>:::note</p>

<p>GitHub 会根据学生邮箱自动选择学校
之后会要求选择用什么做证明，选第一项即可，学生证是最好的证明
如果学生证的中文下没有对应的英文，建议自行补充（用便利贴粘一下）</p>

<p>:::</p>

<p>:::warning</p>

<p>拍照不允许使用虚拟摄像机</p>

<p>:::</p>

<p>如果顺利，大概一天就会通过申请</p>

<h2 id="3-领取福利">3. 领取福利</h2>

<p>认证通过后回到 Billing and licensing 下的 Education benefits 即可领取学生权益</p>

<h2 id="4-copilot-配置">4. Copilot 配置</h2>

<h3 id="1-github-部分">1. GitHub 部分</h3>

<p>在 Copilot 设置中把能勾的全勾上😏</p>

<div style="text-align: center;">

![Copilot 设置](https://cloud.tsinghua.edu.cn/f/d16d169140c64ae093e4/?dl=1)

</div>

<p>在 GitHub 上可以使用网页版 Copilot，和其他 AI 工具类似：<a href="https://github.com/copilot">github.com/copilot</a></p>

<h3 id="2-vs-code">2. VS Code</h3>

<p>:::note</p>

<p>只介绍 C/C++，Python 照猫画虎就行</p>

<p>:::</p>

<p><a href="https://code.visualstudio.com/">官网链接</a></p>

<div style="text-align: center;">

![安装时此页建议都勾选](https://cloud.tsinghua.edu.cn/f/c1cdbced69f04efe8899/?dl=1)

</div>

<p>下载安装打开后，安装 <strong>C/C++</strong> 和 <strong>Code Runner</strong> 插件</p>

<div style="text-align: center;">

![](https://cloud.tsinghua.edu.cn/f/f24218f47ccf4f458ef0/?dl=1)

</div>

<div style="text-align: center;">

![](https://cloud.tsinghua.edu.cn/f/b1accf61e57d4245b017/?dl=1)

</div>

<h3 id="编译环境g">编译环境：g++</h3>

<p>各位同学应该都有 Dev-C++，其中已经包含了 g++ 编译器</p>

<p><strong>配置步骤：</strong></p>

<ol>
  <li>打开 Dev-C++ 安装目录下的 <strong>MinGW64\bin</strong> 文件夹，将路径复制下来</li>
  <li>在开始菜单搜索<strong>环境变量</strong>，选择<strong>编辑账户的环境变量</strong></li>
  <li>找到 <strong>Path</strong> 这个变量，在其中添加刚刚复制的路径</li>
  <li>在终端输入以下命令验证安装：</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>g++ <span class="nt">-v</span>
</code></pre></div></div>

<ol>
  <li>配置好之后打开 VS Code（需重启），再找一个 cpp 文件点右上角的运行键即可在下方终端运行程序了</li>
</ol>

<h3 id="进阶clang-编译器与-clangd-扩展">进阶：Clang 编译器与 clangd 扩展</h3>

<p>:::note</p>

<p>参考资料：
<a href="https://zhuanlan.zhihu.com/p/398790625">[万字长文] Visual Studio Code 配置 C/C++ 开发环境的最佳实践(VSCode + Clangd + XMake)</a></p>

<p>:::</p>

<p>微软 IntelliSense 的替代品，具有更好的代码补全，比 g++ 更详细的编译报错，调用函数时能显示其参数的原有定义等等功能，能改善开发体验，但只是锦上添花。</p>

<div style="text-align: center;">

![比如我先定义一个 deleteNeuron 函数](https://cloud.tsinghua.edu.cn/f/062a3e810ca141b594f7/?dl=1)

</div>

<div style="text-align: center;">

![之后调用时便会显示出其意义](https://cloud.tsinghua.edu.cn/f/6ac80dad6a014c8598b8/?dl=1)

</div>

<p><strong>安装步骤：</strong></p>

<ol>
  <li><strong>安装 Clang 编译器</strong>
    <ul>
      <li><a href="https://clang.llvm.org/">官网链接</a></li>
      <li>根据系统选择相应的安装方式</li>
    </ul>
  </li>
  <li><strong>安装 clangd 扩展</strong>
    <ul>
      <li>在 VS Code 中搜索并安装 <strong>clangd</strong> 扩展</li>
    </ul>
  </li>
</ol>

<div style="text-align: center;">

![](https://cloud.tsinghua.edu.cn/f/ef8b994dd74e42dc8570/?dl=1)

</div>

<h3 id="3-github-copilot-扩展安装">3. GitHub Copilot 扩展安装</h3>

<p>在扩展中下载 <strong>GitHub Copilot</strong></p>

<p>安装成功后右下角会弹出登录提醒，登录刚刚开通 Copilot 的账号即可</p>

<p>随后即可使用 Copilot 的所有功能啦！</p>

<p>包括：</p>
<ul>
  <li><strong>智能代码补全</strong></li>
  <li><strong>编辑器内联聊天</strong></li>
  <li><strong>右侧聊天栏</strong></li>
  <li><strong>Agent Mode</strong></li>
</ul>]]></content><author><name>wuli</name><email>1328433750@qq.com</email></author><summary type="html"><![CDATA[GitHub Copilot ![](https://cloud.tsinghua.edu.cn/f/9f1f76c21f1a4b99b11c/?dl=1) 想要体验各种最新模型？ ![](https://cloud.tsinghua.edu.cn/f/5ebd11963c4a432db045/?dl=1) 想要更集成的 AI coding 体验？ ![更方便的对话窗口&amp;代码补全](https://cloud.tsinghua.edu.cn/f/962377f286c848f7aab6/?dl=1) 那就来看看并白嫖GitHub Copilot 吧！ :::note 在通过 github 教育认证后，可以免费使用 10 刀每月的 copilot pro 订阅+4 刀每月的 github pro 订阅（为期两年，两年学生认证到期后可重新认证续期） ::: 1. 准备工作 首先用学生邮箱注册一个 GitHub 账号（已经有 GitHub 号的同学可以添加学生邮箱，并设置成首选） 然后将个人主页按下图模板设置好： ![公开档案](https://cloud.tsinghua.edu.cn/f/00c752a6701241b3a6f1/?dl=1) ![支付信息](https://cloud.tsinghua.edu.cn/f/fecb151fb46b40848469/?dl=1) 然后需要设置双重验证，在手机上下载 Microsoft Authenticator（应用商店搜索即可） 在 GitHub 的 password and authentication 栏目中（就在 payment information 下面），在 Two-factor methods 下选择Authenticator app，按照网页要求进行设置即可 ![双重验证](https://cloud.tsinghua.edu.cn/f/3d4a25df739b4a66a4fc/?dl=1) :::warning 这一步会生成账户的恢复代码，注意留存好 ::: 2. 学生认证 :::note 新注册的 GitHub 账号可能会认证失败，稍等几天就好 ::: :::warning 认证过程中记得关掉代理，若在校外需挂学校 VPN，GitHub 会通过网络定位你是不是在校内 ::: 来到 Billing and licensing 下的 Education benefits ![入口](https://cloud.tsinghua.edu.cn/f/4d1e24f3f312493ebc54/?dl=1) 进入后按指示操作即可 :::note GitHub 会根据学生邮箱自动选择学校 之后会要求选择用什么做证明，选第一项即可，学生证是最好的证明 如果学生证的中文下没有对应的英文，建议自行补充（用便利贴粘一下） ::: :::warning 拍照不允许使用虚拟摄像机 ::: 如果顺利，大概一天就会通过申请 3. 领取福利 认证通过后回到 Billing and licensing 下的 Education benefits 即可领取学生权益 4. Copilot 配置 1. GitHub 部分 在 Copilot 设置中把能勾的全勾上😏 ![Copilot 设置](https://cloud.tsinghua.edu.cn/f/d16d169140c64ae093e4/?dl=1) 在 GitHub 上可以使用网页版 Copilot，和其他 AI 工具类似：github.com/copilot 2. VS Code :::note 只介绍 C/C++，Python 照猫画虎就行 ::: 官网链接 ![安装时此页建议都勾选](https://cloud.tsinghua.edu.cn/f/c1cdbced69f04efe8899/?dl=1) 下载安装打开后，安装 C/C++ 和 Code Runner 插件 ![](https://cloud.tsinghua.edu.cn/f/f24218f47ccf4f458ef0/?dl=1) ![](https://cloud.tsinghua.edu.cn/f/b1accf61e57d4245b017/?dl=1) 编译环境：g++ 各位同学应该都有 Dev-C++，其中已经包含了 g++ 编译器 配置步骤： 打开 Dev-C++ 安装目录下的 MinGW64\bin 文件夹，将路径复制下来 在开始菜单搜索环境变量，选择编辑账户的环境变量 找到 Path 这个变量，在其中添加刚刚复制的路径 在终端输入以下命令验证安装： g++ -v 配置好之后打开 VS Code（需重启），再找一个 cpp 文件点右上角的运行键即可在下方终端运行程序了 进阶：Clang 编译器与 clangd 扩展 :::note 参考资料： [万字长文] Visual Studio Code 配置 C/C++ 开发环境的最佳实践(VSCode + Clangd + XMake) ::: 微软 IntelliSense 的替代品，具有更好的代码补全，比 g++ 更详细的编译报错，调用函数时能显示其参数的原有定义等等功能，能改善开发体验，但只是锦上添花。 ![比如我先定义一个 deleteNeuron 函数](https://cloud.tsinghua.edu.cn/f/062a3e810ca141b594f7/?dl=1) ![之后调用时便会显示出其意义](https://cloud.tsinghua.edu.cn/f/6ac80dad6a014c8598b8/?dl=1) 安装步骤： 安装 Clang 编译器 官网链接 根据系统选择相应的安装方式 安装 clangd 扩展 在 VS Code 中搜索并安装 clangd 扩展 ![](https://cloud.tsinghua.edu.cn/f/ef8b994dd74e42dc8570/?dl=1) 3. GitHub Copilot 扩展安装 在扩展中下载 GitHub Copilot 安装成功后右下角会弹出登录提醒，登录刚刚开通 Copilot 的账号即可 随后即可使用 Copilot 的所有功能啦！ 包括： 智能代码补全 编辑器内联聊天 右侧聊天栏 Agent Mode]]></summary></entry><entry><title type="html">本地正常、GitHub 线上图片/音乐失效终极解决实录</title><link href="https://wuli-git.github.io/2026/03/04/test%E6%B8%B2%E6%9F%93%E5%92%8C%E9%9F%B3%E4%B9%90%E5%86%85%E5%B5%8C.html" rel="alternate" type="text/html" title="本地正常、GitHub 线上图片/音乐失效终极解决实录" /><published>2026-03-04T00:00:00+08:00</published><updated>2026-03-04T00:00:00+08:00</updated><id>https://wuli-git.github.io/2026/03/04/test%E6%B8%B2%E6%9F%93%E5%92%8C%E9%9F%B3%E4%B9%90%E5%86%85%E5%B5%8C</id><content type="html" xml:base="https://wuli-git.github.io/2026/03/04/test%E6%B8%B2%E6%9F%93%E5%92%8C%E9%9F%B3%E4%B9%90%E5%86%85%E5%B5%8C.html"><![CDATA[<h1 id="jekyll-text-主题博客网页音乐图片路径避坑完整解决方案">Jekyll TeXt 主题博客：网页音乐&amp;图片路径避坑完整解决方案</h1>

<h2 id="一前言">一、前言</h2>
<p>在使用 <strong>Jekyll + TeXt 主题</strong> 搭建 GitHub Pages 博客时，经常遇到一个经典问题：</p>
<blockquote>
  <p>本地预览图片、音乐都能正常加载播放，一旦上传到 GitHub Pages 线上，<strong>图片无法渲染、MP3 音频无法播放</strong>。</p>
</blockquote>

<p>先后尝试了网易云内嵌播放器、iframe 外链播放器、Markdown 同级相对路径、<code class="language-plaintext highlighter-rouge">_assets</code> 资源目录等多种方式，踩遍所有坑，本文完整记录全过程、失败原因、最终规范配置，同时总结通用路径规范，后续写博客直接套用即可。</p>

<h2 id="二前期尝试第三方音乐嵌入方案全部失败">二、前期尝试：第三方音乐嵌入方案（全部失败）</h2>
<h3 id="1-text-主题自带网易云音乐播放器">1. TeXt 主题自带网易云音乐播放器</h3>
<p>TeXt 主题内置了网易云音乐扩展标签，直接引入歌单 ID 即可嵌入：</p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">-</span> 恋人
<span class="nt">&lt;div&gt;</span>&lt;iframe class="extensions extensions--audio" width="330" height="86"
  src="//music.163.com/outchain/player?type=2&amp;id=2600493765&amp;auto=1&amp;height=66"
  frameborder="no" border="0" marginwidth="0" marginheight="0"&gt;
<span class="nt">&lt;/iframe&gt;</span>
<span class="nt">&lt;/div&gt;</span>
<span class="p">
-</span> 你的酒馆对我打了烊
<span class="nt">&lt;div&gt;</span>&lt;iframe class="extensions extensions--audio" width="330" height="86"
  src="//music.163.com/outchain/player?type=2&amp;id=1341964346&amp;auto=1&amp;height=66"
  frameborder="no" border="0" marginwidth="0" marginheight="0"&gt;
<span class="nt">&lt;/iframe&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<h3 id="2-网易云官方-outchain-iframe-外链播放器">2. 网易云官方 Outchain iframe 外链播放器</h3>
<p>使用网易云公开外链播放器嵌入网页：</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 恋人
<span class="nt">&lt;iframe</span> <span class="na">frameborder=</span><span class="s">"no"</span> <span class="na">border=</span><span class="s">"0"</span> <span class="na">marginwidth=</span><span class="s">"0"</span> <span class="na">marginheight=</span><span class="s">"0"</span> <span class="na">width=</span><span class="s">"330"</span> <span class="na">height=</span><span class="s">"86"</span> 
<span class="na">src=</span><span class="s">"https://music.163.com/outchain/player?type=2&amp;id=2600493765&amp;auto=0&amp;height=66"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/iframe&gt;</span>

- 你的酒馆对我打了烊
<span class="nt">&lt;iframe</span> <span class="na">frameborder=</span><span class="s">"no"</span> <span class="na">border=</span><span class="s">"0"</span> <span class="na">marginwidth=</span><span class="s">"0"</span> <span class="na">marginheight=</span><span class="s">"0"</span> <span class="na">width=</span><span class="s">"330"</span> <span class="na">height=</span><span class="s">"86"</span> 
<span class="na">src=</span><span class="s">"https://music.163.com/outchain/player?type=2&amp;id=1341964346&amp;auto=0&amp;height=66"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/iframe&gt;</span>
</code></pre></div></div>

<h3 id="3-失败核心原因">3. 失败核心原因</h3>
<ol>
  <li>网易云、QQ 音乐等平台<strong>严格防盗链 + 版权限制</strong>；</li>
  <li>VIP 歌曲、版权受限歌曲，<strong>外链/iframe 一律无法跨域播放</strong>；</li>
  <li>平台对外链播放器做 Referer、IP 校验，GitHub Pages 域名直接被拦截；</li>
  <li>依赖第三方平台外链，<strong>完全不受自己控制，随时失效</strong>。</li>
</ol>

<blockquote>
  <p>结论：想在博客稳定无限制播放音乐，<strong>放弃第三方平台外链，改用本地 MP3 自主托管</strong>是唯一可行方案。</p>
</blockquote>

<h2 id="三尝试一mp3-与-markdown-文章同级存放">三、尝试一：MP3 与 Markdown 文章同级存放</h2>
<h3 id="目录结构">目录结构</h3>
<p>把音频和文章都放在 <code class="language-plaintext highlighter-rouge">_posts</code> 目录下：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_posts/
├─ test音乐播放器.md
└─ 恋人.mp3
</code></pre></div></div>

<h3 id="嵌入播放代码">嵌入播放代码</h3>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;audio</span> <span class="na">controls</span> <span class="na">preload=</span><span class="s">"none"</span> <span class="na">style=</span><span class="s">"width:100%;"</span><span class="nt">&gt;</span>
<span class="nt">&lt;source</span> <span class="na">src=</span><span class="s">"恋人.mp3"</span> <span class="na">type=</span><span class="s">"audio/mpeg"</span><span class="nt">&gt;</span>
你的浏览器不支持音频播放，请更换浏览器尝试。
</code></pre></div></div>

<h3 id="现象与原因">现象与原因</h3>
<ul>
  <li>✅ 本地 Jekyll 预览：<strong>可以正常播放</strong></li>
  <li>❌ 上传 GitHub Pages：<strong>完全无法加载</strong></li>
</ul>

<p>根本原因：</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">_posts</code> 是 Jekyll 特殊目录，仅负责渲染文章，<strong>不适合存放静态资源</strong>；</li>
  <li>使用<strong>相对路径 <code class="language-plaintext highlighter-rouge">恋人.mp3</code></strong>，本地路由适配，GitHub Pages 根路由解析错乱；</li>
  <li>TeXt 主题路由规则下，<code class="language-plaintext highlighter-rouge">_posts</code> 内静态资源线上不会被正常分发。</li>
</ol>

<p>同时过往图片也出现同款问题：图片放 <code class="language-plaintext highlighter-rouge">_posts</code> 本地能看，线上直接丢失，属于<strong>同一路径规则问题</strong>。</p>

<h2 id="四尝试二自建资源目录-assets-规范托管">四、尝试二：自建资源目录 <code class="language-plaintext highlighter-rouge">assets</code> 规范托管</h2>
<h3 id="1-标准目录结构最终可用版">1. 标准目录结构（最终可用版）</h3>
<blockquote>
  <p>关键：<strong>文件夹不能带下划线，必须是纯 <code class="language-plaintext highlighter-rouge">assets</code></strong></p>
  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>myblog/
├─ _posts/                # 所有博客文章存放处
│  └─ test音乐播放器.md
└─ assets/                # 静态资源根目录（无下划线）
   ├─ images/             # 图片总目录
   │  └─ src-img/         # 分组图片文件夹
   │     ├─ image-1.png
   │     └─ image-2.png
   └─ music/              # 音乐MP3目录
      └─ lover.mp3        # 建议改用英文文件名
</code></pre></div>  </div>
</blockquote>

<h3 id="2-图片标准引入写法本地--线上通用">2. 图片标准引入写法（本地 + 线上通用）</h3>
<p>采用<strong>网站根目录绝对路径</strong>，以 <code class="language-plaintext highlighter-rouge">/</code> 开头：</p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">![</span><span class="nv">00桌宠</span><span class="p">](</span><span class="sx">/assets/images/src-img/image-1.png</span><span class="p">)</span>
</code></pre></div></div>

<ul>
  <li>✅ 适配 TeXt 主题、本地预览、GitHub Pages</li>
  <li>✅ 永久稳定，后续所有图片统一放 <code class="language-plaintext highlighter-rouge">assets/images</code> 即可</li>
</ul>

<h3 id="3-mp3-音频标准嵌入写法">3. MP3 音频标准嵌入写法</h3>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;audio</span> <span class="na">controls</span> <span class="na">preload=</span><span class="s">"none"</span> <span class="na">style=</span><span class="s">"width:100%;"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;source</span> <span class="na">src=</span><span class="s">"/assets/music/lover.mp3"</span> <span class="na">type=</span><span class="s">"audio/mpeg"</span><span class="nt">&gt;</span>
  你的浏览器不支持音频播放，请更换浏览器尝试。
</code></pre></div></div>

<h3 id="关键避坑点">关键避坑点</h3>
<ol>
  <li>❌ 禁止使用 <code class="language-plaintext highlighter-rouge">_assets</code> 带下划线目录：Jekyll 会直接忽略下划线开头文件夹，线上不发布；</li>
  <li>❌ 禁止中文文件名音频：GitHub Pages 对中文 MP3 兼容性极差，图片可兼容、音频极易加载失败，建议统一改成英文命名；</li>
  <li>❌ 禁止使用 <code class="language-plaintext highlighter-rouge">./</code> <code class="language-plaintext highlighter-rouge">../</code> 相对路径，统一用 <strong><code class="language-plaintext highlighter-rouge">/assets/分类/文件名</code> 绝对根路径</strong>；</li>
  <li>静态资源<strong>全部统一放进 assets</strong>，不要散落在 <code class="language-plaintext highlighter-rouge">_posts</code> 或其他自定义目录。</li>
</ol>

<h2 id="五jekyll--text-静态资源路径通用规范总结">五、Jekyll + TeXt 静态资源路径通用规范（总结）</h2>
<h3 id="1-固定目录约定">1. 固定目录约定</h3>
<ul>
  <li>文章：统一放在 <code class="language-plaintext highlighter-rouge">_posts</code></li>
  <li>图片：统一放在 <code class="language-plaintext highlighter-rouge">assets/images/</code> 下级可建子文件夹</li>
  <li>音频/视频：统一放在 <code class="language-plaintext highlighter-rouge">assets/music/</code></li>
  <li>其他文件：<code class="language-plaintext highlighter-rouge">assets/files/</code></li>
</ul>

<h3 id="2-通用引入格式">2. 通用引入格式</h3>
<ul>
  <li>图片：<code class="language-plaintext highlighter-rouge">![描述](/assets/images/子目录/图片名.png)</code></li>
  <li>音频：<code class="language-plaintext highlighter-rouge">&lt;audio src="/assets/music/英文文件名.mp3" controls style="width:100%;"&gt;</code></li>
</ul>

<h3 id="3-永久避坑口诀">3. 永久避坑口诀</h3>
<ol>
  <li>静态资源绝不放 <code class="language-plaintext highlighter-rouge">_posts</code>；</li>
  <li>资源目录不用下划线，只用 <code class="language-plaintext highlighter-rouge">assets</code>；</li>
  <li>全部用<strong>以 / 开头的根绝对路径</strong>，不用相对路径；</li>
  <li>音频、视频文件名只用英文/数字，不用中文和空格；</li>
  <li>放弃网易云/QQ 音乐外链，自主托管 MP3 永久无版权限制、无播放限制。</li>
</ol>

<h2 id="六最终效果">六、最终效果</h2>
<p>按照上述规范配置后：</p>
<ul>
  <li>本地 Jekyll 预览：图片正常渲染、音乐正常播放；</li>
  <li>上传 GitHub Pages 线上：图片、音频全部稳定加载，无任何失效问题；</li>
  <li>后续所有博客文章，直接套用这套目录和路径写法，再也不会出现本地正常线上失效的问题。</li>
</ul>

<p><img src="/assets/images/src-img/image-1.png" alt="00桌宠" /></p>

<audio controls="" preload="none" style="width:100%;">
    <source src="/assets/music/恋人.mp3" type="audio/mpeg" />

</audio>]]></content><author><name>fox</name></author><category term="Jekyll" /><category term="GitHub-Pages" /><category term="Blog" /><summary type="html"><![CDATA[Jekyll TeXt 主题博客：网页音乐&amp;图片路径避坑完整解决方案 一、前言 在使用 Jekyll + TeXt 主题 搭建 GitHub Pages 博客时，经常遇到一个经典问题： 本地预览图片、音乐都能正常加载播放，一旦上传到 GitHub Pages 线上，图片无法渲染、MP3 音频无法播放。 先后尝试了网易云内嵌播放器、iframe 外链播放器、Markdown 同级相对路径、_assets 资源目录等多种方式，踩遍所有坑，本文完整记录全过程、失败原因、最终规范配置，同时总结通用路径规范，后续写博客直接套用即可。 二、前期尝试：第三方音乐嵌入方案（全部失败） 1. TeXt 主题自带网易云音乐播放器 TeXt 主题内置了网易云音乐扩展标签，直接引入歌单 ID 即可嵌入： - 恋人 &lt;div&gt;&lt;iframe class="extensions extensions--audio" width="330" height="86" src="//music.163.com/outchain/player?type=2&amp;id=2600493765&amp;auto=1&amp;height=66" frameborder="no" border="0" marginwidth="0" marginheight="0"&gt; &lt;/iframe&gt; &lt;/div&gt; - 你的酒馆对我打了烊 &lt;div&gt;&lt;iframe class="extensions extensions--audio" width="330" height="86" src="//music.163.com/outchain/player?type=2&amp;id=1341964346&amp;auto=1&amp;height=66" frameborder="no" border="0" marginwidth="0" marginheight="0"&gt; &lt;/iframe&gt; &lt;/div&gt; 2. 网易云官方 Outchain iframe 外链播放器 使用网易云公开外链播放器嵌入网页： - 恋人 &lt;iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width="330" height="86" src="https://music.163.com/outchain/player?type=2&amp;id=2600493765&amp;auto=0&amp;height=66"&gt; &lt;/iframe&gt; - 你的酒馆对我打了烊 &lt;iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width="330" height="86" src="https://music.163.com/outchain/player?type=2&amp;id=1341964346&amp;auto=0&amp;height=66"&gt; &lt;/iframe&gt; 3. 失败核心原因 网易云、QQ 音乐等平台严格防盗链 + 版权限制； VIP 歌曲、版权受限歌曲，外链/iframe 一律无法跨域播放； 平台对外链播放器做 Referer、IP 校验，GitHub Pages 域名直接被拦截； 依赖第三方平台外链，完全不受自己控制，随时失效。 结论：想在博客稳定无限制播放音乐，放弃第三方平台外链，改用本地 MP3 自主托管是唯一可行方案。 三、尝试一：MP3 与 Markdown 文章同级存放 目录结构 把音频和文章都放在 _posts 目录下： _posts/ ├─ test音乐播放器.md └─ 恋人.mp3 嵌入播放代码 &lt;audio controls preload="none" style="width:100%;"&gt; &lt;source src="恋人.mp3" type="audio/mpeg"&gt; 你的浏览器不支持音频播放，请更换浏览器尝试。 现象与原因 ✅ 本地 Jekyll 预览：可以正常播放 ❌ 上传 GitHub Pages：完全无法加载 根本原因： _posts 是 Jekyll 特殊目录，仅负责渲染文章，不适合存放静态资源； 使用相对路径 恋人.mp3，本地路由适配，GitHub Pages 根路由解析错乱； TeXt 主题路由规则下，_posts 内静态资源线上不会被正常分发。 同时过往图片也出现同款问题：图片放 _posts 本地能看，线上直接丢失，属于同一路径规则问题。 四、尝试二：自建资源目录 assets 规范托管 1. 标准目录结构（最终可用版） 关键：文件夹不能带下划线，必须是纯 assets myblog/ ├─ _posts/ # 所有博客文章存放处 │ └─ test音乐播放器.md └─ assets/ # 静态资源根目录（无下划线） ├─ images/ # 图片总目录 │ └─ src-img/ # 分组图片文件夹 │ ├─ image-1.png │ └─ image-2.png └─ music/ # 音乐MP3目录 └─ lover.mp3 # 建议改用英文文件名 2. 图片标准引入写法（本地 + 线上通用） 采用网站根目录绝对路径，以 / 开头： ![00桌宠](/assets/images/src-img/image-1.png) ✅ 适配 TeXt 主题、本地预览、GitHub Pages ✅ 永久稳定，后续所有图片统一放 assets/images 即可 3. MP3 音频标准嵌入写法 &lt;audio controls preload="none" style="width:100%;"&gt; &lt;source src="/assets/music/lover.mp3" type="audio/mpeg"&gt; 你的浏览器不支持音频播放，请更换浏览器尝试。 关键避坑点 ❌ 禁止使用 _assets 带下划线目录：Jekyll 会直接忽略下划线开头文件夹，线上不发布； ❌ 禁止中文文件名音频：GitHub Pages 对中文 MP3 兼容性极差，图片可兼容、音频极易加载失败，建议统一改成英文命名； ❌ 禁止使用 ./ ../ 相对路径，统一用 /assets/分类/文件名 绝对根路径； 静态资源全部统一放进 assets，不要散落在 _posts 或其他自定义目录。 五、Jekyll + TeXt 静态资源路径通用规范（总结） 1. 固定目录约定 文章：统一放在 _posts 图片：统一放在 assets/images/ 下级可建子文件夹 音频/视频：统一放在 assets/music/ 其他文件：assets/files/ 2. 通用引入格式 图片：![描述](/assets/images/子目录/图片名.png) 音频：&lt;audio src="/assets/music/英文文件名.mp3" controls style="width:100%;"&gt; 3. 永久避坑口诀 静态资源绝不放 _posts； 资源目录不用下划线，只用 assets； 全部用以 / 开头的根绝对路径，不用相对路径； 音频、视频文件名只用英文/数字，不用中文和空格； 放弃网易云/QQ 音乐外链，自主托管 MP3 永久无版权限制、无播放限制。 六、最终效果 按照上述规范配置后： 本地 Jekyll 预览：图片正常渲染、音乐正常播放； 上传 GitHub Pages 线上：图片、音频全部稳定加载，无任何失效问题； 后续所有博客文章，直接套用这套目录和路径写法，再也不会出现本地正常线上失效的问题。]]></summary></entry><entry><title type="html">Java面试基础</title><link href="https://wuli-git.github.io/2025/11/08/JAVA%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80.html" rel="alternate" type="text/html" title="Java面试基础" /><published>2025-11-08T00:00:00+08:00</published><updated>2025-11-08T00:00:00+08:00</updated><id>https://wuli-git.github.io/2025/11/08/JAVA%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80</id><content type="html" xml:base="https://wuli-git.github.io/2025/11/08/JAVA%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80.html"><![CDATA[<h6 id="java-和-c-的区别">Java 和 C++ 的区别?</h6>

<ul>
  <li>
    <h2 id="我知道很多人没学过-c但是面试官就是没事喜欢拿咱们-java-和-c-比呀没办法就算没学过-c也要记下来虽然java-和-c-都是面向对象的语言都支持封装继承和多态但是它们还是有挺多不相同的地方java-不提供指针来直接访问内存程序内存更加安全java-的类是单继承的c-支持多重继承虽然-java-的类不可以多继承但是接口可以多继承java-有自动内存管理垃圾回收机制gc不需要程序员手动释放无用内存c-同时支持方法重载和操作符重载但是-java-只支持方法重载操作符重载增加了复杂性这与-java-最初的设计思想不符">我知道很多人没学过 C++，但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀！没办法！！！就算没学过 C++，也要记下来。虽然，Java 和 C++ 都是面向对象的语言，都支持封装、继承和多态，但是，它们还是有挺多不相同的地方：Java 不提供指针来直接访问内存，程序内存更加安全Java 的类是单继承的，C++ 支持多重继承；虽然 Java 的类不可以多继承，但是接口可以多继承。Java 有自动内存管理垃圾回收机制(GC)，不需要程序员手动释放无用内存。C ++同时支持方法重载和操作符重载，但是 Java 只支持方法重载（操作符重载增加了复杂性，这与 Java 最初的设计思想不符）。</h2>
  </li>
</ul>

<p>这八种基本类型都有对应的包装类分别为：Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
 ##### 基本类型和包装类型的区别？</p>

<ul>
  <li>用途：除了定义一些常量和局部变量之外，我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且，包装类型可用于泛型，而基本类型不可以。</li>
  <li>存储方式：基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中，基本数据类型的成员变量（未被 static 修饰 ）存放在 Java 虚拟机的堆中。包装类型属于对象类型，我们知道几乎所有对象实例都存在于堆中。</li>
  <li>
    <h2 id="占用空间相比于包装类型对象类型-基本数据类型占用的空间往往非常小默认值成员变量包装类型不赋值就是-null-而基本类型有默认值且不是-null比较方式对于基本数据类型来说-比较的是值对于包装数据类型来说-比较的是对象的内存地址所有整型包装类对象之间值的比较全部使用-equals-方法">占用空间：相比于包装类型（对象类型）， 基本数据类型占用的空间往往非常小。默认值：成员变量包装类型不赋值就是 null ，而基本类型有默认值且不是 null。比较方式：对于基本数据类型来说，== 比较的是值。对于包装数据类型来说，== 比较的是对象的内存地址。所有整型包装类对象之间值的比较，全部使用 equals() 方法</h2>
  </li>
</ul>

<h5 id="包装类型的缓存机制">包装类型的缓存机制</h5>

<ul>
  <li>当我们使用包装类的静态方法（如 Integer.valueOf()）创建对象时，JVM 不总是新建对象，而是复用常见值范围内的缓存对象。
也就是说：</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true，因为复用了缓存对象
</code></p>

<p>而如果超出缓存范围：</p>

<p><code class="language-plaintext highlighter-rouge">Integer a = Integer.valueOf(200);
Integer b = Integer.valueOf(200);
System.out.println(a == b); // false，</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
###### 什么是JAVA中的自动装箱、拆箱
- 基础类用包装类互相替换
</code></pre></div></div>
<p>Integer i = 10;  //装箱
int n = i;   //拆箱</p>

<p>Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i 等价于 int n = i.intValue();
```</p>
<h5 id="为什么会出现浮点数精度缺失">为什么会出现浮点数精度缺失</h5>

<ul>
  <li>
    <p>计算机是二进制的，而且计算机在表示一个数字时，宽度是有限的，无限循环的小数存储在计算机时，只能被截断，所以就会导致小数精度发生损失</p>
  </li>
  <li>
    <p>解决方法：使用BigDecimal表示大数，equal:比较数值和精度，compareTo，只比较数值</p>
  </li>
  <li>
    <p>超过long整型用BigInteger,其底层是一个数组</p>
  </li>
</ul>

<h5 id="成员变量与局部变量">成员变量与局部变量</h5>
<ul>
  <li>
    <p>语法形式：从语法形式上看，成员变量是属于类的，而局部变量是在代码块或方法中定义的变量或是方法的参数；成员变量可以被 public,private,static 等修饰符所修饰，而局部变量不能被访问控制修饰符及 static 所修饰；但是，成员变量和局部变量都能被 final 所修饰。</p>
  </li>
  <li>存储方式：从变量在内存中的存储方式来看，如果成员变量是使用 static 修饰的，那么这个成员变量是属于类的，如果没有使用 static 修饰，这个成员变量是属于实例的。而对象存在于堆内存，局部变量则存在于栈内存。、</li>
  <li>
    <p>生存时间：从变量在内存中的生存时间上看，成员变量是对象的一部分，它随着对象的创建而存在，而局部变量随着方法的调用而自动生成，随着方法的调用结束而消亡。</p>
  </li>
  <li>默认值：从变量是否有默认值来看，成员变量如果没有被赋初始值，则会自动以类型的默认值而赋值（一种情况例外:被 final 修饰的成员变量也必须显式地赋值），而局部变量则不会自动赋值。</li>
</ul>

<p>*** 静态变量static的作用</p>]]></content><author><name>wuli</name><email>1328433750@qq.com</email></author><summary type="html"><![CDATA[Java 和 C++ 的区别? 我知道很多人没学过 C++，但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀！没办法！！！就算没学过 C++，也要记下来。虽然，Java 和 C++ 都是面向对象的语言，都支持封装、继承和多态，但是，它们还是有挺多不相同的地方：Java 不提供指针来直接访问内存，程序内存更加安全Java 的类是单继承的，C++ 支持多重继承；虽然 Java 的类不可以多继承，但是接口可以多继承。Java 有自动内存管理垃圾回收机制(GC)，不需要程序员手动释放无用内存。C ++同时支持方法重载和操作符重载，但是 Java 只支持方法重载（操作符重载增加了复杂性，这与 Java 最初的设计思想不符）。 这八种基本类型都有对应的包装类分别为：Byte、Short、Integer、Long、Float、Double、Character、Boolean 。 ##### 基本类型和包装类型的区别？ 用途：除了定义一些常量和局部变量之外，我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且，包装类型可用于泛型，而基本类型不可以。 存储方式：基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中，基本数据类型的成员变量（未被 static 修饰 ）存放在 Java 虚拟机的堆中。包装类型属于对象类型，我们知道几乎所有对象实例都存在于堆中。 占用空间：相比于包装类型（对象类型）， 基本数据类型占用的空间往往非常小。默认值：成员变量包装类型不赋值就是 null ，而基本类型有默认值且不是 null。比较方式：对于基本数据类型来说，== 比较的是值。对于包装数据类型来说，== 比较的是对象的内存地址。所有整型包装类对象之间值的比较，全部使用 equals() 方法 包装类型的缓存机制 当我们使用包装类的静态方法（如 Integer.valueOf()）创建对象时，JVM 不总是新建对象，而是复用常见值范围内的缓存对象。 也就是说： Integer a = Integer.valueOf(100); Integer b = Integer.valueOf(100); System.out.println(a == b); // true，因为复用了缓存对象 而如果超出缓存范围： Integer a = Integer.valueOf(200); Integer b = Integer.valueOf(200); System.out.println(a == b); // false， ###### 什么是JAVA中的自动装箱、拆箱 - 基础类用包装类互相替换 Integer i = 10; //装箱 int n = i; //拆箱 Integer i = 10 等价于 Integer i = Integer.valueOf(10) int n = i 等价于 int n = i.intValue(); ``` 为什么会出现浮点数精度缺失 计算机是二进制的，而且计算机在表示一个数字时，宽度是有限的，无限循环的小数存储在计算机时，只能被截断，所以就会导致小数精度发生损失 解决方法：使用BigDecimal表示大数，equal:比较数值和精度，compareTo，只比较数值 超过long整型用BigInteger,其底层是一个数组 成员变量与局部变量 语法形式：从语法形式上看，成员变量是属于类的，而局部变量是在代码块或方法中定义的变量或是方法的参数；成员变量可以被 public,private,static 等修饰符所修饰，而局部变量不能被访问控制修饰符及 static 所修饰；但是，成员变量和局部变量都能被 final 所修饰。 存储方式：从变量在内存中的存储方式来看，如果成员变量是使用 static 修饰的，那么这个成员变量是属于类的，如果没有使用 static 修饰，这个成员变量是属于实例的。而对象存在于堆内存，局部变量则存在于栈内存。、 生存时间：从变量在内存中的生存时间上看，成员变量是对象的一部分，它随着对象的创建而存在，而局部变量随着方法的调用而自动生成，随着方法的调用结束而消亡。 默认值：从变量是否有默认值来看，成员变量如果没有被赋初始值，则会自动以类型的默认值而赋值（一种情况例外:被 final 修饰的成员变量也必须显式地赋值），而局部变量则不会自动赋值。 *** 静态变量static的作用]]></summary></entry><entry><title type="html">Java进阶</title><link href="https://wuli-git.github.io/2025/11/06/java%E8%BF%9B%E9%98%B6.html" rel="alternate" type="text/html" title="Java进阶" /><published>2025-11-06T00:00:00+08:00</published><updated>2025-11-06T00:00:00+08:00</updated><id>https://wuli-git.github.io/2025/11/06/java%E8%BF%9B%E9%98%B6</id><content type="html" xml:base="https://wuli-git.github.io/2025/11/06/java%E8%BF%9B%E9%98%B6.html"><![CDATA[<h1 id="java进阶">JAVA进阶</h1>

<h2 id="集合">集合</h2>

<ul>
  <li>1.几何体系结构：单列集合/双列集合
<img src="image.png" alt="alt text" /></li>
  <li>
    <p>List:有序（存或取的顺序一样），可重复，有索引 
  set:无序，去重复，无索引</p>
  </li>
  <li>2.Collection所有单列对象的接口，不能直接创建对象</li>
</ul>

<pre><code class="language-JAVA">Collection&lt;String&gt; coll=new ArrayList&lt;&gt;();
coll.add("aaa");
coll.clear();
coll.remove("aaa");
boolean result=coll.contains("bbb")
coll.isEmpty()
</code></pre>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- Collection遍历：迭代器遍历(相当于是一个指针，不依赖索引)
/增强for遍历
/Lambda表达式遍历 ```JAVA
- (1)iterator类, Iterator&lt;E&gt; it=list.iterator()//返回一个迭代器对象
boollean hasNext()//判断当前位置是否有元素
String str=it.next()//获取当前位置元素
it.remove//不能用集合的方法增加或者删除
//迭代器遍历
Collection&lt;String&gt; coll=new ArrayList&lt;&gt;();
Iterator&lt;String&gt; it=coll.iterator();
while(it.hasNext){
    String str=it.next();
    System.out.println(str);
} ```
- 增强for迭代器，底层就是迭代器，所有单列集合和数组:其中的s只是一个第三方变量
for(String s:list){
    System.out.println(s);
}
- Lambda表达式遍历， ```JAVA
//匿名内部类
coll.forEach(new Consumer&lt;String&gt;()){
    @Override
    public void accept(String s);
}
//表达式()-&gt;{}
coll.forEach(s-&gt;System.out.println(s)); ```
</code></pre></div></div>

<ul>
  <li>List集合
    <pre><code class="language-JAVA">void add(int index,int element);
remove();
String str=set(int index,E element);
String str=get(int intdex)
//创建一个集合
List&lt;String&gt; list=new ArrayList&lt;&gt;();//实例化创建一个集合
</code></pre>

    <ul>
      <li>List集合的遍历方式
  多了列表迭代器遍历，普通for遍历
  list.ListItrator(),也是一个接口,获取一个列表迭代器对象</li>
    </ul>
  </li>
</ul>

<p>*** 总结：要删除：迭代器\要添加，列表迭代器，剩下的是仅仅只是遍历</p>

<ul>
  <li>ArrayList集合
    <ul>
      <li>底层是Object数组elementDate，添加第一个元素，会出现一个默认长度为10的数组，当数组满了，会自动扩容1.5倍，若还是放不下，则以新数组为主</li>
    </ul>
  </li>
  <li>LinkedList集合
    <ul>
      <li>底层是双链表，查询慢，增删快</li>
    </ul>
  </li>
</ul>

<h5 id="泛型">泛型</h5>

<ul>
  <li>
    <p>约束操作的的数据类型<String>统一数据类型</String></p>
  </li>
  <li>
    <p>只支持引用数据类型，对应包装类，因为会转成object类型</p>
  </li>
  <li>翻译的擦除，伪泛型
<code class="language-plaintext highlighter-rouge">public class ArrayList(E){};//泛型类</code>
<code class="language-plaintext highlighter-rouge">public &lt;E&gt; void show(T t){}//泛型方法</code></li>
  <li>泛型接口public interface List<E>{};相当于在接口中给出方法声明，然后需要用实例类来实现，但是不能new 对象</E></li>
</ul>]]></content><author><name>wuli</name><email>1328433750@qq.com</email></author><summary type="html"><![CDATA[JAVA进阶 集合 1.几何体系结构：单列集合/双列集合 List:有序（存或取的顺序一样），可重复，有索引 set:无序，去重复，无索引 2.Collection所有单列对象的接口，不能直接创建对象 Collection&lt;String&gt; coll=new ArrayList&lt;&gt;(); coll.add("aaa"); coll.clear(); coll.remove("aaa"); boolean result=coll.contains("bbb") coll.isEmpty() - Collection遍历：迭代器遍历(相当于是一个指针，不依赖索引) /增强for遍历 /Lambda表达式遍历 ```JAVA - (1)iterator类, Iterator&lt;E&gt; it=list.iterator()//返回一个迭代器对象 boollean hasNext()//判断当前位置是否有元素 String str=it.next()//获取当前位置元素 it.remove//不能用集合的方法增加或者删除 //迭代器遍历 Collection&lt;String&gt; coll=new ArrayList&lt;&gt;(); Iterator&lt;String&gt; it=coll.iterator(); while(it.hasNext){ String str=it.next(); System.out.println(str); } ``` - 增强for迭代器，底层就是迭代器，所有单列集合和数组:其中的s只是一个第三方变量 for(String s:list){ System.out.println(s); } - Lambda表达式遍历， ```JAVA //匿名内部类 coll.forEach(new Consumer&lt;String&gt;()){ @Override public void accept(String s); } //表达式()-&gt;{} coll.forEach(s-&gt;System.out.println(s)); ``` List集合 void add(int index,int element); remove(); String str=set(int index,E element); String str=get(int intdex) //创建一个集合 List&lt;String&gt; list=new ArrayList&lt;&gt;();//实例化创建一个集合 List集合的遍历方式 多了列表迭代器遍历，普通for遍历 list.ListItrator(),也是一个接口,获取一个列表迭代器对象 *** 总结：要删除：迭代器\要添加，列表迭代器，剩下的是仅仅只是遍历 ArrayList集合 底层是Object数组elementDate，添加第一个元素，会出现一个默认长度为10的数组，当数组满了，会自动扩容1.5倍，若还是放不下，则以新数组为主 LinkedList集合 底层是双链表，查询慢，增删快 泛型 约束操作的的数据类型统一数据类型 只支持引用数据类型，对应包装类，因为会转成object类型 翻译的擦除，伪泛型 public class ArrayList(E){};//泛型类 public &lt;E&gt; void show(T t){}//泛型方法 泛型接口public interface List{};相当于在接口中给出方法声明，然后需要用实例类来实现，但是不能new 对象]]></summary></entry></feed>