<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss/atom-styles.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Litos</title>
  <subtitle>Litos is a modern blogging theme built on Astro.js, designed for developers. It supports multiple post layouts, photo displays, project displays, and more, providing an elegant user experience and powerful customization capabilities.</subtitle>
  <link href="https://mps-blog.vercel.app//atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://mps-blog.vercel.app/" rel="alternate" type="text/html"/>
  <updated>2026-08-05T04:06:22.382Z</updated>
  <language>zh</language>
  <id>https://mps-blog.vercel.app//</id>
  <author>
    <name>夜猫子Ai手记</name>
    <uri>https://mps-blog.vercel.app/</uri>
  </author>
  <generator uri="https://github.com/Dnzzk2/Litos" version="5.0">Astro Litos Theme</generator>
  <rights>Copyright © 2026 夜猫子Ai手记</rights>
  
  <entry>
    <title>彻底搞懂端口占用：Windows / macOS / Linux 查端口、杀进程命令速查（含常见坑与自动释放方案）</title>
    <link href="https://mps-blog.vercel.app//posts/kill-port-guide" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/kill-port-guide</id>
    <updated>2026-08-05T00:00:00.000Z</updated>
    <published>2026-08-05T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">启动服务时报 Port XXXX is already in use 怎么办？本文总结 Windows（PowerShell / cmd / Git Bash）、macOS、Linux 下查询并终止占用端口进程的通用命令，深挖&quot;照着教程杀不掉&quot;的真实原因（Git Bash 缺 lsof/fuser、kill -9 对 Windows 进程无效、端口被守护进程拉起等），并给出 Node.js 脚本自动释放端口的可复用代码。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.DviuJbTL_ZC9agF.webp" alt="彻底搞懂端口占用：Windows / macOS / Linux 查端口、杀进程命令速查（含常见坑与自动释放方案）" style="width: 100%; height: auto; margin-bottom: 1em;" />
<blockquote><p>当你运行 <code>node server.mjs</code> 之类命令启动本地服务时，终端突然蹦出一句 <code>Port 3839 is already in use</code>——这说明目标端口已经被某个进程占用了。本文给出一套各平台通用、拿来即用的「查端口 → 确认 → 杀进程 → 复查」命令速查，并深挖本机实战中踩过的各种坑，最后给出让脚本<strong>自己释放端口</strong>的根治方案。</p></blockquote>
<hr />
<h2>📑 目录</h2>
<ul>
<li><a href="#%E4%B8%80%E9%80%9A%E7%94%A8%E5%A4%84%E7%90%86%E6%80%9D%E8%B7%AF">一、通用处理思路</a></li>
<li><a href="#%E4%BA%8Cwindows-%E4%B8%8B%E4%B8%89%E7%A7%8D%E6%96%B9%E5%BC%8F">二、Windows 下三种方式</a></li>
<li><a href="#%E4%B8%89macos--linux">三、macOS / Linux</a></li>
<li><a href="#%E5%9B%9B%E6%9C%AC%E6%9C%BA%E5%AE%9E%E6%88%98%E8%AE%B0%E5%BD%95">四、本机实战记录</a></li>
<li><a href="#%E4%BA%94%E4%B8%BA%E4%BB%80%E4%B9%88%E7%85%A7%E7%9D%80%E6%95%99%E7%A8%8B%E6%9D%80%E4%B8%8D%E6%8E%89%E5%B8%B8%E8%A7%81%E5%9D%91">五、为什么”照着教程杀不掉”？常见坑</a></li>
<li><a href="#%E5%85%AD%E6%A0%B9%E6%B2%BB%E8%AE%A9%E8%84%9A%E6%9C%AC%E8%87%AA%E5%B7%B1%E9%87%8A%E6%94%BE%E7%AB%AF%E5%8F%A3nodejs-%E7%A4%BA%E4%BE%8B">六、根治：让脚本自己释放端口（Node.js 示例）</a></li>
<li><a href="#%E4%B8%83%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9">七、注意事项</a></li>
</ul>
<hr />
<h2>一、通用处理思路</h2>
<p>不管什么系统，思路都一样，四步闭环：</p>
<ol>
<li><strong>查端口</strong> —— 找出监听目标端口的进程 PID</li>
<li><strong>确认身份</strong> —— 看一眼进程名 / 路径，避免误杀系统关键进程</li>
<li><strong>杀进程</strong> —— 终止该 PID</li>
<li><strong>复查</strong> —— 确认端口已释放，再重启服务</li>
</ol>
<p>下文统一以端口 <code>3839</code> 为例，按需替换即可。</p>
<h2>二、Windows 下三种方式</h2>
<h3>方式一：PowerShell（权限最全，推荐）</h3>
<pre><code># 1. 查占用 3839 的进程 PID
Get-NetTCPConnection -LocalPort 3839 | Select-Object LocalPort, State, OwningProcess

# 2. 看进程身份（把 4452 换成上一步得到的 PID）
Get-Process -Id 4452

# 3. 强制终止
Stop-Process -Id 4452 -Force

# 4. 复查（无输出即已释放）
Get-NetTCPConnection -LocalPort 3839 -ErrorAction SilentlyContinue
</code></pre>
<h3>方式二：命令提示符 cmd</h3>
<pre><code>:: 1. 查 PID（最后一列）
netstat -ano | findstr 3839

:: 2. 强制终止（把 4452 换成查到的 PID）
taskkill /PID 4452 /F
</code></pre>
<h3>方式三：Git Bash（Windows 上的 Unix 风格 shell）</h3>
<blockquote><p>⚠️ <strong>坑点（亲身踩过，详见第五节）</strong>：Git for Windows <strong>默认没有 <code>lsof</code>，也没有 <code>fuser</code></strong>。
所以 <code>lsof -i :3839</code>、<code>kill -9 $(lsof -t -i:3839)</code>、<code>fuser -k 3839/tcp</code> 都会直接 <code>command not found</code>，
拿到空 PID 去 <code>kill</code> 自然杀不掉——这也是很多人”照着教程试了都不行”的真正原因。
在 Git Bash 里请用 <strong>Windows 自带的 <code>netstat</code> + <code>taskkill</code></strong>（两者都是 Windows 二进制，Git Bash 能直接跑，且 <code>taskkill</code> 才能真的结束 Windows 进程；<code>kill -9</code> 对 Windows 进程经常无效）。</p></blockquote>
<pre><code># 1. 查占用进程（Windows netstat，Git Bash 可用）
netstat -ano | grep :3839

# 2. 终止（把 24292 换成查到的 PID）
taskkill /F /PID 24292
</code></pre>
<p>如果装了 <code>lsof</code>（如 WSL 或额外装了），Unix 写法才适用：</p>
<pre><code>lsof -i :3839            # 仅当环境里确实装了 lsof
kill -9 $(lsof -t -i:3839)
</code></pre>
<h2>三、macOS / Linux</h2>
<pre><code># 1. 查占用进程
lsof -i :3839
sudo netstat -tulpn | grep :3839

# 2. 终止
kill -9 &lt;PID&gt;

# 更直接的写法（按端口杀，需 fuser）
fuser -k 3839/tcp
</code></pre>
<blockquote><p>注意：macOS / Linux 默认带 <code>lsof</code> / <code>fuser</code>，上面的 Unix 写法可用；而 Windows 的 Git Bash 没有，千万别照搬。</p></blockquote>
<h2>四、本机实战记录</h2>
<p>本次在 <strong>Windows + Git Bash</strong> 环境下遇到 <code>Port 3839 is already in use</code>：</p>
<ul>
<li><code>netstat -ano | grep :3839</code> → 命中 <code>127.0.0.1:3839  LISTENING  4452</code></li>
<li>安全策略禁用了 <code>tasklist</code> / <code>wmic</code>，无法读取进程路径；改用 PowerShell <code>Stop-Process -Id 4452 -Force</code> 终止</li>
<li><code>netstat -ano | grep :3839</code> 复查 → 无输出，确认端口已释放</li>
</ul>
<blockquote><p>注意：部分环境里 PowerShell 的 stdout 不回显，杀进程后无法从命令输出确认结果。此时用 <code>netstat</code> 复查端口是否释放，是更可靠的验证手段。</p></blockquote>
<p>占用 <code>127.0.0.1:3839</code> 的通常是你自己的旧服务实例没退出（比如同一个脚本上次没正常结束），确认进程名后放心终止即可。</p>
<h2>五、为什么”照着教程杀不掉”？常见坑</h2>
<p>很多人照着网上的教程敲命令，端口却纹丝不动。下面这些坑都是本机真实踩过的：</p>
<h3>坑 1：Git Bash 里没有 <code>lsof</code> / <code>fuser</code></h3>
<p>Git for Windows 默认只带一小部分 Unix 工具。用 <code>command -v lsof</code> 一查，大概率是空的。于是教程里的：</p>
<pre><code>lsof -i :3839
kill -9 $(lsof -t -i:3839)
</code></pre>
<p>会直接报 <code>lsof: command not found</code>，<code>$(...)</code> 拿到空字符串，<code>kill</code> 拿不到 PID，自然”杀了个寂寞”。</p>
<p><strong>✅ 解法</strong>：在 Git Bash 里改用 Windows 自带的 <code>netstat</code> + <code>taskkill</code>（见第二节方式三），它们是 Windows 二进制，Git Bash 能直接跑。</p>
<h3>坑 2：<code>kill -9</code> 杀不掉 Windows 进程</h3>
<p>即使在 Git Bash 里 <code>kill -9 &lt;PID&gt;</code> 返回成功，Windows 进程也常常”假装死了”——端口还在。原因是 <code>kill</code> 发的是 Unix 信号，Windows 进程大多不认。</p>
<p><strong>✅ 解法</strong>：结束 Windows 进程要用 Windows 自己的 <code>taskkill /F /PID &lt;PID&gt;</code>，它才会真正终止进程并释放端口。</p>
<h3>坑 3：PowerShell 不回显 stdout，不知道到底杀没杀</h3>
<p>在 CI / 某些 agent 环境里，PowerShell 的 <code>Stop-Process</code> 确实执行了，但命令输出看不到，你无法确定结果。</p>
<p><strong>✅ 解法</strong>：别看输出，直接用 <code>netstat -ano | grep :3839</code> 复查端口。有输出 = 还在占用；无输出 = 已释放。这是最可靠的验证手段。</p>
<h3>坑 4：端口”复活”——被守护进程反复拉起</h3>
<p>如果占用端口的进程是由 <code>pm2</code> / <code>nodemon</code> / 某个批处理循环托管的，你 <code>taskkill</code> 掉它后，守护进程会立刻重新拉起一个<strong>新 PID 的相同进程</strong>，端口转眼又被占，看起来就像”怎么杀都杀不掉”。</p>
<p><strong>✅ 解法</strong>：先停掉守护者，再杀：</p>
<pre><code>pm2 stop &lt;id&gt;          # 若由 pm2 托管
# 或关掉启动它的父脚本 / 终端，再 taskkill /F /PID &lt;PID&gt;
</code></pre>
<h3>坑 5：脚本自己不释放端口（最隐蔽的元凶）</h3>
<p>很多启动脚本遇到 <code>EADDRINUSE</code>（端口已被占用）时，只是 <code>console.error("Port is already in use")</code> 然后退出，<strong>既不杀掉占用者，也不换端口</strong>。结果是：上次没退干净的实例一直占着端口，你每次重跑都撞同一个端口，陷入”报错 → 手动杀 → 又报错”的死循环。</p>
<p><strong>✅ 解法</strong>：让脚本自己在启动时检测并释放端口，见下一节。</p>
<h2>六、根治：让脚本自己释放端口（Node.js 示例）</h2>
<p>与其每次手动查、手动杀，不如在脚本里加上”端口被占就自动 kill 占用者并重试监听”的逻辑。下面是一段<strong>跨平台、可直接抄</strong>的模板（Windows 用 <code>netstat</code>+<code>taskkill</code>，mac/Linux 用 <code>lsof</code>+<code>kill</code>）：</p>
<pre><code>import http from "node:http";
import { execSync } from "node:child_process";

const PORT = 3839;

// 查占用端口的进程 PID（按平台选用不同命令）
function findPidOnPort(port) {
  try {
    if (process.platform === "win32") {
      const out = execSync(
        `netstat -ano | findstr :${port} | findstr LISTENING`,
        { windowsHide: true }
      ).toString();
      for (const line of out.split(/\r?\n/)) {
        const m = line.trim().match(new RegExp(`:${port}\\s+.*\\s+(\\d+)\\s*$`));
        if (m) return parseInt(m[1], 10);
      }
    } else {
      const out = execSync(`lsof -ti :${port}`, { windowsHide: true }).toString();
      const pid = parseInt(out.trim().split(/\s+/)[0], 10);
      if (!isNaN(pid)) return pid;
    }
  } catch {
    // 无命中 / 命令不存在，忽略
  }
  return null;
}

// 强制终止进程（按平台选用不同命令）
function killPid(pid) {
  const cmd =
    process.platform === "win32" ? `taskkill /F /PID ${pid}` : `kill -9 ${pid}`;
  try {
    execSync(cmd, { windowsHide: true });
    return true;
  } catch {
    return false;
  }
}

function handler(req, res) {
  res.end("ok");
}

const MAX_RETRIES = 3;
let retries = 0;

function startServer() {
  const server = http.createServer(handler);
  server.listen(PORT, "127.0.0.1", () =&gt; {
    console.log(`Listening on http://127.0.0.1:${PORT}`);
  });

  server.on("error", (e) =&gt; {
    if (e.code === "EADDRINUSE") {
      if (retries &lt; MAX_RETRIES) {
        retries++;
        const pid = findPidOnPort(PORT);
        // 不杀自己；pid === process.pid 的极端情况跳过
        if (pid &amp;&amp; pid !== process.pid) {
          console.warn(
            `Port ${PORT} 被 PID ${pid} 占用，尝试终止后重试 (${retries}/${MAX_RETRIES})...`
          );
          if (killPid(pid)) {
            console.warn(`已终止 PID ${pid}，1 秒后重试...`);
            setTimeout(startServer, 1000);
            return;
          }
          console.error(`无法终止 PID ${pid}，请手动执行：taskkill /F /PID ${pid}`);
        } else {
          console.error(`端口 ${PORT} 被占用且无法定位占用者。`);
        }
        process.exit(1);
      } else {
        console.error(`端口 ${PORT} 重试 ${MAX_RETRIES} 次仍被占用。`);
        process.exit(1);
      }
    } else {
      console.error(`Server error: ${e.message}`);
      process.exit(1);
    }
  });
}

startServer();
</code></pre>
<p><strong>原理</strong>：<code>server.listen</code> 失败触发 <code>error</code> 事件，若错误码是 <code>EADDRINUSE</code>，就 <code>findPidOnPort</code> 找到占用者 → <code>killPid</code> 强杀 → 等 1 秒后 <code>startServer</code> 重新监听，最多重试 3 次。命中”坑 5”的场景时，这一套能自动清掉上次没退干净的实例，你只需再跑一次脚本即可。</p>
<blockquote><p>安全提示：自动强杀会终止占用端口的进程。本地开发端口（如 <code>127.0.0.1:3839</code>）一般只属于你自己，风险可控；但<strong>生产环境或重要服务端口请勿盲目套用</strong>，应先确认占用者身份。</p></blockquote>
<h2>七、注意事项</h2>
<ul>
<li><strong>先确认再杀</strong>：端口冲突常由你自己的旧服务实例未退出导致，但务必看一眼进程名，避免误杀数据库、SSH 等关键进程（尤其用自动释放脚本时）。</li>
<li><strong>系统级工具可能被禁用</strong>：某些安全策略会禁用 <code>tasklist</code> / <code>wmic</code> / <code>sc</code> 等。遇到工具不可用提示时，改用 <code>netstat</code>（查）+ PowerShell <code>Stop-Process</code> / <code>taskkill</code>（杀）。</li>
<li><strong>验证靠复查</strong>：无论用哪种方式杀，最后都 <code>netstat -ano | grep :3839</code> 复查一次，无输出才是真释放。</li>
<li><strong>根治端口冲突</strong>：首选在启动脚本里加「启动前自动检测并释放端口」的逻辑（见第六节），其次才是改用随机 / 可配置端口，省去每次手动清理。</li>
<li><strong><code>/F</code> 与 <code>-9</code> 都是强制终止</strong>：会立即结束进程且不给其清理机会，仅在常规退出无效时使用。</li>
</ul>
<hr />
<blockquote><p>本文命令与第六节代码均已在本机验证可用。如果对你有帮助，欢迎在评论区交流更优雅的端口管理姿势。</p></blockquote>]]></content>
    <category term="端口" />
    <category term="Windows" />
    <category term="网络" />
    <category term="终端" />
    <category term="教程" />
    <category term="排错" />
  </entry>
  <entry>
    <title>白嫖Anthropic：免费用户用Claude网页端编程</title>
    <link href="https://mps-blog.vercel.app//posts/claude-mcp-cloudflare-tunnel" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/claude-mcp-cloudflare-tunnel</id>
    <updated>2026-06-26T06:37:39.000Z</updated>
    <published>2026-06-26T06:37:39.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">通过Cloudflare Tunnel将coding-tools-mcp接入Claude网页版，让免费用户获得本地代码编辑能力。文章详细列出安装cloudflared、创建隧道、配置MCP服务、添加Connector的完整步骤，并剖析原方案的八大弊端与风险。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.BUh-vbQQ_8BDgE.webp" alt="白嫖Anthropic：免费用户用Claude网页端编程" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>白嫖Anthropic，免费用户也能够使用Claude进行编程，支持Claude Sonnet 4.6/4.5 thinking 模型</p>
<p>核心思路：将 coding-tools-mcp 接入到网页版本 Claude 当中，这样我们即可让网页版 Claude 获得本地代码编辑能力，化身 Claude Code。</p>
<p>架构链路：</p>
<p>Claude 网页版 → 公网 Tunnel → 本地 MCP Server → 你的代码目录</p>
<p>准备工作：</p>
<ol>
<li>一个能正常使用的 Claude 网页账号</li>
<li>Python 环境</li>
<li>一个测试用的代码目录</li>
</ol>
<p>第一步：安装 cloudflared</p>
<p>cloudflared 是 Cloudflare Tunnel 的命令行工具。后面要靠它把本地 8000 端口临时映射到公网去给 Claude 网页端访问。</p>
<p>macOS 直接用 Homebrew：</p>
<pre><code>brew install cloudflared
</code></pre>
<p>Windows 可以用 winget：</p>
<pre><code>winget install --id Cloudflare.cloudflared
</code></pre>
<p>Linux 就按自己的发行版来，去 Cloudflare 文档里下载对应包也行。装完先看一眼版本，确认命令能跑：</p>
<pre><code>cloudflared --version
</code></pre>
<p>第二步：创建 Cloudflare Quick Tunnel</p>
<p>现在把本地 8000 端口暴露出去：</p>
<pre><code>cloudflared tunnel --url http://localhost:8000
</code></pre>
<p>终端里会出现一个 trycloudflare.com 的临时地址。先别关这个窗口，后面配置 MCP 服务要用到它。这个 Tunnel 用完就关。它是公网入口，不是长期服务。</p>
<p>第三步：安装 coding-tools-mcp</p>
<p>安装 MCP 工具包：</p>
<pre><code>pip install coding-tools-mcp
</code></pre>
<p>我更建议放到虚拟环境里，后面清理也方便：</p>
<pre><code>python -m venv .venv
source .venv/bin/activate
pip install coding-tools-mcp
</code></pre>
<p>第四步：启动 MCP 服务</p>
<p>接下来配置几个 OAuth 环境变量，然后启动服务。这里的 client-id、client-secret、password 不要照抄示例，自己随便生成一组。关键是别太短，也别发给别人。</p>
<p>macOS / Linux：</p>
<pre><code>export CODING_TOOLS_MCP_OAUTH_CLIENT_ID="your-client-id"
export CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET="your-client-secret"
export CODING_TOOLS_MCP_OAUTH_PASSWORD="your-login-password"
export CODING_TOOLS_MCP_SERVER_URL="填入刚才 Cloudflare Tunnel 输出的地址"

coding-tools-mcp \
  --workspace /path/to/your/repo \
  --host 127.0.0.1 \
  --port 8000 \
  --oauth-mode
</code></pre>
<p>Windows PowerShell：</p>
<pre><code>$env:CODING_TOOLS_MCP_OAUTH_CLIENT_ID="your-client-id"
$env:CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET="your-client-secret"
$env:CODING_TOOLS_MCP_OAUTH_PASSWORD="your-login-password"
$env:CODING_TOOLS_MCP_SERVER_URL="https://xxx.trycloudflare.com/"

coding-tools-mcp `
  --workspace C:\Users\HP\Desktop\1 `
  --host 127.0.0.1 `
  --port 8000 `
  --oauth-mode
</code></pre>
<p>Windows CMD：</p>
<pre><code>set CODING_TOOLS_MCP_OAUTH_CLIENT_ID=your-client-id
set CODING_TOOLS_MCP_OAUTH_CLIENT_SECRET=your-client-secret
set CODING_TOOLS_MCP_OAUTH_PASSWORD=your-login-password
set CODING_TOOLS_MCP_SERVER_URL=https://xxx.trycloudflare.com/

coding-tools-mcp --workspace C:\Users\HP\Desktop\1 --host 127.0.0.1 --port 8000 --oauth-mode
</code></pre>
<p>将 —workspace 后面的内容填写为你希望 Claude 操作的临时目录。建议只给一个干净的小项目目录，别把整个根目录、下载目录、桌面等全暴露过去。</p>
<p>第五步：在 Claude 网页端添加 Connector</p>
<ol>
<li>打开侧边栏里的 Customize</li>
<li>选择 Connector</li>
<li>点击添加 Custom Connect</li>
<li>填入前面配置的服务地址和 OAuth 信息</li>
<li>点击 Connect</li>
<li>按页面提示完成验证</li>
</ol>
<p>连上之后，Claude 应该能看到 coding-tools-mcp 暴露出来的工具。这时候就可以让 Claude 做一些简单的代码任务了，比如读项目结构、定位报错、改一个函数、补一小段测试。</p>
<h2>原方案弊端</h2>


















































<table><thead><tr><th>问题</th><th>严重程度</th><th>说明</th></tr></thead><tbody><tr><td>Cloudflare Tunnel 公网暴露</td><td>高</td><td>trycloudflare.com 地址公开可访问，OAuth 凭据强度依赖自身设置</td></tr><tr><td>工作目录完整暴露</td><td>高</td><td>Claude 能看到 —workspace 下所有内容，包括 .env、密钥</td></tr><tr><td>OAuth 弱认证</td><td>中</td><td>手动生成的静态凭据，无令牌过期/刷新机制</td></tr><tr><td>Tunnel 频繁断开</td><td>中</td><td>免费 Tunnel 非长连接，断开后需重新配置</td></tr><tr><td>非真正的 Claude Code</td><td>中</td><td>网页版一次回复只调一次工具，无 agentic 自主循环</td></tr><tr><td>上下文消耗快</td><td>中</td><td>MCP 工具定义重复加载，多文件项目很容易打满窗口</td></tr><tr><td>免费用户速率限制</td><td>中</td><td>对话频率和长度严格受限</td></tr><tr><td>数据隐私风险</td><td>中</td><td>代码经 Anthropic 服务器处理，免费用户数据可能用于训练</td></tr></tbody></table>
<h2>更优方案：Claude 桌面端 + 本地 MCP（推荐）</h2>
<p>如果你能安装 Claude 桌面客户端，可以完全绕开 cloudflared 和公网暴露：</p>
<pre><code>Claude 桌面客户端 ← 本地进程通信 → MCP Server（localhost）← 你的代码目录
</code></pre>
<p>配置文件 <code>claude_desktop_config.json</code>：</p>
<pre><code>{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "D:/Projects/your-repo"
      ]
    }
  }
}
</code></pre>
<p>优势：数据全程不离开本机、不需要 cloudflared 和 OAuth、支持 agentic 连续工具调用、一次配置永久生效。</p>
<h2>Token / 上下文优化策略</h2>





















<table><thead><tr><th>手段</th><th>效果</th></tr></thead><tbody><tr><td>Claude Context MCP</td><td>Token 降低约 40%，代码库向量化索引</td></tr><tr><td>AgenticStore Token Optimizer</td><td>自动削注释和空白</td></tr><tr><td>Cloudflare Code Mode</td><td>Token 消耗降低 99%，AI 生成代码块一次性执行</td></tr></tbody></table>
<p>多账号轮换：注册 2-3 个 Claude 账号，桌面端 Sign Out 后登录下一个，MCP 配置存本地、切换账号依然生效。</p>
<h2>连接后怎么使用</h2>
<p>直接输入自然语言即可：</p>
<ul>
<li>看项目结构：「先看看我这个项目有哪些文件」</li>
<li>读代码：「读一下 main.py，告诉我这个项目是干什么的」</li>
<li>定位报错：「运行 main.py 报错了，报错信息是 xxx，帮我看看」</li>
<li>改代码：「把 utils.py 里的 fetch_data 改成用异步请求」</li>
<li>补测试：「给 calculator.py 里每个函数写单元测试」</li>
<li>重构：「把 config.py 里所有配置项改成从 .env 读取」</li>
</ul>
<p>使用技巧：先说「读一下 xxx」让 Claude 先了解代码再说怎么改；描述结果而非步骤；贴完整报错信息；一次只做一件事，改完验证再继续。</p>
<p>工具权限建议：读操作设 Auto（自动允许），写/删操作保持 Needs approval（需批准）。</p>
<h2>安全提醒</h2>
<ul>
<li>不要在暴露的工作目录下存放 .env、私钥、密码文件</li>
<li>用完及时关闭 cloudflared 终端</li>
<li>凭据不要硬编码在脚本里，不要分享给他人</li>
<li>有条件优先使用 Claude 桌面端 + 本地 MCP</li>
</ul>
<p>结论：把 MCP 接入网页版 Claude 确实能让免费用户获得本地代码编辑能力，但免费额度非常有限——基本两次对话就耗尽 token，适合尝鲜体验，正经干活还是乖乖付费。</p>]]></content>
    <category term="Claude" />
    <category term="MCP" />
    <category term="Cloudflare Tunnel" />
    <category term="编程工具" />
    <category term="免费方案" />
  </entry>
  <entry>
    <title>MIMO API 代理部署全记录：本地代理与 Cloudflare Worker 方案</title>
    <link href="https://mps-blog.vercel.app//posts/mimo-openai-proxy" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/mimo-openai-proxy</id>
    <updated>2026-06-11T16:00:00.000Z</updated>
    <published>2026-06-11T16:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">从原理到实战，详解如何为小米 MIMO API 搭建 OpenAI 兼容代理。涵盖 JWT 自动获取、CORS 修复、本地 Flask 代理、Cloudflare Worker 代理及 IP 绑定导致的 401 问题与重试方案。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.D7P1ydVB_Z205S3v.webp" alt="MIMO API 代理部署全记录：本地代理与 Cloudflare Worker 方案" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>背景</h2>
<p>小米 MIMO API（<code>api.xiaomimimo.com</code>）提供了一个免费的 AI 对话接口，但它并非标准的 OpenAI 兼容格式——需要先通过 <code>/bootstrap</code> 端点获取 JWT，再用 JWT 调用 <code>/v1/chat/completions</code>。这让绝大多数支持自定义 API 的聊天客户端（如 ChatBox、NextChat、LobeChat 等）无法直接接入。</p>
<p>本文将记录从零搭建 MIMO OpenAI 兼容代理的完整过程，涵盖两种方案：</p>




















<table><thead><tr><th>方案</th><th>适用场景</th><th>难度</th></tr></thead><tbody><tr><td>本地 Python Flask 代理</td><td>本机使用、调试方便</td><td>简单</td></tr><tr><td>Cloudflare Worker 代理</td><td>多人共享、无需本地运行</td><td>中等</td></tr></tbody></table>
<hr />
<h2>技术原理</h2>
<h3>MIMO API 认证流程</h3>
<pre><code>┌──────────┐     GET /bootstrap      ┌──────────────┐
│          │ ───────────────────────→ │              │
│  客户端   │ ←─────────────────────── │  MIMO API    │
│          │   { jwt: "xxx.xxx.xxx" } │              │
│          │                          │              │
│          │  POST /v1/chat/completions              │
│          │  Authorization: Bearer xxx              │
│          │  X-Mimo-Source: mimocode-cli-free       │
│          │ ───────────────────────→ │              │
│          │ ←─────────────────────── │              │
│          │   SSE stream response    │              │
└──────────┘                          └──────────────┘
</code></pre>
<p>关键点：</p>
<ol>
<li><strong>Bootstrap</strong>：无认证，返回 JWT，有效期约 5 分钟</li>
<li><strong>Chat</strong>：需 <code>Authorization: Bearer {jwt}</code> + <code>X-Mimo-Source: mimocode-cli-free</code> 两个 Header</li>
<li><strong>响应</strong>：标准 SSE（Server-Sent Events）流式格式，<code>data:</code> 前缀，最后以 <code>data: [DONE]</code> 结束</li>
</ol>
<h3>代理要做的事</h3>
<pre><code>聊天客户端（OpenAI 格式）  →  代理  →  MIMO API
</code></pre>
<ol>
<li>暴露 <code>/v1/models</code> —— 返回可用模型列表</li>
<li>暴露 <code>/v1/chat/completions</code> —— 接收 OpenAI 格式请求</li>
<li>自动管理 JWT：到期前 5 分钟自动刷新</li>
<li>转发请求到 MIMO，透传 SSE 流</li>
</ol>
<hr />
<h2>方案一：本地 Python Flask 代理</h2>
<h3>完整代码</h3>
<p>创建 <code>mimo-proxy.py</code>：</p>
<pre><code>import time
import requests
from flask import Flask, Response, request, stream_with_context
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

MIMO_BASE = "https://api.xiaomimimo.com/api/free-ai"
_jwt_cache = {"token": None, "expires_at": 0}


def get_jwt():
    """获取 JWT，缓存 5 分钟"""
    now = time.time()
    if _jwt_cache["token"] and now &lt; _jwt_cache["expires_at"]:
        return _jwt_cache["token"]

    resp = requests.get(f"{MIMO_BASE}/bootstrap", timeout=10)
    resp.raise_for_status()
    token = resp.json()["jwt"]
    _jwt_cache["token"] = token
    _jwt_cache["expires_at"] = now + 270  # 4.5 分钟刷新
    return token


@app.route("/v1/models", methods=["GET", "OPTIONS"])
def list_models():
    return {
        "object": "list",
        "data": [{"id": "mimo-v2.5-pro", "object": "model"}],
    }


@app.route("/v1/chat/completions", methods=["POST", "OPTIONS"])
def chat_completions():
    jwt = get_jwt()
    body = request.get_json(force=True)

    headers = {
        "Authorization": f"Bearer {jwt}",
        "X-Mimo-Source": "mimocode-cli-free",
        "Content-Type": "application/json",
    }

    resp = requests.post(
        f"{MIMO_BASE}/v1/chat/completions",
        json=body,
        headers=headers,
        stream=True,
        timeout=120,
    )

    def generate():
        for chunk in resp.iter_content(chunk_size=None):
            if chunk:
                yield chunk

    return Response(
        stream_with_context(generate()),
        content_type="text/event-stream; charset=utf-8",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        },
    )


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8787, threaded=True)
</code></pre>
<h3>部署步骤</h3>
<pre><code># 安装依赖
pip install flask flask-cors requests

# 启动代理
python mimo-proxy.py
</code></pre>
<h3>客户端配置</h3>
<p>将聊天客户端的 API Base URL 设为：</p>
<pre><code>http://127.0.0.1:8787/v1
</code></pre>
<p>API Key 任意填写（代理层不校验），模型选 <code>mimo-v2.5-pro</code>。</p>
<h3>CORS 修复</h3>
<p>前端页面直连代理时，浏览器会发送 OPTIONS 预检请求。<code>flask-cors</code> 的 <code>CORS(app)</code> 一行即可自动处理所有跨域问题。</p>
<p>如果遇到 <strong>“non ISO-8859-1 code point”</strong> 错误，那是浏览器 fetch API 层面的限制——请求 Header 值必须为纯 ASCII。排查方向：</p>
<ul>
<li>API Key 中是否含中文</li>
<li>自定义 Header 中是否含特殊字符</li>
<li>Base URL 是否正确编码</li>
</ul>
<hr />
<h2>方案二：Cloudflare Worker 代理</h2>
<p>Cloudflare Worker 是一个免费（每天 10 万次请求）的边缘计算平台，适合搭建无需本地常驻的代理服务。</p>
<h3>第一版：通用 CORS Worker</h3>
<p>最初使用了一个通用 CORS Worker，接收 POST 请求后转发：</p>
<pre><code>// 通用 CORS Worker —— 不推荐
export default {
  async fetch(request) {
    const { url, method, headers, data } = await request.json();
    const resp = await fetch(url, { method, headers, body: data });
    const body = await resp.text();
    return new Response(JSON.stringify({ status: resp.status, body_text: body }), {
      headers: { "Content-Type": "application/json" },
    });
  },
};
</code></pre>
<p>前端页面先调用 <code>/bootstrap</code> 获取 JWT，再通过 Worker 调用 <code>/v1/chat/completions</code>。</p>
<p><strong>问题</strong>：Bootstrap 成功返回 JWT，但 Chat 请求返回 <code>401 Invalid Token</code>。</p>
<h3>根因分析：JWT IP 绑定</h3>
<pre><code>请求 1 (bootstrap)： 用户 → CF 边缘节点 A → MIMO API  → JWT (绑定节点 A 的 IP)
请求 2 (chat)：     用户 → CF 边缘节点 B → MIMO API  → 401 (IP 不匹配!)
</code></pre>
<p>Cloudflare 采用 Anycast 架构，两次请求可能路由到不同边缘节点，导致出口 IP 不一致。MIMO API 的 JWT 绑定了请求来源 IP，IP 不一致则校验失败。</p>
<p>本地代理不会出现此问题，因为本机 IP 始终一致。</p>
<h3>第二版：专用 Worker + 重试机制</h3>
<p>思路：将 Bootstrap 和 Chat 放在同一个 Worker 实例内，加入重试逻辑——401 时清除 JWT 缓存 → 重新 Bootstrap → 立即重试 Chat，利用重试落在同一节点的概率。</p>
<pre><code>// mimo-worker.js —— 推荐方案
let _jwt = null;
let _jwtExp = 0;

async function getJwt(forceRefresh = false) {
  const now = Date.now();
  if (!forceRefresh &amp;&amp; _jwt &amp;&amp; now &lt; _jwtExp) return _jwt;

  const resp = await fetch(`${MIMO_BASE}/bootstrap`);
  const data = await resp.json();
  _jwt = data.jwt;
  _jwtExp = now + 270_000; // 4.5 分钟
  return _jwt;
}

async function chatWithRetry(messages, retries = 2) {
  for (let i = 0; i &lt; retries; i++) {
    const forceRefresh = i &gt; 0;
    const jwt = await getJwt(forceRefresh);

    const resp = await fetch(`${MIMO_BASE}/v1/chat/completions`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${jwt}`,
        "X-Mimo-Source": "mimocode-cli-free",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ model: "mimo-v2.5-pro", messages }),
    });

    if (resp.status === 401 &amp;&amp; i &lt; retries - 1) continue;
    return resp;
  }
}

// 暴露 OpenAI 兼容端点
// - GET /v1/models → 返回模型列表
// - POST /v1/chat/completions → 代理到 MIMO
</code></pre>
<h3>部署到 Cloudflare</h3>
<pre><code># 安装 Wrangler CLI
npm install -g wrangler

# 登录
wrangler login

# 创建 Worker
wrangler init mimo-proxy

# 部署
wrangler deploy
</code></pre>
<p>部署后获得 Worker URL，例如 <code>https://mimo.xxx.workers.dev</code>，客户端配置：</p>
<pre><code>API Base URL: https://mimo.xxx.workers.dev/v1
</code></pre>
<h3>重试机制的有效性</h3>
<p>重试不是 100% 可靠的解决方案，但在实践中：</p>





















<table><thead><tr><th>重试次数</th><th>成功率（估计）</th></tr></thead><tbody><tr><td>0（不重试）</td><td>~40% — 取决于两次请求是否落在同一节点</td></tr><tr><td>1 次重试</td><td>~70%</td></tr><tr><td>2 次重试</td><td>~88%</td></tr></tbody></table>
<p>如果两次重试仍失败，Worker 返回 401，前端可提示用户重试。这只是概率问题，多试几次通常能通过。</p>
<h3>进一步优化方向</h3>
<ol>
<li><strong>Scheduled Trigger</strong>：用 Cron Trigger 定时预热 JWT，减少首次请求延迟</li>
<li><strong>KV 持久化</strong>：将 JWT 存入 Cloudflare KV，跨 Worker 实例共享（但无法解决核心 IP 绑定问题）</li>
<li><strong>Durable Objects</strong>：用 DO 保证 Bootstrap 和 Chat 在同一实例内执行，从根本上解决 IP 一致性问题</li>
<li><strong>回退到本地代理</strong>：最稳妥的方案，本机 IP 始终一致</li>
</ol>
<hr />
<h2>方案对比与选择</h2>








































<table><thead><tr><th>维度</th><th>本地 Flask 代理</th><th>CF Worker + 重试</th></tr></thead><tbody><tr><td>部署难度</td><td>一条命令</td><td>需 Wrangler CLI</td></tr><tr><td>稳定性</td><td>JWT 100% 有效</td><td>有小概率 401 需重试</td></tr><tr><td>可用范围</td><td>仅本机</td><td>任意设备</td></tr><tr><td>运行依赖</td><td>需本地 Python 进程常驻</td><td>无，CF 托管</td></tr><tr><td>首包延迟</td><td>低（localhost）</td><td>中（走 CF 网络）</td></tr><tr><td>适用场景</td><td>个人开发调试</td><td>团队共享、移动端使用</td></tr></tbody></table>
<p><strong>结论</strong>：个人使用推荐本地代理，简单可靠；多人共享或希望在手机上使用推荐 Worker 方案。</p>
<hr />
<h2>附录：完整文件清单</h2>

























<table><thead><tr><th>文件</th><th>说明</th></tr></thead><tbody><tr><td><code>mimo-proxy.py</code></td><td>Python Flask 本地代理</td></tr><tr><td><code>mimo-worker.js</code></td><td>Cloudflare Worker 专用代理</td></tr><tr><td><code>mimo-chat.html</code></td><td>纯前端聊天测试页面（通过 Worker 调用 MIMO）</td></tr><tr><td><code>mimo-test.html</code></td><td>最小化连通性测试页面</td></tr></tbody></table>
<p>所有代码均为 MIT 协议，可自由修改使用。</p>]]></content>
    <category term="MIMO" />
    <category term="API" />
    <category term="Cloudflare" />
    <category term="代理" />
    <category term="Python" />
    <category term="Worker" />
  </entry>
  <entry>
    <title>零成本 Agent 工作流实战：Multica + OpenCode + Codespaces + LongCat 全流程部署指南</title>
    <link href="https://mps-blog.vercel.app//posts/multica-opencode-codespaces-longcat-agent-workflow" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/multica-opencode-codespaces-longcat-agent-workflow</id>
    <updated>2026-05-28T00:00:00.000Z</updated>
    <published>2026-05-28T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">把&quot;任务丢进 Issue，Agent 自动写代码&quot;做成日常工作流，全程零成本。从 LongCat API Key 申请、Multica 注册、Codespace 配置到 Agent 跑通第一个任务的完整部署方案，附进阶技巧、流量管控和故障排查。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.DMOwRouP_Zbqr1Y.webp" alt="零成本 Agent 工作流实战：Multica + OpenCode + Codespaces + LongCat 全流程部署指南" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h1>零成本 Agent 工作流实战：Multica + OpenCode + Codespaces + LongCat 全流程部署指南</h1>
<blockquote><p>把”任务丢进 Issue，Agent 自动写代码”做成日常工作流，全程零成本。</p><p>这不是 demo，是一套你今天就能搭起来、明天就能开始干活的真实流水线。</p></blockquote>
<hr />
<h2>目录</h2>
<ul>
<li><a href="#%E4%B8%80%E8%BF%99%E5%A5%97%E6%96%B9%E6%A1%88%E5%88%B0%E5%BA%95%E5%9C%A8%E5%81%9A%E4%BB%80%E4%B9%88">一、这套方案到底在做什么</a></li>
<li><a href="#%E4%BA%8C%E5%9B%9B%E4%B8%AA%E7%BB%84%E4%BB%B6%E5%90%84%E8%87%AA%E6%89%AE%E6%BC%94%E7%9A%84%E8%A7%92%E8%89%B2">二、四个组件各自扮演的角色</a></li>
<li><a href="#%E4%B8%89%E5%89%8D%E7%BD%AE%E5%87%86%E5%A4%87%E6%B8%85%E5%8D%95">三、前置准备清单</a></li>
<li><a href="#%E5%9B%9B%E8%AF%A6%E7%BB%86%E9%83%A8%E7%BD%B2%E6%AD%A5%E9%AA%A4">四、详细部署步骤</a>
<ul>
<li><a href="#step-1%E7%94%B3%E8%AF%B7-longcat-api-key">Step 1：申请 LongCat API Key</a></li>
<li><a href="#step-2%E6%B3%A8%E5%86%8C-multica-%E5%B9%B6%E8%8E%B7%E5%8F%96-token">Step 2：注册 Multica 并获取 Token</a></li>
<li><a href="#step-3%E5%87%86%E5%A4%87%E4%B8%80%E4%B8%AA-github-%E4%BB%93%E5%BA%93">Step 3：准备一个 GitHub 仓库</a></li>
<li><a href="#step-4%E9%85%8D%E7%BD%AE-codespaces-secrets">Step 4：配置 Codespaces Secrets</a></li>
<li><a href="#step-5%E7%BC%96%E5%86%99-devcontainerjson">Step 5：编写 devcontainer.json</a></li>
<li><a href="#step-6%E7%BC%96%E5%86%99-installsh%E9%A6%96%E6%AC%A1%E5%AE%89%E8%A3%85%E8%84%9A%E6%9C%AC">Step 6：编写 install.sh（首次安装脚本）</a></li>
<li><a href="#step-7%E7%BC%96%E5%86%99-opencode-%E7%9A%84-provider-%E9%85%8D%E7%BD%AE">Step 7：编写 OpenCode 的 Provider 配置</a></li>
<li><a href="#step-8%E7%BC%96%E5%86%99-startsh%E6%AF%8F%E6%AC%A1%E5%90%AF%E5%8A%A8%E8%84%9A%E6%9C%AC">Step 8：编写 start.sh（每次启动脚本）</a></li>
<li><a href="#step-9%E5%90%AF%E5%8A%A8-codespace-%E5%B9%B6%E9%AA%8C%E8%AF%81">Step 9：启动 Codespace 并验证</a></li>
<li><a href="#step-10%E5%9C%A8-multica-%E5%88%9B%E5%BB%BA-agent-%E5%B9%B6%E8%B7%91%E9%80%9A%E7%AC%AC%E4%B8%80%E4%B8%AA%E4%BB%BB%E5%8A%A1">Step 10：在 Multica 创建 Agent 并跑通第一个任务</a></li>
</ul>
</li>
<li><a href="#%E4%BA%94%E8%8E%B7%E5%BE%97%E6%9B%B4%E5%A5%BD-agent-%E4%BD%93%E9%AA%8C%E7%9A%84%E8%BF%9B%E9%98%B6%E6%8A%80%E5%B7%A7">五、获得更好 Agent 体验的进阶技巧</a></li>
<li><a href="#%E5%85%AD%E6%B5%81%E9%87%8F%E8%B5%B0%E5%90%91%E4%B8%8E%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95%E7%AE%A1%E6%8E%A7">六、流量走向与历史记录管控</a></li>
<li><a href="#%E4%B8%83%E5%B8%B8%E8%A7%81%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5">七、常见故障排查</a></li>
<li><a href="#%E5%85%AB%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A%E4%B8%8E%E7%99%BD%E5%AB%96%E7%BB%8F%E9%AA%8C">八、心得体会与白嫖经验</a></li>
</ul>
<hr />
<h2>一、这套方案到底在做什么</h2>
<p>如果你用过 Cursor、Claude Code 之类的 AI 编程工具，应该有过这种感受：<strong>每次打开都要重新解释项目背景，多个任务并行就管不过来，跑过的经验也沉淀不下来</strong>。这套方案要解决的就是这个问题——把 AI 编程 Agent 装进一个真正像”团队成员”的协作环境里：</p>
<ul>
<li>你像分配同事一样在看板上给 Agent 派活；</li>
<li>Agent 自己读代码、改代码、跑命令，过程实时可见；</li>
<li>任务完成或卡住，Agent 主动汇报进展；</li>
<li>整套环境跑在云端，你关掉浏览器它也在跑。</li>
</ul>
<p>而且——<strong>全程不花钱</strong>。</p>
<p>最终效果大概是这样的工作流：</p>
<pre><code>你在 Multica Web 端写一条 Issue 「给 user 模块加单元测试」
        │
        ▼
Multica 把任务推给云端 Codespace 里的 daemon
        │
        ▼
daemon 调起 OpenCode CLI，加载项目代码
        │
        ▼
OpenCode 通过 LongCat API 思考、生成代码、跑 pytest
        │
        ▼
执行日志实时回传到 Multica 看板，最终产出 PR
</code></pre>
<hr />
<h2>二、四个组件各自扮演的角色</h2>
<p>理解每个组件的职责，后面调试出问题时才知道该看哪一层。</p>
<h3>Multica（任务管理 + 协作前台）</h3>
<p>开源的 Managed Agents 平台，定位是”AI 编程团队的项目管理系统”。它提供 Web 看板、Issue、Agent 资料卡、运行时管理等能力，你和 Agent 在同一个界面里协作。本身不执行代码，只是把任务调度给你的本地（或 Codespace 里的）daemon。</p>
<h3>OpenCode（实际干活的 Agent）</h3>
<p>终端里的开源 AI 编码 Agent，支持任意 OpenAI 兼容的 LLM。它负责真正读代码、改代码、跑测试。Multica 把它当作一种 “runtime” 来调用。</p>
<h3>LongCat API（模型算力）</h3>
<p>美团开源的 LongCat 系列大模型对应的 API 平台，提供 OpenAI 兼容的接口和<strong>每日数千万 Token 级</strong>的免费额度。其中 <code>LongCat-Flash-Thinking-2601</code> 是开源 SOTA 级别的推理/Agent 模型，工具调用能力很强。</p>
<h3>GitHub Codespaces（运行环境）</h3>
<p>云端 Linux 容器，每个 GitHub 个人账号每月有 <strong>120 core-hours</strong> 免费额度（对应 2 核机型每月约 60 小时使用时长）。我们把 Multica daemon 和 OpenCode CLI 都跑在里面，你的本地电脑不需要装任何东西。</p>
<h3>整体架构</h3>
<pre>flowchart LR
    A[人类用户] --&gt;|创建 Issue| B[Multica<br />任务管理层]
    B --&gt;|分发任务| C[OpenCode Agent<br />执行层]
    C --&gt;|API 调用| D[LongCat API<br />模型算力层]
    C --&gt;|读写代码| E[GitHub Codespaces<br />运行环境层]
    E -.-&gt;|宿主| B
    E -.-&gt;|宿主| C
    C --&gt;|状态/日志回写| B</pre>
<hr />
<h2>三、前置准备清单</h2>
<p>正式开搞前确认以下账号都准备好。每一项后面括号里写了为什么要它，方便你判断是否能省略。</p>






























<table><thead><tr><th>账号/工具</th><th>用途</th><th>备注</th></tr></thead><tbody><tr><td>GitHub 账号</td><td>开 Codespace、托管代码</td><td>必须，免费即可</td></tr><tr><td>Multica 账号</td><td>任务管理</td><td>在 <a href="https://multica.ai" rel="noopener noreferrer" target="_blank">multica.ai</a> 注册，免费</td></tr><tr><td>LongCat 平台账号</td><td>拿 API Key</td><td>在 <a href="https://longcat.chat/platform" rel="noopener noreferrer" target="_blank">longcat.chat/platform</a> 注册</td></tr><tr><td>一个浏览器</td><td>完成所有操作</td><td>全程不需要装本地软件</td></tr></tbody></table>
<hr />
<h2>四、详细部署步骤</h2>
<blockquote><p>接下来每一步我都会写到”你应该看到什么”的级别。如果某一步的实际输出和文档里描述的不一样，<strong>先停下来排查</strong>，不要继续往下走。</p></blockquote>
<h3>Step 1：申请 LongCat API Key</h3>
<h4>1.1 注册并登录平台</h4>
<p>访问 <a href="https://longcat.chat/platform" rel="noopener noreferrer" target="_blank">longcat.chat/platform</a>，用手机号或邮箱注册一个账号。登录后进入控制台。</p>
<h4>1.2 创建 API Key</h4>
<p>在左侧菜单找到「API Keys」或「密钥管理」，点击「创建新密钥」。</p>
<ul>
<li>名字随便起，比如 <code>codespace-agent</code>；</li>
<li>创建后会弹出一串以 <code>ak_</code> 开头的字符串（具体前缀以平台为准）；</li>
<li><strong>这个 Key 只会完整显示一次，立刻复制保存到密码管理器</strong>，关掉弹窗就再也看不到完整值。</li>
</ul>
<h4>1.3 记录关键信息</h4>
<p>把以下三项信息记下来，后面 Step 7 要用：</p>
<pre><code>API Key:    sk-xxxxxxxxxxxxxxxxxxxx       (你刚拿到的)
Base URL:   https://api.longcat.chat/openai/v1
模型名:     LongCat-Flash-Chat            (轻量任务用)
            LongCat-Flash-Thinking-2601   (复杂推理/Agent 任务用,推荐默认)
</code></pre>
<h4>1.4 验证 Key 可用（可选但建议）</h4>
<p>打开任何一台能联网的电脑，跑下面这条 curl，确认 Key 没拿错：</p>
<pre><code>curl https://api.longcat.chat/openai/v1/chat/completions \
  -H "Authorization: Bearer YOUR_LONGCAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "LongCat-Flash-Chat",
    "messages": [{"role": "user", "content": "你好"}]
  }'
</code></pre>
<p>如果返回了一段 JSON 且里面有 <code>"role":"assistant"</code> 字段，说明 Key 可用。如果返回 401，说明 Key 错了或者粘贴时多带了空格；返回 429 说明额度用完，等到第二天 0 点重置。</p>
<hr />
<h3>Step 2：注册 Multica 并获取 Token</h3>
<h4>2.1 注册账号</h4>
<p>访问 <a href="https://multica.ai" rel="noopener noreferrer" target="_blank">multica.ai</a>，点 Sign Up，用邮箱或 GitHub OAuth 登录都可以。</p>
<h4>2.2 创建 Workspace</h4>
<p>第一次登录会引导你创建一个 Workspace。Workspace 是 Multica 的隔离单元——每个 Workspace 有独立的 Issue、Agent、Runtime。给它起个名字，比如 <code>solo-lab</code>。</p>
<h4>2.3 生成 Personal Access Token (PAT)</h4>
<p>进入 <code>Settings → Personal Access Tokens → New Token</code>：</p>
<ul>
<li>Name：<code>codespace-bootstrap</code>；</li>
<li>创建后会得到一串以 <code>mul_</code> 开头的 token；</li>
<li><strong>同样只显示一次，立刻保存</strong>。</li>
</ul>
<blockquote><p><strong>关于 Token 的安全选择</strong>：Multica 实际上有三种 Token——浏览器 cookie、PAT (<code>mul_</code>) 和 daemon token (<code>mdt_</code>)。PAT 可以访问你所有的 Workspace，daemon token 只绑定到单个 Workspace。出于最小权限原则，<strong>生产环境建议在 Multica Web 端生成专门的 daemon token 给 Codespace 使用</strong>（<code>Workspace Settings → Daemon Tokens</code>）。本教程为了简化流程先用 PAT，跑通后建议换成 daemon token。</p></blockquote>
<hr />
<h3>Step 3：准备一个 GitHub 仓库</h3>
<p>这个仓库扮演两个角色：</p>
<ol>
<li>它是 Codespace 的”宿主”——Codespace 必须从某个仓库启动；</li>
<li>它也是 Agent 实际操作的代码库（你想让 Agent 帮你改的项目）。</li>
</ol>
<p>如果你已有项目就用现成的；没有的话新建一个空仓库 <code>agent-playground</code> 即可。<strong>仓库可以是 Private</strong>，Codespaces Secrets 只对 owner 可见，不会泄露。</p>
<p>在仓库根目录创建一个 <code>.devcontainer/</code> 文件夹，后面所有配置文件都放这里。</p>
<pre><code>agent-playground/
├── .devcontainer/
│   ├── devcontainer.json
│   ├── install.sh
│   └── start.sh
├── .gitignore
└── README.md
</code></pre>
<hr />
<h3>Step 4：配置 Codespaces Secrets</h3>
<p>Codespaces Secrets 是 GitHub 帮你管理敏感环境变量的机制，<strong>不会被写进仓库、不会随 fork 流出</strong>，但会被注入到容器的环境变量里。</p>
<h4>4.1 进入设置页</h4>
<p>访问 <a href="https://github.com/settings/codespaces" rel="noopener noreferrer" target="_blank">github.com/settings/codespaces</a>（个人级别 Secrets，对你所有仓库可用）。</p>
<p>也可以用仓库级别 Secrets：进入仓库 <code>Settings → Secrets and variables → Codespaces</code>。个人级别更方便，仓库级别更隔离，自己看着选。</p>
<h4>4.2 添加两个 Secret</h4>
<p>点击 <code>New secret</code>，依次添加：</p>
<p><strong>第一个：</strong></p>
<ul>
<li>Name: <code>LONGCAT_API_KEY</code></li>
<li>Value: Step 1 拿到的 LongCat API Key</li>
<li>Repository access: 选你刚创建的 <code>agent-playground</code> 仓库</li>
</ul>
<p><strong>第二个：</strong></p>
<ul>
<li>Name: <code>MULTICA_TOKEN</code></li>
<li>Value: Step 2 拿到的 Multica PAT（<code>mul_</code> 开头）</li>
<li>Repository access: 同上</li>
</ul>
<blockquote><p>⚠️ Codespaces Secrets 名字<strong>大小写不敏感</strong>，统一用全大写命名最稳妥。</p></blockquote>
<hr />
<h3>Step 5：编写 devcontainer.json</h3>
<p>这是 Codespace 的”出厂配置”。把下面内容保存为 <code>.devcontainer/devcontainer.json</code>：</p>
<pre><code>{
  "name": "multica-opencode-agent",
  "image": "mcr.microsoft.com/devcontainers/universal:latest",

  // 安装常用语言运行时(按你项目需要增减)
  "features": {
    "ghcr.io/devcontainers/features/node:1": {},
    "ghcr.io/devcontainers/features/python:1": {},
  },

  // 声明这个仓库需要的 Secrets。即便你已经在 Step 4 配过,
  // 这里再声明一次会让其他用户 fork 时收到提示
  "secrets": {
    "LONGCAT_API_KEY": {
      "description": "LongCat API Key, 在 longcat.chat/platform 申请",
    },
    "MULTICA_TOKEN": {
      "description": "Multica PAT 或 Daemon Token, 在 Multica Settings 生成",
    },
  },

  // 容器首次创建时执行(只跑一次)
  "postCreateCommand": "bash .devcontainer/install.sh",

  // 每次容器启动时执行(stop/resume 后也会跑)
  "postStartCommand": "bash .devcontainer/start.sh",

  // 让 secrets 在容器里以同名环境变量暴露
  "remoteEnv": {
    "LONGCAT_API_KEY": "${localEnv:LONGCAT_API_KEY}",
    "MULTICA_TOKEN": "${localEnv:MULTICA_TOKEN}",
  },
}
</code></pre>
<h4>关键字段拆解</h4>
<ul>
<li><code>image</code>: 用 GitHub 官方维护的 universal 镜像，自带常见工具链。也可以换成更精简的 <code>debian</code> 镜像，但需要自己装更多依赖。</li>
<li><code>features</code>: 声明式安装额外工具。<code>node</code> 是因为 OpenCode 的 npm 安装路径需要它（即便你不用 npm 装 OpenCode，留着也无害）。</li>
<li><code>secrets</code>: <strong>是给 fork 你这份配置的人看的提示</strong>，本身不存值，值还是从 Step 4 配的 Secrets 来。</li>
<li><code>postCreateCommand</code> vs <code>postStartCommand</code>: 前者只在容器<strong>首次创建</strong>时跑（装东西），后者每次 Codespace 唤醒都跑（拉服务）。这个区分非常重要，否则每次唤醒都重新装一遍 OpenCode 会很慢。</li>
<li><code>remoteEnv</code> + <code>${localEnv:XXX}</code>: 这是 Codespaces 注入 Secret 的标准写法。<code>localEnv</code> 在 Codespaces 上下文中实际指向”宿主环境”，也就是 Codespaces 的 Secret 注入点。</li>
</ul>
<hr />
<h3>Step 6：编写 install.sh（首次安装脚本）</h3>
<p>把下面内容保存为 <code>.devcontainer/install.sh</code>，并确保有执行权限（用 git 提交时若发现没权限，跑一下 <code>chmod +x .devcontainer/install.sh</code>）。</p>
<pre><code>#!/usr/bin/env bash
set -euo pipefail

echo "== 首次安装：OpenCode + Multica CLI =="

# ---------- 安装 OpenCode ----------
echo "[1/2] Installing OpenCode..."
curl -fsSL https://opencode.ai/install | bash
# 官方安装脚本会把二进制放到 ~/.opencode/bin/opencode
# 把它加到当前 shell 的 PATH (start.sh 会再处理一次持久化)
export PATH="$HOME/.opencode/bin:$PATH"
opencode --version

# ---------- 安装 Multica CLI ----------
echo "[2/2] Installing Multica CLI..."
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
multica version

echo "== 首次安装完成 =="
</code></pre>
<h4>这一步的注意点</h4>
<ol>
<li><code>set -euo pipefail</code> 让脚本任一步出错都立即退出，避免装一半留下半截环境。</li>
<li>OpenCode 官方安装脚本会写入 <code>~/.bashrc</code> 让 PATH 生效，但<strong>当前这次脚本执行的 shell 不会自动 reload</strong>，所以我们手动 export 一次。</li>
<li>这个脚本只在 <code>postCreateCommand</code> 阶段跑一次，对应 Codespace <strong>首次创建</strong>或<strong>重建</strong>的时机。日常 stop/resume 不会重跑。</li>
</ol>
<hr />
<h3>Step 7：编写 OpenCode 的 Provider 配置</h3>
<p>让 OpenCode 知道 LongCat 在哪、用哪个模型。我们不在 install.sh 里写死 API Key，而是用变量替换语法 <code>{env:VAR_NAME}</code>，这样 Key 永远只活在环境变量里、不落到磁盘。</p>
<p>在仓库根目录新建 <code>opencode.json</code>（注意：这是项目级配置，OpenCode 会自动读取项目根目录下的同名文件）：</p>
<pre><code>{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "longcat": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "LongCat (Meituan)",
      "options": {
        "baseURL": "https://api.longcat.chat/openai/v1",
        "apiKey": "{env:LONGCAT_API_KEY}"
      },
      "models": {
        "LongCat-Flash-Chat": {
          "name": "LongCat Chat (轻量,日常对话)",
          "limit": { "context": 128000, "output": 8192 }
        },
        "LongCat-Flash-Thinking-2601": {
          "name": "LongCat Thinking 2601 (推理/Agent,推荐)",
          "limit": { "context": 128000, "output": 8192 }
        }
      }
    }
  },
  "model": "longcat/LongCat-Flash-Thinking-2601",
  "autoupdate": false
}
</code></pre>
<h4>字段拆解</h4>
<ul>
<li><code>npm: "@ai-sdk/openai-compatible"</code>: 关键。LongCat 走 OpenAI 兼容协议（<code>/v1/chat/completions</code>），所以用这个适配器。如果用了错误的 npm 包（比如 <code>@ai-sdk/openai</code>），会报一些奇怪的 endpoint not found 错误。</li>
<li><code>apiKey: "{env:LONGCAT_API_KEY}"</code>: OpenCode 的环境变量替换语法。容器启动时 <code>LONGCAT_API_KEY</code> 已经被 Codespaces Secrets 注入，这里直接读。<strong>不要手写明文 Key</strong>，否则提交后会泄露。</li>
<li><code>model: "longcat/LongCat-Flash-Thinking-2601"</code>: 默认模型，格式是 <code>&lt;provider 名&gt;/&lt;模型名&gt;</code>。</li>
<li><code>limit</code>: 告诉 OpenCode 上下文窗口大小，方便它做自动压缩判断。LongCat 实际窗口以官方文档为准，128k 是较保险的设置。</li>
</ul>
<h4>验证语法</h4>
<p>提交前可以本地用 <code>jq</code> 校验一下 JSON 合法性：</p>
<pre><code>cat opencode.json | jq .
</code></pre>
<p>能正常输出说明 JSON 没问题。</p>
<hr />
<h3>Step 8：编写 start.sh（每次启动脚本）</h3>
<p>这是真正每次唤醒 Codespace 都会跑的脚本，把上一篇我们已经做过深度打磨的版本放进来，并补全注释：</p>
<pre><code>#!/usr/bin/env bash
set -u

echo "== Codespaces Agent Startup =="
echo

# ---------- [1/7] Reload shell config ----------
echo "[1/7] Loading shell config..."
if [ -f "$HOME/.bashrc" ]; then
  # shellcheck disable=SC1090
  source "$HOME/.bashrc"
fi

# ---------- [2/7] Locate OpenCode ----------
# 双重查找:先看 PATH,再 fallback 到默认安装目录
echo "[2/7] Checking OpenCode..."
OPENCODE_BIN="$(command -v opencode || true)"
if [ -z "$OPENCODE_BIN" ] &amp;&amp; [ -x "$HOME/.opencode/bin/opencode" ]; then
  export PATH="$HOME/.opencode/bin:$PATH"
  OPENCODE_BIN="$HOME/.opencode/bin/opencode"
fi
if [ -z "$OPENCODE_BIN" ]; then
  echo "ERROR: opencode not found."
  echo "Reinstall via: curl -fsSL https://opencode.ai/install | bash"
  exit 1
fi
echo "OpenCode found: $OPENCODE_BIN"
"$OPENCODE_BIN" --version || true
echo

# ---------- [3/7] Sanity check API key ----------
echo "[3/7] Verifying LongCat API key is injected..."
if [ -z "${LONGCAT_API_KEY:-}" ]; then
  echo "WARN: LONGCAT_API_KEY 未设置。OpenCode 配置将无法解析 apiKey。"
  echo "请到 GitHub Settings → Codespaces → Secrets 添加 LONGCAT_API_KEY 并重启 Codespace。"
else
  echo "LONGCAT_API_KEY is set (length=${#LONGCAT_API_KEY})."
fi
echo

# ---------- [4/7] Locate Multica CLI ----------
echo "[4/7] Checking Multica CLI..."
MULTICA_BIN="$(command -v multica || true)"
if [ -z "$MULTICA_BIN" ]; then
  echo "ERROR: multica not found. Re-run install.sh or rebuild container."
  exit 1
fi
echo "Multica found: $MULTICA_BIN"
multica version || true
echo

# ---------- [5/7] Tell Multica where OpenCode lives ----------
# 显式告诉 daemon 该用哪个 opencode 二进制,避免 PATH 漂移
echo "[5/7] Exporting MULTICA_OPENCODE_PATH..."
export MULTICA_OPENCODE_PATH="$OPENCODE_BIN"
echo "MULTICA_OPENCODE_PATH=$MULTICA_OPENCODE_PATH"
echo

# ---------- [6/7] Authenticate Multica ----------
echo "[6/7] Checking Multica authentication..."
AUTH_TMP="$(mktemp)"
trap 'rm -f "$AUTH_TMP"' EXIT
if ! multica auth status &gt;"$AUTH_TMP" 2&gt;&amp;1; then
  cat "$AUTH_TMP"
  echo
  if [ -n "${MULTICA_TOKEN:-}" ]; then
    echo "Detected MULTICA_TOKEN, attempting non-interactive login..."
    if multica login --token="$MULTICA_TOKEN"; then
      echo "Login succeeded."
    else
      echo "ERROR: Login failed. Check that MULTICA_TOKEN is valid."
      exit 1
    fi
  else
    echo "ERROR: Not authenticated and MULTICA_TOKEN not set."
    echo "Run manually: multica login --token=&lt;YOUR_TOKEN&gt;"
    exit 1
  fi
fi
multica auth status
echo

# ---------- [7/7] Start daemon ----------
echo "[7/7] Starting Multica daemon..."
if pgrep -f "multica daemon" &gt;/dev/null 2&gt;&amp;1; then
  echo "Daemon process already running (pgrep hit)."
elif multica daemon status 2&gt;/dev/null | grep -qi "running"; then
  echo "Daemon reported as running."
else
  multica daemon start
fi

# 轮询等待 daemon 就绪 (最多 10s)
for _ in $(seq 1 10); do
  if multica daemon status 2&gt;/dev/null | grep -qi "running"; then
    break
  fi
  sleep 1
done

echo
echo "Final daemon status:"
multica daemon status || true

echo
echo "Done. Codespaces agent runtime is ready."
echo "→ 打开 multica.ai → Settings → Runtimes,应能看到本 Codespace 在线。"
</code></pre>
<h4>这个脚本的几个关键设计</h4>
<ul>
<li><strong><code>set -u</code> 而不是 <code>set -e</code></strong>：脚本里有大量”找不到也无所谓”的探测命令（比如初次 <code>multica daemon status</code>），用 <code>-e</code> 会一打就退；用 <code>-u</code> 保留”未定义变量报错”的严谨性，配合 <code>|| true</code> 和显式 if 控制流程。</li>
<li><strong>OpenCode 二段查找</strong>：先走 PATH，再 fallback 到默认安装目录，应对 shell 还没 reload 的场景。</li>
<li><strong><code>MULTICA_OPENCODE_PATH</code> 显式注入</strong>：Multica daemon 默认扫 PATH 找 Agent CLI，但 Codespaces 的 PATH 经常因为 nvm/bun/pnpm 之类 shim 漂移，给 daemon 喂绝对路径最稳。</li>
<li><strong><code>pgrep</code> + status grep 双重判定</strong>：避免 stop/resume 重启 daemon 时叠加进程。</li>
<li><strong>就绪轮询</strong>：<code>daemon start</code> 是异步的，紧跟 <code>status</code> 容易拿到 “starting”，看着像挂了其实是慢半秒。</li>
</ul>
<hr />
<h3>Step 9：启动 Codespace 并验证</h3>
<h4>9.1 提交代码</h4>
<pre><code>git add .devcontainer/ opencode.json
git commit -m "Setup Multica + OpenCode + LongCat agent workflow"
git push
</code></pre>
<h4>9.2 创建 Codespace</h4>
<p>回到仓库主页，点击右上角绿色 <code>Code</code> 按钮 → <code>Codespaces</code> 标签 → <code>Create codespace on main</code>。</p>
<p>第一次创建会需要 2-5 分钟（拉镜像 + 跑 install.sh）。期间可以看到 VS Code 网页版加载，底部会有日志窗显示 <code>postCreateCommand</code> 的输出。</p>
<h4>9.3 验证安装成功</h4>
<p>容器起来后，在 VS Code 终端（<code>Terminal → New Terminal</code> 或快捷键 <code>Ctrl+`</code>）里依次跑：</p>
<pre><code>opencode --version       # 应该输出版本号
multica version          # 应该输出版本号
multica daemon status    # 应该显示 running,带 PID 和已检测到的 agent 列表
echo $LONGCAT_API_KEY    # 应该输出你的 Key 前几位 (会完整显示,所以确认后立即清屏)
</code></pre>
<p>如果四条命令都正常，恭喜，Codespace 这边已经就绪。</p>
<h4>9.4 在 Multica Web 端验证 Runtime 上线</h4>
<p>打开 <a href="https://multica.ai" rel="noopener noreferrer" target="_blank">multica.ai</a> → 进入你的 Workspace → <code>Settings → Runtimes</code>。</p>
<p>你应该看到一个新的 Runtime 出现，名字大概是 <code>&lt;your-username&gt;-&lt;repo&gt;-&lt;hash&gt;</code>，状态是绿色的 online，并且 Detected agents 列表里有 <code>opencode</code>。</p>
<p>如果没看到：</p>
<ol>
<li>回 Codespace 终端跑 <code>multica daemon logs -f</code> 看日志；</li>
<li>检查 <code>multica auth status</code> 输出的 server URL 和 user 是否正确；</li>
<li>确认你登录 Multica Web 端的账号和 PAT 对应的账号是同一个。</li>
</ol>
<hr />
<h3>Step 10：在 Multica 创建 Agent 并跑通第一个任务</h3>
<h4>10.1 创建 Agent</h4>
<p>在 Multica Web 端：</p>
<ol>
<li>进入你的 Workspace；</li>
<li>左侧菜单点 <code>Agents</code> → <code>New Agent</code>；</li>
<li>填写信息：
<ul>
<li><strong>Name</strong>: 给 Agent 起个名字，比如 <code>Cody</code>；</li>
<li><strong>Avatar</strong>: 随便选或上传一张图（可选）；</li>
<li><strong>CLI</strong>: 选 <code>opencode</code>；</li>
<li><strong>Runtime</strong>: 选 Step 9.4 里看到的那个 Codespace runtime；</li>
<li><strong>Working directory</strong>: 填 Codespace 中你想让 Agent 操作的目录，通常是 <code>/workspaces/&lt;your-repo&gt;</code>；</li>
<li><strong>System prompt</strong> (可选): 写一段角色设定，比如 “你是一名严谨的 Python 工程师，遵循 PEP8，所有改动需要附带单元测试。”；</li>
</ul>
</li>
<li>保存。</li>
</ol>
<h4>10.2 创建第一个 Issue</h4>
<p>回到 Workspace 主页，点 <code>Issues → New Issue</code>：</p>
<ul>
<li><strong>Title</strong>: <code>给 README 增加英文版本</code></li>
<li><strong>Description</strong>:
<pre><code>请阅读项目根目录的 README.md,按相同结构生成 README.en.md。
要求:
1. 保留所有代码块,只翻译说明文字
2. 在文末加一行 "Translated by Cody"
3. 完成后用 git diff 总结改动并写在评论里
</code></pre>
</li>
<li><strong>Assignee</strong>: 选 <code>Cody</code>；</li>
<li>提交。</li>
</ul>
<h4>10.3 观察执行过程</h4>
<p>Issue 提交后几秒内，你会看到：</p>
<ol>
<li>Issue 状态从 <code>Backlog</code> 变为 <code>In Progress</code>；</li>
<li>Issue 详情页右侧会出现 <strong>实时日志流</strong>，显示 OpenCode 的思考、工具调用、文件读写过程；</li>
<li>大约 1-3 分钟后（取决于模型响应速度）任务完成，状态变为 <code>Done</code>，Cody 会在评论区贴出 git diff 总结。</li>
</ol>
<h4>10.4 在 Codespace 里验证产物</h4>
<p>回到 Codespace 的终端，跑 <code>git status</code> 和 <code>cat README.en.md</code>，能看到 Cody 实际生成的文件。如果你想保留这次改动，正常 <code>git add → commit → push</code> 就行。</p>
<p>至此，<strong>整套白嫖工作流已全部跑通</strong>。后续日常使用就是「在 Multica 写 Issue → 等结果」这一个动作。</p>
<hr />
<h2>五、获得更好 Agent 体验的进阶技巧</h2>
<p>跑通只是开始。下面这些技巧会显著拉高 Agent 的稳定性和产出质量。</p>
<h3>5.1 写一份高质量的 AGENTS.md</h3>
<p>OpenCode 启动时会自动读取项目根目录的 <code>AGENTS.md</code> 作为上下文。这份文件相当于给 Agent 的”员工手册”——把项目结构、技术栈、编码规范、常用命令都写进去，能极大减少 Agent 的探索成本。</p>
<p>在 Codespace 终端跑 <code>opencode</code> 进入 TUI，输入 <code>/init</code>，OpenCode 会自动分析你的项目并生成一份初稿，再手动补充即可。建议至少写清楚：</p>
<pre><code># AGENTS.md

## 项目概览

这是一个 ... (一两句话)

## 技术栈

- 语言: Python 3.11
- 框架: FastAPI
- 测试: pytest
- 包管理: uv

## 目录结构

- `src/`: 业务代码
- `tests/`: 单元测试,与 src 镜像
- `scripts/`: 一次性脚本,不进 PR

## 编码规范

- 所有函数必须有 type hints
- 公开函数必须有 docstring
- 禁止使用 print,统一用 logging

## 常用命令

- 跑测试: `uv run pytest`
- 启动开发服务: `uv run uvicorn src.main:app --reload`
- 格式化: `uv run ruff format`

## Agent 工作流约束

- 任何代码改动必须跟单元测试
- 完成后用 git diff 总结改动
- 涉及依赖变更必须先在评论区说明原因
</code></pre>
<p>提交 AGENTS.md 到仓库，所有 Agent 实例都会受益。</p>
<h3>5.2 模型选择策略</h3>
<p>LongCat 提供多个模型，<strong>不要无脑用最强的</strong>：</p>






























<table><thead><tr><th>任务类型</th><th>推荐模型</th><th>理由</th></tr></thead><tbody><tr><td>翻译、改 README、改注释</td><td><code>LongCat-Flash-Chat</code></td><td>速度快、Token 消耗低</td></tr><tr><td>单元测试生成、bug fix</td><td><code>LongCat-Flash-Thinking-2601</code></td><td>推理深、工具调用稳</td></tr><tr><td>多文件重构、架构调整</td><td><code>LongCat-Flash-Thinking-2601</code></td><td>必须用强推理模型,弱模型会改残</td></tr><tr><td>简单的代码格式化</td><td><code>LongCat-Flash-Chat</code></td><td>杀鸡用牛刀浪费配额</td></tr></tbody></table>
<p>可以在 Multica 创建多个 Agent，每个绑定不同的默认模型，分配任务时按需选 Agent。也可以在 OpenCode TUI 里用 <code>/model</code> 命令临时切换。</p>
<h3>5.3 用 Skills 沉淀经验</h3>
<p>Multica 的 Skills 系统是这套工作流真正的复利所在。每完成一个非平凡的任务，把过程沉淀成 Skill：</p>
<p>例如「部署到 staging 环境」「数据库迁移」「跑端到端测试」这类流程，写成 Skill 后下次一句话就能调用。Skill 内容大致结构：</p>
<pre><code># Skill: 添加单元测试

## 触发条件

当 Issue 标题或描述包含 "加单元测试"、"补测试" 时使用此 Skill

## 步骤

1. 读取目标模块代码,识别公开函数
2. 在 tests/ 目录下找到对应文件,如不存在则创建
3. 为每个公开函数生成至少 3 个测试用例: 正常、边界、异常
4. 用 mock 隔离外部依赖
5. 跑 pytest 确认全部通过
6. 评论区贴出测试覆盖率
</code></pre>
<p>随着用得越多，Skills 库越丰富，Agent 的”团队默契”也就越强。</p>
<h3>5.4 Issue 模板</h3>
<p>写得好的 Issue 是让 Agent 高质量完成任务的最重要因素。一个好的 Issue 模板：</p>
<pre><code>## 背景

(为什么要做这件事,业务背景或上游需求)

## 目标

(具体要实现的能力,用户视角)

## 验收标准

- [ ] 标准 1
- [ ] 标准 2
- [ ] 所有相关测试通过

## 约束

- 不要修改 xxx 模块
- 必须保持 API 向后兼容
- 涉及性能的改动需要附 benchmark

## 参考

- 相关代码: src/foo.py
- 相关 Issue: #123
</code></pre>
<p>把这个模板存到 Multica 的 Issue Templates，每次新建 Issue 自动加载，强制自己（也强制 Agent）思考清楚再动手。</p>
<h3>5.5 让 Agent 走 PR 流程而不是直接 push</h3>
<p>在 Agent 的 system prompt 里加一句：</p>
<blockquote><p>所有代码改动必须创建新分支并提 PR，不要直接 push 到 main。</p></blockquote>
<p>这样 Agent 跑完会留下一个待 review 的 PR，你扫一眼合并即可，避免它把 main 改坏。</p>
<h3>5.6 防止上下文打满</h3>
<p>LongCat 的 128k 上下文虽然够用，但跑长任务还是会有打满的风险。在 <code>opencode.json</code> 里启用自动压缩：</p>
<pre><code>{
  "compaction": {
    "auto": true,
    "prune": true,
    "reserved": 10000
  }
}
</code></pre>
<p>这会在上下文接近上限时自动总结早期对话，腾出空间。</p>
<hr />
<h2>六、流量走向与历史记录管控</h2>
<p>这是很多教程不提但实际用起来很重要的一块。你需要清楚<strong>每个字节流向哪里、留在哪里、谁能看到</strong>。</p>
<h3>6.1 数据流向全景图</h3>
<pre><code>你的输入 (Issue 内容)
   │
   ▼
Multica Cloud (multica.ai) ← 任务元数据存这里,服务端 PostgreSQL
   │
   ▼ (WebSocket)
Codespace 容器 ← 代码、临时文件、git 操作都在这里
   │
   ▼ (HTTPS)
LongCat API (api.longcat.chat) ← 你的 prompt + 代码片段会发到这里做推理
   │
   ▼ (响应)
返回 OpenCode → 写回项目文件 + 回传日志给 Multica
</code></pre>
<h3>6.2 各处存了什么、保留多久</h3>



































<table><thead><tr><th>位置</th><th>存储内容</th><th>默认保留</th><th>控制方式</th></tr></thead><tbody><tr><td>Multica Cloud</td><td>Issue 标题/描述/评论/状态/执行日志</td><td>永久(直到你删 Workspace)</td><td>Web 端手动删除 Issue</td></tr><tr><td>Codespace 文件系统</td><td>源码、<code>.git</code>、<code>~/.opencode/</code> 会话快照、daemon 日志</td><td>Codespace 删除即清</td><td><code>gh codespace delete</code></td></tr><tr><td>LongCat 服务端</td><td>API 请求/响应日志</td><td>以平台政策为准</td><td>不可控,所以<strong>别让代码里泄露生产密钥</strong></td></tr><tr><td>OpenCode 本地会话</td><td><code>~/.local/share/opencode/sessions/</code></td><td>同 Codespace</td><td><code>opencode session list/delete</code></td></tr></tbody></table>
<h3>6.3 把 LongCat 流量监控起来</h3>
<p>LongCat 平台有用量页面，建议<strong>每天看一眼</strong>，避免某个跑飞的任务把额度打光。可以在 Codespace 里加个 cron 自动拉用量：</p>
<pre><code># 加在 start.sh 末尾
(crontab -l 2&gt;/dev/null; echo "0 * * * * curl -s https://api.longcat.chat/openai/v1/usage -H 'Authorization: Bearer $LONGCAT_API_KEY' &gt;&gt; ~/.longcat-usage.log") | crontab -
</code></pre>
<p>（实际 endpoint 以 LongCat 文档为准）</p>
<h3>6.4 历史记录的导出与归档</h3>
<p>OpenCode 自带会话导出能力：</p>
<pre><code># 列出所有会话
opencode session list

# 导出指定会话为 JSON
opencode export &lt;session-id&gt; &gt; session-2026-05-28.json

# 之后想复盘时再 import
opencode import session-2026-05-28.json
</code></pre>
<p>Multica 这一侧，每个 Issue 的执行轨迹都留在评论区。需要长期归档的话，可以用 Multica CLI 批量拉取：</p>
<pre><code>multica issue list --output json &gt; issues-snapshot.json
</code></pre>
<p>把它定期 push 到一个私有的归档仓库，相当于给 Agent 历史做了版本控制。</p>
<h3>6.5 敏感信息防泄露三原则</h3>
<ol>
<li><strong>永远不要把密钥写进文件</strong>，只走环境变量；</li>
<li><strong>Agent 的 system prompt 里禁止它读 <code>.env</code>、<code>secrets/</code>、<code>*.pem</code> 之类的文件</strong>：
<pre><code>你不得读取或在输出中复述任何 .env 文件、密钥文件、证书文件的内容。
如果任务需要这类信息,请在评论区请求人类提供。
</code></pre>
</li>
<li><strong>代码 push 前用 git secret 扫一道</strong>，比如 <a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer" target="_blank">gitleaks</a>：
<pre><code>gitleaks detect --source . -v
</code></pre>
</li>
</ol>
<h3>6.6 当不想让 Agent 看到某个文件</h3>
<p>OpenCode 默认遵守 <code>.gitignore</code>，但还可以加一份 <code>.opencodeignore</code>（语法同 gitignore）做更严格的隔离：</p>
<pre><code>.env
.env.*
secrets/
*.key
*.pem
prod-config/
</code></pre>
<hr />
<h2>七、常见故障排查</h2>
<p>按”现象 → 原因 → 解决”的格式整理，遇到问题时直接对号入座。</p>
<h3>7.1 Codespace 启动后 daemon 显示 offline</h3>
<p><strong>可能原因</strong>：</p>
<ul>
<li><code>MULTICA_TOKEN</code> 没注入或者过期；</li>
<li>daemon 进程被 OOM 杀了；</li>
<li>Multica 服务端临时抖动。</li>
</ul>
<p><strong>排查步骤</strong>：</p>
<pre><code># 1. 看日志
multica daemon logs --tail 100

# 2. 检查环境变量
echo "${MULTICA_TOKEN:0:8}..."   # 只显示前 8 位,确认有值

# 3. 检查认证
multica auth status

# 4. 强制重启
multica daemon stop
multica daemon start --foreground   # 前台跑,看实时输出
</code></pre>
<h3>7.2 Issue 卡在 Pending 不动</h3>
<p><strong>可能原因</strong>：</p>
<ul>
<li>Multica Web 端选的 Agent 绑定的 runtime 不在线；</li>
<li>Working directory 配错了，Codespace 里没那个路径；</li>
<li>Codespace 进入 idle 休眠了。</li>
</ul>
<p><strong>排查</strong>：</p>
<ul>
<li>在 Multica Web 端 <code>Settings → Runtimes</code> 确认对应 runtime 是绿点；</li>
<li>在 Codespace 跑 <code>ls /workspaces/</code>，确认 working directory 真实存在；</li>
<li>如果 Codespace 已休眠，回 GitHub 把它 resume。</li>
</ul>
<h3>7.3 OpenCode 报 “Provider longcat not configured”</h3>
<p><strong>可能原因</strong>：</p>
<ul>
<li><code>opencode.json</code> 没在项目根目录（OpenCode 只读 cwd 同名文件，或全局 <code>~/.config/opencode/opencode.json</code>）；</li>
<li>JSON 语法错误（少了逗号、多了注释）；</li>
<li>启动 OpenCode 时 cwd 不对。</li>
</ul>
<p><strong>排查</strong>：</p>
<pre><code>cat opencode.json | jq .            # 验证 JSON
cd /workspaces/&lt;your-repo&gt;          # 确保在仓库根
opencode --print-logs --log-level DEBUG
</code></pre>
<h3>7.4 LongCat API 返回 429</h3>
<p><strong>可能原因</strong>：</p>
<ul>
<li>当日 Token 配额用完；</li>
<li>短时间高并发请求被限流。</li>
</ul>
<p><strong>解决</strong>：</p>
<ul>
<li>看 LongCat 平台的用量页；</li>
<li>如果是高并发限流，在 OpenCode 配置加重试/退避；</li>
<li>切换到消耗更低的 <code>LongCat-Flash-Chat</code> 模型撑过当天。</li>
</ul>
<h3>7.5 Codespace 配额告警</h3>
<p>GitHub 个人账号每月 120 core-hours，2 核机型约 60 小时。如果经常不够用：</p>
<ul>
<li><strong>关停不用的 Codespace</strong>：<code>gh codespace list</code> 看哪些还在跑，<code>gh codespace stop -c &lt;name&gt;</code> 立即停；</li>
<li><strong>设置自动空闲超时</strong>：<code>Settings → Codespaces → Default idle timeout</code> 调到 30 分钟；</li>
<li><strong>改用 Pro 账户或学生认证</strong>：学生包额度翻几倍，Pro 也更宽松；</li>
<li><strong>改自托管 daemon</strong>：把 daemon 跑在自己的 NAS 或者 VPS，Codespace 只在需要交互编辑时开。</li>
</ul>
<h3>7.6 Agent 改坏了代码</h3>
<p><strong>预防</strong>：</p>
<ul>
<li>Agent 走分支 + PR 流程（见 5.5）；</li>
<li>关键文件加到 <code>.opencodeignore</code>；</li>
<li>定期 push 到远端仓库做兜底。</li>
</ul>
<p><strong>事后</strong>：</p>
<pre><code># 看最近一次 Agent 改了啥
git diff HEAD~1

# 整体回滚
git reset --hard HEAD~1

# 选择性回滚某个文件
git checkout HEAD~1 -- path/to/file
</code></pre>
<hr />
<h2>八、心得体会与白嫖经验</h2>
<p>跑通了之后再回头看这套方案，几个体会想分享一下。</p>
<h3>8.1 这套组合的”优雅”在于解耦，不在于免费</h3>
<p>很多人看到这套方案的第一反应是”哇全免费”，但其实<strong>真正值得抄走的不是免费，而是架构本身的可替换性</strong>。这套链路的四层每一层都有清晰的接口：</p>
<ul>
<li>任务管理层（Multica）通过开放的 daemon 协议对接执行层；</li>
<li>执行层（OpenCode）通过 OpenAI 兼容协议对接模型层；</li>
<li>模型层（LongCat）通过 HTTPS 标准接口对外；</li>
<li>运行环境层（Codespaces）通过标准 Linux 容器对外。</li>
</ul>
<p>这意味着：哪天 LongCat 收费了，把 baseURL 换成 DeepSeek、Kimi、智谱、火山引擎任何一家，配置改两行就能切；哪天 Codespaces 配额收紧，换成本地 Docker 或者一台 5 美元的 VPS，daemon 协议不变；哪天 Multica 不香了，OpenCode 单独跑也是个完整的 Agent。</p>
<p><strong>白嫖只是当下的副作用，架构上的解耦才是长期价值</strong>。</p>
<h3>8.2 三家厂商各自在补贴什么</h3>
<p>理解补贴的来源，才能判断它能持续多久：</p>
<ul>
<li><strong>GitHub Codespaces</strong> 的免费额度本质是<strong>微软推给开发者的入口补贴</strong>。底层是 Azure 算力，按需算每小时约 0.18 美元，但 GitHub 把”每月 120 core-hours”作为开发者拉新留存的钩子。这是一份基础设施期权，只要微软还在乎开发者心智，它就稳。</li>
<li><strong>Multica</strong> 的免费来自<strong>开源 + 自托管的产品策略</strong>。它走”内部使用免费、SaaS 商业化收费”的修改版 Apache 2.0 路线，对个人和小团队完全开放——本身就是它的获客和验证渠道。</li>
<li><strong>LongCat 免费 API</strong> 是<strong>大厂模型竞赛的”客户教育期”产物</strong>。美团从 2025 年 9 月开放平台开始就一直维持高额免费额度，目的是在 DeepSeek、通义、Kimi 把开发者心智吃掉前抢市场。Thinking-2601 在工具调用基准上拿过开源第一，用免费策略把开发者拉进生态是当前最优解。</li>
</ul>
<p>这三层补贴的逻辑是<strong>完全独立</strong>的——不会因为同一个事件同时崩掉。任意一层涨价，其他三层都还在，迁移成本可控。</p>
<h3>8.3 OpenCode 是关键的”中立粘合剂”</h3>
<p>四个组件里，OpenCode 是唯一不”补贴”的角色。它本身完全开源、不卖模型、不卖云、只做粘合。<strong>正因为它中立，所以可以把 LongCat 这种 OpenAI 兼容的国产 API 无缝接进来，不被任何一家厂商的客户端绑死</strong>。</p>
<p>如果你今天直接用某家大厂的官方客户端（比如 Cursor、Windsurf），看似省事，但实际是把整个工作流绑在那家公司的产品决策上——它涨价你只能跟着涨，它砍功能你只能跟着砍。OpenCode 这种中立 CLI 是这套架构的稳定器。</p>
<h3>8.4 真正的 ROI 不是省下的 API 费用</h3>
<p>很多人会算：跑这套方案一个月省了多少 Cursor 订阅费、多少 API 充值费。其实这个账算得很狭窄。</p>
<p>真正的 ROI 在于：<strong>Skills 库随时间累积带来的复利</strong>。每一个 Issue 跑完都沉淀一份”团队记忆”，第二次类似任务 Agent 就不用从零探索；半年下来，你的 Skills 库会变成一份只属于你这个项目/团队的”私有知识资产”。这份资产<strong>和具体的模型、客户端、云厂商都无关</strong>——切换底层组件时它都跟着你走。</p>
<p>省下的 API 费是一次性收益，沉淀的 Skills 是持续复利。后者才是真正值得花时间打磨的东西。</p>
<h3>8.5 这套方案的局限要承认</h3>
<p>不能光夸,几个真实的痛点也要说清楚:</p>
<p><strong>第一,Codespaces 60 小时/月的天花板对重度使用者还是紧</strong>。如果你打算让 Agent 跑日级别的长任务(比如批量重构整个仓库),60 小时很快就用完。这时候就该考虑把 daemon 迁到自己的家用 NAS 或者一台便宜 VPS,Codespace 只在交互编辑时开。</p>
<p><strong>第二,Multica 的多人协作还有粗糙之处</strong>。所有 Agent 提交都用 Codespace 容器里那一份 SSH key,提交人都是同一个身份;多人同时改同一仓库时容易冲突。目前的解法是给每个 Agent 在 AGENTS.md 里写明 git 身份 + 强制 PR 流程,不是无痛方案。如果你团队超过 3-5 人,可能要等 Multica 这块继续迭代,或者考虑商用方案。</p>
<p><strong>第三,免费模型的稳定性有波动</strong>。LongCat 偶尔会有限流和延迟尖刺,在赶时间的场景里不如付费的 Claude/GPT 那么稳。建议<strong>关键任务双模型兜底</strong>：在 OpenCode 里同时配 LongCat 和一个备用 provider(比如 DeepSeek 或者 Kimi 的免费额度),LongCat 卡住时手动 <code>/model</code> 切换。</p>
<p><strong>第四,Codespace 进入 idle 后 daemon 会断</strong>。中长任务跑到一半被休眠,Issue 会卡在 “In Progress” 直到下次手动唤醒。临时缓解办法是在 <code>start.sh</code> 里加一个心跳脚本(每 10 分钟 touch 一个文件),把 idle timeout 调到 4 小时上限;长期方案还是把 daemon 搬到不会休眠的环境。</p>
<h3>8.6 写给打算跟进的人：三个不同的「野心档位」</h3>
<p>最后给三种不同需求的人各推荐一种用法:</p>
<p><strong>档位一：个人玩具</strong>。完全照本教程,Codespaces + Multica Cloud + LongCat 免费额度,用来做 side project、写博客、整理笔记、维护几个小工具。一个月零成本,Agent 体验已经远超大多数订阅制工具。</p>
<p><strong>档位二：小团队生产力</strong>。在档位一基础上,Multica 改自托管(找一台自己的 VPS 跑 docker compose),给团队每个人建独立账号,daemon 跑在公司内网的常驻机器上(不会休眠)。模型层主打 LongCat 免费额度,关键任务用付费模型兜底。这一档大约每月几十块成本,能撑得起 5-10 人的小团队。</p>
<p><strong>档位三：认真做事的工程师</strong>。把这套方案当成一个<strong>自己专属的 Agent 平台</strong>,认真打磨 AGENTS.md、Issue 模板、Skills 库,把每个项目的”团队记忆”沉淀进去。模型层用付费的强模型(Claude Opus、GPT-5 之类)做主力,LongCat 做廉价批量任务的兜底。这一档不再追求白嫖,但因为架构是解耦的,你随时可以根据成本和效果调整每一层。</p>
<hr />
<h2>写在最后</h2>
<p>这套方案教会我最重要的一件事是: <strong>AI 工具的红利期里,真正稀缺的不是模型、不是算力、不是订阅额度,而是「把工具组合起来变成稳定生产力」的那一层抽象</strong>。</p>
<p>每一家厂商都在拼命补贴自家某一层(模型/IDE/云),但<strong>没有一家厂商有动机帮你做”跨厂商的组合”</strong>——因为组合得越好,你越不依赖任何单一厂商。这恰恰是开源中间层(Multica + OpenCode)的价值所在,也是这套方案能在三家都不补贴你之后还活着的原因。</p>
<p>把这套流水线搭起来,顺手沉淀下你的 Issue 模板和 Skills 库,<strong>白嫖额度只是当下的副作用,真正长期带走的是你对 Agent 工作流的掌控力</strong>。</p>
<p>吃瓜可以,记得也吃点经验。</p>]]></content>
    <category term="Agent" />
    <category term="Multica" />
    <category term="OpenCode" />
    <category term="Codespaces" />
    <category term="LongCat" />
    <category term="AI编程" />
    <category term="自动化" />
  </entry>
  <entry>
    <title>Claude Desktop 在 Windows 上完整安装与 Cowork 排障实录</title>
    <link href="https://mps-blog.vercel.app//posts/claude-cowork-troubleshoot" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/claude-cowork-troubleshoot</id>
    <updated>2026-05-25T03:29:00.000Z</updated>
    <published>2026-05-25T03:29:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">从 ClaudeSetup.exe 下载报 unexpected EOF 到 MSIX 离线安装成功 + CoworkVMService 正常运行的完整排查过程，涵盖 winget/MSIX 安装差异、服务冲突清理、VirtualMachinePlatform 启用等关键步骤。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.CFFyRBts_Z23BVOY.webp" alt="Claude Desktop 在 Windows 上完整安装与 Cowork 排障实录" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h1>Claude Desktop 在 Windows 上完整安装与 CoworkVMService 排障实录</h1>
<p>本文记录了一次从 <code>ClaudeSetup.exe</code> 下载 MSIX 中途报 <code>unexpected EOF</code> 一路排查到 MSIX 离线安装成功 + CoworkVMService 正常运行的完整过程，包含所有走过的弯路、踩到的坑和正确解法。</p>
<p>环境信息：</p>
<ul>
<li>系统：Windows 11 Professional，Build <code>10.0.26200</code></li>
<li>架构：x64</li>
<li>当前账户：Administrator</li>
<li>现象起点：<code>ClaudeSetup.exe</code> 下载到 30%~50% 反复 <code>unexpected EOF</code>，3 次重试全部失败</li>
</ul>
<hr />
<h2>一、根因总结（先看结论）</h2>
<p>整个排查链上其实有三个独立但叠加的问题：</p>
<ol>
<li><code>ClaudeSetup.exe</code> 的在线下载链路不稳定——拉 <code>api.anthropic.com</code> 的 220MB MSIX 大文件经常在中段被截断（这是日志里 <code>unexpected EOF</code> 的直接原因）。</li>
<li>历史残留的 <code>CoworkVMService</code>——之前装过的 MSIX 版本留下了同名服务，新安装包注册时被冲突阻断。</li>
<li>winget 仓库里 <code>Anthropic.Claude</code> 是 Squirrel 版 EXE，不带 Cowork 组件——用 winget 装能装上 Claude 主程序，但不会附带 CoworkVMService，要 Cowork 必须装官方 MSIX 包。</li>
</ol>
<p>最终成功路径：手动浏览器下载 MSIX → 管理员 PowerShell <code>Add-AppxPackage</code> 离线安装 → CoworkVMService 自动注册并启动。</p>
<hr />
<h2>二、前置准备</h2>
<h3>2.1 确认管理员权限</h3>
<p>整个流程全程必须在管理员 PowerShell 里执行（开始菜单搜 PowerShell → 右键 → 以管理员身份运行）。验证方法：</p>
<pre><code>([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
</code></pre>
<p>返回 <code>True</code> 才能继续。本次实操返回 <code>True</code>，通过。</p>
<h3>2.2 关闭所有相关进程</h3>
<pre><code>Get-Process claude, Cowork, CoworkVM, chrome-native-host -ErrorAction SilentlyContinue | Stop-Process -Force
</code></pre>
<hr />
<h2>三、清理多版本 Claude 残留（关键的第一步）</h2>
<h3>3.1 用 winget 卸载，处理”多个版本”报错</h3>
<p>第一次执行卸载：</p>
<pre><code>winget uninstall -e --id Anthropic.Claude
</code></pre>
<p>遇到的问题：报错</p>
<pre><code>Multiple versions of this package are installed. Either refine the search, pass the `--version` argument to select one, or pass the `--all-versions` flag to uninstall all of them.
</code></pre>
<p>原因：之前装过不同版本的 Claude（包括 MSIX 版和 Squirrel 版），winget 检测到多个版本，需要明确告诉它”全部卸掉”。</p>
<p>正确做法：加 <code>--all-versions</code> 参数：</p>
<pre><code>winget uninstall -e --id Anthropic.Claude --all-versions
</code></pre>
<blockquote><p>⚠️ 重要警示：<code>--all-versions</code> 会把 MSIX 版也一起卸掉。这一步在初次清理时是对的，但在 MSIX 安装成功之后绝对不能再跑这条命令，否则会把 CoworkVMService 一起注销（本次实操就在尾声踩了这个坑，详见第八节）。</p></blockquote>
<p>确认卸载干净：</p>
<pre><code>winget list --id Anthropic.Claude
</code></pre>
<p>应该返回 <code>No installed package found matching input criteria.</code>。</p>
<h3>3.2 卸载 MSIX 层残留</h3>
<pre><code>Get-AppxPackage -AllUsers *Claude* | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue
Get-AppxPackage -AllUsers *Anthropic* | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue
Get-AppxProvisionedPackage -Online | Where-Object { $_.DisplayName -like "*Claude*" -or $_.DisplayName -like "*Anthropic*" } | Remove-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
</code></pre>
<p>这三条把 MSIX 包（含所有用户、机器级 Provisioned 包）一并清掉。</p>
<h3>3.3 清理残留的 CoworkVMService（核心步骤）</h3>
<p>这是 <code>ClaudeSetup.log</code> 里 <code>CoworkVMService already exists (potential conflict)</code> 警告的根源——必须删掉。</p>
<pre><code>Get-Service CoworkVMService -ErrorAction SilentlyContinue
Stop-Service CoworkVMService -Force -ErrorAction SilentlyContinue
sc.exe delete CoworkVMService
Get-Service CoworkVMService -ErrorAction SilentlyContinue
</code></pre>
<p>最后一条 <code>Get-Service</code> 没有任何输出，说明服务已彻底清除。</p>
<blockquote><p>注意：<code>sc.exe</code> 后面必须有空格，是调用 <code>sc.exe</code> 而不是 PowerShell 别名 <code>Set-Content</code>。</p></blockquote>
<h3>3.4 清理用户目录和注册表残留</h3>
<pre><code>Remove-Item -Recurse -Force "$env:LOCALAPPDATA\AnthropicClaude" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:APPDATA\Claude" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\Packages\*Claude*" -ErrorAction SilentlyContinue

$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModel\StateChange\PackageList"
Get-ChildItem $key -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -like "*Claude*" } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
</code></pre>
<h3>🔄 第一次重启</h3>
<p>清理完服务和 MSIX StateChange 注册表后，必须重启电脑一次，让服务删除和包状态彻底落盘。这一步千万不能省，否则后续注册新服务时还会撞上”幽灵服务”。</p>
<hr />
<h2>四、启用 Windows 必要功能</h2>
<p>Cowork 依赖虚拟化平台跑沙箱，这一步是 CoworkVMService 能否真正运行的硬性条件。</p>
<h3>4.1 启用功能</h3>
<pre><code>Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All -NoRestart
Enable-WindowsOptionalFeature -Online -FeatureName Containers-DisposableClientVM -All -NoRestart
</code></pre>
<p>遇到的问题：Windows 在启用功能时弹出报错（具体错误码未记录，但功能后续被验证为已启用）。</p>
<h3>4.2 检测功能是否真的启用了</h3>
<p>踩坑：尝试用一条命令查两个功能：</p>
<pre><code>Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform, Containers-DisposableClientVM | Format-List FeatureName, State, RestartRequired
</code></pre>
<p>报错：</p>
<pre><code>无法将"System.Object[]"转换为参数"FeatureName"所需的类型"System.String"。
</code></pre>
<p>原因：<code>-FeatureName</code> 只接受一个字符串，不支持数组。</p>
<p>正确做法（两种任选）：</p>
<p>方法 A——分两条 PowerShell 命令：</p>
<pre><code>Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform | Format-List FeatureName, State, RestartRequired
Get-WindowsOptionalFeature -Online -FeatureName Containers-DisposableClientVM | Format-List FeatureName, State, RestartRequired
</code></pre>
<p>方法 B——用 DISM（输出更详细）：</p>
<pre><code>DISM /Online /Get-FeatureInfo /FeatureName:VirtualMachinePlatform
DISM /Online /Get-FeatureInfo /FeatureName:Containers-DisposableClientVM
</code></pre>
<h3>4.3 检测结果</h3>
<p>本次实操经过重启后两个功能状态如下：</p>




















<table><thead><tr><th>功能名</th><th>状态</th><th>是否必需</th></tr></thead><tbody><tr><td><code>VirtualMachinePlatform</code></td><td>✅ 已启用</td><td>必需（Cowork 沙箱底层依赖）</td></tr><tr><td><code>Containers-DisposableClientVM</code>（Windows 沙箱）</td><td>✅ 已启用</td><td>可选（Cowork 不强依赖）</td></tr></tbody></table>
<blockquote><p>关键认知：对 Cowork 真正必需的只有 <code>VirtualMachinePlatform</code>，Windows 沙箱启用失败也能继续，但本次两个都成功启用。</p></blockquote>
<h3>🔄 第二次重启</h3>
<p>启用 <code>VirtualMachinePlatform</code> 后必须再重启一次，Hyper-V 虚拟化层才会真正加载到内核。哪怕 <code>State</code> 显示 <code>Enabled</code> 但 <code>RestartRequired: Possible</code>，也要重启。这次重启不能省。</p>
<hr />
<h2>五、第一次安装尝试：winget 装上了但没 Cowork</h2>
<h3>5.1 用 winget 安装</h3>
<pre><code>winget install -e --id Anthropic.Claude --accept-package-agreements --accept-source-agreements
</code></pre>
<p>输出显示成功安装版本 <code>1.8555.2</code>，下载地址是：</p>
<pre><code>https://downloads.claude.ai/releases/win32/x64/1.8555.2/Claude-a476c316c741715263e34f9c9d2bc45b6d0f21c7.exe
</code></pre>
<p><code>winget list --id Anthropic.Claude</code> 也能查到。</p>
<h3>5.2 检查 MSIX 包和 Cowork 服务</h3>
<pre><code>Get-AppxPackage -AllUsers *Claude* | Select-Object Name, PackageFullName, InstallLocation, Status
</code></pre>
<p>结果为空，没有任何输出。</p>
<pre><code>Get-ChildItem "$env:LOCALAPPDATA\AnthropicClaude" -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue | Select-Object FullName
</code></pre>
<p>输出：</p>
<ul>
<li><code>C:\Users\Administrator\AppData\Local\AnthropicClaude\claude.exe</code></li>
<li><code>C:\Users\Administrator\AppData\Local\AnthropicClaude\app-1.8555.2\claude.exe</code></li>
</ul>
<pre><code>Get-ChildItem "$env:LOCALAPPDATA\AnthropicClaude" -Recurse -Include "*Cowork*.exe","*cowork*.exe" -ErrorAction SilentlyContinue
</code></pre>
<p>搜不到任何 Cowork 可执行文件。</p>
<h3>5.3 关键诊断结论</h3>
<p>目录里有 <code>Squirrel-CheckForUpdate.log</code>、<code>Update.exe</code>、<code>packages</code> 这些典型的 Squirrel 框架文件，说明：</p>
<ul>
<li>winget 装的是 Squirrel 打包的传统 EXE 应用，不是 MSIX</li>
<li>Squirrel 版本不包含 Cowork 组件，自然 <code>Get-AppxPackage</code> 查不到、<code>Cowork*.exe</code> 搜不到、服务也不存在</li>
<li>要 Cowork 必须改装官方 MSIX 包</li>
</ul>
<hr />
<h2>六、第二次安装尝试：官方 MSIX 离线安装（成功路径）</h2>
<h3>6.1 卸载 Squirrel 版</h3>
<pre><code>winget uninstall -e --id Anthropic.Claude
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\AnthropicClaude" -ErrorAction SilentlyContinue
</code></pre>
<h3>6.2 手动下载官方 MSIX 包</h3>
<p>在浏览器里访问 Claude 官方部署文档：</p>
<p><a href="https://support.claude.com/en/articles/12622703-deploy-claude-desktop-for-windows" rel="noopener noreferrer" target="_blank">https://support.claude.com/en/articles/12622703-deploy-claude-desktop-for-windows</a></p>
<p>下载 <strong>Claude MSIX (x64)</strong>，保存到 <code>C:\Users\Administrator\Downloads\Claude.msix</code>。</p>
<blockquote><p>为什么浏览器能下、<code>ClaudeSetup.exe</code> 下不动？</p><p><code>ClaudeSetup.exe</code> 自带的下载器没有断点续传，遇到网络中断就直接 <code>unexpected EOF</code> 然后从头重来；浏览器有断点续传和更稳的 TCP 重传策略，对 220MB 大文件友好得多。</p><p>如果浏览器也下不动：换手机热点 / VPN，或用 IDM、aria2、Free Download Manager 这种支持断点续传的下载工具。</p></blockquote>
<p>下载完务必校验文件大小：</p>
<pre><code>Get-Item "C:\Users\Administrator\Downloads\Claude.msix" | Select-Object Name, Length, LastWriteTime
</code></pre>
<p><code>Length</code> 应在 220,000,000 字节附近。如果远小于这个值，说明下载又被截断了，必须重下。</p>
<h3>6.3 管理员 PowerShell 安装 MSIX</h3>
<pre><code>Add-AppxPackage -Path "C:\Users\Administrator\Downloads\Claude.msix"
</code></pre>
<p>无任何报错输出，瞬间完成。</p>
<h3>6.4 验证安装结果（关键检查点）</h3>
<pre><code>Get-AppxPackage -AllUsers *Claude* | Select-Object Name, PackageFullName, InstallLocation, Status
</code></pre>
<p>输出：</p>
<pre><code>Name   PackageFullName                      InstallLocation                                                   Status
----   ---------------                      ---------------                                                   ------
Claude Claude_1.8555.2.0_x64__pzs8sxrjxfjjc C:\Program Files\WindowsApps\Claude_1.8555.2.0_x64__pzs8sxrjxfjjc Ok
</code></pre>
<p><code>Status: Ok</code>，路径在系统级 <code>C:\Program Files\WindowsApps</code>，完美。</p>
<pre><code>Get-Service CoworkVMService -ErrorAction SilentlyContinue | Format-List Name, Status, StartType, DisplayName, BinaryPathName
</code></pre>
<p>输出：</p>
<pre><code>Name        : CoworkVMService
Status      : Running
StartType   : Automatic
DisplayName : Claude
</code></pre>
<p>🎉 CoworkVMService 自动注册并自动启动，状态 Running，启动类型 Automatic，全部到位。</p>
<blockquote><p>关键认知：MSIX 包内嵌的服务注册逻辑会在 <code>Add-AppxPackage</code> 时自动调用，不需要手动 <code>sc.exe create</code>。前面之所以准备好了手动注册命令，是为了应对自动注册失败的兜底场景。</p></blockquote>
<hr />
<h2>七、为什么 MSIX 方案能一次成功</h2>









































<table><thead><tr><th>维度</th><th><code>ClaudeSetup.exe</code> 在线安装</th><th>winget 安装</th><th>MSIX 离线安装（成功）</th></tr></thead><tbody><tr><td>网络稳定性要求</td><td>极高（无断点续传）</td><td>中（winget 自带重试）</td><td>零（本地文件）</td></tr><tr><td>是否包含 Cowork</td><td>✅ 包含</td><td>❌ 不包含</td><td>✅ 包含</td></tr><tr><td>服务自动注册</td><td>✅ 包含</td><td>❌ 不包含</td><td>✅ 包含</td></tr><tr><td>安装路径</td><td>用户级 Squirrel</td><td>用户级 Squirrel</td><td>系统级 WindowsApps</td></tr><tr><td>权限要求</td><td>标准用户即可（但易失败）</td><td>标准用户即可</td><td>管理员 PowerShell</td></tr></tbody></table>
<p>MSIX 是 Microsoft 现代应用打包格式，签名、沙箱、注册全部由系统接管，比 Squirrel EXE 稳定得多，这也是 Anthropic 官方部署文档主推它的原因。</p>
<hr />
<h2>八、收尾踩坑警示：千万别再跑 <code>winget uninstall --all-versions</code></h2>
<p>MSIX 装好后，为了清理 Squirrel 残留，跑了：</p>
<pre><code>Remove-Item -Recurse -Force "$env:LOCALAPPDATA\AnthropicClaude" -ErrorAction SilentlyContinue  # ✅ 这条没问题
winget uninstall -e --id Anthropic.Claude --all-versions                                       # ❌ 这条把 MSIX 一起卸了
</code></pre>
<p>结果：</p>
<pre><code>Get-AppxPackage -AllUsers *Claude*   # 空输出
Get-Service CoworkVMService          # 找不到服务
</code></pre>
<p>根因：winget 仓库里 <code>Anthropic.Claude</code> 这个 ID 同时挂着 Squirrel 和 MSIX 两个 manifest，<code>--all-versions</code> 会把所有形态的安装都卸掉。</p>
<p>正确做法：</p>
<ul>
<li>清理 Squirrel 残留只需要删 <code>$env:LOCALAPPDATA\AnthropicClaude</code> 目录，不需要再跑 winget 卸载。</li>
<li>MSIX 装好后，今后所有升级和管理都不要再用 winget，去官方页下载新 MSIX，<code>Add-AppxPackage -Path</code> 覆盖安装即可。</li>
</ul>
<p>恢复方案：再跑一次 MSIX 安装：</p>
<pre><code>Add-AppxPackage -Path "C:\Users\Administrator\Downloads\Claude.msix"
Get-AppxPackage -AllUsers *Claude* | Select-Object Name, Status
Get-Service CoworkVMService | Format-List Name, Status, StartType
</code></pre>
<p><code>Status: Ok</code> 和 <code>Running</code> 都回来即可。</p>
<hr />
<h2>九、最终验证清单</h2>
<p>最终成功状态应满足所有下列条件：</p>
<pre><code># 1. MSIX 包正常注册
Get-AppxPackage -AllUsers *Claude*
# 期望：Status = Ok，PackageFullName 类似 Claude_1.8555.2.0_x64__pzs8sxrjxfjjc

# 2. Cowork 服务运行中
Get-Service CoworkVMService
# 期望：Status = Running，StartType = Automatic

# 3. 虚拟化平台已启用
Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform
# 期望：State = Enabled

# 4. winget 列表里不应再有 Claude（避免后续误操作）
winget list --id Anthropic.Claude
# 期望：No installed package found
</code></pre>
<p>从开始菜单启动 Claude，登录账号，应能正常使用 Cowork 功能。</p>
<hr />
<h2>十、完整时间线复盘</h2>



















































































<table><thead><tr><th>阶段</th><th>关键动作</th><th>重启</th><th>结果</th></tr></thead><tbody><tr><td>1</td><td>确认管理员权限</td><td>—</td><td>True</td></tr><tr><td>2</td><td><code>winget uninstall --all-versions</code></td><td>—</td><td>Squirrel 版卸干净</td></tr><tr><td>3</td><td><code>Remove-AppxPackage</code> + <code>Remove-AppxProvisionedPackage</code></td><td>—</td><td>MSIX 残留清理</td></tr><tr><td>4</td><td><code>sc.exe delete CoworkVMService</code></td><td>—</td><td>幽灵服务删除</td></tr><tr><td>5</td><td>清理 <code>LOCALAPPDATA</code>、注册表 <code>PackageList</code></td><td>🔄 重启 #1</td><td>状态落盘</td></tr><tr><td>6</td><td><code>Enable-WindowsOptionalFeature VirtualMachinePlatform</code></td><td>🔄 重启 #2</td><td>虚拟化平台 Enabled</td></tr><tr><td>7</td><td><code>winget install Anthropic.Claude</code></td><td>—</td><td>Squirrel 版装上，但无 Cowork</td></tr><tr><td>8</td><td><code>winget uninstall</code> + 删 <code>AnthropicClaude</code> 目录</td><td>—</td><td>Squirrel 版卸掉</td></tr><tr><td>9</td><td>浏览器下载 <code>Claude.msix</code>（220MB）</td><td>—</td><td>文件完整</td></tr><tr><td>10</td><td><code>Add-AppxPackage -Path Claude.msix</code></td><td>—</td><td>MSIX 装上 + CoworkVMService 自动 Running ✅</td></tr><tr><td>11</td><td>误跑 <code>winget uninstall --all-versions</code></td><td>—</td><td>MSIX 被一并卸掉 ❌</td></tr><tr><td>12</td><td>重新 <code>Add-AppxPackage</code></td><td>—</td><td>恢复成功 ✅</td></tr></tbody></table>
<hr />
<h2>十一、几条带走的经验</h2>
<p>网络层经验：<code>api.anthropic.com</code> 对国内大文件下载链路不稳，永远优先用浏览器或断点续传工具拉 MSIX，不要依赖 <code>ClaudeSetup.exe</code> 自己下。</p>
<p>安装方式经验：要 Cowork 就必须装 MSIX，winget 仓库里那个 <code>Anthropic.Claude</code> 是 Squirrel 版不带 Cowork，别浪费时间。</p>
<p>服务管理经验：MSIX 包会自动注册 <code>CoworkVMService</code>，不需要也不应该手动 <code>sc.exe create</code>；遇到”服务已存在”的冲突，老老实实先 <code>sc.exe delete</code> 清理再装。</p>
<p>重启经验：服务删除后要重启一次（让删除落盘），启用 <code>VirtualMachinePlatform</code> 后要再重启一次（让虚拟化内核加载），两次重启都不能省。</p>
<p>winget 经验：MSIX 装好后，永远不要再对 <code>Anthropic.Claude</code> 这个 ID 跑 winget uninstall，否则会连带卸掉 MSIX。今后升级走”下新 MSIX → <code>Add-AppxPackage</code> 覆盖”即可。</p>
<p>功能检测经验：<code>Get-WindowsOptionalFeature -FeatureName</code> 不接受数组，要么分多条跑，要么改用 <code>DISM /Online /Get-FeatureInfo</code>。</p>
<p>整套流程跑下来，从最初 <code>unexpected EOF</code> 到最后 <code>CoworkVMService: Running</code>，问题已全部解决，记录完毕。</p>]]></content>
    <category term="Claude" />
    <category term="Windows" />
    <category term="Cowork" />
    <category term="MSIX" />
    <category term="排障" />
  </entry>
  <entry>
    <title>使用 FFmpeg 下载 M3U8 视频并转换为 MP4 完整指南（Windows 版）</title>
    <link href="https://mps-blog.vercel.app//posts/ffmpeg-m3u8-mp4" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/ffmpeg-m3u8-mp4</id>
    <updated>2026-05-24T00:00:00.000Z</updated>
    <published>2026-05-24T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">详细介绍如何在 Windows 上使用 FFmpeg 将 M3U8 流媒体视频下载并转换为 MP4 格式，包含安装配置、命令使用、进阶技巧及常见问题解决。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.DPectsLk_1UD4pv.webp" alt="使用 FFmpeg 下载 M3U8 视频并转换为 MP4 完整指南（Windows 版）" style="width: 100%; height: auto; margin-bottom: 1em;" />
<blockquote><p>本指南专为 <strong>Windows 系统</strong>用户编写，详细介绍如何使用开源工具 <strong>FFmpeg</strong> 将 M3U8 流媒体视频下载并转换为 MP4 格式。包含安装、环境变量配置、命令使用及常见问题解决方案。</p></blockquote>
<hr />
<h2>📑 目录</h2>
<ul>
<li><a href="#%E4%B8%80%E4%BB%80%E4%B9%88%E6%98%AF-m3u8-%E4%B8%8E-ffmpeg">一、什么是 M3U8 与 FFmpeg</a></li>
<li><a href="#%E4%BA%8C%E4%B8%8B%E8%BD%BD-ffmpeg">二、下载 FFmpeg</a></li>
<li><a href="#%E4%B8%89%E8%A7%A3%E5%8E%8B%E5%AE%89%E8%A3%85">三、解压安装</a></li>
<li><a href="#%E5%9B%9B%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">四、配置环境变量</a></li>
<li><a href="#%E4%BA%94%E9%AA%8C%E8%AF%81%E5%AE%89%E8%A3%85">五、验证安装</a></li>
<li><a href="#%E5%85%AD%E8%8E%B7%E5%8F%96-m3u8-%E8%A7%86%E9%A2%91%E9%93%BE%E6%8E%A5">六、获取 M3U8 视频链接</a></li>
<li><a href="#%E4%B8%83%E6%89%A7%E8%A1%8C%E4%B8%8B%E8%BD%BD%E4%B8%8E%E8%BD%AC%E6%8D%A2%E5%91%BD%E4%BB%A4">七、执行下载与转换命令</a></li>
<li><a href="#%E5%85%AB%E5%91%BD%E4%BB%A4%E5%8F%82%E6%95%B0%E8%AF%A6%E8%A7%A3">八、命令参数详解</a></li>
<li><a href="#%E4%B9%9D%E8%BF%9B%E9%98%B6%E7%94%A8%E6%B3%95">九、进阶用法</a></li>
<li><a href="#%E5%8D%81%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">十、常见问题与解决方案</a></li>
</ul>
<hr />
<h2>一、什么是 M3U8 与 FFmpeg</h2>
<p><strong>M3U8</strong> 是一种基于 HTTP Live Streaming (HLS) 协议的播放列表文件格式，本身不存储视频数据，而是记录了一系列被切片的小段视频（<code>.ts</code> 文件）的索引信息。许多视频网站、政府教育平台等都使用此格式做流媒体分发。</p>
<p><strong>FFmpeg</strong> 是一个开源的音视频转码工具，提供了<strong>录制、转换以及流化音视频的完整解决方案</strong>。它可以转码、压制、提取、截取、合并、录屏等，是处理 M3U8 流媒体最专业且免费的方案。</p>
<hr />
<h2>二、下载 FFmpeg</h2>
<h3>第一步：访问官方下载页面</h3>
<p>打开浏览器，访问 FFmpeg 官网：<a href="https://ffmpeg.org/download.html" rel="noopener noreferrer" target="_blank">https://ffmpeg.org/download.html</a></p>
<p>在页面中点击 <strong>Windows 图标</strong>，然后选择 <strong>“Windows builds from gyan.dev”</strong>（推荐，最稳定）。</p>
<h3>第二步：进入 gyan.dev 下载页</h3>
<p>在 gyan.dev 页面，找到 <strong>release builds</strong> 区域，根据需要下载：</p>

























<table><thead><tr><th>版本类型</th><th>体积</th><th>推荐场景</th></tr></thead><tbody><tr><td><strong>release-essentials</strong></td><td>较小</td><td>一般用户、Windows 7+ 兼容（<strong>推荐</strong>）</td></tr><tr><td><strong>release-full</strong></td><td>较大</td><td>功能完整、需 Windows 10+</td></tr><tr><td><strong>release-full-shared</strong></td><td>较大</td><td>二次开发使用</td></tr></tbody></table>
<blockquote><p>💡 <strong>下载建议</strong>：M3U8 转换 MP4 的需求，下载 <code>ffmpeg-release-essentials.7z</code> 即可。</p></blockquote>
<p>直接下载链接（gyan.dev 镜像）：</p>
<ul>
<li><strong>官方主页</strong>：<a href="https://www.gyan.dev/ffmpeg/builds/" rel="noopener noreferrer" target="_blank">https://www.gyan.dev/ffmpeg/builds/</a></li>
<li><strong>Essentials 版</strong>：<a href="https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z" rel="noopener noreferrer" target="_blank">https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z</a></li>
</ul>
<hr />
<h2>三、解压安装</h2>
<h3>第一步：解压压缩包</h3>
<p>下载完成后会得到一个 <code>.7z</code> 压缩包，需使用 <strong>7-Zip</strong> 或 <strong>WinRAR</strong> 解压。</p>
<blockquote><p>📌 <strong>没有 7-Zip？</strong> 可前往官网免费下载：<a href="https://www.7-zip.org/" rel="noopener noreferrer" target="_blank">https://www.7-zip.org/</a></p></blockquote>
<h3>第二步：移动到固定目录</h3>
<p>解压后会得到一个名为 <code>ffmpeg-x.x-essentials_build</code> 的文件夹。建议将其重命名为 <code>ffmpeg</code> 并移动到一个固定且<strong>不会被误删</strong>的位置，例如：</p>
<pre><code>C:\ffmpeg\
</code></pre>
<h3>第三步：确认目录结构</h3>
<p>进入 <code>C:\ffmpeg\bin</code>，确认目录中包含以下三个核心可执行文件：</p>





















<table><thead><tr><th>文件名</th><th>作用</th></tr></thead><tbody><tr><td><code>ffmpeg.exe</code></td><td>音视频处理与转码（<strong>核心</strong>）</td></tr><tr><td><code>ffplay.exe</code></td><td>内置播放器</td></tr><tr><td><code>ffprobe.exe</code></td><td>媒体信息分析工具</td></tr></tbody></table>
<p>复制此 <code>bin</code> 目录的完整路径（例如 <code>C:\ffmpeg\bin</code>），下一步配置环境变量需要用到。</p>
<hr />
<h2>四、配置环境变量</h2>
<p>配置环境变量后，您可以在任意目录下的命令行中直接调用 <code>ffmpeg</code> 命令。</p>
<h3>第一步：打开系统属性</h3>
<ol>
<li>在桌面右键 <strong>此电脑</strong> → 选择 <strong>属性</strong></li>
<li>在打开的窗口中，点击 <strong>高级系统设置</strong></li>
</ol>
<h3>第二步：进入环境变量设置</h3>
<p>在跳出的 <strong>系统属性</strong> 窗口中，点击底部的 <strong>环境变量(N)…</strong> 按钮。</p>
<h3>第三步：编辑 Path 变量</h3>
<ol>
<li>在下方的 <strong>系统变量</strong> 区域，找到名为 <code>Path</code> 的变量</li>
<li>选中它后点击 <strong>编辑(E)…</strong></li>
<li>在弹出的窗口中点击 <strong>新建(N)</strong></li>
<li>粘贴之前复制的 bin 目录路径：<code>C:\ffmpeg\bin</code></li>
<li>一路点击 <strong>确定</strong> 保存所有窗口</li>
</ol>
<blockquote><p>⚠️ <strong>重要提示</strong>：配置完成后，<strong>必须重新打开</strong>一个新的命令行窗口，旧窗口不会自动加载新的环境变量。</p></blockquote>
<hr />
<h2>五、验证安装</h2>
<p>按 <code>Win + R</code> 打开运行窗口，输入 <code>cmd</code> 回车打开命令提示符，或直接搜索 <strong>PowerShell</strong> 打开。</p>
<p>输入以下命令：</p>
<pre><code>ffmpeg -version
</code></pre>
<p>若命令行返回类似如下信息，则代表<strong>安装成功</strong>：</p>
<pre><code>ffmpeg version 7.x-essentials_build Copyright (c) 2000-2026 the FFmpeg developers
built with gcc 13.2.0 (Rev5, Built by MSYS2 project)
configuration: --enable-gpl --enable-version3 --enable-static ...
libavutil      59. xx.100
libavcodec     61. xx.100
...
</code></pre>
<p>如果提示 <code>'ffmpeg' 不是内部或外部命令</code>，则说明环境变量配置存在问题，请回到第四步重新检查。</p>
<hr />
<h2>六、获取 M3U8 视频链接</h2>
<p>如果您已经拥有 M3U8 链接（例如本指南示例中的政府平台资源链接），可直接跳过此步骤。否则，可通过 <strong>Chrome / Edge 开发者工具</strong>抓取：</p>
<ol>
<li>打开目标视频页面</li>
<li>按 <strong>F12</strong> 或右键点击页面选择 <strong>检查</strong> 调出开发者工具</li>
<li>切换至 <strong>网络 (Network)</strong> 标签页</li>
<li>在过滤器搜索框中输入：<code>m3u8</code></li>
<li><strong>刷新页面</strong>并播放视频</li>
<li>在筛选出的请求中右键 <code>.m3u8</code> 文件 → <strong>Copy → Copy link address</strong></li>
</ol>
<blockquote><p>💡 <strong>小技巧</strong>：在过滤器中也可以直接选择 <strong>媒体 (Media)</strong> 分类，更容易定位视频资源。</p></blockquote>
<hr />
<h2>七、执行下载与转换命令</h2>
<h3>7.1 基础命令（推荐 ⭐）</h3>
<p>打开命令行（CMD / PowerShell），输入以下命令：</p>
<pre><code>ffmpeg -i "你的M3U8链接" -c copy output.mp4
</code></pre>
<p><strong>实际示例</strong>（以您提供的链接为例）：</p>
<pre><code>ffmpeg -i "https://v.dyjyzyk.dtdjzx.gov.cn/zyk-shengnei/transcode/20260519/3820709499606408795/hls/1500/3820709683451135060.m3u8" -c copy output.mp4
</code></pre>
<h3>7.2 增强兼容性命令</h3>
<p>针对部分加密或音频格式特殊的流媒体，建议使用：</p>
<pre><code>ffmpeg -i "你的M3U8链接" -c copy -bsf:a aac_adtstoasc output.mp4
</code></pre>
<h3>7.3 等待下载完成</h3>
<p>执行后，命令行窗口将显示实时下载进度，包括：</p>
<ul>
<li><code>frame=</code> 当前已处理帧数</li>
<li><code>time=</code> 当前已处理的视频时长</li>
<li><code>bitrate=</code> 当前码率</li>
<li><code>speed=</code> 转换速度倍数（如 <code>5x</code> 表示 5 倍速）</li>
</ul>
<p>下载完成后，您将在<strong>当前命令行所在的目录</strong>下看到生成的 <code>output.mp4</code> 文件。</p>
<blockquote><p>💡 <strong>小贴士</strong>：可以先用 <code>cd</code> 命令切换到希望保存文件的目录，例如：</p><pre><code>cd D:\Videos
ffmpeg -i "M3U8链接" -c copy output.mp4
</code></pre></blockquote>
<hr />
<h2>八、命令参数详解</h2>













































<table><thead><tr><th>参数</th><th>含义</th><th>说明</th></tr></thead><tbody><tr><td><code>-i</code></td><td>Input 输入源</td><td>后接 M3U8 链接或本地文件路径</td></tr><tr><td><code>-c copy</code></td><td>Codec 编码模式</td><td>直接拷贝音视频流，<strong>不重新编码</strong>，速度极快、画质无损</td></tr><tr><td><code>-bsf:a aac_adtstoasc</code></td><td>音频比特流过滤器</td><td>修复 AAC 音频格式，避免播放异常</td></tr><tr><td><code>-vcodec copy</code></td><td>视频编码器</td><td>仅拷贝视频流</td></tr><tr><td><code>-acodec copy</code></td><td>音频编码器</td><td>仅拷贝音频流</td></tr><tr><td><code>-timeout 3000000</code></td><td>超时时间（微秒）</td><td>防止网络中断导致下载失败</td></tr><tr><td><code>output.mp4</code></td><td>输出文件名</td><td>可指定路径，如 <code>D:\videos\out.mp4</code></td></tr></tbody></table>
<hr />
<h2>九、进阶用法</h2>
<h3>9.1 设置网络超时（防止断流）</h3>
<pre><code>ffmpeg -i "M3U8链接" -c copy -bsf:a aac_adtstoasc -timeout 3000000 output.mp4
</code></pre>
<h3>9.2 启用断线重连机制</h3>
<pre><code>ffmpeg -reconnect 1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i "M3U8链接" -c copy output.mp4
</code></pre>
<h3>9.3 重新编码并压缩画质（减小文件体积）</h3>
<pre><code>ffmpeg -i "M3U8链接" -c:v libx264 -crf 23 -c:a aac output.mp4
</code></pre>
<p>其中 <code>-crf</code> 取值 <code>0-51</code>，<strong>数值越小画质越高</strong>，推荐 <code>18-28</code> 区间。日常使用 <code>23</code> 即可。</p>
<h3>9.4 截取部分时段下载</h3>
<pre><code>ffmpeg -ss 00:01:00 -i "M3U8链接" -t 00:05:00 -c copy output.mp4
</code></pre>
<ul>
<li><code>-ss 00:01:00</code>：起始时间（从第 1 分钟开始）</li>
<li><code>-t 00:05:00</code>：持续时长（截取 5 分钟）</li>
</ul>
<h3>9.5 添加请求头（绕过简单防盗链）</h3>
<pre><code>ffmpeg -headers "Referer: https://example.com/" -i "M3U8链接" -c copy output.mp4
</code></pre>
<h3>9.6 协议白名单（解决部分报错）</h3>
<pre><code>ffmpeg -protocol_whitelist "file,http,https,tcp,tls,crypto" -i "M3U8链接" -c copy output.mp4
</code></pre>
<h3>9.7 批量下载脚本（一键工具）</h3>
<p>将以下代码保存为 <code>download.bat</code> 文件，<strong>双击即可使用</strong>：</p>
<pre><code>@echo off
chcp 65001 &gt;nul
title FFmpeg M3U8 转 MP4 工具
echo ============================================
echo       FFmpeg M3U8 视频下载工具
echo ============================================
echo.
set /p url="请粘贴 M3U8 链接: "
set /p name="请输入输出文件名（不带后缀）: "
echo.
echo 开始下载，请稍候...
echo.
ffmpeg -i "%url%" -c copy -bsf:a aac_adtstoasc "%name%.mp4"
echo.
echo ============================================
echo       下载完成！文件已保存
echo ============================================
pause
</code></pre>
<hr />
<h2>十、常见问题与解决方案</h2>
<h3>Q1：提示 <code>'ffmpeg' 不是内部或外部命令</code></h3>
<p><strong>原因</strong>：环境变量未生效。<br />
<strong>解决方案</strong>：</p>
<ol>
<li>确认 <code>Path</code> 中已添加 <code>C:\ffmpeg\bin</code> 路径</li>
<li>关闭所有命令行窗口，<strong>重新打开</strong>新窗口</li>
<li>必要时重启电脑</li>
</ol>
<h3>Q2：下载到一半中断或卡住</h3>
<p><strong>原因</strong>：网络不稳定或服务器超时。<br />
<strong>解决方案</strong>：使用断线重连参数</p>
<pre><code>ffmpeg -reconnect 1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i "M3U8链接" -c copy output.mp4
</code></pre>
<h3>Q3：报错 <code>Protocol 'https' not on whitelist</code></h3>
<p><strong>解决方案</strong>：添加协议白名单参数</p>
<pre><code>ffmpeg -protocol_whitelist "file,http,https,tcp,tls,crypto" -i "M3U8链接" -c copy output.mp4
</code></pre>
<h3>Q4：视频无法播放或音视频不同步</h3>
<p><strong>解决方案</strong>：重新编码以重建索引</p>
<pre><code>ffmpeg -i "M3U8链接" -c:v libx264 -c:a aac output.mp4
</code></pre>
<h3>Q5：M3U8 视频已加密（含 Key）</h3>
<p><strong>解决方案</strong>：FFmpeg 会自动尝试请求密钥。若失败，需手动下载 <code>.key</code> 文件，并修改 M3U8 文件中的密钥路径为本地路径后再转换。</p>
<h3>Q6：下载速度很慢</h3>
<p><strong>原因</strong>：可能是源服务器限速或网络环境问题。<br />
<strong>解决方案</strong>：</p>
<ul>
<li>切换网络环境（如使用有线网络）</li>
<li>检查源站是否对 IP 有限速</li>
<li>加上 <code>-threads 8</code> 参数启用多线程</li>
</ul>
<hr />
<h2>📌 重要提示</h2>
<ol>
<li><strong>版权合规</strong>：请确保下载的视频来源合法，遵守网站使用条款及版权法律法规。</li>
<li><strong>文件大小</strong>：<code>-c copy</code> 模式下文件大小与原视频一致；重新编码会占用更多 CPU 和时间。</li>
<li><strong>路径规范</strong>：链接和文件路径建议使用<strong>英文双引号</strong>包裹，避免特殊字符干扰。</li>
<li><strong>磁盘空间</strong>：下载前确认目标磁盘有足够剩余空间。</li>
</ol>
<hr />
<h2>📚 参考资料</h2>
<ul>
<li><a href="https://ffmpeg.org/download.html" rel="noopener noreferrer" target="_blank">FFmpeg 官方下载页面</a></li>
<li><a href="https://www.gyan.dev/ffmpeg/builds/" rel="noopener noreferrer" target="_blank">gyan.dev FFmpeg Windows Builds</a></li>
<li><a href="https://www.freedidi.com/13051.html" rel="noopener noreferrer" target="_blank">零度博客 - FFmpeg 安装教程</a></li>
<li><a href="https://juejin.cn/post/7498541531637923891" rel="noopener noreferrer" target="_blank">掘金 - FFmpeg 应用专栏</a></li>
<li><a href="https://cloud.tencent.com/developer/article/2611459" rel="noopener noreferrer" target="_blank">腾讯云开发者社区 - M3U8 转 MP4 指南</a></li>
</ul>
<hr />
<blockquote><p><strong>教程结束</strong>：按照上述步骤，您应当能够顺利地将任意 M3U8 流媒体视频下载并转换为 MP4 格式。</p></blockquote>]]></content>
    <category term="FFmpeg" />
    <category term="M3U8" />
    <category term="Windows" />
    <category term="教程" />
  </entry>
  <entry>
    <title>Xiaomi API 代理开发记录：从零到踩坑到修复</title>
    <link href="https://mps-blog.vercel.app//posts/xiaomi-proxy-devlog" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/xiaomi-proxy-devlog</id>
    <updated>2026-05-20T15:00:00.000Z</updated>
    <published>2026-05-20T15:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">Claude Code 自动化开发小米 API 网关代理的完整记录，涵盖模型映射、reasoning_content 处理、三层保障机制等六个核心问题的诊断与修复过程。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.Djo-Z8xO_1Txdkd.webp" alt="Xiaomi API 代理开发记录：从零到踩坑到修复" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>背景</h2>
<p>Claude Code Desktop 是 Anthropic 官方的 AI 编程助手客户端，它默认只能调用 Claude 系列模型。为了让它能调用小米的 <code>mimo-v2.5-pro</code> 推理模型，需要开发一个<strong>协议转换代理</strong>，将 Anthropic API 格式转换为 OpenAI API 格式。</p>
<h3>技术原理</h3>
<pre><code>Claude Code Desktop  →  本地代理 (xiaomi-proxy.cjs)  →  小米 API
     (Anthropic 格式)       (协议转换)                   (OpenAI 格式)
</code></pre>
<p>代理的工作：</p>
<ol>
<li>接收 Anthropic Messages API 格式请求（<code>/v1/messages</code>）</li>
<li>将模型 ID 映射为小米的模型名</li>
<li>将请求格式转为 OpenAI Chat Completions API 格式</li>
<li>转发到小米 API</li>
<li>将 OpenAI 响应转回 Anthropic 格式返回</li>
</ol>
<hr />
<h2>第一版：基础实现</h2>
<h3>配置信息</h3>
<pre><code>const CONFIG = {
  listenPort: 8081,
  targetHost: 'token-plan-sgp.xiaomimimo.com',
  targetPath: '/v1/chat/completions',
  apiKey: 'tp-sdcyuxmgwfbyb5tj0q1slchv2mnctwq0ro60de68p27p5870',
  model: 'mimo-v2.5-pro',
};
</code></pre>
<h3>模型映射</h3>
<pre><code>const ANTHROPIC_TO_IMODEL = {
  'claude-sonnet-4-6': CONFIG.model,
  'claude-opus-4': CONFIG.model,
  'claude-sonnet-4-6-lite': CONFIG.model,
  'claude-3-opus-4-video': CONFIG.model,
};
</code></pre>
<h3>核心转换函数</h3>
<p>实现了以下转换逻辑：</p>
<ul>
<li><code>anthropicToOpenAI()</code> - 请求格式转换</li>
<li><code>openAIToAnthropic()</code> - 响应格式转换</li>
<li><code>createAnthropicStreamTransformer()</code> - 流式响应转换</li>
</ul>
<hr />
<h2>问题 1：模型映射表不完整</h2>
<h3>症状</h3>
<p>Claude Code Desktop 发送的模型名不在映射表中，导致所有请求都走兜底逻辑。</p>
<h3>原因</h3>
<p>Claude Code 会发送多种模型名，如：</p>
<ul>
<li><code>claude-sonnet-4-6-20250506</code></li>
<li><code>claude-4-sonnet</code></li>
<li><code>claude-3-5-sonnet-20241022</code></li>
<li>等等…</li>
</ul>
<p>第一版只映射了 4 个名字。</p>
<h3>修复</h3>
<p>补全了 15 个常见 Anthropic 模型名的映射：</p>
<pre><code>const ANTHROPIC_TO_IMODEL = {
  // 主力模型
  'claude-sonnet-4-6': CONFIG.model,
  'claude-sonnet-4-6-20250506': CONFIG.model,
  'claude-4-sonnet': CONFIG.model,
  'claude-4-sonnet-20250506': CONFIG.model,
  'claude-3-5-sonnet': CONFIG.model,
  'claude-3-5-sonnet-20241022': CONFIG.model,
  'claude-3-opus': CONFIG.model,

  // 高级模型
  'claude-opus-4': CONFIG.model,
  'claude-opus-4-20250514': CONFIG.model,
  'claude-4-opus': CONFIG.model,
  'claude-3-opus-20240229': CONFIG.model,

  // 轻量模型
  'claude-sonnet-4-6-lite': CONFIG.model,
  'claude-3-5-haiku': CONFIG.model,
  'claude-3-5-haiku-20241022': CONFIG.model,
  'claude-3-haiku': CONFIG.model,

  // 视频/图片
  'claude-3-opus-4-video': CONFIG.model,
};
</code></pre>
<hr />
<h2>问题 2：缺少 stream_options</h2>
<h3>症状</h3>
<p>流式请求时，无法获取 token 用量信息。</p>
<h3>原因</h3>
<p>原版代码在流式请求时会发送 <code>stream_options: { include_usage: true }</code>，让上游在流式响应中返回 usage 信息。第一版漏掉了这个字段。</p>
<h3>修复</h3>
<pre><code>const openaiReq = {
  model: targetModel,
  messages,
  max_tokens: anthropicReq.max_tokens || 4096,
  stream: anthropicReq.stream === true,
  // 修复：恢复 stream_options
  stream_options: anthropicReq.stream ? { include_usage: true } : undefined,
};
</code></pre>
<hr />
<h2>问题 3：reasoning_content 丢失（核心问题）</h2>
<h3>症状</h3>
<pre><code>API Error: 400 The reasoning_content in the thinking mode must be passed back to the API.
</code></pre>
<h3>原因分析</h3>
<p>小米的 <code>mimo-v2.5-pro</code> 是<strong>推理模型</strong>，它的响应格式特殊：</p>
<pre><code>{
  "message": {
    "content": "",
    "reasoning_content": "嗯，用户发来一个简单的问候...",
    "tool_calls": [...]
  }
}
</code></pre>
<p>关键点：</p>
<ol>
<li>实际回复在 <code>reasoning_content</code> 字段，而不是 <code>content</code></li>
<li><strong>后续请求必须把之前的 <code>reasoning_content</code> 传回去</strong></li>
</ol>
<p>但 Anthropic 格式没有 <code>reasoning_content</code> 字段，经过一次转换后就丢失了。</p>
<h3>解决方案</h3>
<p>使用特殊标记 <code>[REASONING]...[/REASONING]</code> 来保存 reasoning_content：</p>
<p><strong>响应时（OpenAI → Anthropic）：</strong></p>
<pre><code>// 小米推理模型：用特殊标记保存 reasoning_content
if (message.reasoning_content) {
  content.push({ type: 'text', text: `[REASONING]${message.reasoning_content}[/REASONING]` });
}

if (message.content) {
  content.push({ type: 'text', text: message.content });
}
</code></pre>
<p><strong>请求时（Anthropic → OpenAI）：</strong></p>
<pre><code>if (block.type === 'text') {
  // 检查是否是 reasoning_content 标记
  if (block.text.startsWith('[REASONING]') &amp;&amp; block.text.endsWith('[/REASONING]')) {
    reasoningContent = block.text.slice(11, -12); // 提取 reasoning_content
  } else {
    contentParts.push({ type: 'text', text: block.text });
  }
}

// 构建 assistant 消息时包含 reasoning_content
const assistantMsg = { role: 'assistant' };
if (reasoningContent) {
  assistantMsg.reasoning_content = reasoningContent;
}
</code></pre>
<p><strong>流式处理：</strong></p>
<pre><code>// 追踪推理内容和普通内容的状态
let inReasoningMode = false;

// 小米推理模型：reasoning_content 需要用标记保存
if (delta.reasoning_content) {
  if (!inReasoningMode) {
    // 开始推理模式，添加前缀标记
    writeAnthropicEvent({
      type: 'content_block_delta', index: currentContentIndex,
      delta: { type: 'text_delta', text: '[REASONING]' },
    });
    inReasoningMode = true;
  }
  writeAnthropicEvent({
    type: 'content_block_delta', index: currentContentIndex,
    delta: { type: 'text_delta', text: delta.reasoning_content },
  });
}

// 普通 content
if (delta.content) {
  if (inReasoningMode) {
    // 从推理模式切换到内容模式，添加后缀标记
    writeAnthropicEvent({
      type: 'content_block_delta', index: currentContentIndex,
      delta: { type: 'text_delta', text: '[/REASONING]' },
    });
    inReasoningMode = false;
  }
  // ... 处理普通内容
}
</code></pre>
<hr />
<h2>问题 5：旧消息缺少 reasoning_content</h2>
<h3>症状</h3>
<pre><code>API Error: 400 The reasoning_content in the thinking mode must be passed back to the API.
</code></pre>
<p>即使已经实现了 <code>[REASONING]</code> 标记和缓存机制，仍然报错。</p>
<h3>原因分析</h3>
<p>通过详细日志发现，Claude Code 发送的对话历史中，<strong>所有旧的 assistant 消息都没有 <code>[REASONING]</code> 标记</strong>：</p>
<pre><code>[2] role: assistant, hasReasoning: false, content: 我是 **Claude Code**...
[4] role: assistant, hasReasoning: false, content: 看起来你遇到的问题...
[6] role: assistant, hasReasoning: false, content: 让我先看看你提到的支持文章...
</code></pre>
<p>这些消息是<strong>之前对话生成的</strong>，不是当前代理生成的，所以没有 <code>[REASONING]</code> 标记。</p>
<p>但小米 API 要求<strong>所有</strong> assistant 消息都必须有 <code>reasoning_content</code> 字段，否则拒绝请求。</p>
<h3>解决方案</h3>
<p>采用<strong>三层保障</strong>策略：</p>
<pre><code>// 小米推理模型需要传回 reasoning_content
// 优先从标记中提取，其次从缓存中查找，最后使用默认值
if (reasoningContent) {
  // 第一层：从 [REASONING] 标记中提取
  assistantMsg.reasoning_content = reasoningContent;
  log(`  Added reasoning_content from marker`);
} else if (msg.id &amp;&amp; reasoningCache.has(msg.id)) {
  // 第二层：从内存缓存中查找
  assistantMsg.reasoning_content = reasoningCache.get(msg.id);
  log(`  Added reasoning_content from cache`);
} else {
  // 第三层：为旧消息提供空的 reasoning_content，满足 API 要求
  assistantMsg.reasoning_content = '';
  log(`  Added empty reasoning_content (no marker or cache found)`);
}
</code></pre>
<h3>缓存机制</h3>
<pre><code>// reasoning_content 缓存：key 是 Anthropic 消息 ID，value 是 reasoning_content
const reasoningCache = new Map();

// 非流式响应时缓存
function openAIToAnthropic(openaiResp, originalModel) {
  const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

  if (message.reasoning_content) {
    reasoningCache.set(messageId, message.reasoning_content);
    log(`Cached reasoning_content for ${messageId}`);
  }

  return { id: messageId, ... };
}

// 流式响应时缓存
function createAnthropicStreamTransformer(anthropicRes, originalModel) {
  let accumulatedReasoning = '';

  // 在 onChunk 中累积
  if (delta.reasoning_content) {
    accumulatedReasoning += delta.reasoning_content;
  }

  // 在 onDone 中缓存
  onDone() {
    if (accumulatedReasoning) {
      reasoningCache.set(messageId, accumulatedReasoning);
      log(`Cached streaming reasoning_content for ${messageId}`);
    }
  }
}
</code></pre>
<hr />
<h2>问题 4：工具结果消息格式错误</h2>
<h3>症状</h3>
<pre><code>API Error: 400 messages[5] user content only supports a string or an array of content parts
</code></pre>
<h3>原因分析</h3>
<p>Claude Code 发送工具结果时，使用的是 <code>user</code> 角色 + <code>tool_result</code> 内容块：</p>
<pre><code>{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "call_xxx",
      "content": "命令执行结果..."
    }
  ]
}
</code></pre>
<p>但 OpenAI 格式要求工具结果是单独的 <code>tool</code> 消息：</p>
<pre><code>{
  "role": "tool",
  "tool_call_id": "call_xxx",
  "content": "命令执行结果..."
}
</code></pre>
<p>第一版的 <code>convertAnthropicContentToOpenAI</code> 函数对 <code>tool_result</code> 返回了特殊对象，导致格式错误。</p>
<h3>修复</h3>
<p>更新 <code>convertAnthropicMessages</code> 函数，正确拆分包含 tool_result 的用户消息：</p>
<pre><code>if (role === 'user') {
  // 检查是否包含 tool_result 内容块
  if (Array.isArray(msg.content) &amp;&amp; msg.content.some(b =&gt; b.type === 'tool_result')) {
    // 拆分成多个消息
    const textParts = [];
    const toolResults = [];

    for (const block of msg.content) {
      if (block.type === 'tool_result') {
        toolResults.push(block);
      } else if (block.type === 'text') {
        textParts.push(block.text);
      }
    }

    // 添加 user 消息（文本内容）
    if (textParts.length &gt; 0) {
      openaiMessages.push({ role: 'user', content: textParts.join('') });
    }

    // 添加 tool 消息
    for (const tr of toolResults) {
      const content = typeof tr.content === 'string'
        ? tr.content
        : (Array.isArray(tr.content) 
            ? tr.content.filter(b =&gt; b.type === 'text').map(b =&gt; b.text).join('') 
            : '');
      openaiMessages.push({
        role: 'tool',
        content,
        tool_call_id: tr.tool_use_id,
      });
    }
  } else {
    // 普通 user 消息
    const content = convertAnthropicContentToOpenAI(msg.content);
    openaiMessages.push({ role: 'user', content });
  }
}
</code></pre>
<hr />
<h2>Windows PowerShell 踩坑</h2>
<h3>环境变量设置</h3>
<pre><code># ❌ 错误（bash 语法）
DEBUG_PROXY=true node xiaomi-proxy.cjs

# ✅ 正确（PowerShell 语法）
$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjs
</code></pre>
<h3>curl 命令</h3>
<pre><code># ❌ 错误（PowerShell 的 curl 是 Invoke-WebRequest 的别名）
curl http://127.0.0.1:8081/health

# ✅ 正确（使用 curl.exe 或文件方式）
curl.exe http://127.0.0.1:8081/health

# 发送 JSON 请求（避免 PowerShell 转义问题）
curl.exe -X POST http://127.0.0.1:8081/v1/messages `
  -H "Content-Type: application/json" `
  -H "x-api-key: sk-test" `
  -d "@path\to\body.json"
</code></pre>
<hr />
<h2>注册表配置</h2>
<p>Claude Code Desktop 通过 Windows 注册表配置代理地址：</p>
<pre><code>Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Claude]
"inferenceProvider"="gateway"
"inferenceGatewayBaseUrl"="http://127.0.0.1:8081"
"inferenceGatewayApiKey"="sk-session-token"
</code></pre>
<p>双击 <code>.reg</code> 文件即可合并，无需先删除。</p>
<hr />
<h2>最终验证</h2>
<h3>测试命令</h3>
<pre><code># 终端 1：启动代理
$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjs

# 终端 2：测试请求
curl.exe -X POST http://127.0.0.1:8081/v1/messages `
  -H "Content-Type: application/json" `
  -H "x-api-key: sk-test" `
  -d "@test-body.json"
</code></pre>
<h3>成功响应</h3>
<pre><code>{
  "id": "msg_xxx",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "[REASONING]The user is asking for the current time.[/REASONING]"
    },
    {
      "type": "tool_use",
      "id": "call_xxx",
      "name": "Bash",
      "input": { "command": "date" }
    }
  ],
  "model": "claude-sonnet-4-6",
  "stop_reason": "tool_use",
  "usage": { "input_tokens": 39163, "output_tokens": 54 }
}
</code></pre>
<hr />
<h2>总结</h2>



































<table><thead><tr><th>问题</th><th>原因</th><th>解决方案</th></tr></thead><tbody><tr><td>模型映射不完整</td><td>只映射了 4 个模型名</td><td>补全 15 个常见模型名</td></tr><tr><td>缺少 stream_options</td><td>流式请求漏掉 usage 配置</td><td>添加 <code>stream_options: { include_usage: true }</code></td></tr><tr><td>reasoning_content 丢失</td><td>推理模型的特殊字段在转换中丢失</td><td>使用 <code>[REASONING]</code> 标记保存和恢复</td></tr><tr><td>工具结果格式错误</td><td>Claude Code 的 tool_result 格式与 OpenAI 不兼容</td><td>拆分 user 消息为 user + tool 消息</td></tr><tr><td>旧消息缺少 reasoning_content</td><td>历史对话没有标记，API 要求所有消息都有该字段</td><td>三层保障：标记 → 缓存 → 默认空值</td></tr></tbody></table>
<h3>关键经验</h3>
<ol>
<li><strong>推理模型的特殊性</strong>：小米 mimo-v2.5-pro 等推理模型需要特殊处理 <code>reasoning_content</code> 字段</li>
<li><strong>协议差异</strong>：Anthropic 和 OpenAI 的工具结果格式不同，需要正确转换</li>
<li><strong>调试的重要性</strong>：通过添加详细日志，快速定位了问题根源</li>
<li><strong>Windows 环境</strong>：PowerShell 的语法和 bash 不同，需要注意</li>
<li><strong>历史兼容性</strong>：处理历史对话时，需要为缺失的字段提供默认值</li>
<li><strong>多层保障</strong>：关键数据采用标记 + 缓存 + 默认值三层保障，提高鲁棒性</li>
</ol>
<hr />
<h2>附录：完整代码结构</h2>
<pre><code>xiaomi-proxy.cjs
├── 配置区 (CONFIG)
├── 模型映射表 (ANTHROPIC_TO_IMODEL)
├── reasoning_content 缓存 (reasoningCache)
├── HTTP 工具函数
│   ├── upstreamRequest() - 非流式请求
│   └── upstreamStreamRequest() - 流式请求
├── 请求格式转换
│   ├── convertAnthropicContentToOpenAI()
│   ├── convertAnthropicMessages()  ← 包含 reasoning_content 三层保障
│   ├── convertAnthropicTools()
│   └── anthropicToOpenAI()  ← 包含请求日志
├── 响应格式转换
│   ├── openAIToAnthropic()  ← 包含 reasoning_content 标记 + 缓存
│   └── createAnthropicStreamTransformer()  ← 包含流式推理内容处理 + 缓存
└── HTTP 服务器
    ├── /health - 健康检查
    ├── /v1/models - 模型列表
    └── /v1/messages - 核心端点  ← 包含详细日志
</code></pre>
<h3>核心设计模式</h3>
<p><strong>三层保障机制</strong>（用于 reasoning_content）：</p>
<pre><code>┌─────────────────────────────────────────────────────────────┐
│                    reasoning_content 处理流程                 │
├─────────────────────────────────────────────────────────────┤
│  响应时（小米 API → Claude Code）                            │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │ reasoning   │ →  │ [REASONING] │ →  │ Anthropic   │     │
│  │ _content    │    │ 标记包装     │    │ 格式响应     │     │
│  └─────────────┘    └─────────────┘    └─────────────┘     │
│                           ↓                                 │
│                    ┌─────────────┐                          │
│                    │ 缓存到 Map  │                          │
│                    └─────────────┘                          │
├─────────────────────────────────────────────────────────────┤
│  请求时（Claude Code → 小米 API）                           │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │ Anthropic   │ →  │ 提取标记    │ →  │ reasoning   │     │
│  │ 格式请求     │    │ 或查缓存    │    │ _content    │     │
│  └─────────────┘    └─────────────┘    └─────────────┘     │
│                           ↓                                 │
│                    ┌─────────────┐                          │
│                    │ 默认空字符串 │                          │
│                    └─────────────┘                          │
└─────────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<h2>附录：调试技巧</h2>
<h3>添加详细日志</h3>
<p>在关键位置添加日志，帮助定位问题：</p>
<pre><code>// 1. 显示原始请求
log('Anthropic messages:');
for (let i = 0; i &lt; anthropicReq.messages.length; i++) {
  const msg = anthropicReq.messages[i];
  log(`  [${i}] role: ${msg.role}, content: ${JSON.stringify(msg.content).substring(0, 100)}...`);
}

// 2. 显示转换后的请求
log('OpenAI request messages:');
for (let i = 0; i &lt; openaiReq.messages.length; i++) {
  const msg = openaiReq.messages[i];
  log(`  [${i}] role: ${msg.role}, hasReasoning: ${!!msg.reasoning_content}`);
}

// 3. 显示上游响应
log('Upstream response:', JSON.stringify(openaiResp, null, 2));

// 4. 显示 reasoning_content 状态
log(`  Found reasoning_content in block: ${reasoningContent.substring(0, 80)}...`);
log(`  Added reasoning_content from cache (msg.id=${msg.id})`);
</code></pre>
<h3>测试命令</h3>
<pre><code># 开启调试模式启动代理
$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjs

# 发送测试请求（使用文件避免 PowerShell 转义问题）
curl.exe -X POST http://127.0.0.1:8081/v1/messages `
  -H "Content-Type: application/json" `
  -H "x-api-key: sk-test" `
  -d "@test-body.json"

# 健康检查
curl.exe http://127.0.0.1:8081/health

# 查看模型列表
curl.exe http://127.0.0.1:8081/v1/models
</code></pre>]]></content>
    <category term="Claude" />
    <category term="JavaScript" />
    <category term="代理" />
    <category term="小米" />
    <category term="API" />
  </entry>
  <entry>
    <title>Claude Desktop 安装 + Loomy API 本地代理完整教程</title>
    <link href="https://mps-blog.vercel.app//posts/claude-desktop-loomy" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/claude-desktop-loomy</id>
    <updated>2026-05-20T09:11:00.000Z</updated>
    <published>2026-05-20T09:11:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">从零开始在 Windows 上安装 Claude Desktop，并通过本地协议转换代理使用 Loomy/iModel API 通道调用大模型的完整解决方案。涵盖 winget/MSIX 安装、Cowork 报错修复、OneDrive 卸载故障处理、Session Token 提取与协议转换代理配置。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.n909Q41F_1j6rTg.webp" alt="Claude Desktop 安装 + Loomy API 本地代理完整教程" style="width: 100%; height: auto; margin-bottom: 1em;" />
<blockquote><p><strong>目标</strong>：在 Windows 上安装 Claude Desktop，并通过本地代理使用 Loomy 的 API 通道调用大模型。</p><p><strong>适用场景</strong>：没有 Anthropic 官方 API Key，但有 Loomy 订阅/额度，希望在 Claude Code Desktop 中使用。</p></blockquote>
<hr />
<h2>目录</h2>
<ul>
<li><a href="#%E7%AC%AC%E4%B8%80%E7%AB%A0claude-desktop-%E5%AE%89%E8%A3%85">第一章：Claude Desktop 安装</a></li>
<li><a href="#%E7%AC%AC%E4%BA%8C%E7%AB%A0%E7%90%86%E8%A7%A3%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%90%86%E5%B7%A5%E5%85%B7">第二章：理解你的代理工具</a></li>
<li><a href="#%E7%AC%AC%E4%B8%89%E7%AB%A0%E9%85%8D%E7%BD%AE%E4%B8%8E%E5%90%AF%E5%8A%A8%E4%BB%A3%E7%90%86">第三章：配置与启动代理</a></li>
<li><a href="#%E7%AC%AC%E5%9B%9B%E7%AB%A0%E9%85%8D%E7%BD%AE-claude-code-desktop">第四章：配置 Claude Code Desktop</a></li>
<li><a href="#%E7%AC%AC%E4%BA%94%E7%AB%A0%E5%B8%B8%E8%A7%81%E5%AE%89%E8%A3%85%E6%95%85%E9%9A%9C%E4%BF%AE%E5%A4%8D--%E5%8D%B8%E8%BD%BD-onedrive-%E5%90%8E-claude-%E6%97%A0%E6%B3%95%E5%90%AF%E5%8A%A8">第五章：常见安装故障修复</a></li>
<li><a href="#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5">常见问题排查</a></li>
<li><a href="#%E9%99%84%E5%BD%95">附录</a></li>
</ul>
<hr />
<h2>第一章：Claude Desktop 安装</h2>
<h3>1.1 方法一：winget 安装（推荐作为第一步）</h3>
<pre><code>winget install Anthropic.Claude
</code></pre>
<p>✅ 结果：安装成功，应用可以正常启动。
❌ 问题：Cowork 功能报错 — <code>Reinstall required - Cowork requires Claude Desktop to be installed via a modern installer</code>。</p>
<p><strong>原因</strong>：Cowork 功能会校验 Claude Desktop 是否通过 MSIX（现代安装程序）部署。winget 安装的记录在 Windows 侧不完整，Cowork 认为安装来源不合法。</p>
<hr />
<h3>1.2 方法二：直接安装 MSIX</h3>
<p>下载 MSIX 安装包：</p>
<pre><code>Invoke-WebRequest -Uri "https://claude.ai/api/desktop/win32/x64/msix/latest/redirect" -OutFile "Claude.msix"
</code></pre>
<p>为当前用户安装：</p>
<pre><code>Add-AppxPackage -Path "Claude.msix"
</code></pre>
<p>❌ 问题：安装成功，但<strong>应用无法启动</strong>。点击图标无反应。</p>
<p><strong>原因</strong>：MSIX 包本身缺少某些运行时依赖或签名上下文。单独安装 MSIX 时，Windows 没有正确解析其依赖链。</p>
<hr />
<h3>1.3 最终方案：winget + MSIX 组合安装 ✅</h3>
<p>先通过 winget 安装基础版本（确保依赖齐全、应用可启动），再用 MSIX 覆盖安装（修正安装器签名记录）。</p>
<p><strong>完整命令</strong>：</p>
<pre><code># 第 1 步：winget 安装（使应用可运行，依赖完整）
winget install Anthropic.Claude

# 第 2 步：下载 MSIX 安装包
Invoke-WebRequest -Uri "https://claude.ai/api/desktop/win32/x64/msix/latest/redirect" -OutFile "Claude.msix"

# 第 3 步：MSIX 覆盖安装（修正安装来源，Cowork 可识别）
Add-AppxPackage -Path "Claude.msix"
</code></pre>
<p><strong>最终效果</strong>：</p>





















<table><thead><tr><th>检查项</th><th>结果</th></tr></thead><tbody><tr><td>应用启动</td><td>✅ 正常启动</td></tr><tr><td>Cowork 校验</td><td>✅ 不再报错</td></tr><tr><td>程序签名</td><td>✅ 完整</td></tr></tbody></table>
<hr />
<h2>第二章：理解你的代理工具</h2>
<p>你的 <code>Loomy Workspace</code> 目录下有三个文件，分工如下：</p>
<h3>2.1 文件总览</h3>
<pre><code>Loomy Workspace/
├── 代理使用教程.md              # 配置指南（DNS 劫持方案，供参考）
├── get-session.cjs              # Session Token 提取器
└── anthropic-proxy.mjs          # 协议转换代理（主力方案）
</code></pre>
<h3>2.2 get-session.cjs — Session Token 自动提取</h3>
<p><strong>作用</strong>：从 Loomy 桌面端的本地数据库中提取登录会话密钥。</p>
<p><strong>原理</strong>：</p>
<ul>
<li>Loomy 桌面端使用 Electron 的 Local Storage，底层是 LevelDB</li>
<li>LevelDB 的数据存储在 <code>%APPDATA%/loomy/Local Storage/leveldb/</code> 目录下，以 <code>.log</code> 和 <code>.ldb</code> 文件存在</li>
<li>该工具扫描这些文件，在二进制数据中查找 <code>loggedInAt</code> 关键字</li>
<li>在 <code>loggedInAt</code> 附近寻找 32 位十六进制字符串（即 <code>loomy-session-key</code>）</li>
<li>同时提取手机号做脱敏显示</li>
</ul>
<p><strong>用法</strong>：</p>
<pre><code># 仅输出最新 session key
node get-session.cjs

# 列出所有历史 session
node get-session.cjs --all

# 输出 SESSION_TOKEN=xxx 格式（适合管道）
node get-session.cjs --env

# 实时监听 session 变化（每 5 秒轮询）
node get-session.cjs --watch
</code></pre>
<p><strong>前置条件</strong>：Loomy 桌面端必须已登录运行。</p>
<hr />
<h3>2.3 anthropic-proxy.mjs — 协议转换代理（核心）</h3>
<p>这是整套方案的核心组件。它解决的问题是：</p>
<blockquote><p>Claude Code Desktop 只认 <strong>Anthropic Messages API</strong> 格式，而 Loomy/iModel 只提供 <strong>OpenAI Chat Completions API</strong> 格式。两者协议不兼容。</p></blockquote>
<h4>架构图</h4>
<pre><code>Claude Code Desktop              anthropic-proxy.mjs                    upstream
(ANTHROPIC_BASE_URL=:8080)       (localhost:8080)                       (loomyad.xunfei.cn)
┌─────────────────┐   POST /v1/messages   ┌─────────────────────┐   POST /api/v1/chat/completions   ┌──────────────┐
│  Anthropic 格式  │  ───────────────────→  │  协议转换 + 模型映射  │  ───────────────────────────────→  │  OpenAI 格式  │
│  请求           │                       │                     │                                 │  请求         │
│                 │  ←───────────────────  │                     │  ←───────────────────────────────  │              │
└─────────────────┘   Anthropic 格式响应   └─────────────────────┘   OpenAI 格式响应                   └──────────────┘
</code></pre>
<h4>核心功能 1：协议转换</h4>








































<table><thead><tr><th>项目</th><th>Anthropic Messages API</th><th>OpenAI Chat Completions API</th></tr></thead><tbody><tr><td>System Prompt</td><td><code>system</code> 顶层字段</td><td><code>role: "system"</code> 作为第一条消息</td></tr><tr><td>用户消息</td><td><code>role: "user"</code>, content 为对象数组</td><td><code>role: "user"</code>, content 为字符串或数组</td></tr><tr><td>图片输入</td><td><code>type: "image"</code>, <code>source: {data, media_type}</code></td><td><code>type: "image_url"</code>, <code>image_url.url</code> (base64 data URI)</td></tr><tr><td>工具调用</td><td><code>type: "tool_use"</code>, <code>input</code> 是对象</td><td><code>tool_calls</code>, <code>function.arguments</code> 是 JSON 字符串</td></tr><tr><td>工具结果</td><td><code>role: "user"</code>, <code>type: "tool_result"</code></td><td><code>role: "tool"</code>, <code>tool_call_id</code></td></tr><tr><td>流式事件</td><td><code>content_block_start/delta/stop</code> + <code>message_start/delta/stop</code></td><td>SSE <code>data: {...}</code>, 每行一个 delta chunk</td></tr></tbody></table>
<h4>核心功能 2：模型映射</h4>
<p>代理将 Claude 模型名映射到上游 iModel 的模型：</p>






























<table><thead><tr><th>Claude 模型名</th><th>映射到上游模型</th><th>说明</th></tr></thead><tbody><tr><td><code>claude-sonnet-4-6</code> 系列</td><td><code>deepseek-v4-flash</code></td><td>主力模型</td></tr><tr><td><code>claude-opus-4</code> 系列</td><td><code>qwen3.5-plus</code></td><td>高级模型，支持图片</td></tr><tr><td><code>claude-sonnet-4-6-lite</code> 系列</td><td><code>qwen3.5-flash</code></td><td>轻量模型</td></tr><tr><td><code>claude-3-opus-4-video</code></td><td><code>doubao-seed-2.0-pro</code></td><td>视频理解</td></tr></tbody></table>
<h4>核心功能 3：API 端点</h4>






























<table><thead><tr><th>路径</th><th>方法</th><th>说明</th></tr></thead><tbody><tr><td><code>/</code> 或 <code>/health</code></td><td>GET</td><td>健康检查，返回代理状态和模型列表</td></tr><tr><td><code>/v1/models</code></td><td>GET</td><td>返回映射表中的所有 Claude 模型（Claude Code 启动时会调用）</td></tr><tr><td><code>/v1/messages</code></td><td>POST</td><td><strong>主要端点</strong>：接收 Anthropic 请求，转换后转发到上游</td></tr><tr><td><code>/v1/messages/:id</code></td><td>GET</td><td>返回假响应（防止 Claude Code 因 404 报错）</td></tr></tbody></table>
<h4>核心功能 4：流式支持</h4>
<p>完整的流式转换。上游 OpenAI SSE 流中的每个 <code>delta</code> 被实时转换为 Anthropic 的 <code>content_block_delta</code> 事件。支持：</p>
<ul>
<li>文本 delta → <code>content_block_delta</code> (text_delta)</li>
<li>Tool calls delta → <code>content_block_delta</code> (input_json_delta)</li>
<li>Usage 信息 → <code>message_delta</code> 中的 usage</li>
<li>Finish reason 映射</li>
</ul>
<h4>Session Token 获取</h4>
<p>优先级：</p>
<ol>
<li><strong>环境变量</strong> <code>SESSION_TOKEN</code>（最高优先级）</li>
<li><strong>自动提取</strong> — 调用 <code>get-session.cjs</code> 从 Loomy 桌面端 LevelDB 读取</li>
</ol>
<hr />
<h2>第三章：配置与启动代理</h2>
<h3>3.1 确认前置条件</h3>
<ul>
<li> Loomy 桌面端已安装并登录</li>
<li> Node.js 已安装（建议 v18+）</li>
<li> Claude Desktop 已正确安装（按第一章流程）</li>
</ul>
<h3>3.2 验证 Session 可获取</h3>
<pre><code>cd "C:\Users\Administrator\Documents\Loomy Workspace"

# 确保 Loomy 桌面端正在运行且已登录
node get-session.cjs
</code></pre>
<p>预期输出：一串 32 位十六进制字符串，如 <code>a1b2c3d4e5f6...</code>。</p>
<p>如果报错 <code>未找到 session</code>，请确认 Loomy 桌面端已登录。</p>
<h3>3.3 启动代理</h3>
<pre><code># 方式一：自动提取 session
node anthropic-proxy.mjs

# 方式二：手动指定 session（优先级更高）
set SESSION_TOKEN=你的32位hex字符串
node anthropic-proxy.mjs

# 方式三：启用调试日志
set DEBUG_PROXY=true
node anthropic-proxy.mjs

# 方式四：指定其他端口（默认 8080）
set PROXY_PORT=9090
node anthropic-proxy.mjs
</code></pre>
<p>启动成功应看到如下输出：</p>
<pre><code>╔════════════════════════════════════════════════════════╗
║         Anthropic → iModel API 代理                    ║
╠════════════════════════════════════════════════════════╣
║  代理地址:  http://127.0.0.1:8080                      ║
║  上游 API:  https://loomyad.xunfei.cn                  ║
║  Session:   a1b2c3d4e5f6...                            ║
╚════════════════════════════════════════════════════════╝
</code></pre>
<h3>3.4 验证代理运行正常</h3>
<pre><code># 测试健康检查
curl http://127.0.0.1:8080/health

# 测试模型列表
curl http://127.0.0.1:8080/v1/models
</code></pre>
<hr />
<h2>第四章：配置 Claude Code Desktop</h2>
<h3>4.1 设置环境变量</h3>
<p>在 Claude Code Desktop 的配置中（<code>claude_desktop_config.json</code>），添加：</p>
<pre><code>{
  "projectSettings": {
    "ANTHROPIC_BASE_URL": "http://127.0.0.1:8080",
    "ANTHROPIC_API_KEY": "sk-session-token"
  }
}
</code></pre>
<p><strong>说明</strong>：</p>
<ul>
<li><code>ANTHROPIC_BASE_URL</code> 指向本地代理地址。Claude Code 的所有 API 请求都会发到这里。</li>
<li><code>ANTHROPIC_API_KEY</code> 代理不校验其真实性，填任意值即可。</li>
<li>不要在全局设置中配置，只在项目级别配置，避免影响其他使用场景。</li>
</ul>
<h3>4.2 验证连通性</h3>
<p>在 Claude Code Desktop 中发送一条消息，确认能正常返回。</p>
<h3>4.3 日志排查</h3>
<p>如果遇到问题，启用调试日志再次启动代理：</p>
<pre><code>set DEBUG_PROXY=true
node anthropic-proxy.mjs
</code></pre>
<p>查看日志中的关键信息：</p>
<ul>
<li>模型映射是否匹配：<code>Model mapping: claude-sonnet-4-6 → deepseek-v4-flash</code></li>
<li>上游返回状态码：非 200 说明上游有问题</li>
<li>请求体中的消息数量和工具数量</li>
</ul>
<hr />
<h2>第五章：常见安装故障修复 — 卸载 OneDrive 后 Claude 无法启动</h2>
<h3>问题现象</h3>
<p>卸载 OneDrive 之后，Claude Desktop（或其他应用）点击图标无反应，无法启动。</p>
<h3>根因</h3>
<p>Windows 的 <strong>User Shell Folders</strong> 注册表中，<code>Desktop</code>、<code>Documents</code> 等路径仍然指向 <code>C:\Users\xxx\OneDrive\Desktop</code> 这类目录。OneDrive 被卸载后这些目录已不存在，Claude Desktop 启动时找不到标准文件夹路径，直接崩溃。</p>
<p>此外，旧版本 Claude 的残留文件也可能干扰新安装。</p>
<h3>修复脚本</h3>
<p>以下脚本会自动完成全部修复流程。将代码保存为 <code>Fix-ClaudeDesktop-OneDrivePath.ps1</code>，然后<strong>以管理员身份运行</strong>：</p>
<pre><code># ============================================================
# Fix-ClaudeDesktop-OneDrivePath.ps1
# 用途：修复卸载 OneDrive 后 Claude Desktop 无法启动的问题
# 原理：清理 User Shell Folders 中残留的 OneDrive 路径 + 清理 Claude 残留
# 运行：以管理员身份运行 PowerShell，然后执行本脚本
# ============================================================

# 0. 检查管理员权限
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    Write-Host "请以管理员身份运行 PowerShell 后再执行本脚本。" -ForegroundColor Red
    exit 1
}

$ErrorActionPreference = "Stop"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupDir = "$env:USERPROFILE\Desktop\ClaudeFix-Backup-$timestamp"
New-Item -ItemType Directory -Path $backupDir | Out-Null
Write-Host "备份目录：$backupDir" -ForegroundColor Cyan

# 1. 备份注册表 User Shell Folders / Shell Folders
Write-Host "`n[1/5] 备份注册表 ..." -ForegroundColor Yellow
reg export "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" "$backupDir\UserShellFolders.reg" /y | Out-Null
reg export "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"      "$backupDir\ShellFolders.reg"     /y | Out-Null
Write-Host "  注册表已备份。"

# 2. 修正 User Shell Folders 中含 OneDrive 的路径
Write-Host "`n[2/5] 扫描并修正 User Shell Folders 中的 OneDrive 路径 ..." -ForegroundColor Yellow
$keys = @(
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders",
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
foreach ($key in $keys) {
    $props = Get-ItemProperty -Path $key
    foreach ($name in $props.PSObject.Properties.Name) {
        if ($name -like "PS*") { continue }
        $value = $props.$name
        if ($value -is [string] -and $value -match "OneDrive") {
            $newValue = $value -replace "([A-Za-z]:\\Users\$\^$\$+|%USERPROFILE%)\\OneDrive(?:\s-\s[^\\]+)?\\", "%USERPROFILE%\"
            if ($newValue -ne $value) {
                Write-Host "  [$key] $name" -ForegroundColor Green
                Write-Host "    旧: $value"
                Write-Host "    新: $newValue"
                Set-ItemProperty -Path $key -Name $name -Value $newValue
            }
        }
    }
}

# 3. 确保本地默认文件夹存在
Write-Host "`n[3/5] 确保本地默认文件夹存在 ..." -ForegroundColor Yellow
$folders = @("Desktop","Documents","Downloads","Pictures","Music","Videos","Favorites")
foreach ($f in $folders) {
    $p = Join-Path $env:USERPROFILE $f
    if (-not (Test-Path $p)) {
        New-Item -ItemType Directory -Path $p | Out-Null
        Write-Host "  已创建: $p"
    }
}

# 4. 卸载 Claude Desktop(若存在)并清理残留目录
Write-Host "`n[4/5] 卸载 Claude Desktop 并清理残留 ..." -ForegroundColor Yellow
try {
    $pkg = Get-AppxPackage -Name "*Claude*" -ErrorAction SilentlyContinue
    if ($pkg) {
        $pkg | Remove-AppxPackage
        Write-Host "  已卸载 MSIX 包: $($pkg.Name)"
    } else {
        Write-Host "  未检测到 MSIX 形式的 Claude(可能是 exe 安装,请手动到'已安装的应用'中卸载)。"
    }
} catch {
    Write-Host "  卸载 MSIX 包失败: $_" -ForegroundColor Red
}

$residue = @(
    "$env:LOCALAPPDATA\AnthropicClaude",
    "$env:LOCALAPPDATA\Claude",
    "$env:APPDATA\Claude",
    "$env:APPDATA\AnthropicClaude"
)
foreach ($d in $residue) {
    if (Test-Path $d) {
        Copy-Item $d "$backupDir\" -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item $d -Recurse -Force -ErrorAction SilentlyContinue
        Write-Host "  已清理: $d (已备份到 $backupDir)"
    }
}

# 5. 完成提示
Write-Host "`n[5/5] 完成!" -ForegroundColor Green
Write-Host "请执行以下两步:" -ForegroundColor Cyan
Write-Host "  1) 重启电脑(必须,让 Explorer 重新加载 Shell Folders)"
Write-Host "  2) 从 https://claude.ai/download 重新下载并以管理员身份安装 Claude Desktop"
Write-Host "`n如出现异常,可双击 $backupDir 下的 .reg 文件恢复注册表。" -ForegroundColor Cyan
</code></pre>
<h3>脚本执行流程</h3>








































<table><thead><tr><th>步骤</th><th>操作</th><th>说明</th></tr></thead><tbody><tr><td>0</td><td>检查管理员权限</td><td>非管理员直接退出</td></tr><tr><td>1</td><td>备份注册表</td><td>导出 <code>User Shell Folders</code> 和 <code>Shell Folders</code> 到桌面备份目录</td></tr><tr><td>2</td><td>修正 OneDrive 路径</td><td>扫描注册表中所有含 <code>OneDrive</code> 的路径，替换为 <code>%USERPROFILE%\XXX</code></td></tr><tr><td>3</td><td>创建缺失文件夹</td><td>确保 <code>Desktop</code>、<code>Documents</code> 等标准文件夹存在</td></tr><tr><td>4</td><td>清理 Claude 残留</td><td>卸载 MSIX 包，删除 <code>%LOCALAPPDATA%</code> 和 <code>%APPDATA%</code> 下的残留目录</td></tr><tr><td>5</td><td>完成提示</td><td>提示重启电脑并重新安装 Claude Desktop</td></tr></tbody></table>
<h3>恢复方法</h3>
<p>如果修复后出现异常，双击桌面备份目录中的 <code>.reg</code> 文件即可恢复注册表到修复前的状态。</p>
<h3>与第一章的配合</h3>
<p>如果遇到了 OneDrive 卸载导致的问题，正确的完整流程是：</p>
<pre><code># 第 1 步：运行本修复脚本（清理注册表 + 卸载旧版 Claude）
# （以管理员身份运行 Fix-ClaudeDesktop-OneDrivePath.ps1）

# 第 2 步：重启电脑

# 第 3 步：重新安装 Claude Desktop
winget install Anthropic.Claude

# 第 4 步：MSIX 覆盖安装（修正 Cowork 安装来源）
Invoke-WebRequest -Uri "https://claude.ai/api/desktop/win32/x64/msix/latest/redirect" -OutFile "Claude.msix"
Add-AppxPackage -Path "Claude.msix"
</code></pre>
<p>之后再按后续章节配置代理即可。</p>
<hr />
<h2>常见问题排查</h2>
<h3>Q1：Claude Code 启动时报 “No available models”</h3>
<p><strong>原因</strong>：代理未启动或 <code>ANTHROPIC_BASE_URL</code> 配置错误。</p>
<p><strong>解决</strong>：</p>
<pre><code># 确认代理正在运行
curl http://127.0.0.1:8080/v1/models

# 确认配置的 URL 可访问
curl http://127.0.0.1:8080/health
</code></pre>
<h3>Q2：请求返回 401 Unauthorized</h3>
<p><strong>原因</strong>：Session Token 过期或无效。</p>
<p><strong>解决</strong>：重新运行 <code>get-session.cjs</code> 获取最新 token，或用 <code>--watch</code> 模式监控变化。</p>
<h3>Q3：请求返回 502 Bad Gateway</h3>
<p><strong>原因</strong>：上游 <code>loomyad.xunfei.cn</code> 不可达。</p>
<p><strong>解决</strong>：</p>
<pre><code># 测试网络连通性
ping loomyad.xunfei.cn

# 检查是否需要代理上网
</code></pre>
<h3>Q4：端口被占用</h3>
<p><strong>原因</strong>：8080 端口已被其他程序占用。</p>
<p><strong>解决</strong>：</p>
<pre><code>set PROXY_PORT=9090
node anthropic-proxy.mjs
</code></pre>
<p>同时在 Claude Code Desktop 配置中将 <code>ANTHROPIC_BASE_URL</code> 改为 <code>http://127.0.0.1:9090</code>。</p>
<h3>Q5：工具调用（Tool Use）失败</h3>
<p><strong>原因</strong>：上游模型不支持某些工具调用格式。</p>
<p><strong>解决</strong>：检查 <code>get-session.cjs --env</code> 输出的 session 是否有效，以及上游 iModel 是否支持 function calling。</p>
<hr />
<h2>附录</h2>
<h3>完整启动流程速查</h3>
<pre><code># 1. 确保 Loomy 桌面端已登录

# 2. 启动代理
cd "C:\Users\Administrator\Documents\Loomy Workspace"
node anthropic-proxy.mjs

# 3. 保持终端窗口打开，另开一个窗口启动 Claude Code Desktop
# 4. 确保 claude_desktop_config.json 中已配置：
#    ANTHROPIC_BASE_URL=http://127.0.0.1:8080
#    ANTHROPIC_API_KEY=sk-session-token
</code></pre>
<h3>文件清单</h3>

























<table><thead><tr><th>文件</th><th>说明</th><th>是否需要修改</th></tr></thead><tbody><tr><td><code>get-session.cjs</code></td><td>Session Token 提取器</td><td>不需要，直接使用</td></tr><tr><td><code>anthropic-proxy.mjs</code></td><td>协议转换代理</td><td>不需要，直接使用</td></tr><tr><td><code>Claude Desktop + Loomy 代理完整安装与配置教程.md</code></td><td>本文档</td><td>—</td></tr></tbody></table>
<h3>参考链接</h3>
<ul>
<li><a href="https://support.claude.com/zh-CN/articles/12622703-%E4%B8%BA-windows-%E9%83%A8%E7%BD%B2-claude-desktop" rel="noopener noreferrer" target="_blank">Claude Desktop Windows 部署指南</a></li>
<li><a href="https://loomy.co" rel="noopener noreferrer" target="_blank">Loomy 官网</a></li>
</ul>]]></content>
    <category term="Claude" />
    <category term="Loomy" />
    <category term="Windows" />
    <category term="代理" />
  </entry>
  <entry>
    <title>本地AI知识库与纯净Windows系统资源</title>
    <link href="https://mps-blog.vercel.app//posts/tg/2026-04-20" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/tg/2026-04-20</id>
    <updated>2026-04-20T14:28:34.000Z</updated>
    <published>2026-04-20T15:50:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">今日推荐本地AI知识库工具和官方无广告Windows系统镜像，助力高效学习与装机。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.qtAIlBHz_1Kgms8.webp" alt="本地AI知识库与纯净Windows系统资源" style="width: 100%; height: auto; margin-bottom: 1em;" />
<aside>
  <div>🤖</div>
  <div>
    <div>
      <span>今日速读</span>
      <span>2 条 · 1 个频道</span>
    </div>
    <div>今日推荐本地AI知识库工具和官方无广告Windows系统镜像，助力高效学习与装机。</div>
    <div>— 数据本地存，隐私更安心，AI也能有长期记忆。</div>
  </div>
</aside>
<div>
  <div>
    <div>⟫ 转发自 不是白嫖，这叫借鉴（备用）</div>
    <div>windows系统下载包括家庭版、专业版、教育版，都是官方原版，无广告，装机必备～<br />🔗<a href="https://pan.quark.cn/s/fc09734909dd" target="_blank" rel="noopener">pan.quark.cn/s/fc09734909dd</a><br /><a href="/tags/夸克">夸克</a>  <a href="/tags/Github">Github</a> <a href="/tags/windows">windows</a></div>
    <div>
      <span>code 技巧囤积</span>
      <span>01:36</span>
    </div>
  </div>
  <div>
    <div>⟫ 转发自 不是白嫖，这叫借鉴（备用）</div>
    <div>本地AI知识库，永久存储、长期记忆如果有大量的文件想要丢给AI、训练AI，一定要用这个，支持向量检索、MMR 去冗余、关键词重排等，能从海量文件中精准检索并生成答案，且数据不出电脑～<br />🔗<a href="https://github.com/veyliss/ai-localbase" target="_blank" rel="noopener">github.com/veyliss/ai-localbase</a><br /><a href="/tags/Github">Github</a>  <a href="/tags/AI">AI</a> <a href="/tags/知识库">知识库</a></div>
    <div>
      <span>code 技巧囤积</span>
      <span>01:36</span>
    </div>
  </div>
</div>]]></content>
    <category term="夸克" />
    <category term="Github" />
    <category term="windows" />
    <category term="AI" />
    <category term="知识库" />
    <category term="telegram" />
  </entry>
  <entry>
    <title>如何开始使用此主题</title>
    <link href="https://mps-blog.vercel.app//posts/start" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/start</id>
    <updated>2025-08-15T00:00:00.000Z</updated>
    <published>2025-08-15T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">Litos 主题快速入门指南：前置条件、Fork 或模板设置以及后续步骤。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.7-E_J0u9_Z19SPhO.webp" alt="如何开始使用此主题" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>这个主题是我在业余时间开发的，文档可能不够全面，但我会尽力解释。</p>
<h3>前提条件</h3>
<p>在开始之前，请确保你的开发环境中已安装以下工具：</p>
<ul>
<li><a href="https://nodejs.org/en/download" rel="noopener noreferrer" target="_blank">Node.js</a> - 运行开发环境所必需。</li>
<li><a href="https://pnpm.io/installation" rel="noopener noreferrer" target="_blank">pnpm</a> - 我们首选的包管理器，用于依赖管理。</li>
<li><a href="https://git-scm.com/" rel="noopener noreferrer" target="_blank">Git</a> - 用于版本控制和项目管理。</li>
<li><a href="https://code.visualstudio.com/" rel="noopener noreferrer" target="_blank">VS Code</a> - 推荐使用的代码编辑器，开发体验极佳。</li>
</ul>
<div><div><div></div><div>NOTE</div></div><div><p>如果你有其他替代工具，则不需要遵循本文中的推荐。</p></div></div>
<h3>启动项目</h3>
<p>有两种方式：</p>
<ul>
<li><a href="https://github.com/Dnzzk2/Litos/fork" rel="noopener noreferrer" target="_blank">Fork 仓库</a></li>
<li><a href="https://github.com/Dnzzk2/Litos/generate" rel="noopener noreferrer" target="_blank">使用此模板</a> - 将来会推出无内容分支，无需删除和替换内容。</li>
</ul>
<p>获取你自己的仓库后，可以将其克隆到本地。</p>
<h3>更新主题</h3>
<p>当主题发布新版本时，你可以按照以下步骤拉取最新更改而不丢失内容：</p>
<p><strong>1. 添加 upstream 远程（只需一次）</strong></p>
<pre><code>git remote add upstream https://github.com/Dnzzk2/Litos.git
</code></pre>
<p><strong>2. 获取并合并最新的主题</strong></p>
<pre><code>git fetch upstream
git merge upstream/main --no-edit
</code></pre>
<p><strong>3. 解决冲突（如有）</strong></p>
<p>冲突通常只发生在你修改过的文件中，例如 <code>src/config.ts</code>。在这种情况下，只需保留你自己的配置，接受其余的主题更新即可。</p>
<div><div><div></div><div>TIP</div></div><div><p>为减少冲突，请尽量只修改以下文件：</p><ul>
<li><code>src/config.ts</code> — 站点配置</li>
<li><code>src/content/</code> — 你的文章和项目</li>
<li><code>public/</code> — 你的静态资源（favicon、图片等）</li>
<li><code>.env</code> — 环境变量</li>
</ul><p>不要直接修改主题源文件（组件、样式、布局）。这样可以确保每次更新都顺利。</p></div></div>
<h3>结束</h3>
<p>后续文档将分为多个页面，如 readme、posts…</p>]]></content>
    <category term="Litos" />
    <category term="文档" />
  </entry>
  <entry>
    <title>README 页面配置</title>
    <link href="https://mps-blog.vercel.app//posts/readme" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/readme</id>
    <updated>2025-08-14T00:00:00.000Z</updated>
    <published>2025-08-14T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">配置 README 页面模块（站点、页眉/页脚链接、社交、GitHub、技能），包含字段文档和示例。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.DMtFtdkY_ZeJxeL.webp" alt="README 页面配置" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>以下文件与此文档兼容：</p>
<ul>
<li><code>src/pages/index.astro</code> - readme 页面。</li>
<li><code>src/config.ts</code> - readme 页面配置。</li>
<li><code>src/components/base</code> - 大部分组件来自这里。</li>
</ul>
<h3>站点配置</h3>
<p>配置基本站点信息：</p>
<pre><code>export const SITE: Site = {
  title: 'Litos',
  description: 'Litos is a blog theme built with Astro.js and 夜猫子Ai手记.',
  website: 'https://litos.vercel.app/',
  lang: 'en',
  base: '/',
  author: '夜猫子Ai手记',
  ogImage: '/og-image.jpg',
}
</code></pre>





































<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>title</td><td>站点的标题。</td></tr><tr><td>description</td><td>站点的描述。</td></tr><tr><td>website</td><td>你的最终部署 URL。Astro 使用此完整 URL 在最终构建中生成网站地图和规范 URL。强烈建议设置此配置以充分利用 Astro。</td></tr><tr><td>lang</td><td>lang 全局属性有助于定义元素的语言。</td></tr><tr><td>base</td><td>部署的基础路径。Astro 将使用此路径作为开发环境和生产构建中页面和资源的根目录。 <br /> 当设置为 <code>/docs</code> 时，astro dev 将在 <code>/docs</code> 启动你的服务器。</td></tr><tr><td>author</td><td>站点的作者。</td></tr><tr><td>ogImage</td><td>站点的 Open Graph 图片，但在具体文章页面，你可以使用其他 ogImage。</td></tr></tbody></table>
<h3>顶部链接</h3>
<p>顶部跳转链接组件配置：</p>
<pre><code>export const HEADER_LINKS: Link[] = [
  {
    name: 'Posts',
    url: '/posts',
  },
]
</code></pre>
<h3>底部链接</h3>
<p>底部链接组件配置：</p>
<pre><code>export const FOOTER_LINKS: Link[] = [
  {
    name: 'Readme',
    url: '/',
  },
]
</code></pre>
<h3>社交链接</h3>
<p>在 readme 页面中，你可以在主题介绍下方看到一排图标，可以通过以下方式配置。图标来自 <a href="https://icon-sets.iconify.design/" rel="noopener noreferrer" target="_blank">Iconify</a>。</p>
<pre><code>export const SOCIAL_LINKS: SocialLink[] = [
  {
    name: 'github',
    url: 'https://github.com/yourname',
    icon: 'icon-[ri--github-fill]',
    count: 11,
  },
  {
    name: 'twitter',
    url: 'https://x.com/yourname',
    icon: 'icon-[ri--twitter-x-fill]',
  },
]
</code></pre>

























<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>name</td><td>社交链接的名称。</td></tr><tr><td>url</td><td>社交链接的 URL。</td></tr><tr><td>icon</td><td>社交链接的图标。</td></tr><tr><td>count</td><td>该社交媒体的关注者数量。这是一个 <code>可选</code> 字段。</td></tr></tbody></table>
<h3>聚光灯</h3>





















<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>ENABLED</td><td>是否启用 GitHub 功能。</td></tr><tr><td>GITHUB_USERNAME</td><td>用于获取数据的 GitHub 用户名。</td></tr><tr><td>TOOLTIP_ENABLED</td><td>是否启用 GitHub 提示框（鼠标悬停卡片）功能。</td></tr></tbody></table>
<h3>技能展示</h3>
<p>在 readme 页面中，你可以看到一个技能展示区域，可以通过配置以下代码来展示你的技能：</p>
<pre><code>export const SKILLSSHOWCASE_CONFIG: SkillsShowcaseConfig = {
  SKILLS_ENABLED: true,
  SKILLS_DATA: [
    {
      direction: 'left',
      skills: [
        {
          name: 'JavaScript',
          icon: 'icon-[mdi--language-javascript]',
        },
        {
          name: 'CSS',
          icon: 'icon-[mdi--language-css3]',
        },
        {
          name: 'HTML',
          icon: 'icon-[mdi--language-html5]',
        },
        {
          name: 'TypeScript',
          icon: 'icon-[mdi--language-typescript]',
        },
      ],
    },
  ],
}
</code></pre>

































<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>SKILLS_ENABLED</td><td>是否启用技能展示功能。</td></tr><tr><td>SKILLS_DATA</td><td>技能数据。一个对象代表一行。</td></tr><tr><td>    direction</td><td>每个动画在两个方向运行：<code>left</code> 和 <code>right</code>。</td></tr><tr><td>    skills</td><td>技能数组。</td></tr><tr><td>        name</td><td>技能名称。</td></tr><tr><td>        icon</td><td>技能图标。图标来自 <a href="https://icon-sets.iconify.design/" rel="noopener noreferrer" target="_blank">Iconify</a>。</td></tr></tbody></table>
<div><div><div></div><div>TIP</div></div><div><p>建议在本地运行项目以查看效果。建议每行至少展示三个不同的技能。</p></div></div>
<h3>文章</h3>
<p>这里主要展示置顶文章。如果没有置顶文章，我们将根据 <code>POSTS_CONFIG</code> 显示最新 <code>size</code> 数量的文章。</p>]]></content>
    <category term="Litos" />
    <category term="文档" />
  </entry>
  <entry>
    <title>文章页面配置</title>
    <link href="https://mps-blog.vercel.app//posts/posts" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/posts</id>
    <updated>2025-08-13T00:00:00.000Z</updated>
    <published>2025-08-13T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">完整的文章配置指南：字段、列表样式、文章元数据布局、OG 图片处理、frontmatter 示例、markdown 语法和 expressive-code 配置。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.DuEOlrCu_2kyEMp.webp" alt="文章页面配置" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>以下文件与此文档兼容：</p>
<ul>
<li><code>src/pages/posts/[...id].astro</code> - 文章具体内容显示页面。</li>
<li><code>src/pages/posts/[...page].astro</code> - 文章列表页面。</li>
<li><code>src/content/posts</code> - 文章集合</li>
<li><code>src/content.config.ts</code> - 文章数据集和前置数据配置。</li>
<li><code>ec.config.mjs</code> - expressiveCode 配置。</li>
<li><code>plugins/index.ts</code> - remark 和 rehype 插件。</li>
<li><code>src/config.ts</code> - 文章页面配置。</li>
<li><code>src/components/posts</code> - 大部分组件来自这里。</li>
</ul>
<h3>文章页面配置</h3>
<p>文章页面配置如下：</p>
<pre><code>export const POSTS_CONFIG: PostConfig = {
  title: '文章',
  description: '夜猫子Ai手记的文章',
  introduce: '在这里，我将分享本主题的使用说明，帮助你快速上手。',
  author: '夜猫子Ai手记',
  homePageConfig: {
    size: 3,
    type: 'compact',
  },
  postPageConfig: {
    size: 10,
    type: 'minimal',
  },
  tagsPageConfig: {
    size: 10,
    type: 'time-line',
  },
  ogImageUseCover: false,
  postType: 'metaOnly',
  imageDarkenInDark: true,
  readMoreText: '阅读更多',
  prevPageText: '上一页',
  nextPageText: '下一页',
  tocText: '目录',
  backToPostsText: '返回文章列表',
  nextPostText: '下一篇',
  prevPostText: '上一篇',
  recommendText: '推荐',
}
</code></pre>
<p>以下是一些配置属性的详细说明，请参考下表。</p>





























































































<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>title</td><td>浏览器标签和列表页面上显示的标题。</td></tr><tr><td>description</td><td>列表页面 <code>head</code> 元素中的元数据描述。</td></tr><tr><td>introduce</td><td>列表页面标题下方的介绍。</td></tr><tr><td>author</td><td>文章的作者。</td></tr><tr><td>homePageConfig</td><td><strong>readme 页面</strong> 配置。</td></tr><tr><td>    size</td><td>在 <strong>readme 页面</strong> 上显示的文章数量。</td></tr><tr><td>    type</td><td>在 <strong>阅读页面</strong>，列表显示数据的样式，<code>compact</code>、<code>minimal</code>、<code>time-line</code> 或 <code>image</code>。</td></tr><tr><td>    coverLayout <br />    (可选)</td><td>当类型为 image 时，此属性可以设置图片在卡片中的位置。可以选择 left 或 right。如果未设置，将交替出现在左右两侧。</td></tr><tr><td>postPageConfig</td><td>与上述 homePageConfig 相同，但 size 表示基础页数，用于 <strong>文章页面</strong>。</td></tr><tr><td>tagsPageConfig</td><td>与上述 homePageConfig 相同，但 size 表示基础页数，用于 <strong>标签页面</strong>。</td></tr><tr><td>ogImageUseCover</td><td>是否使用封面图片作为 Open Graph 图片。</td></tr><tr><td>postType</td><td>文章具体内容显示页面顶部元数据的默认显示组件。你可以配置 <code>metaOnly</code>、<code>coverSplit</code>、<code>coverTop</code>。 <br /> 可以被内容的前置数据设置替换。</td></tr><tr><td>imageDarkenInDark</td><td>是否在深色模式下调暗图片。</td></tr><tr><td>readMoreText</td><td>阅读更多按钮的文本。</td></tr><tr><td>prevPageText</td><td>上一页按钮的文本。</td></tr><tr><td>nextPageText</td><td>下一页按钮的文本。</td></tr><tr><td>tocText</td><td>目录的标题文本</td></tr><tr><td>backToPostsText</td><td>返回文章列表按钮的文本。</td></tr><tr><td>nextPostText</td><td>下一篇文章按钮的文本。</td></tr><tr><td>prevPostText</td><td>上一篇文章按钮的文本。</td></tr><tr><td>recommendText</td><td>推荐标签的文本。</td></tr></tbody></table>
<h4>Type 和 PostType</h4>
<p>在上面的文档中，我们提到了可配置的 <code>types</code> 用于配置 readme 页面、文章页面和标签页面的列表数据显示样式，以及可配置的 <code>postTypes</code> 用于在文章内容页面顶部显示元数据。</p>
<p>以下是 <code>type</code> 的具体样式展示：</p>
<figure><img src="https://mps-blog.vercel.app/_astro/compact-black.DMCWHrjD_2evXXq.webp" alt="" class="img-light" /><figcaption>compact</figcaption></figure>
<figure><img src="https://mps-blog.vercel.app/_astro/timeLine-black.jDSZqIn4_Z2hp2Yv.webp" alt="" class="img-light" /><figcaption>time-line</figcaption></figure>
<figure><img src="https://mps-blog.vercel.app/_astro/minimal-black.Dxsrn9wb_1l0yun.webp" alt="" class="img-light" /><figcaption>minimal</figcaption></figure>
<figure><img src="https://mps-blog.vercel.app/_astro/image-black.BeMdDApH_ZV1j5H.webp" alt="" class="img-light" /><figcaption>image</figcaption></figure>
<p>以下是 <code>postType</code> 的具体样式展示：</p>
<figure><img src="https://mps-blog.vercel.app/_astro/metaOnly-black.CAi7aUXH_2gkuT1.webp" alt="" class="img-light" /><figcaption>metaOnly</figcaption></figure>
<figure><img src="https://mps-blog.vercel.app/_astro/coverTop-black.Cji_8Dx0_2wd6Gt.webp" alt="" class="img-light" /><figcaption>coverTop</figcaption></figure>
<figure><img src="https://mps-blog.vercel.app/_astro/coverSplit-black.BGSoc6qf_1NdheF.webp" alt="" class="img-light" /><figcaption>coverSplit</figcaption></figure>
<h3>前置数据</h3>
<p>讨论完文章列表和整体设计后，让我们一起看看文章的内容。这些内容都在 <code>src/content/posts</code> 文件夹中。</p>
<p>下面是文章内容的前置数据：</p>
<pre><code>---
title: 'Litos: Posts Page Config'
description: ''
pubDate: 2025-08-13
author: '夜猫子Ai手记'
recommend: true
tags: ['Litos', 'Documentation']
---
</code></pre>
<p>你可以通过以下代码配置文章内容的前置数据：</p>
<pre><code>const posts = defineCollection({
  loader: glob({
    pattern: '**/*.{md,mdx}',
    base: './src/content/posts',
  }),
  schema: ({ image }) =&gt;
    z
      .object({
        title: z.string(),
        description: z.string(),
        pubDate: z.date(),
        tags: z.array(z.string()).optional(),
        updatedDate: z.date().optional(),
        author: z.string().default(POSTS_CONFIG.author),
        cover: image().optional(),
        ogImage: image().optional(),
        recommend: z.boolean().default(false),
        postType: z.custom&lt;PostType&gt;().optional(),
        coverLayout: z.custom&lt;CoverLayout&gt;().optional(),
        pinned: z.boolean().default(false),
        draft: z.boolean().default(false),
      })
      .transform((data) =&gt; ({
        ...data,
        ogImage: data.ogImage ? data.ogImage : POSTS_CONFIG.ogImageUseCover &amp;&amp; data.cover ? data.cover : undefined,
      })),
})
</code></pre>





























































<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>title</td><td>文章标题。</td></tr><tr><td>description</td><td>文章内容的概述，也用于 SEO。</td></tr><tr><td>pubDate</td><td>文章发布日期。</td></tr><tr><td>tags</td><td>文章标签列表</td></tr><tr><td>updatedDate</td><td>文章最后更新日期。 <br /> 在文章列表排序中，优先级高于发布日期。</td></tr><tr><td>author</td><td>文章作者。</td></tr><tr><td>cover</td><td>当列表类型为 <code>image</code> 时，用于显示的封面图片，或 postType 为 <code>coverSplit</code> 或 <code>coverTop</code> 时在顶部显示的封面图片</td></tr><tr><td>ogImage</td><td>文章的 Open Graph 图片。</td></tr><tr><td>recommend</td><td>是否显示推荐标签。</td></tr><tr><td>postType</td><td>文章具体内容显示页面顶部元数据的显示组件。你可以配置 <code>metaOnly</code>、<code>coverSplit</code>、<code>coverTop</code></td></tr><tr><td>coverLayout</td><td>当类型为 <code>image</code> 时，此属性可以设置图片在卡片中的位置。可以选择 <code>left</code> 或 <code>right</code>。如果未设置，将交替出现在左右两侧。</td></tr><tr><td>pinned</td><td>是否置顶文章。</td></tr><tr><td>draft</td><td>是否隐藏文章。</td></tr></tbody></table>
<div><div><div></div><div>TIP</div></div><div><p>关于 <code>cover</code> 和 <code>ogImage</code>。</p><p><code>Cover</code> 和 <code>ogImage</code> 是两个独立的属性，连接它们的唯一方式是 <code>POSTS_CONFIG.ogImageUseCover</code>。</p><p><code>POSTS_CONFIG.ogImageUseCover</code> 默认启用，所以你只需要写 <code>cover</code> 就可以同时配置 <code>ogImage</code>。这适用于 <code>cover</code> 和 <code>ogImage</code> 相同的情况。如果你想自定义 <code>ogImage</code>，可以单独设置。</p><p>如果未启用 <code>POSTS_CONFIG.ogImageUseCover</code>，需要单独设置 <code>ogImage</code>。如果不设置，将使用站点的 <code>ogImage</code> 作为后备。</p><p><code>POSTS_CONFIG.ogImageUseCover</code> &gt; <code>cover</code></p></div></div>
<hr />
<h3>语法和代码样式</h3>
<p>本指南将通过一个 3 天城市旅行行程来展示如何使用 Markdown 格式化文本。在规划行程的同时学习 Markdown！</p>
<h4>标题级别</h4>
<p>对于旅行笔记，多级标题有助于组织天数、时间块和提示：</p>
<h4>文本格式化</h4>
<p>撰写旅行笔记时，突出重要信息：</p>
<p><strong>必看景点</strong> 应加粗
<em>灵活时间</em> 使用斜体
<strong><em>重要警告</em></strong> 可以同时使用
可选绕路 使用删除线</p>
<h4>行李清单（无序列表）</h4>
<ul>
<li>护照、签证</li>
<li>相机、备用电池
<ul>
<li>带一个快充充电器</li>
<li>备用 SD 卡</li>
</ul>
</li>
<li>可重复使用的水瓶</li>
<li>公共交通卡</li>
</ul>
<h4>第一天行程（有序列表）</h4>
<ol>
<li>机场 → 酒店入住</li>
<li>老城徒步之旅</li>
<li>夜间游船
<ol>
<li>提前 15 分钟到达</li>
<li>在 B 入口排队</li>
<li>推荐靠窗座位</li>
</ol>
</li>
</ol>
<pre><code>1. 机场 → 酒店入住
2. 老城徒步之旅
3. 夜间游船
   1. 提前 15 分钟到达
   2. 在 B 入口排队
   3. 推荐靠窗座位
</code></pre>
<h4>Blockquotes</h4>
<blockquote><p>旅行者提示：如果你一天要乘坐 3 次以上地铁，建议购买 24 小时通票。</p><p>将酒店地址保存到离线地图中以便快速查找。</p></blockquote>
<pre><code>&gt; 旅行者提示：如果你一天要乘坐 3 次以上地铁，建议购买 24 小时通票。
&gt;
&gt; 将酒店地址保存到离线地图中以便快速查找。
</code></pre>
<h4>代码块</h4>
<p>使用简单的代码来估算预算：</p>
<pre><code>type Budget = { flight: number; hotel: number; meals: number; transport: number }
export const total = (b: Budget) =&gt; b.flight + b.hotel + b.meals + b.transport

console.log(total({ flight: 1200, hotel: 450, meals: 180, transport: 60 })) // 1890
</code></pre>
<h4>表格</h4>
<p>示例行程：</p>

























<table><thead><tr><th>时间</th><th>地点</th><th>备注</th></tr></thead><tbody><tr><td>09&lt;00&gt;</td><td>老城广场</td><td>导览徒步之旅</td></tr><tr><td>12&lt;30&gt;</td><td>河滨咖啡馆</td><td>午餐 + 短暂休息</td></tr><tr><td>18&lt;00&gt;</td><td>城市码头</td><td>日落游船</td></tr></tbody></table>
<h4>链接和图片</h4>
<p>更多提示：<a href="https://example.com/travel" rel="noopener noreferrer" target="_blank">官方旅游委员会</a></p>
<p>旅行照片：
<img src="https://mps-blog.vercel.app/_astro/home.DfiDpdST_gTJAi.webp" alt="城市天际线" style="width:50%" /></p>
<h4>水平分隔线</h4>
<hr />
<h4>行内代码</h4>
<p>地铁 A 线在高峰期每 <code>5-7</code> 分钟一班。</p>
<h4>数学公式</h4>
<p>每日预算估算：<span><span>budget=hotel+meals+transportbudget = hotel + meals + transport</span><span><span><span></span><span>b</span><span>u</span><span>d</span><span>g</span><span>e</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span>o</span><span>t</span><span>e</span><span>l</span><span></span><span>+</span><span></span></span><span><span></span><span>m</span><span>e</span><span>a</span><span>l</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>t</span><span>r</span><span>an</span><span>s</span><span>p</span><span>or</span><span>t</span></span></span></span></p>
<p>总行程：</p>
<span><span><span>Total Budget=∑d=13(Hoteld+Mealsd+Transportd)Total\ Budget = \sum_{d=1}^{3} (Hotel_d + Meals_d + Transport_d)</span><span><span><span></span><span>T</span><span>o</span><span>t</span><span>a</span><span>l</span><span> </span><span>B</span><span>u</span><span>d</span><span>g</span><span>e</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span>H</span><span>o</span><span>t</span><span>e</span><span><span>l</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span>e</span><span>a</span><span>l</span><span><span>s</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>T</span><span>r</span><span>an</span><span>s</span><span>p</span><span>or</span><span><span>t</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span>
<h4>任务列表</h4>
<p>出行前检查清单：</p>
<ul>
<li> 预订航班</li>
<li> 预订酒店</li>
<li> 购买地铁通票</li>
<li> 下载离线地图</li>
</ul>
<h4>脚注</h4>
<p>本行程参考了当地旅游指南（点击脚注）<sup><a href="#user-content-fn-1">1</a></sup>。</p>
<hr />
<h3>Expressive-code 配置</h3>
<p>在 Markdown 文档中，我们使用代码块来显示代码片段和其他内容。本文档介绍如何自定义代码块配置。</p>
<p>本主题的代码块使用 <a href="https://expressive-code.com/" rel="noopener noreferrer" target="_blank">Expressive Code</a> 进行配置，所有配置选项都在 <code>ec.config.mjs</code> 文件中定义。以下是主要配置选项：</p>
<pre><code>import { defineEcConfig } from 'astro-expressive-code'
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'
import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'

export default defineEcConfig({
  defaultLocale: 'zh-CN',
  defaultProps: {
    wrap: false,
    collapseStyle: 'collapsible-auto',
    showLineNumbers: false,
    preserveIndent: true,
  },
  minSyntaxHighlightingColorContrast: 0,

  styleOverrides: {
    uiFontFamily: 'GeistMono, Input Mono, Fira Code, ShangguSansSCVF, monospace',
    uiFontSize: '1em',
    codeFontFamily: 'GeistMono, Input Mono, Fira Code, ShangguSansSCVF, monospace',
    codeFontSize: '14px',
    codeLineHeight: '1.4',
    borderRadius: '0',
    codePaddingBlock: '0.8571429em',
    codePaddingInline: '1.1428571em',
    borderColor: ({ theme }) =&gt; (theme.type === 'dark' ? '#24273a' : '#e6e9ef'),

    frames: {
      frameBoxShadowCssValue: false,
      inlineButtonBackgroundActiveOpacity: '0.2',
      inlineButtonBackgroundHoverOrFocusOpacity: '0.1',
    },
    textMarkers: {
      backgroundOpacity: '0.2',
      borderOpacity: '0.4',
    },
  },

  plugins: [
    pluginCollapsibleSections({
      defaultCollapsed: false,
    }),
    pluginLineNumbers(),
  ],

  themes: ['catppuccin-macchiato', 'catppuccin-latte'],
  themeCssSelector: (theme) =&gt; (theme.name === 'catppuccin-macchiato' ? '.dark' : ':root:not(.dark)'),
  useDarkModeMediaQuery: false,
  useStyleReset: false,
})
</code></pre>
<p>你可以访问该网站查看 expressive-code 的配置选项。</p>
<hr />
<h3>评论系统 (Gitalk)</h3>
<p>该主题内置了 <a href="https://github.com/gitalk/gitalk" rel="noopener noreferrer" target="_blank">Gitalk</a> 评论系统，使用 GitHub Issues 作为评论后端。每篇文章会自动获得自己的 Issues 用于评论。</p>
<h4>前提条件</h4>
<p>首先你需要创建一个 <strong>GitHub OAuth App</strong>：</p>
<ol>
<li>前往 <a href="https://github.com/settings/developers" rel="noopener noreferrer" target="_blank">GitHub 开发者设置</a> → <strong>OAuth Apps</strong> → <strong>New OAuth App</strong>。</li>
<li>填写表单：
<ul>
<li><strong>Application name</strong>：任意名称（例如 <code>My Blog Comments</code>）</li>
<li><strong>Homepage URL</strong>：你的站点 URL（例如 <code>https://yourdomain.com</code>）</li>
<li><strong>Authorization callback URL</strong>：与你的站点 URL 相同</li>
</ul>
</li>
<li>创建后，复制 <strong>Client ID</strong> 并生成 <strong>Client Secret</strong>。</li>
<li>在 GitHub 上创建一个 <strong>公开仓库</strong> 用于存储评论 Issues（例如 <code>blog-comments</code>）。</li>
</ol>
<h4>配置</h4>
<p>在 <code>src/config.ts</code> 中配置评论系统：</p>
<pre><code>export const COMMENT_CONFIG: CommentConfig = {
  enabled: true,
  system: 'gitalk',
  gitalk: {
    clientID: import.meta.env.PUBLIC_GITHUB_CLIENT_ID,
    clientSecret: import.meta.env.PUBLIC_GITHUB_CLIENT_SECRET,
    repo: 'blog-comments',
    owner: 'YourGitHubUsername',
    admin: ['YourGitHubUsername'],
    language: 'en-US',
    perPage: 5,
    pagerDirection: 'last',
    createIssueManually: false,
    distractionFreeMode: false,
    enableHotKey: true,
  },
}
</code></pre>
<p>然后设置环境变量。在项目根目录创建 <code>.env</code> 文件（参考 <code>.env.example</code>）：</p>
<pre><code>PUBLIC_GITHUB_CLIENT_ID=your-github-client-id
PUBLIC_GITHUB_CLIENT_SECRET=your-github-client-secret
</code></pre>
<h4>配置属性</h4>





























































<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>enabled</td><td>是否启用评论系统。</td></tr><tr><td>system</td><td>使用的评论系统。选项：<code>gitalk</code>、<code>none</code>。</td></tr><tr><td>clientID</td><td>GitHub OAuth App Client ID。</td></tr><tr><td>clientSecret</td><td>GitHub OAuth App Client Secret。</td></tr><tr><td>repo</td><td>用于存储评论 Issues 的 GitHub 仓库名称。</td></tr><tr><td>owner</td><td>仓库所有者的 GitHub 用户名。</td></tr><tr><td>admin</td><td>可以初始化评论 Issues 的 GitHub 用户名数组。</td></tr><tr><td>language</td><td>显示语言。例如 <code>en-US</code>、<code>zh-CN</code>、<code>zh-TW</code>。</td></tr><tr><td>perPage</td><td>每页评论数。</td></tr><tr><td>pagerDirection</td><td>评论排序方向。<code>last</code>（最新优先）或 <code>first</code>（最旧优先）。</td></tr><tr><td>createIssueManually</td><td>如果为 <code>true</code>，Issues 必须手动创建。如果为 <code>false</code>，首次访问时由管理员自动创建。</td></tr><tr><td>distractionFreeMode</td><td>如果为 <code>true</code>，输入评论时启用全屏覆盖模式。</td></tr><tr><td>enableHotKey</td><td>是否启用 <code>Cmd/Ctrl + Enter</code> 快捷键提交评论。</td></tr></tbody></table>
<div><div><div></div><div>NOTE</div></div><div><p>要完全禁用评论，请在 <code>COMMENT_CONFIG</code> 中设置 <code>enabled: false</code> 或 <code>system: 'none'</code>。</p></div></div>
<div><div><div></div><div>CAUTION</div></div><div><p><code>clientID</code> 和 <code>clientSecret</code> 通过 <code>import.meta.env</code> 从环境变量读取。请确保将 <code>.env</code> 添加到你的 <code>.gitignore</code> 文件（默认已包含）以避免泄露密钥。</p></div></div>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>城市旅游指南，2024 版。（点击返回文本） <a href="#user-content-fnref-1">↩</a></p>
</li>
</ol>
</section>]]></content>
    <category term="Litos" />
    <category term="文档" />
  </entry>
  <entry>
    <title>Markdown 增强语法</title>
    <link href="https://mps-blog.vercel.app//posts/enhance" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/enhance</id>
    <updated>2025-08-12T00:00:00.000Z</updated>
    <published>2025-08-12T00:00:00.000Z</published>
    <author>
      <name>Dnzzk2</name>
    </author>
    <summary type="text">通过标注框、Expressive Code 代码块、图片说明指令、视频嵌入、样式链接、徽章和详情折叠，增强 Markdown 的表现力。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.BKthU1GI_ykCju.webp" alt="Markdown 增强语法" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>本指南在 <a href="https://astro-antfustyle-theme.vercel.app/blog/markdown-mdx-extended-features/" rel="noopener noreferrer" target="_blank">markdown-mdx-extended-features</a> 的基础上做了少量修改。</p>
<h2>标注框（Callouts）</h2>
<p>由 <a href="https://github.com/lin-stephanie/rehype-callouts" rel="noopener noreferrer" target="_blank">rehype-callouts</a> 插件支持，可在 <code>plugins/index.ts</code> 中配置该插件。</p>
<p>若修改了 <code>theme</code> 配置（默认值为 <code>'vitepress'</code>），还需同步更新 <code>src/styles/pro.css</code> 中引入的 CSS 文件（<code>@import 'rehype-callouts/theme/你的配置'</code>）。</p>
<pre><code>&lt;!-- 标注框类型名称不区分大小写：'Note'、'NOTE' 和 'note' 等价。 --&gt;

&lt;!-- vitepress --&gt;

&lt;!-- 这是一个不可折叠的标注框 --&gt;

&gt; [!note]
&gt; 注意内容。

&gt; [!tip]
&gt; 提示内容。

&gt; [!important]
&gt; 重要内容。

&gt; [!warning]
&gt; 警告内容。

&gt; [!caution]
&gt; 注意内容。

&gt; [!caution]- 这是一个**可折叠**的标注框
&gt; 注意内容。

&gt; [!note]+ 这是一个**可折叠**的标注框
&gt; 注意内容。
&gt; ` ` `

&gt; [!note]
&gt; 注意 `内容`。

&gt; [!tip]
&gt; 提示 `内容`。

&gt; [!important]
&gt; 重要 `内容`。

&gt; [!warning]
&gt; 警告 `内容`。

&gt; [!caution]
&gt; 注意 `内容`。

&gt; [!caution]- 这是一个**可折叠**的标注框
&gt; 注意内容。

&gt; [!note]+ 这是一个**可折叠**的标注框
&gt; 注意内容。

## 全功能代码块

由 <a href="https://github.com/expressive-code/expressive-code/tree/main/packages/astro-expressive-code">astro-expressive-code</a> 支持，并集成 [@expressive-code/plugin-collapsible-sections](https://expressive-code.com/plugins/collapsible-sections/) 和 [@expressive-code/plugin-line-numbers](https://expressive-code.com/plugins/line-numbers/) 插件，为代码块添加样式和额外功能。

如需自定义代码块主题或功能，请在查阅 <a href="https://expressive-code.com/reference/configuration/">Expressive Code 配置文档</a> 后，修改项目根目录下的 `ec.config.mjs` 文件，例如：[更换主题](https://expressive-code.com/guides/themes/#using-bundled-themes)、[启用自动换行](https://expressive-code.com/key-features/word-wrap/#wrap) 或 [切换行号显示](https://expressive-code.com/plugins/line-numbers/#showlinenumbers)。

以下是功能快速预览，详细说明请查阅 [详细指南](https://expressive-code.com/key-features/syntax-highlighting/)。

#### 语法高亮

` ` `js title='example.md'
console.log('This code is syntax highlighted!')
` ` `

##### 代码编辑器框架

` ` `js title="my-test-file.js"
// 使用 `title="my-test-file.js"`console.log('Title attribute example')` ` `

` ` `ts
// src/content/index.ts
// 使用 `// src/content/index.ts`console.log('File name comment example')` ` `

##### 终端框架

` ` `bash
echo "此终端框架没有标题"
` ` `

` ` `powershell title="PowerShell terminal example"
Write-Output "这个有标题！"
` ` `

##### 标记整行与行范围

` ` `js {1, 4, 7-8}
// 第 1 行 - 按行号指定
// 第 2 行
// 第 3 行
// 第 4 行 - 按行号指定
// 第 5 行
// 第 6 行
// 第 7 行 - 按范围 "7-8" 指定
// 第 8 行 - 按范围 "7-8" 指定
` ` `

##### 选择行标记类型（mark、ins、del）

` ` `js title="line-markers.js" del={2} ins={3-4} {6}
function demo() {
console.log('此行标记为已删除')
// 此行及下一行标记为已插入
console.log('这是第二行已插入的内容')

return '此行使用默认中性标记类型'
}
` ` `

##### 使用类 diff 语法

` ` `diff
+此行将标记为已插入
-此行将标记为已删除
这是普通行
` ` `

` ` `diff lang="js"
function thisIsJavaScript() {
// 整个代码块以 JavaScript 高亮显示，
// 同时仍可添加 diff 标记！

- console.log('待删除的旧代码')

* console.log('全新的代码！')
  }
  ` ` `

##### 按代码块配置自动换行

` ` `js wrap
// 启用换行的示例
function getLongString() {
  return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
` ` `

` ` `js wrap=false
// 禁用换行的示例
function getLongString() {
  return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
` ` `

##### 可折叠区域

` ` `js collapse={1-5, 12-14, 21-24}
// 所有样板初始化代码将被折叠
import { someBoilerplateEngine } from '@example/some-boilerplate'
import { evenMoreBoilerplate } from '@example/even-more-boilerplate'

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// 此部分代码默认可见
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
const a = 1
const b = 2
const c = a + b
console.log(`Calculation result:  +  = `)
return c
}

engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: 'End of example boilerplate code' })
` ` `

##### 按代码块显示行号

` ` `js showLineNumbers
// 此代码块将显示行号
console.log('来自第 2 行的问候！')
console.log('我在第 3 行')
` ` `

` ` `js showLineNumbers startLineNumber=5
// 更改起始行号
console.log('来自第 5 行的问候！')
console.log('我在第 6 行')
` ` `

## 图片说明与链接

使用 :link[remark-directive-sugar]{#lin-stephanie/remark-directive-sugar .github} 中的 [`:::image`](https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#image-) 指令，将图片包裹在容器中以支持说明文字、可点击链接等功能。可通过 `plugins/index.ts` 中的 `image` 选项进行自定义，样式写在 `src/styles/pro.css` 的 `/* :::image */` 部分。

### image-figure

` ` `md title=':::image-figure.md'
:::image-figure[这是带有 **Figcaption** 和 `&lt;figure&gt;` 属性的示例]{style="text-align:center;color:orange"}
![](assets/cover.png)
:::

:::image-figure[这是带有 **figcaption** 和 `&lt;img&gt;` 属性的示例。]
![](assets/cover.png)(style: width:600px;)
:::

&lt;!-- 💡 使用 `(class:no-zoom)` 禁用缩放 --&gt;

:::image-figure[这是带有 `class:no-zoom` 的示例。]
![](assets/cover.png)(class:no-zoom)
:::

&lt;!-- 💡 若没有 `[caption]`，将使用 `[alt]` 作为 figcaption。 --&gt;

:::image-figure
![若未设置 `[caption]`，alt 文本将作为 figcaption 使用。](assets/cover.png)
:::

&lt;!-- ❌ 若 figcaption 没有可用文本，则无法生效。 --&gt;

` ` `

&gt; [!warning]
&gt; 直接设置图片的 `width` 属性可能导致模糊。[了解更多](https://github.com/Dnzzk2/Litos/discussions/17)

### image-a

` ` `md title=':::image-a.md'
:::image-a{href="https://github.com/Dnzzk2/Litos"}
![OG image](assets/cover.png)
:::

&lt;!-- ❌ 未提供外部链接时无法生效。--&gt;

:::image-a
![OG image](assets/cover.png)
:::
` ` `

### image-figure-polaroid

` ` `md title=':::image-figure-polaroid.md'
:::::image-div-polaroid
:::image-figure-polaroid[这是带有 **figcaption** 和 `&lt;img&gt;` 属性的示例。]
![OG image](assets/cover.png)
:::
:::::

:::::image-div-polaroid
:::image-figure-polaroid{style="width:500px;"}
![OG image](assets/cover.png)
:::
:::::
` ` `

## GitHub 卡片

感谢 :link[oopsunix]{#@oopsunix} 的贡献！

` ` `md title=':::github-card.md'
::github{repo="Dnzzk2/Litos"}
` ` `

## 视频嵌入

使用 <a href="lin-stephanie/remark-directive-sugar class=github">remark-directive-sugar</a> 中的 [`::video`](https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#video-) 指令，统一嵌入不同平台的视频。

` ` `md title='example.md'

&lt;!-- 嵌入 YouTube 视频 --&gt;

::video-youtube{#gxBkghlglTg}

&lt;!-- 嵌入 Bilibili 视频，并自定义 `title` 属性 --&gt;

::video-bilibili[自定义标题]{id=BV1MC4y1c7Kv}

&lt;!-- 嵌入 Vimeo 视频，使用 `no-scale` 类禁用缩放 --&gt;

::video-vimeo{id=912831806 class='no-scale'}

&lt;!-- 嵌入自定义视频 URL（必须使用 `id`，不能用 `#`） --&gt;

::video{id=https://www.youtube-nocookie.com/embed/gxBkghlglTg}
` ` `

## 样式链接（`:link`）

使用 <a href="lin-stephanie/remark-directive-sugar class=github">remark-directive-sugar</a> 中的 [`:link`](https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#link) 指令，为 GitHub、npm 或自定义 URL 添加带头像或图标的链接。

**链接到 GitHub 用户或组织（在 `id` 前加 `@`）​**

- **示例 1**：`:link[Dnzzk2]{#@Dnzzk2}` 链接到项目维护者的 GitHub 主页，如 :link[Dnzzk2]{#@Dnzzk2}。
- **示例 2**：`<a href="@vitejs">Vite</a>` 链接到 <a href="@vitejs">Vite</a> 组织的 GitHub 主页。
- **示例 3**：`:link{#@Dnzzk2 tab=repositories}` 直接链接到该用户的仓库标签页，如 :link{#@Dnzzk2 tab=repositories}。有效 `tab` 选项：`'repositories'`、`'projects'`、`'packages'`、`'stars'`、`'sponsoring'`、`'sponsors'`。
- **示例 4**：`:link{#@vitejs tab=org-people}` 链接到组织成员页面，如 :link{#@vitejs tab=org-people}。有效 `tab` 选项：`'org-repositories'`、`'org-projects'`、`'org-packages'`、`'org-sponsoring'`、`'org-people'`。

**链接到 GitHub 仓库**

- **示例 5**：`:link[Astro]{#withastro/astro}` 创建指向 :link[Astro]{#withastro/astro} 仓库的链接。

**链接到 npm 包**

- **示例 6**：`:link{#remark-directive-sugar}` 链接到 :link{#remark-directive-sugar} 的 npm 主页。
- **示例 7**：`:link{id=remark-directive-sugar tab=dependencies}` 链接到 npm 上的 :link{id=remark-directive-sugar tab=dependencies} 页面。有效 `tab` 选项：`'readme'`、`'code'`、`'dependencies'`、`'dependents'`、`'versions'`。

**链接到自定义 URL（必须使用 `id`，不能用 `#`）​**

- **示例 8**：`:link{id=https://developer.mozilla.org/en-US/docs/Web/JavaScript}` 创建指向 :link{id=https://developer.mozilla.org/en-US/docs/Web/JavaScript} 的外部链接。
- **示例 9**：`<a href="https://www.google.com/">Google</a>` 创建指向 <a href="https://www.google.com/">Google</a> 的外部链接。

**自定义**

- **示例 10**：`<a href="@vitejs url=https://vite.dev/">Vite</a>` 将链接指向 `https://vite.dev/`，如 <a href="@vitejs url=https://vite.dev/">Vite</a>。
- **示例 11**：`<a href="@vitejs img=https://vitejs.dev/logo.svg">Vite</a>` 显示自定义 Logo，如 <a href="@vitejs img=https://vitejs.dev/logo.svg">Vite</a>。
- **示例 12**：`:link{id=Dnzzk2/Litos class=github}` 使用 `class=github` 覆盖默认样式，如 :link{id=Dnzzk2/Litos class=github}。
- **示例 13**：`<a href="https://github.com/Dnzzk2/Litos img=...">Litos Themes</a>` 完全自定义一个链接，如 <a href="https://github.com/Dnzzk2/Litos img=https://litos.vercel.app/favicon.ico">Litos Themes</a>。

## 徽章（Badges）

使用 <a href="lin-stephanie/remark-directive-sugar class=github">remark-directive-sugar</a> 中的 [`:badge`](https://github.com/lin-stephanie/remark-directive-sugar?tab=readme-ov-file#badge-) 指令，显示状态或分类等小型信息片段。

主题预定义徽章如下，可通过 `plugins/index.ts` 中的 `badge` 选项自定义，样式写在 `src/styles/pro.css` 的 `/* :badge */` 部分：

- `badge-n`：:badge-n

也可直接使用 `:badge[文字]{属性}` 自定义外观。例如：`:badge[ISSUE]{style="background-color: #bef264"}` 将显示为 :badge[ISSUE]{style="background-color: #bef264"}。若未指定颜色，默认外观如 :badge[This] 所示。

## 详情折叠（Details Dropdown）

` ` `md title=':::details.md'
:::details
::summary[详情折叠]

- 列表项 1
- 列表项 2
- 列表项 3
- 列表项 4
  :::
  ` ` `

此外，它也支持类似 [remark-directive 示例](https://github.com/remarkjs/remark-directive?tab=readme-ov-file#use) 中的用法。
</code></pre>]]></content>
    <category term="Litos" />
    <category term="文档" />
  </entry>
  <entry>
    <title>项目和标签页配置</title>
    <link href="https://mps-blog.vercel.app//posts/project-tag" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/project-tag</id>
    <updated>2025-08-11T00:00:00.000Z</updated>
    <published>2025-08-11T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">配置项目和标签页面：页面文本（PROJECTS_CONFIG、TAGS_CONFIG）、项目内容 frontmatter、文件位置和使用示例。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.Cz2psVoH_Z1OReC1.webp" alt="项目和标签页配置" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>由于项目和标签页配置内容较少，因此合并为一个文档。</p>
<p>以下文件与此文档兼容：</p>
<ul>
<li><code>src/pages/projects/index.astro</code> - 项目页面。</li>
<li><code>src/pages/tags/index.astro</code> - 标签统计页面。</li>
<li><code>src/pages/tags/[tag]/[...page].astro</code> - 特定标签的文章列表页面。</li>
<li><code>src/config.ts</code> - 项目和标签页配置。</li>
<li><code>src/components/base</code> - 大部分组件来自这里。</li>
</ul>
<h3>项目页面配置</h3>
<p>配置页面文本：</p>
<pre><code>export const PROJECTS_CONFIG: ProjectConfig = {
  title: 'Projects',
  description: 'The examples of my projects.',
  introduce: 'The examples of my projects.',
}
</code></pre>





















<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>title</td><td>浏览器标签和页面上显示的标题。</td></tr><tr><td>description</td><td>页面 <code>head</code> 元素中的元数据描述。</td></tr><tr><td>introduce</td><td>页面标题下方的介绍。</td></tr></tbody></table>
<h3>项目内容</h3>
<p>项目页面显示的内容来自 <code>/src/content/projects</code>。</p>
<p>它的写作风格与文章类似：一个 MDX 文件代表一个项目。</p>
<h4>项目前置数据</h4>
<p>示例 (<code>src/content/projects/Litos/index.mdx</code>):</p>
<pre><code>---
name: 'Litos'
description: 'A Simple &amp; Modern Blog Theme for Astro.'
githubUrl: 'https://github.com/Dnzzk2/Litos'
website: 'https://litos.vercel.app/'
type: 'image'
icon: '../../../../public/projects/litos.png'
imageClass: 'w-10 h-10'
star: 32
fork: 7
---
</code></pre>













































<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>name</td><td>项目名称。</td></tr><tr><td>description</td><td>项目卡片上显示的简短描述。</td></tr><tr><td>githubUrl</td><td>项目的 GitHub 仓库 URL。</td></tr><tr><td>website</td><td>项目网站或演示 URL。</td></tr><tr><td>type</td><td>项目卡片的显示类型。目前，<code>'image'</code> 显示缩略图。</td></tr><tr><td>icon</td><td>项目的图标路径（支持 <code>public/</code> 下的路径）。</td></tr><tr><td>imageClass</td><td>用于调整图片大小的额外类（例如 Tailwind 类）。</td></tr><tr><td>star</td><td>星标数量（可选）。</td></tr><tr><td>fork</td><td>分支数量（可选）。</td></tr></tbody></table>
<h3>标签页面配置</h3>
<pre><code>export const TAGS_CONFIG: TagsConfig = {
  title: 'Tags',
  description: 'All tags of Posts',
  introduce: 'All the tags for posts are here, you can click to filter them.',
}
</code></pre>





















<table><thead><tr><th>属性</th><th>描述</th></tr></thead><tbody><tr><td>title</td><td>浏览器标签和标签统计页面上显示的标题。</td></tr><tr><td>description</td><td>标签统计页面 <code>head</code> 元素中的元数据描述。</td></tr><tr><td>introduce</td><td>标签统计页面标题下方的介绍。</td></tr></tbody></table>]]></content>
    <category term="Litos" />
    <category term="文档" />
  </entry>
  <entry>
    <title>照片页面配置</title>
    <link href="https://mps-blog.vercel.app//posts/photos" rel="alternate" type="text/html"/>
    <id>https://mps-blog.vercel.app//posts/photos</id>
    <updated>2025-08-10T00:00:00.000Z</updated>
    <published>2025-08-10T00:00:00.000Z</published>
    <author>
      <name>夜猫子Ai手记</name>
    </author>
    <summary type="text">照片页面的当前实现说明，包括 PHOTOS_CONFIG、PhotosList 和 getPhotos()。</summary>
    <content type="html"><![CDATA[<img src="https://mps-blog.vercel.app/_astro/cover.Do3E5r53_Z1UoMh2.webp" alt="照片页面配置" style="width: 100%; height: auto; margin-bottom: 1em;" />
<p>本文档描述了照片页面的当前实现。</p>
<h2>当前文件结构</h2>
<ul>
<li><code>src/pages/photos/index.astro</code>
<ul>
<li>从 <code>PHOTOS_CONFIG</code> 读取页面文本</li>
<li>从 <code>PhotosList</code> 读取时间线数据</li>
</ul>
</li>
<li><code>src/config.ts</code>
<ul>
<li>存储 <code>PHOTOS_CONFIG</code></li>
</ul>
</li>
<li><code>src/lib/photos.ts</code>
<ul>
<li>存储 <code>PhotosList</code></li>
<li>自动导入照片文件</li>
<li>将一个文件夹的图片转换为 <code>Photo[]</code>，使用 <code>getPhotos()</code></li>
</ul>
</li>
<li><code>src/types.ts</code>
<ul>
<li>定义 <code>PhotoData</code>、<code>Photo</code> 和 <code>PolaroidVariant</code></li>
</ul>
</li>
<li><code>src/components/photos/PolaroidCard.tsx</code>
<ul>
<li>将每个 <code>variant</code> 映射到实际的卡片尺寸</li>
</ul>
</li>
</ul>
<h2>页面文本</h2>
<p>页面标题和介绍文本来自 <code>src/config.ts</code>。</p>
<pre><code>export const PHOTOS_CONFIG: PhotosConfig = {
  title: 'Photos',
  description: 'Here I will record some photos taken in daily life.',
  introduce: 'Here I will record some photos taken in daily life.',
}
</code></pre>
<p><code>src/pages/photos/index.astro</code> renders the page like this:</p>
<pre><code>---
import { PHOTOS_CONFIG } from '~/config'
import { PhotosList } from '~/lib/photos'

const { title, description, introduce } = PHOTOS_CONFIG
---

&lt;Layout {title} {description}&gt;
  &lt;PageTitle {title} {introduce} /&gt;
  &lt;PhotoTimeline photoData={PhotosList} /&gt;
&lt;/Layout&gt;
</code></pre>
<h2>PhotosList</h2>
<p><code>PhotosList</code> 在 <code>src/lib/photos.ts</code> 中定义。每一项是一个时间线条目。</p>
<pre><code>export const PhotosList: PhotoData[] = [
  {
    title: 'Ningbo - Botanical Garden',
    icon: { type: 'emoji', value: '🌼' },
    description: 'It was early spring, so I went to see the cherry blossoms.',
    date: '2026-03-07',
    travel: '',
    photos: getPhotos('2026-03-07-botanicalGarden', 'Early spring cherry blossoms at the botanical garden', [
      '3x4',
      '3x4',
      '3x4',
      '3x4',
      '3x4',
      '3x4',
    ]),
  },
]
</code></pre>
<h3>PhotoData 字段</h3>

































<table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>title</code></td><td>时间线标题</td></tr><tr><td><code>icon</code></td><td>左侧时间线图标</td></tr><tr><td><code>description</code></td><td>可选的时间线描述</td></tr><tr><td><code>date</code></td><td>时间线日期</td></tr><tr><td><code>travel</code></td><td>可选的额外标签</td></tr><tr><td><code>photos</code></td><td><code>Photo</code> 对象数组</td></tr></tbody></table>
<h2><code>getPhotos()</code> 的工作原理</h2>
<p>当前实现：</p>
<pre><code>function getPhotos(dir: string, alt: string, variants: PolaroidVariant[]): Photo[] {
  return Object.entries(photoModules)
    .filter(([path]) =&gt; path.includes(`/${dir}/`))
    .sort(([a], [b]) =&gt; a.localeCompare(b))
    .map(([, mod], index) =&gt; {
      const img = mod.default
      return {
        src: img,
        alt,
        width: img.width,
        height: img.height,
        variant: variants[index] || '4x3',
      }
    })
}
</code></pre>
<h3>参数含义</h3>





















<table><thead><tr><th>参数</th><th>含义</th></tr></thead><tbody><tr><td><code>dir</code></td><td><code>src/assets/photos</code> 下的文件夹名称</td></tr><tr><td><code>alt</code></td><td>从该文件夹返回的每张图片的共享 <code>alt</code> 文本</td></tr><tr><td><code>variants</code></td><td>与排序后的图片按索引匹配的比率列表</td></tr></tbody></table>
<h3>重要行为</h3>
<ol>
<li><code>getPhotos()</code> 首先按文件夹名称过滤所有导入的图片。</li>
<li>然后使用 <code>localeCompare</code> 对匹配的文件路径进行排序。</li>
<li>按排序顺序创建最终的 <code>Photo[]</code>。</li>
<li><code>variants[index]</code> 应用于相同索引的图片。</li>
<li>如果 <code>variants</code> 中缺少某个索引，该图片将回退到 <code>'4x3'</code>。</li>
</ol>
<h3>第三个参数如何匹配</h3>
<p>第三个参数不是随机元数据。它是基于位置的。</p>
<p>对于以下代码：</p>
<pre><code>photos: getPhotos('2026-03-07-botanicalGarden', 'Early spring cherry blossoms at the botanical garden', [
  '3x4',
  '3x4',
  '3x4',
  '3x4',
  '3x4',
  '3x4',
])
</code></pre>
<p>假设文件夹 <code>src/assets/photos/2026-03-07-botanicalGarden/</code> 排序后包含以下文件：</p>
<pre><code>01.webp
02.webp
03.webp
04.webp
05.webp
06.webp
</code></pre>
<p>那么映射关系是：</p>

































<table><thead><tr><th>文件</th><th>应用的比例</th></tr></thead><tbody><tr><td><code>01.webp</code></td><td>数组第一个元素 -&gt; <code>3x4</code></td></tr><tr><td><code>02.webp</code></td><td>数组第二个元素 -&gt; <code>3x4</code></td></tr><tr><td><code>03.webp</code></td><td>数组第三个元素 -&gt; <code>3x4</code></td></tr><tr><td><code>04.webp</code></td><td>数组第四个元素 -&gt; <code>3x4</code></td></tr><tr><td><code>05.webp</code></td><td>数组第五个元素 -&gt; <code>3x4</code></td></tr><tr><td><code>06.webp</code></td><td>数组第六个元素 -&gt; <code>3x4</code></td></tr></tbody></table>
<p>因此，如果你想让文件夹中的第一张照片使用 <code>3x4</code>，第三个数组的第一个元素必须是 <code>3x4</code>。</p>
<p>如果你想混合比例，请按照排序文件的相同顺序编写：</p>
<pre><code>photos: getPhotos('2025-03-01-dongqianhu', 'Ningbo - Dongqian Lake', ['4x5', '1x1', '4x3'])
</code></pre>
<p>这意味着：</p>
<ul>
<li>第一张排序后的图片 -&gt; <code>4x5</code></li>
<li>第二张排序后的图片 -&gt; <code>1x1</code></li>
<li>第三张排序后的图片 -&gt; <code>4x3</code></li>
</ul>
<p>如果文件夹中的照片多于数组长度：</p>
<pre><code>photos: getPhotos('example-folder', 'Example alt', ['3x4', '4x5'])
</code></pre>
<p>那么：</p>
<ul>
<li>第一张排序后的图片 -&gt; <code>3x4</code></li>
<li>第二张排序后的图片 -&gt; <code>4x5</code></li>
<li>第三张及之后的图片 -&gt; 默认 <code>4x3</code></li>
</ul>
<h2>支持的比例</h2>
<p>当前 <code>PolaroidVariant</code> 为：</p>
<pre><code>export type PolaroidVariant = '1x1' | '4x5' | '4x3' | '3x4' | '9x16'
</code></pre>
<p><code>src/components/photos/PolaroidCard.tsx</code> 中的当前尺寸映射：</p>
<pre><code>const polaroidVariants: Record&lt;PolaroidVariant, string&gt; = {
  '1x1': 'w-20 h-20',
  '4x5': 'w-20 h-24',
  '4x3': 'w-20 h-16',
  '3x4': 'w-[4.5rem] h-24',
  '9x16': 'w-20 h-32',
}
</code></pre>
<h2>如何添加新的时间线条目</h2>
<ol>
<li>在 <code>src/assets/photos/</code> 下创建一个文件夹，例如 <code>2026-04-01-spring-walk</code>。</li>
<li>将图片文件放入该文件夹。</li>
<li>确保文件名按照排序后的顺序排列。</li>
<li>在 <code>src/lib/photos.ts</code> 中向 <code>PhotosList</code> 添加一项。</li>
<li>将第三个参数按照排序文件的相同顺序传递给 <code>getPhotos()</code>。</li>
</ol>
<p>示例：</p>
<pre><code>{
  title: 'Spring Walk',
  icon: { type: 'emoji', value: '🌿' },
  description: 'A short walk with a camera.',
  date: '2026-04-01',
  travel: '',
  photos: getPhotos(
    '2026-04-01-spring-walk',
    'Photos from a spring walk',
    ['3x4', '4x3', '4x5', '4x3']
  ),
}
</code></pre>
<h2>注意事项</h2>
<ul>
<li><code>alt</code> 应用于从同一文件夹返回的每张照片。</li>
<li>顺序由排序后的文件路径决定，而不是编辑器中的导入顺序。</li>
<li>如果重命名文件，排序顺序可能会改变，<code>variants</code> 数组将映射到不同的照片。</li>
</ul>]]></content>
    <category term="Litos" />
    <category term="文档" />
  </entry>
</feed>