<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: lizhaopeng-cn</title>
    <description>The latest articles on Forem by lizhaopeng-cn (@lizhaopengcn).</description>
    <link>https://forem.com/lizhaopengcn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3895162%2F3d029651-5359-4fda-92b7-87a7171460f8.jpeg</url>
      <title>Forem: lizhaopeng-cn</title>
      <link>https://forem.com/lizhaopengcn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lizhaopengcn"/>
    <language>en</language>
    <item>
      <title>Claude Code 模型切换与 CCR 路由：踩坑全记录</title>
      <dc:creator>lizhaopeng-cn</dc:creator>
      <pubDate>Tue, 28 Apr 2026 10:50:58 +0000</pubDate>
      <link>https://forem.com/lizhaopengcn/claude-code-mo-xing-qie-huan-yu-ccr-lu-you-cai-keng-quan-ji-lu-4bal</link>
      <guid>https://forem.com/lizhaopengcn/claude-code-mo-xing-qie-huan-yu-ccr-lu-you-cai-keng-quan-ji-lu-4bal</guid>
      <description>&lt;p&gt;这篇是两个调试 session 的踩坑记录，讲清楚三件事：为什么 CCR 的 &lt;code&gt;/model haiku&lt;/code&gt; 路由不生效、为什么走 OpenRouter 用 Claude 内置别名比三方 model ID 好、以及官方通道独有什么。&lt;/p&gt;

&lt;h2&gt;
  
  
  目录
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;问题一：/model haiku 没走 ALIAS_MAP&lt;/li&gt;
&lt;li&gt;问题二：直接 claude 启动为什么 /model 还能切换&lt;/li&gt;
&lt;li&gt;问题三：内置别名 vs 三方 model ID 差在哪里&lt;/li&gt;
&lt;li&gt;跨 Provider 切换的隐形地雷：thinking block 签名污染&lt;/li&gt;
&lt;li&gt;官方独有功能清单&lt;/li&gt;
&lt;li&gt;小结&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  问题一：/model haiku 没走 ALIAS_MAP
&lt;/h2&gt;

&lt;p&gt;现象：CCR 里配了 Haiku 路由规则，&lt;code&gt;/model haiku&lt;/code&gt; 切换后却命中了 background 规则，实际跑的是 &lt;strong&gt;Sonnet 4.6&lt;/strong&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  排查过程
&lt;/h3&gt;

&lt;p&gt;在 &lt;code&gt;router.js&lt;/code&gt; 里看到的配置是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.5-haiku-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而 Claude Code 实际发给 CCR 的 HTTP 请求里，&lt;code&gt;body.model&lt;/code&gt; 的值是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude-haiku-4-5-20251001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;多了 &lt;code&gt;-20251001&lt;/code&gt; 日期后缀。&lt;code&gt;ALIAS_MAP["claude-haiku-4-5-20251001"]&lt;/code&gt; → &lt;code&gt;undefined&lt;/code&gt; → router 返回 &lt;code&gt;null&lt;/code&gt; → 回落到默认规则。&lt;/p&gt;

&lt;p&gt;CCR 内置了一条判断：haiku 主要是 Claude Code 的 background 任务模型，所以 fallback 路由命中的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.6-sonnet-20260217&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最终用的是 Sonnet 4.6，不是 Haiku。&lt;/p&gt;

&lt;h3&gt;
  
  
  还有第二个 bug
&lt;/h3&gt;

&lt;p&gt;即使 key 补全了，value 里也有问题：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.5-haiku-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;//                                          ^^^&lt;/span&gt;
&lt;span class="c1"&gt;//                           模型名写成了 4.5，实际应为 4.5-haiku&lt;/span&gt;
&lt;span class="c1"&gt;//                           且这个 slug 不在 or.models 白名单里&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenRouter 侧如果 model 不在白名单里会直接拒掉，所以 key 即使匹配上了也会报 404。&lt;/p&gt;

&lt;h3&gt;
  
  
  修法
&lt;/h3&gt;

&lt;p&gt;ALIAS_MAP 里的 key 必须和 Claude Code 发出的 &lt;code&gt;body.model&lt;/code&gt; 完全一致。先用日志拦一条请求确认真实值，再写配置，不要靠推测。Sonnet 和 Opus 同样要校验：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 正确写法（以当前版本为例）&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.7-opus-20260416&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.6-sonnet-20260217&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;同时把 &lt;code&gt;claude-haiku-4-5&lt;/code&gt; 加入 &lt;code&gt;config.json&lt;/code&gt; 的 &lt;code&gt;or.models&lt;/code&gt; 白名单，否则 OpenRouter 还是会拒。&lt;/p&gt;




&lt;h2&gt;
  
  
  问题二：直接 claude 启动为什么 /model 还能切换
&lt;/h2&gt;

&lt;p&gt;这是另一个 session 里发现的事，起因是这个启动画面：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▐▛███▜▌   Claude Code v2.1.121
▝▜█████▛▘  Opus 4.7 · API Usage Billing
  ▘▘ ▝▝    /Users/xtuul
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;直接 &lt;code&gt;claude&lt;/code&gt; 启动（不是 &lt;code&gt;ccr code&lt;/code&gt;），执行 &lt;code&gt;/model opus&lt;/code&gt; 后，下一条消息正常用了 Opus 4.7。"这不是应该没走 CCR 就无法路由吗？"&lt;/p&gt;

&lt;h3&gt;
  
  
  关键：ANTHROPIC_BASE_URL 指向 OpenRouter
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;~/.zshrc&lt;/code&gt; 里设了：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://openrouter.ai/api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-or-v1-..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-4.7-opus-20260416"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code 直接读这三个变量，绕过 CCR 但&lt;strong&gt;不直连 &lt;code&gt;api.anthropic.com&lt;/code&gt;&lt;/strong&gt;，请求打到了 OpenRouter。&lt;/p&gt;

&lt;p&gt;实际调用链：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;你 → claude (v2.1.121)
       ① 读 ANTHROPIC_BASE_URL → https://openrouter.ai/api
       ② 读 ANTHROPIC_AUTH_TOKEN → Authorization: Bearer sk-or-v1-...
       ③ /model opus → 会话状态设为 opus，写 settings.json
       ④ 发请求时把 opus 展开为 claude-opus-4-7
     ↓
POST https://openrouter.ai/api/v1/messages
body: { "model": "claude-opus-4-7", ... }
     ↓
OpenRouter（实现了 Anthropic Messages API 协议）
     ↓
Anthropic 原厂 API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/model opus&lt;/code&gt; 只做两件事：改会话状态、持久化到 &lt;code&gt;settings.json&lt;/code&gt;。endpoint 和 token 由环境变量决定，和 &lt;code&gt;/model&lt;/code&gt; 无关。&lt;/p&gt;

&lt;p&gt;OpenRouter 的 &lt;code&gt;/v1/messages&lt;/code&gt; 端点跟 Anthropic 协议兼容，所以 Claude Code 完全意识不到中间多了一跳。&lt;/p&gt;

&lt;h3&gt;
  
  
  CCR vs 直挂 OpenRouter 的区别
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;路径&lt;/th&gt;
&lt;th&gt;CCR 参与&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ccr code&lt;/code&gt; 启动&lt;/td&gt;
&lt;td&gt;claude → 127.0.0.1:3456 (CCR) → openrouter/其他&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直接 &lt;code&gt;claude&lt;/code&gt; 启动（有 ANTHROPIC_BASE_URL）&lt;/td&gt;
&lt;td&gt;claude → openrouter.ai 直连&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CCR 只在你需要更复杂的路由时才必要：多 provider 别名切换、按 model 改写请求体、剥 Claude 专属字段给其他厂商等。对"我就是要用 Anthropic 模型"这种场景，环境变量直挂就够。&lt;/p&gt;




&lt;h2&gt;
  
  
  问题三：内置别名 vs 三方 model ID 差在哪里
&lt;/h2&gt;

&lt;p&gt;同一次 session 里做了一个实验，贴出两次 &lt;code&gt;/context&lt;/code&gt; 的输出：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用 &lt;code&gt;ANTHROPIC_MODEL=anthropic/claude-4.7-opus-20260416&lt;/code&gt; 时：&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⛁ ⛁ ⛁ ⛀ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   anthropic/claude-4.7-opus-20260416
                           39.6k/200k tokens (20%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;切换 &lt;code&gt;/model opus&lt;/code&gt; 之后：&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛀ ⛀ ⛁ ⛁ ⛁ ⛁ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶   Opus 4.7
                                                   claude-opus-4-7
                                                   63.6k/1m tokens (6%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;同一个 Opus 4.7，上下文窗口差了 5 倍（200k vs 1M）。&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  为什么会这样
&lt;/h3&gt;

&lt;p&gt;Claude Code 二进制里硬编码了一张 model 能力表，从 binary strings 里能找到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model="claude-opus-4-7"
model="claude-sonnet-4-6"
model="claude-haiku-4-5"
// 以及 if(H!=="claude-opus-4-7") 这类硬编码分支判断
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;只有这三个&lt;strong&gt;裸 ID&lt;/strong&gt;（不带厂商前缀，不带日期后缀）命中这张表。命中了，Claude Code 才会：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;带上 &lt;code&gt;anthropic-beta: context-1m-...&lt;/code&gt; header（解锁 1M context）&lt;/li&gt;
&lt;li&gt;发送 &lt;code&gt;thinking: { type: "enabled", budget_tokens: ... }&lt;/code&gt; 参数（开启 extended thinking）&lt;/li&gt;
&lt;li&gt;查表设对 &lt;code&gt;max_tokens&lt;/code&gt; 上限（opus 32k / sonnet 8k / haiku 8k）&lt;/li&gt;
&lt;li&gt;按模型单价算 cache 定价，&lt;code&gt;/cost&lt;/code&gt; 才能显示准确&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;遇到 &lt;code&gt;anthropic/claude-4.7-opus-20260416&lt;/code&gt; 这种带前缀带后缀的 ID，表里查不到，全部按保守值处理。&lt;/p&gt;

&lt;h3&gt;
  
  
  /model 切换的优先级
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;启动时&lt;/strong&gt;：读 &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt; 作为初始值，原样进入会话状态，不做 allowlist 校验&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;执行 &lt;code&gt;/model opus&lt;/code&gt; 后&lt;/strong&gt;：覆盖会话状态，写 &lt;code&gt;settings.json&lt;/code&gt;，后续请求发 &lt;code&gt;claude-opus-4-7&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;此时 &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt; 完全被忽略&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;两种 ID OpenRouter 都能接（它对裸 claude id 会自动补 &lt;code&gt;anthropic/&lt;/code&gt; 前缀），但 Claude Code 侧的能力开关只认裸 ID。&lt;/p&gt;

&lt;h3&gt;
  
  
  如果把 ANTHROPIC_MODEL 换成非 Claude 模型
&lt;/h3&gt;

&lt;p&gt;比如 &lt;code&gt;openai/gpt-5.4-20260305&lt;/code&gt; 或 &lt;code&gt;qwen/qwen3.6-plus-04-02&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;启动时能用，但有几个硬伤：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/model opus&lt;/code&gt; 一按，立刻切回 Claude&lt;/td&gt;
&lt;td&gt;硬编码别名表里只有 Claude&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt caching 不生效&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cache_control&lt;/code&gt; 字段 GPT/Qwen 不认，直接丢弃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extended thinking 参数丢失&lt;/td&gt;
&lt;td&gt;只有 Claude 4.x 支持，转发给别家时被过滤掉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool use 结构错乱&lt;/td&gt;
&lt;td&gt;Anthropic 的 &lt;code&gt;tool_use/tool_result&lt;/code&gt; block 翻译成 OpenAI &lt;code&gt;tool_calls&lt;/code&gt; 时会丢 &lt;code&gt;id&lt;/code&gt; 关联&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统 prompt 错乱&lt;/td&gt;
&lt;td&gt;CC 的 system prompt 是为 Claude 写的（"你是 Anthropic 的 CLI"等），GPT/Qwen 读到会输出奇怪内容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;这恰好解释了 CCR 存在的价值&lt;/strong&gt;：CCR 是专门处理这些场景的中间层：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude（只会发 claude-opus-4-7）
  ↓
CCR :3456
  ↓  ① 改写 model: claude-opus-4-7 → openai/gpt-5.4-...
     ② 剥掉 cache_control / thinking 字段，不让它们透传给不认识的 provider
     ③ 支持 glm: / qwen: 前缀做会话内一次性切换
     ④ 注册自定义别名表，/model &amp;lt;alias&amp;gt; 可以映射到任意 provider
  ↓
OpenRouter / 其他厂商
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  跨 Provider 切换的隐形地雷：thinking block 签名污染
&lt;/h2&gt;

&lt;p&gt;在同一个 session 里，把 &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt; 换成 DeepSeek 试了一次，随后切回 Claude，报了这个错：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages.11.content.0: Invalid `signature` in `thinking` block
provider_name: Anthropic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而且同一条 &lt;code&gt;messages.11&lt;/code&gt; 被所有后续请求反复报错：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;req_011CaVYB... messages.11.content.0
req_011CaVYD... messages.11.content.0
req_011CaVYJ... messages.11.content.0
req_011CaVYK... messages.11.content.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  机制
&lt;/h3&gt;

&lt;p&gt;Claude 4.x 在开启 extended thinking 时，响应里会有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thinking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thinking"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...思考摘要..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123...HMAC签名"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;signature&lt;/code&gt; 是 Anthropic 服务端用私钥签发的 HMAC。多轮对话时，客户端必须把历史里的 thinking block &lt;strong&gt;原样&lt;/strong&gt;（含 signature）回传，Anthropic 后端会验签。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;切到 DeepSeek 时发生了什么：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Claude Code 把带 thinking block 的历史原样打包，POST 给 OpenRouter，目标改成 &lt;code&gt;deepseek/deepseek-v3.2-...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OpenRouter 翻译成 DeepSeek API 格式，调 DeepSeek，得到响应&lt;/li&gt;
&lt;li&gt;OpenRouter 把 DeepSeek 的响应翻译回 Anthropic 格式，&lt;strong&gt;伪造了一个 thinking block&lt;/strong&gt;（塞了个占位 signature）&lt;/li&gt;
&lt;li&gt;Claude Code 把这个"伪 thinking block"写进会话 history&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;切回 Claude 时：&lt;/strong&gt; 包含伪 thinking block 的完整 history POST 到 Anthropic 原厂 → 验签失败 → 400。&lt;/p&gt;

&lt;p&gt;Qwen 能继续用，因为 Qwen 后端不在乎 signature，OpenRouter 转过去时直接把 thinking block 丢掉。但只要目标是 Anthropic 原厂，&lt;code&gt;messages.11&lt;/code&gt; 那个坏掉的 block 就会一直触发同一个错误。&lt;/p&gt;

&lt;h3&gt;
  
  
  修复
&lt;/h3&gt;

&lt;p&gt;只能清掉那条消息才能恢复：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 在 Claude Code 里执行&lt;/span&gt;
/clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;清完历史之后切回 &lt;code&gt;/model opus&lt;/code&gt; 就正常了。&lt;/p&gt;

&lt;h3&gt;
  
  
  为什么 CCR 不会出现这个 bug
&lt;/h3&gt;

&lt;p&gt;CCR 的 transformer 出站前会扫描 history，根据目标 provider 决定保留还是剥离 thinking block：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CCR 伪代码&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetProvider&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stripThinkingBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stripForeignThinkingBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 目标是 Claude 但历史里有外来 block，也剥掉&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;直挂环境变量走 OpenRouter 没有这层保护，Claude Code 发什么 OpenRouter 就原样转什么。&lt;/p&gt;




&lt;h2&gt;
  
  
  官方独有功能清单
&lt;/h2&gt;

&lt;p&gt;这部分是 session 里顺带搜出来的，列几个有硬证据的。&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto 模式（任务复杂度自动切换 Opus/Sonnet/Haiku）
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;需要 &lt;strong&gt;Max 订阅 + &lt;code&gt;/login&lt;/code&gt; 登录 + 环境里没有 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;/&lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;是 Anthropic 服务端 feature flag 控制的订阅特性，按账号灰度放开&lt;/li&gt;
&lt;li&gt;GitHub issue #46616 里明确写了触发条件：一旦检测到 API key 类环境变量，Auto 模式直接禁用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只要你用了 OpenRouter（需要设 &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt;），Auto 模式就永远用不了。&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Caching 命中率
&lt;/h3&gt;

&lt;p&gt;多个独立报告显示，走 OpenRouter 的 Claude 请求，prompt cache 效果明显不如直连：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;opencode #1245&lt;/code&gt;：OpenRouter 下 cache 只对 system message 生效&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Zed #52576&lt;/code&gt;：&lt;code&gt;native_tokens_cached&lt;/code&gt; 始终是 0，形同关闭&lt;/li&gt;
&lt;li&gt;SillyTavern 讨论：1 小时 TTL 在 OpenRouter 任何 Anthropic provider 下都不工作&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai-sdk-provider #35&lt;/code&gt;：prompt caching 完全没命中，长期未修&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1M Context 的计费入口
&lt;/h3&gt;

&lt;p&gt;OpenRouter 路径下，即使你用了内置别名解锁了 1M context，如果 Anthropic 侧需要额外收费，报错如下：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Error: Extra usage is required for 1M context
run /extra-usage to enable, or /model to switch to standard context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/extra-usage&lt;/code&gt; 这个命令在官方订阅通道才有对应入口，OpenRouter 路径没有。&lt;/p&gt;

&lt;h3&gt;
  
  
  其他官方独占
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;官方现状&lt;/th&gt;
&lt;th&gt;三方现状&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto 模式&lt;/td&gt;
&lt;td&gt;Max 订阅 + Opus 4.7&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-04-04 起 Pro/Max 订阅额度&lt;/td&gt;
&lt;td&gt;官方独用&lt;/td&gt;
&lt;td&gt;第三方 harness 不能消耗订阅 quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extended thinking 签名完整性&lt;/td&gt;
&lt;td&gt;原生签发+验签&lt;/td&gt;
&lt;td&gt;跨 provider 切换会污染 history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/fast&lt;/code&gt; 的 Opus 4.6 Fast 变体&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;OpenRouter 的 model 列表里没有这个变体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files API / Code Execution&lt;/td&gt;
&lt;td&gt;官方 beta endpoint&lt;/td&gt;
&lt;td&gt;OpenRouter 不转发非 &lt;code&gt;/v1/messages&lt;/code&gt; endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Agents / Remote Trigger&lt;/td&gt;
&lt;td&gt;官方 API&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code 的 session 同步&lt;/td&gt;
&lt;td&gt;CLI/桌面/Web 互通&lt;/td&gt;
&lt;td&gt;只能本地 CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新模型首发&lt;/td&gt;
&lt;td&gt;发布当天可用&lt;/td&gt;
&lt;td&gt;通常延迟几小时到几天&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;另一个硬事实：2026-04-04 Anthropic 封锁了第三方 agentic 工具消耗 Pro/Max 订阅 quota，原话是"usage will be billed to your API account"。加上 Claude Code 本身在 2026-04-21 从 Pro 计划里被移除（仅 Max 5x+ 保留），订阅价值已经集中在 Max 路径。&lt;/p&gt;




&lt;h2&gt;
  
  
  小结
&lt;/h2&gt;

&lt;p&gt;三件事的结论：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CCR ALIAS_MAP&lt;/strong&gt;：key 必须与 Claude Code 发出的 &lt;code&gt;body.model&lt;/code&gt; 完全一致，带日期后缀。先抓一条请求日志确认真实值，再写配置。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;内置别名 vs 三方 ID&lt;/strong&gt;：走 OpenRouter 时，用 &lt;code&gt;/model opus&lt;/code&gt; 而不是 &lt;code&gt;ANTHROPIC_MODEL=anthropic/claude-4.7-opus-20260416&lt;/code&gt;，能多拿到 1M context、extended thinking 参数、正确的 max_tokens 和 cache 定价，差距显著。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;跨 provider 混用会话&lt;/strong&gt;：遇到 thinking block 就是定时炸弹。直挂 OpenRouter 没有 CCR 那层 transformer 保护，切到非 Anthropic provider 再切回来，只能 &lt;code&gt;/clear&lt;/code&gt; 重来。&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>claude</category>
      <category>ccr</category>
      <category>devtools</category>
    </item>
    <item>
      <title>把 Claude Code 搬进飞书和 Telegram：cc-connect + CCR 完整架构、安装流程与踩坑全记录</title>
      <dc:creator>lizhaopeng-cn</dc:creator>
      <pubDate>Tue, 28 Apr 2026 09:04:04 +0000</pubDate>
      <link>https://forem.com/lizhaopengcn/ba-claude-code-ban-jin-fei-shu-he-telegramcc-connect-ccr-wan-zheng-jia-gou-an-zhuang-liu-cheng-yu-cai-keng-quan-ji-lu-1d0l</link>
      <guid>https://forem.com/lizhaopengcn/ba-claude-code-ban-jin-fei-shu-he-telegramcc-connect-ccr-wan-zheng-jia-gou-an-zhuang-liu-cheng-yu-cai-keng-quan-ji-lu-1d0l</guid>
      <description>&lt;p&gt;&lt;strong&gt;本篇讲什么&lt;/strong&gt;：一套让你在手机上用 Claude Code 的工程化方案——&lt;code&gt;IM（飞书 / Telegram）⇄ cc-connect ⇄ Claude Code ⇄ CCR ⇄ OpenRouter ⇄ 多家大模型&lt;/code&gt;。含语音转写（Groq Whisper）、MCP（Stitch）、launchd 守护、模型按需切换，外加 13 个一路踩过的坑。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本篇不讲什么&lt;/strong&gt;：Claude Code 本身怎么用、OpenRouter 怎么注册、飞书/Telegram 是什么。默认你已经跑过 &lt;code&gt;claude&lt;/code&gt; 桌面端，有 OpenRouter 账号，对命令行不陌生。&lt;/p&gt;

&lt;h2&gt;
  
  
  目录
&lt;/h2&gt;

&lt;h2&gt;
  
  
  一、为什么要做这套东西
&lt;/h2&gt;

&lt;p&gt;日常写代码离不开 Claude Code，但桌面端有个硬伤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;开会 / 出门 / 蹲厕所&lt;/strong&gt; 的时候脑子里冒出改一行代码的想法，没电脑在旁边就凉了&lt;/li&gt;
&lt;li&gt;想让它后台跑一个重构 / 搜资料任务，也不方便时不时回电脑前看进度&lt;/li&gt;
&lt;li&gt;偶尔一条语音表达比打字快得多，Claude Code 桌面端不吃语音&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;目标：&lt;/strong&gt; 手机上通过飞书 / Telegram 直接和 Claude Code 对话，可发文字、发语音、切模型、恢复之前的 session、用 MCP 工具。电脑端和手机端最好能看到同一批 session。&lt;/p&gt;

&lt;p&gt;用到的组件：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cc-connect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IM 桥接器，把飞书/Telegram 消息转成 Claude Code 的输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;claude-code-router&lt;/code&gt; (CCR)&lt;/td&gt;
&lt;td&gt;本地反向代理，把 Claude Code 的请求路由到 OpenRouter / 其他厂商，支持按 alias 切模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenRouter&lt;/td&gt;
&lt;td&gt;聚合多家模型（Anthropic / OpenAI / Google / Qwen ...），一个 key 搞定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg + Groq Whisper&lt;/td&gt;
&lt;td&gt;把飞书/Telegram 的语音消息转成文字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchd&lt;/td&gt;
&lt;td&gt;macOS 上把 cc-connect 做成开机自启的守护进程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  二、整体架构
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────┐         ┌────────────────┐
│   飞书客户端   │         │ Telegram 客户端 │
└────────┬───────┘         └────────┬───────┘
         │  消息 / 语音               │
         ▼                            ▼
         ┌───────────────────────────────┐
         │    cc-connect (launchd daemon)│   ← 监听两个平台，做消息协议转换
         │                               │
         │   ┌───────────────────────┐   │
         │   │ Groq Whisper (语音转) │   │
         │   └───────────────────────┘   │
         └──────────────┬────────────────┘
                        │  fork 子进程
                        ▼
           ┌────────────────────────────┐
           │  claude (Claude Code CLI)  │
           │                            │
           │   ANTHROPIC_BASE_URL=      │
           │     http://127.0.0.1:3456  │
           │   ANTHROPIC_API_KEY=dummy  │
           └──────────────┬─────────────┘
                          │  Anthropic 协议
                          ▼
           ┌────────────────────────────┐
           │   CCR (claude-code-router) │
           │   127.0.0.1:3456           │
           │                            │
           │   router.js:               │
           │   - /model &amp;lt;alias&amp;gt;         │
           │   - "alias:" 前缀          │
           │   - longContextThreshold   │
           │   - default Router         │
           └──────────────┬─────────────┘
                          │  各家原生协议
                          ▼
           ┌────────────────────────────────┐
           │  OpenRouter / 其他 provider    │
           │  anthropic/claude-4.7-opus     │
           │  anthropic/claude-4.6-sonnet   │
           │  anthropic/claude-4.5-haiku    │
           │  openai/gpt-5.4                │
           │  openai/gpt-5.3-codex          │
           │  google/gemini-3.1-pro         │
           │  qwen/qwen3.6-plus             │
           │  ...:free 系列用另一个 key     │
           └────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;几个关键的数据流：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;文字消息&lt;/strong&gt;：IM → cc-connect → claude 子进程 (stdin) → CCR → OpenRouter → 回流&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;语音消息&lt;/strong&gt;：IM → cc-connect 下载 ogg/mp3 → ffmpeg 转码 → Groq Whisper → 文本 → 当作文字消息走&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP 工具&lt;/strong&gt;：claude 子进程按需 HTTP 调用 &lt;code&gt;https://stitch.googleapis.com/mcp&lt;/code&gt;（header 里带 key）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;session 持久化&lt;/strong&gt;：每轮对话的 jsonl 存在 &lt;code&gt;~/.claude/projects/-Users-xtuul/&lt;/code&gt;，和桌面端 &lt;code&gt;ccr code&lt;/code&gt; 共用一个目录&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  三、安装流程（macOS，一步步来）
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;假设你已经装过 Node.js（推荐 22.x，用 nvm 管理）、Claude Code CLI、以及有一个 OpenRouter 账号。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.1 装 CCR（claude-code-router）
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @musistudio/claude-code-router
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;配置文件在 &lt;code&gt;~/.claude-code-router/&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;config.json&lt;/code&gt; — Providers 列表、Router 规则、APIKEY 等&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;router.js&lt;/code&gt; — 自定义路由脚本（可选，但强烈推荐）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;config.json&lt;/code&gt; 示例（我本机跑着的就是这套，两个账号：&lt;code&gt;or&lt;/code&gt; 是付费主账号，&lt;code&gt;or1&lt;/code&gt; 跑免费模型）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LOG"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"HOST"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"APIKEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"API_TIMEOUT_MS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"600000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"or"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"api_base_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://openrouter.ai/api/v1/chat/completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"api_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-or-v1-...主 key..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-4.7-opus-20260416"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-4.6-sonnet-20260217"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-4.5-haiku-20251001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-5.4-20260305"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-5.3-codex-20260224"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"google/gemini-3.1-pro-preview-20260219"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"qwen/qwen3.6-plus-04-02"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transformer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"openrouter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"or1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"api_base_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://openrouter.ai/api/v1/chat/completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"api_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-or-v1-...备用 key 跑免费模型..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"openrouter/free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"nvidia/nemotron-3-super-120b-a12b:free"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transformer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"openrouter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Router"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s2"&gt;"or,anthropic/claude-4.7-opus-20260416"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"or,anthropic/claude-4.6-sonnet-20260217"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="s2"&gt;"or,anthropic/claude-4.7-opus-20260416"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"or,anthropic/claude-4.7-opus-20260416"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longContextThreshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"webSearch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="s2"&gt;"or,openai/gpt-5.4-20260305"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CUSTOM_ROUTER_PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/你/.claude-code-router/router.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;provider &lt;code&gt;name&lt;/code&gt; 就是 Router 值里 &lt;code&gt;,&lt;/code&gt; 前面那一段&lt;/strong&gt;。我习惯用短名（&lt;code&gt;or&lt;/code&gt; / &lt;code&gt;or1&lt;/code&gt;），你叫 &lt;code&gt;openrouter&lt;/code&gt; / &lt;code&gt;openrouter-free&lt;/code&gt; 都行，只要 Router / router.js 里两边对得上&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transformer: { use: ["openrouter"] }&lt;/code&gt; 是 CCR 让 OpenRouter 协议生效的开关。不加这行，部分字段（比如 reasoning content、工具调用 schema）会发错格式给上游&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;longContextThreshold&lt;/code&gt; 我拉到 &lt;code&gt;180000&lt;/code&gt;：2026 年的模型上下文基本都是 200k+ / 1M，没必要 120k 就切模型&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webSearch: ""&lt;/code&gt; 是显式置空 —— 我不用 CCR 的 webSearch 路由，Claude Code 自己的 WebFetch 更好用&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  几个重要的 Router 字段含义
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;兜底模型。其他路由规则都没命中时用这个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;background&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后台压缩/总结用的轻量模型。一般比 default 便宜/快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;think&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;带 extended thinking 的长链路推理模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;longContext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单次请求 &lt;strong&gt;input tokens ≥ longContextThreshold&lt;/strong&gt; 时切到这个模型。&lt;strong&gt;注意：是单次请求的 input tokens，不是整个上下文窗口&lt;/strong&gt;，而且是&lt;strong&gt;无状态&lt;/strong&gt;的 —— 这次超了走这个，下次没超还是走 default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webSearch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;带 web 搜索能力的模型（Gemini 系列通常最强）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发送图片时用的 vision 模型（要选&lt;strong&gt;分析&lt;/strong&gt;模型，不是&lt;strong&gt;生成&lt;/strong&gt;模型）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;router.js&lt;/code&gt;（自定义路由，强烈推荐）
&lt;/h4&gt;

&lt;p&gt;这个脚本给你两个人工切模型的方式：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;模式 A — 持久切换&lt;/strong&gt;：在 claude 聊天里输 &lt;code&gt;/model &amp;lt;alias&amp;gt;&lt;/code&gt;，整个 session 都用这个 alias&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;模式 B — 一次性切换&lt;/strong&gt;：消息以 &lt;code&gt;alias:&lt;/code&gt; 开头，只这条走 alias，之后自动回退&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALIAS_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ── or（主账号 / 付费模型）&lt;/span&gt;
  &lt;span class="c1"&gt;// ⚠️ Anthropic 系列的 key 必须用"带日期后缀的完整 ID"，原因见下文&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.7-opus-20260416&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.6-sonnet-20260217&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.5-haiku-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ── 其他家的模型，alias 短名就行&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,openai/gpt-5.4-20260305&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;codex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,openai/gpt-5.3-codex-20260224&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,google/gemini-3.1-pro-preview-20260219&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qwen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,qwen/qwen3.6-plus-04-02&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ── or1（免费账号 / 免费模型）&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or1,openrouter/free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nvidia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or1,nvidia/nemotron-3-super-120b-a12b:free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PREFIX_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z&lt;/span&gt;&lt;span class="se"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z0-9_-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;(\s&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;matchPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PREFIX_RE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ALIAS_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stripped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trimStart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 模式 A：body.model 是裸 alias（来自 /model &amp;lt;alias&amp;gt;）&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;rawModel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rawModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALIAS_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ALIAS_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 模式 B：最后一条 user 消息以 "alias:" 开头&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lastUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;lastUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matchPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;lastUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripped&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Claude Code 会注入 &amp;lt;system-reminder&amp;gt; 等前置 text block，真实输入常在后面&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lastUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matchPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripped&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 都不匹配，回落 Router 默认规则&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  为什么 Anthropic 模型的 key 必须写完整 ID
&lt;/h4&gt;

&lt;p&gt;这是个很隐蔽的坑。&lt;/p&gt;

&lt;p&gt;Claude Code &lt;strong&gt;客户端本身对 Anthropic 自家模型名做了硬编码处理&lt;/strong&gt;，和对第三方模型的处理完全不一样：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/model gpt&lt;/code&gt;（第三方）→ body.model 原样发 &lt;code&gt;"gpt"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/model opus&lt;/code&gt;（Anthropic 自家）→ 从二进制硬编码表里查到 &lt;code&gt;opus&lt;/code&gt; → &lt;code&gt;claude-opus-4-7&lt;/code&gt;，&lt;strong&gt;body.model 发 &lt;code&gt;claude-opus-4-7&lt;/code&gt;&lt;/strong&gt;（Claude Code 内部只认这三个裸 ID：&lt;code&gt;claude-opus-4-7&lt;/code&gt; / &lt;code&gt;claude-sonnet-4-6&lt;/code&gt; / &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以如果你 &lt;code&gt;ALIAS_MAP&lt;/code&gt; 里写的是 &lt;code&gt;"opus": "..."&lt;/code&gt;，CCR 收到的 &lt;code&gt;body.model&lt;/code&gt; 其实是 &lt;code&gt;claude-opus-4-7&lt;/code&gt;——查不到，返回 &lt;code&gt;null&lt;/code&gt;，回落到 &lt;code&gt;config.json&lt;/code&gt; 的默认 Router。&lt;/p&gt;

&lt;p&gt;更坑的是：Anthropic 家 haiku/sonnet/opus 的短 ID 回落时，CCR 往往会把它归为"后台/轻量任务"，命中 &lt;code&gt;Router.background&lt;/code&gt;。结果你 &lt;code&gt;/model haiku&lt;/code&gt; 实际跑的是 &lt;code&gt;Router.background&lt;/code&gt; 里配的那个模型（我的是 Sonnet）。第一次看到"怎么切了还是不对"的时候很难想到是键名对不上。&lt;/p&gt;

&lt;h4&gt;
  
  
  顺便省一次切换：启动直连 OpenRouter 也能复用这套映射
&lt;/h4&gt;

&lt;p&gt;我 &lt;code&gt;.zshrc&lt;/code&gt; 里配了：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://openrouter.ai/api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-or-v1-..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-4.7-opus-20260416"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样直接敲 &lt;code&gt;claude&lt;/code&gt;（不走 CCR）时，Claude Code 把 &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt; 原样写进 &lt;code&gt;body.model&lt;/code&gt;，请求直发 OpenRouter。&lt;strong&gt;但 body.model 是 &lt;code&gt;anthropic/claude-4.7-opus-20260416&lt;/code&gt; 这个完整字符串&lt;/strong&gt;，不是 &lt;code&gt;claude-opus-4-7&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;然后我&lt;strong&gt;也走 CCR&lt;/strong&gt; 的场景（&lt;code&gt;cc&lt;/code&gt; alias = &lt;code&gt;ccr code&lt;/code&gt;），启动时 body.model 又会变成 &lt;code&gt;claude-opus-4-7&lt;/code&gt;（Claude Code 内部从 &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt; 推断别名再展开）——两套字符串都可能出现在 &lt;code&gt;body.model&lt;/code&gt; 里。&lt;/p&gt;

&lt;p&gt;所以 &lt;code&gt;ALIAS_MAP&lt;/code&gt; 最干净的做法是把&lt;strong&gt;两种形态的 key 都列进去&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALIAS_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// /model opus 切换后 Claude Code 会展开成这个裸 ID&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.7-opus-20260416&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.6-sonnet-20260217&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,anthropic/claude-4.5-haiku-20251001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// 其他家短名就行&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or,openai/gpt-5.4-20260305&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样启动 claude 就是 Opus，&lt;code&gt;/model sonnet&lt;/code&gt; 切过去也是 Sonnet，&lt;code&gt;/model haiku&lt;/code&gt; 是 Haiku。&lt;strong&gt;不需要启动一次再切一次&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;验证 CCR 跑起来：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ccr start                &lt;span class="c"&gt;# 前台起&lt;/span&gt;
&lt;span class="c"&gt;# 或者&lt;/span&gt;
ccr code                 &lt;span class="c"&gt;# 用 CCR 作为后端启动 claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ccr code&lt;/code&gt; 能跑通就说明 CCR → OpenRouter 的链路是通的。&lt;/p&gt;




&lt;h3&gt;
  
  
  3.2 装 cc-connect
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; cc-connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;配置目录 &lt;code&gt;~/.cc-connect/&lt;/code&gt;，核心是 &lt;code&gt;config.toml&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="py"&gt;language&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"en"&lt;/span&gt;

&lt;span class="nn"&gt;[[projects]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-project"&lt;/span&gt;

  &lt;span class="nn"&gt;[projects.agent]&lt;/span&gt;
    &lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claudecode"&lt;/span&gt;
    &lt;span class="nn"&gt;[projects.agent.options]&lt;/span&gt;
      &lt;span class="py"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;
      &lt;span class="py"&gt;work_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/Users/你"&lt;/span&gt;              &lt;span class="c"&gt;# 和桌面端 ccr code 的 cwd 对齐，后面讲为什么&lt;/span&gt;
      &lt;span class="py"&gt;router_url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:3456"&lt;/span&gt;  &lt;span class="c"&gt;# 指向 CCR&lt;/span&gt;
      &lt;span class="py"&gt;router_api_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"dummy"&lt;/span&gt;              &lt;span class="c"&gt;# ⚠️ 不能留空！后面讲为什么&lt;/span&gt;

  &lt;span class="c"&gt;# ── 飞书&lt;/span&gt;
  &lt;span class="nn"&gt;[[projects.platforms]]&lt;/span&gt;
    &lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"feishu"&lt;/span&gt;
    &lt;span class="nn"&gt;[projects.platforms.options]&lt;/span&gt;
      &lt;span class="py"&gt;app_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cli_xxx"&lt;/span&gt;
      &lt;span class="py"&gt;app_secret&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"xxx"&lt;/span&gt;

  &lt;span class="c"&gt;# ── Telegram&lt;/span&gt;
  &lt;span class="nn"&gt;[[projects.platforms]]&lt;/span&gt;
    &lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"telegram"&lt;/span&gt;
    &lt;span class="nn"&gt;[projects.platforms.options]&lt;/span&gt;
      &lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"xxx:yyy"&lt;/span&gt;           &lt;span class="c"&gt;# @BotFather 建 bot 给你的&lt;/span&gt;
      &lt;span class="py"&gt;allow_from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"你的TG数字ID"&lt;/span&gt; &lt;span class="c"&gt;# 或 "*" 允许所有人，建议先宽后紧&lt;/span&gt;

&lt;span class="nn"&gt;[log]&lt;/span&gt;
  &lt;span class="py"&gt;level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"info"&lt;/span&gt;

&lt;span class="nn"&gt;[speech]&lt;/span&gt;
  &lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"groq"&lt;/span&gt;
  &lt;span class="py"&gt;language&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"zh"&lt;/span&gt;
  &lt;span class="nn"&gt;[speech.groq]&lt;/span&gt;
    &lt;span class="py"&gt;api_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gsk_..."&lt;/span&gt;
    &lt;span class="py"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"whisper-large-v3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  飞书 app_id / app_secret 怎么拿
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;去 &lt;a href="https://open.feishu.cn/" rel="noopener noreferrer"&gt;飞书开放平台&lt;/a&gt; 创建「企业自建应用」&lt;/li&gt;
&lt;li&gt;开启「机器人」能力&lt;/li&gt;
&lt;li&gt;权限里勾上「以应用身份发消息」、「读取用户发给机器人的消息」、「获取群组信息」等&lt;/li&gt;
&lt;li&gt;事件订阅&lt;strong&gt;必须切到「长连接模式」&lt;/strong&gt;（cc-connect 用的就是 WebSocket），HTTP 回调模式会连不上&lt;/li&gt;
&lt;li&gt;把应用发布到版本，企业管理员通过&lt;/li&gt;
&lt;li&gt;凭据与基础信息页找 App ID / App Secret&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Telegram token 怎么拿
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;在 Telegram 找 &lt;code&gt;@BotFather&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/newbot&lt;/code&gt; → 起个名字 → 起个 username（必须 &lt;code&gt;_bot&lt;/code&gt; 结尾）&lt;/li&gt;
&lt;li&gt;BotFather 返回 &lt;code&gt;xxx:yyy&lt;/code&gt; 格式的 token，就是 &lt;code&gt;token&lt;/code&gt; 字段填的值&lt;/li&gt;
&lt;li&gt;先填 &lt;code&gt;allow_from = "*"&lt;/code&gt; 跑通后，看 cc-connect 日志里的 &lt;code&gt;telegram: message from unauthorized user &amp;lt;数字&amp;gt;&lt;/code&gt;，那个数字就是你的 TG user ID，再改回 &lt;code&gt;"你的数字ID"&lt;/code&gt; 收紧权限&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  验证前台跑通
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.cc-connect
cc-connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;两个平台都 &lt;code&gt;platform started&lt;/code&gt; 就说明连上了。手机上发条消息试试。&lt;/p&gt;




&lt;h3&gt;
  
  
  3.3 装 ffmpeg（语音必需）
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;踩坑&lt;/strong&gt;：&lt;code&gt;[speech] enabled = true&lt;/code&gt; 配了也没用，cc-connect 需要 ffmpeg 把飞书/Telegram 发过来的 &lt;code&gt;ogg&lt;/code&gt;/&lt;code&gt;oga&lt;/code&gt; 转成 Groq 认的格式。报错长这样：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Voice message requires ffmpeg for format conversion. Please install ffmpeg.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;装完之后不用重启 cc-connect，下次收到语音就能转了（cc-connect 是 exec 调用 ffmpeg 的，PATH 里能找到就行）。&lt;/p&gt;

&lt;h4&gt;
  
  
  Groq Whisper API key 怎么拿
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://console.groq.com/" rel="noopener noreferrer"&gt;console.groq.com&lt;/a&gt; 注册&lt;/li&gt;
&lt;li&gt;API Keys 页新建 key，&lt;code&gt;gsk_...&lt;/code&gt; 开头&lt;/li&gt;
&lt;li&gt;免费额度足够日常用，模型用 &lt;code&gt;whisper-large-v3&lt;/code&gt; 中文识别效果不错&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  3.4 改成 launchd 守护进程（开机自启）
&lt;/h3&gt;

&lt;p&gt;前面是前台跑，关终端就挂了。正式用要装成守护：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.cc-connect          &lt;span class="c"&gt;# ⚠️ 必须在含 config.toml 的目录下跑，不能用 --config 参数&lt;/span&gt;
cc-connect daemon &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;成功后：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cc-connect daemon status   &lt;span class="c"&gt;# 查状态&lt;/span&gt;
cc-connect daemon logs &lt;span class="nt"&gt;-f&lt;/span&gt;  &lt;span class="c"&gt;# 流式看日志&lt;/span&gt;
cc-connect daemon restart  &lt;span class="c"&gt;# 改完 config.toml 后必须 restart 才生效&lt;/span&gt;
cc-connect daemon stop     &lt;span class="c"&gt;# 停&lt;/span&gt;
cc-connect daemon uninstall &lt;span class="c"&gt;# 卸载守护&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;daemon restart&lt;/code&gt; 会给主进程发 SIGTERM，cc-connect 自己会清理它 fork 的 claude 子进程，不用手动杀。&lt;/p&gt;




&lt;h2&gt;
  
  
  四、日常使用
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 切模型
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;持久切换&lt;/strong&gt;（桌面 / 手机都能用）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/model opus      # 切 Claude Opus
/model gpt       # 切 GPT-5.4
/model gemini    # 切 Gemini 3.1 Pro
/model free      # 切免费模型
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;一次性切换&lt;/strong&gt;（只这条走新模型）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpt: 写个 Python 快排
gemini: 帮我 Google 一下最新的 React 19 文档
free: 随便聊聊天
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 cc-connect 内置命令
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出当前 project 下所有 session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`/switch &amp;lt;N\&lt;/td&gt;
&lt;td&gt;uuid前缀\&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;/new&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;开一个全新的 session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/reset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重置当前 session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出所有 bot 命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;/resume&lt;/code&gt; 是 Claude Code 内置命令，在 cc-connect 里会被转发给 claude，但 claude 的 resume picker 是交互式的，IM 里用不了。&lt;strong&gt;cc-connect 用的是 &lt;code&gt;/switch&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 桌面端看手机 session（需要同 work_dir）
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;config.toml&lt;/code&gt; 里 &lt;code&gt;work_dir = "/Users/你"&lt;/code&gt;，和桌面端 &lt;code&gt;ccr code&lt;/code&gt; 默认在 home 目录跑对齐后，两边会共用 &lt;code&gt;~/.claude/projects/-Users-xtuul/&lt;/code&gt; 下的 jsonl。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;但有个限制&lt;/strong&gt;：桌面端 &lt;code&gt;/resume&lt;/code&gt; 的 picker &lt;strong&gt;看不到 cc-connect 起的 session&lt;/strong&gt;，因为 cc-connect 写的 jsonl 缺了几个字段（&lt;code&gt;permission-mode&lt;/code&gt; / &lt;code&gt;file-history-snapshot&lt;/code&gt; / &lt;code&gt;last-prompt&lt;/code&gt;），picker 会过滤掉。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;曲线方案&lt;/strong&gt;：用 UUID 直接 resume&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 找最新的手机 session&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; ~/.claude/projects/-Users-xtuul/&lt;span class="k"&gt;*&lt;/span&gt;.jsonl | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;

&lt;span class="c"&gt;# 拿到文件名里的 UUID 前缀&lt;/span&gt;
ccr code &lt;span class="nt"&gt;--resume&lt;/span&gt; &amp;lt;uuid-prefix&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果你常 resume，包两个 shell 函数（列最近 session、按 UUID 前缀 resume）会更丝滑，但不是刚需——靠肉眼挑最新那个 jsonl 文件 → &lt;code&gt;ccr code --resume &amp;lt;前 8 位&amp;gt;&lt;/code&gt; 也能用。这块我自己还没打磨到想写进 &lt;code&gt;.zshrc&lt;/code&gt; 的程度，以后单独写一篇。&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 桌面启动 alias
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ── 常用：直接启动（权限全放行，适合完全信任的环境）&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;cc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions'&lt;/span&gt;

&lt;span class="c"&gt;# ── 启动时直接指定模型（省一次 /model 切换）&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-opus&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model claude-opus-4-7'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-sonnet&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model claude-sonnet-4-6'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-haiku&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model claude-haiku-4-5-20251001'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-gpt&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model gpt'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-codex&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model codex'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-gemini&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model gemini'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-qwen&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model qwen'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-free&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model free'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;cc-nvidia&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr code --allow-dangerously-skip-permissions --model nvidia'&lt;/span&gt;

&lt;span class="c"&gt;# ── 重启守护 / 路由&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;ccc-restart&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cc-connect daemon restart'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;ccr-restart&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ccr restart'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--model &amp;lt;alias&amp;gt;&lt;/code&gt; 传的就是 ALIAS_MAP 的键——Anthropic 系列要用完整 ID（&lt;code&gt;claude-opus-4-7&lt;/code&gt; 这种），其他家用短名（&lt;code&gt;gpt&lt;/code&gt; / &lt;code&gt;gemini&lt;/code&gt; 等）。这样敲 &lt;code&gt;cc-opus&lt;/code&gt; 启动直接就是 Opus 4.7，省一次 &lt;code&gt;/model&lt;/code&gt; 切换。&lt;/p&gt;




&lt;h2&gt;
  
  
  五、踩坑全记录
&lt;/h2&gt;

&lt;p&gt;按被坑顺序排。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 1：&lt;code&gt;gpt is not a valid model ID&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：在 claude 里 &lt;code&gt;/model gpt&lt;/code&gt;，回 &lt;code&gt;Model may not exist&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：cc-connect 没把请求转到 CCR，直接原样发给 Anthropic API，Anthropic 当然不认识 &lt;code&gt;gpt&lt;/code&gt; 这个模型。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;根因&lt;/strong&gt;：&lt;code&gt;config.toml&lt;/code&gt; 没配 &lt;code&gt;router_url&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[projects.agent.options]&lt;/span&gt;
  &lt;span class="py"&gt;router_url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:3456"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cc-connect 会自动给 claude 子进程设 &lt;code&gt;ANTHROPIC_BASE_URL=&amp;lt;router_url&amp;gt;&lt;/code&gt; 和 &lt;code&gt;NO_PROXY=127.0.0.1&lt;/code&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 2：&lt;code&gt;/login is not a cc-connect command&lt;/code&gt; / &lt;code&gt;/login isn't available in this environment&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：手机发消息，bot 回「Not logged in · Please run /login」；发 &lt;code&gt;/login&lt;/code&gt; 又回「isn't available」。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：claude 子进程启动时发现没 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;，进了登录流程。&lt;code&gt;/login&lt;/code&gt; 是交互式命令，IM 里跑不了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;根因&lt;/strong&gt;：&lt;code&gt;config.toml&lt;/code&gt; 里 &lt;code&gt;router_api_key&lt;/code&gt; 留空了（或注释掉了）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;code&gt;router_api_key&lt;/code&gt; 有&lt;strong&gt;双重作用&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;如果 CCR 开了 APIKEY 校验，这个值要对上&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cc-connect 会把它当作 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; 注入给 claude 子进程&lt;/strong&gt; —— 空值时就不注入 → claude 找不到 key → 弹登录&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：哪怕 CCR 没开校验，也要填个 dummy：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;router_api_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"dummy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  坑 3：&lt;code&gt;Voice message requires ffmpeg for format conversion&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：发语音消息，bot 回 ffmpeg 报错。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;[speech] enabled = true&lt;/code&gt; 只是打开语音识别的开关，转码还是要本地的 ffmpeg。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：&lt;code&gt;brew install ffmpeg&lt;/code&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 4：&lt;code&gt;config.toml not found in /Users/xtuul&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;cc-connect daemon install&lt;/code&gt; 在任意目录跑，报 config 找不到。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;daemon install&lt;/code&gt; 固定从 &lt;strong&gt;CWD&lt;/strong&gt; 找 &lt;code&gt;config.toml&lt;/code&gt;，不是从 &lt;code&gt;~/.cc-connect/&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.cc-connect &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cc-connect daemon &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;不要用 &lt;code&gt;cc-connect --config ~/.cc-connect/config.toml daemon install&lt;/code&gt;&lt;/strong&gt;—— &lt;code&gt;--config&lt;/code&gt; 参数会让它直接切成&lt;strong&gt;前台运行模式&lt;/strong&gt;，不会安装守护。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 5：&lt;code&gt;cc-connect deamon restart&lt;/code&gt;（打错字）→ Telegram getUpdates 冲突
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;deamon&lt;/code&gt;（少一个 &lt;code&gt;a&lt;/code&gt;），执行后终端开始刷日志：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conflict: terminated by other getUpdates request
Failed to get updates, retrying in 3 seconds...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：cc-connect 不认识 &lt;code&gt;deamon&lt;/code&gt; 这个子命令，&lt;strong&gt;静默回落到前台运行&lt;/strong&gt;。这时：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;launchd 管的 daemon     ← 在连 Telegram
         +
你前台新起的 cc-connect ← 也在连 Telegram
         ↓
    两个都 getUpdates，Telegram API 只允许一个长轮询
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Ctrl+C&lt;/code&gt; 掐掉前台的&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cc-connect daemon status&lt;/code&gt; 确认 daemon 还在&lt;/li&gt;
&lt;li&gt;正确拼写 &lt;code&gt;cc-connect daemon restart&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  坑 6：&lt;code&gt;launchctl bootstrap failed: 5: Input/output error&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;cc-connect daemon restart&lt;/code&gt; 报这个。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：launchd 的老毛病，有时候 bootstrap/bootout 之间的状态不同步。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;：拆成两步&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cc-connect daemon stop
&lt;span class="nb"&gt;sleep &lt;/span&gt;2
cc-connect daemon start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  坑 7：daemon 模式下环境变量不生效（MCP / API key）
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;code&gt;.zshrc&lt;/code&gt; 里 &lt;code&gt;export FOO_API_KEY=...&lt;/code&gt;，桌面端能用，daemon 跑的 cc-connect 里调 MCP 或用这个 key 的 subagent 就 401。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Unix 进程环境变量的继承链：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shell (读 .zshrc) → 你手动启动的程序       ✅ 拿得到
launchd (不读任何 shell 配置) → daemon      ❌ 拿不到
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;launchd 启动的守护进程环境是&lt;strong&gt;空的&lt;/strong&gt;（只有 &lt;code&gt;HOME&lt;/code&gt;、&lt;code&gt;PATH&lt;/code&gt; 等几个基础变量）。&lt;code&gt;.zshrc&lt;/code&gt; / &lt;code&gt;.bashrc&lt;/code&gt; / &lt;code&gt;.profile&lt;/code&gt; 它统统不读。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;修复&lt;/strong&gt;（三选一）：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;直接写死在 &lt;code&gt;~/.claude.json&lt;/code&gt; 的 MCP &lt;code&gt;headers&lt;/code&gt; / &lt;code&gt;env&lt;/code&gt; 里&lt;/td&gt;
&lt;td&gt;最简单，key 跟着配置走&lt;/td&gt;
&lt;td&gt;明文存文件里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;launchctl setenv FOO_API_KEY ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不用改配置&lt;/td&gt;
&lt;td&gt;污染全局 launchd，机器重启失效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用前台模式跑 cc-connect&lt;/td&gt;
&lt;td&gt;env 继承最干净&lt;/td&gt;
&lt;td&gt;关终端就挂，要自己做 tmux/screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;我自己选方案 1，&lt;code&gt;~/.claude.json&lt;/code&gt; 里 MCP 的 key 直接写死。权限范围窄的 key（比如某个 MCP 专用的 API key）blast radius 有限，明文在本地 &lt;code&gt;.json&lt;/code&gt; 可以接受。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 8：&lt;code&gt;/resume&lt;/code&gt; picker 看不到 cc-connect 起的 session
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：桌面端 &lt;code&gt;ccr code&lt;/code&gt; 然后 &lt;code&gt;/resume&lt;/code&gt;，picker 显示 &lt;code&gt;No conversations found&lt;/code&gt;，即便 cc-connect 在同一个 &lt;code&gt;work_dir&lt;/code&gt; 下已经写了几十条 jsonl。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：Claude Code 的 resume picker 会过滤 jsonl，要求有 &lt;code&gt;permission-mode&lt;/code&gt; / &lt;code&gt;file-history-snapshot&lt;/code&gt; / &lt;code&gt;last-prompt&lt;/code&gt; 这几个 marker 字段。cc-connect 写的 jsonl 没这些字段（因为它走的是 stdin/stdout 协议，不是 TUI 启动路径）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;协议不对称&lt;/strong&gt;：cc-connect 的 session 桌面端&lt;strong&gt;能读&lt;/strong&gt;（&lt;code&gt;ccr code --resume &amp;lt;uuid&amp;gt;&lt;/code&gt; 直接用 UUID 指定就行），但&lt;strong&gt;不能列在 picker 里&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;绕路方案&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ccr code --resume &amp;lt;uuid&amp;gt;&lt;/code&gt; 直接指定 UUID（见 §4.3 的 &lt;code&gt;ccrr&lt;/code&gt; 函数）&lt;/li&gt;
&lt;li&gt;或者在手机 cc-connect 里用 &lt;code&gt;/list&lt;/code&gt; + &lt;code&gt;/switch&lt;/code&gt; 切&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  坑 9：&lt;code&gt;/switch&lt;/code&gt; 还是 &lt;code&gt;/resume&lt;/code&gt;？
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;cc-connect 用的是 &lt;code&gt;/switch&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/resume&lt;/code&gt; 会被转发给 claude，但 claude 的 resume 是 TUI 交互（弹选择器），在 IM 里跑不了。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/list              ← 列 session
/switch 3          ← 按列表序号切
/switch e255dc42   ← 按 UUID 前缀切
/switch 工作讨论    ← 按 session 名字切（如果你设了）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  坑 10：Stitch 装的时候 &lt;code&gt;No authenticated account found after setup&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：跑 &lt;code&gt;npx @_davideast/stitch-mcp init&lt;/code&gt;，走到 OAuth 那一步，页面跳 Google 登录成功回来，但命令行里报 「No authenticated account found」。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：那个 init 脚本的 OAuth 流程需要你&lt;strong&gt;另开一个终端&lt;/strong&gt;，手动跑：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth login
gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;完了回原终端按回车。很多人以为浏览器登录完就结束了，其实 gcloud CLI 那头还没存凭证。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;更简单的办法&lt;/strong&gt;：别用 &lt;code&gt;npx stitch-mcp init&lt;/code&gt;，直接在 &lt;code&gt;~/.claude.json&lt;/code&gt; 里配 HTTP MCP，见 §3.5。这样不用 gcloud、不用装 Google Cloud SDK，一步到位。&lt;/p&gt;

&lt;p&gt;如果你已经跑过 &lt;code&gt;npx&lt;/code&gt; 那条路，清理一下：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.stitch-mcp
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.config/gcloud    &lt;span class="c"&gt;# 只在你之前没用 gcloud 时这么干！&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.npm/_npx/&lt;span class="k"&gt;*&lt;/span&gt;        &lt;span class="c"&gt;# 清 npx 缓存&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  坑 11：&lt;code&gt;longContextThreshold&lt;/code&gt; 是 sticky 的吗？
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;误解&lt;/strong&gt;：「一旦某次请求超了阈值切到 longContext 模型，之后整个 session 都用这个模型」。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;真相&lt;/strong&gt;：CCR 是&lt;strong&gt;无状态 / 每条请求独立路由&lt;/strong&gt;的。这次请求 input tokens 15万 → 走 longContext；下一条请求只有 3千 tokens → 回到 default。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;longContextThreshold&lt;/code&gt; 说的是&lt;strong&gt;这次请求发给模型的 input tokens&lt;/strong&gt;（包括 system prompt + 历史 + 当前消息），不是整个上下文窗口。&lt;/p&gt;

&lt;p&gt;对 2026 年的 1M 窗口模型来说，阈值可以开大一点（比如 120k），没必要一超 20k 就换模型。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 12：&lt;code&gt;image&lt;/code&gt; 路由选什么模型？
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;误解&lt;/strong&gt;：&lt;code&gt;google/gemini-3.1-flash-image-preview&lt;/code&gt; 是专门处理图片的，肯定最好。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;真相&lt;/strong&gt;：带 &lt;code&gt;flash-image&lt;/code&gt; 或 &lt;code&gt;imagen&lt;/code&gt; 字样的是&lt;strong&gt;生成模型&lt;/strong&gt;（文字→图片），不是&lt;strong&gt;分析模型&lt;/strong&gt;（图片→文字）。你在 claude 里贴图片问它「这张图什么意思」是要分析模型。&lt;/p&gt;

&lt;p&gt;正确选择（按强弱）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;anthropic/claude-4.7-opus-20260416&lt;/code&gt; ← 最均衡&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;anthropic/claude-4.6-sonnet-20260217&lt;/code&gt; ← 便宜一档&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openai/gpt-5.4-20260305&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;google/gemini-3.1-pro-preview-20260219&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  坑 13：飞书事件订阅模式选错
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：飞书 app_id / app_secret 都对，cc-connect 起来了，但发消息没反应。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;排查&lt;/strong&gt;：cc-connect 日志里看有没有 &lt;code&gt;feishu: bot identified&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：飞书开发者后台「事件与回调」那边要选&lt;strong&gt;长连接模式&lt;/strong&gt;（WebSocket），不能选 HTTP 回调（那个需要公网 URL + 白名单 IP）。&lt;/p&gt;




&lt;h2&gt;
  
  
  六、日常运维 cheatsheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 状态&lt;/span&gt;
cc-connect daemon status

&lt;span class="c"&gt;# 日志（持续）&lt;/span&gt;
cc-connect daemon logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="c"&gt;# 或&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.cc-connect/logs/cc-connect.log

&lt;span class="c"&gt;# 改完 config.toml&lt;/span&gt;
cc-connect daemon restart
&lt;span class="c"&gt;# 失败的话&lt;/span&gt;
cc-connect daemon stop &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;2 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cc-connect daemon start

&lt;span class="c"&gt;# 找卡死的 claude 子进程&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-fa&lt;/span&gt; &lt;span class="s2"&gt;"claude.*cc-connect, a bridge"&lt;/span&gt;

&lt;span class="c"&gt;# 极端情况强杀&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"claude.*cc-connect, a bridge"&lt;/span&gt; | xargs &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt;
cc-connect daemon restart

&lt;span class="c"&gt;# CCR&lt;/span&gt;
ccr start              &lt;span class="c"&gt;# 前台跑 CCR&lt;/span&gt;
ccr restart            &lt;span class="c"&gt;# 重启 CCR（配置改完后用）&lt;/span&gt;
ccr code               &lt;span class="c"&gt;# 用 CCR 启动桌面端 claude&lt;/span&gt;
ccr code &lt;span class="nt"&gt;--resume&lt;/span&gt; &amp;lt;uuid-prefix&amp;gt;  &lt;span class="c"&gt;# 按 UUID 恢复 session&lt;/span&gt;
ccr code &lt;span class="nt"&gt;--model&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;         &lt;span class="c"&gt;# 启动时直接指定模型&lt;/span&gt;

&lt;span class="c"&gt;# 快捷重启&lt;/span&gt;
ccc-restart            &lt;span class="c"&gt;# = cc-connect daemon restart&lt;/span&gt;
ccr-restart            &lt;span class="c"&gt;# = ccr restart&lt;/span&gt;

&lt;span class="c"&gt;# OpenRouter 余额&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk-or-v1-..."&lt;/span&gt; https://openrouter.ai/api/v1/auth/key | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  七、几个值得提的设计哲学
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;分层的路由&lt;/strong&gt;：cc-connect（协议层）/ claude（agent 层）/ CCR（路由层）/ OpenRouter（聚合层）。每一层只做自己的事，出问题能精确定位。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;无状态的模型切换&lt;/strong&gt;：CCR 的 Router 是&lt;strong&gt;每个请求独立决策&lt;/strong&gt;的，&lt;code&gt;longContextThreshold&lt;/code&gt; 不是 sticky 的。想要 sticky 就用 &lt;code&gt;/model &amp;lt;alias&amp;gt;&lt;/code&gt;（让 claude 每次都把这个 model 字段发过来）。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;session 互通但 picker 不互通&lt;/strong&gt;：底层 jsonl 文件是共享的（靠 &lt;code&gt;work_dir&lt;/code&gt; 对齐），但各自的 UI 读 jsonl 时有自己的过滤规则。接受这个现实，用 &lt;code&gt;/list&lt;/code&gt; + &lt;code&gt;/switch&lt;/code&gt; + &lt;code&gt;ccr code --resume &amp;lt;uuid&amp;gt;&lt;/code&gt; 绕过。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;launchd ≠ shell&lt;/strong&gt;：守护进程的 env 是一张白纸。配置敏感信息要么写死在 json/toml，要么走 &lt;code&gt;launchctl setenv&lt;/code&gt;，不要指望 &lt;code&gt;.zshrc&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenRouter 分账号跑免费模型&lt;/strong&gt;：主账号付费 key（我叫 &lt;code&gt;or&lt;/code&gt;）跑付费模型，另起一个 &lt;code&gt;or1&lt;/code&gt; provider 专门跑 &lt;code&gt;:free&lt;/code&gt; 模型。这样 rate limit 不打架，配额也清晰。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  八、结语
&lt;/h2&gt;

&lt;p&gt;这套东西跑通之后，日常使用体验是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;地铁上想起个 bug，掏手机飞书对 bot 说一句 &lt;code&gt;gpt: 帮我看下 xxx.py 的 rate limit 逻辑&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;它在你家台式机上实际跑（用你家的 OpenRouter 配额、家里机器上所有已装好的工具链）&lt;/li&gt;
&lt;li&gt;晚上回家敲 &lt;code&gt;cc-opus&lt;/code&gt; 继续，手机上那个 session 用 UUID 一条命令恢复&lt;/li&gt;
&lt;li&gt;语音输入 → Groq Whisper 转文字 → 一样跑&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总体时间成本：从零到跑通大概一个下午，踩过上面 13 个坑之后会很顺。&lt;/p&gt;

&lt;p&gt;完。&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文基于 2026/04 时点的 cc-connect、claude-code-router、Claude Code 版本整理。未来版本可能会修掉其中一些坑（尤其是 resume picker 不认 cc-connect session、router_api_key 必须非空这类），但架构层面的东西应该不会变。&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>从零搭建个人技术博客 · 篇三：用 Claude Code 的 skill 和 command 接管博客增删改查</title>
      <dc:creator>lizhaopeng-cn</dc:creator>
      <pubDate>Fri, 24 Apr 2026 16:31:21 +0000</pubDate>
      <link>https://forem.com/lizhaopengcn/cong-ling-da-jian-ge-ren-ji-zhu-bo-ke-pian-san-yong-claude-code-de-skill-he-command-jie-guan-bo-ke-zeng-shan-gai-cha-3lm0</link>
      <guid>https://forem.com/lizhaopengcn/cong-ling-da-jian-ge-ren-ji-zhu-bo-ke-pian-san-yong-claude-code-de-skill-he-command-jie-guan-bo-ke-zeng-shan-gai-cha-3lm0</guid>
      <description>&lt;p&gt;这是"从零搭建个人技术博客"系列的第三篇。前两篇讲了怎么把站点跑起来、怎么把文章自动分发到 dev.to / Hashnode；这一篇往回退一步，聊写作流本身 —— 写/改/删一篇博客的动作太零碎了，想把它自动化掉。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本篇目标&lt;/strong&gt;：用 Claude Code 的 &lt;strong&gt;slash command&lt;/strong&gt; + &lt;strong&gt;skill&lt;/strong&gt; 给 &lt;code&gt;blog.xtuul.com&lt;/code&gt; 装一个"AI 管家"，任何目录下都能敲：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/blog new &lt;span class="s2"&gt;"pnpm workspace 踩坑"&lt;/span&gt;
/blog list 只看草稿
/blog edit setup-astro-cloudflare-astropaper 把踩坑第 4 条扩展一下
/blog delete some-old-post
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;不在本篇&lt;/strong&gt;：主站搭建（篇一）和跨平台分发（篇二）。&lt;/p&gt;

&lt;h2&gt;
  
  
  目录
&lt;/h2&gt;

&lt;h2&gt;
  
  
  为什么不用脚本
&lt;/h2&gt;

&lt;p&gt;最朴素的方案是写几个 bash 脚本：&lt;code&gt;new-post.sh&lt;/code&gt;、&lt;code&gt;list-posts.sh&lt;/code&gt;、&lt;code&gt;delete-post.sh&lt;/code&gt;。能用，但对我来说有三个硬伤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;frontmatter 的写入仍然要我自己想&lt;/strong&gt;。脚本能填日期、slug、author，但 &lt;code&gt;title&lt;/code&gt; / &lt;code&gt;tags&lt;/code&gt; / &lt;code&gt;description&lt;/code&gt; 这几个字段还是得我自己琢磨。AI 可以顺手把这几件事一起做了。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;自然语言参数处理不了&lt;/strong&gt;。我说"把篇一踩坑第 4 条扩展一下"，脚本怎么知道第 4 条在哪？Claude 能自己读文件理解结构。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;跨会话上下文。&lt;/strong&gt; 脚本不记得"我的博客在哪个目录、用什么 schema、风格是什么"；Claude Code 的 skill 可以把这些写进 &lt;code&gt;SKILL.md&lt;/code&gt;，每次触发自动加载。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以这次直接在 Claude Code 里做。&lt;/p&gt;

&lt;h2&gt;
  
  
  Slash command vs Skill
&lt;/h2&gt;

&lt;p&gt;开动之前先理清两个概念。Claude Code 里能扩展命令的有两样：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Slash command&lt;/th&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;位置&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.claude/commands/xxx.md&lt;/code&gt; 或 &lt;code&gt;.claude/commands/xxx.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.claude/skills/xxx/SKILL.md&lt;/code&gt; 或 &lt;code&gt;.claude/skills/xxx/SKILL.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;触发&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;只能&lt;/strong&gt;显式 &lt;code&gt;/xxx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/xxx&lt;/code&gt; &lt;strong&gt;或&lt;/strong&gt; Claude 根据 description 自动匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结构&lt;/td&gt;
&lt;td&gt;单文件&lt;/td&gt;
&lt;td&gt;文件夹，可带 &lt;code&gt;templates/&lt;/code&gt; &lt;code&gt;references/&lt;/code&gt; &lt;code&gt;examples/&lt;/code&gt; &lt;code&gt;scripts/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长度&lt;/td&gt;
&lt;td&gt;适合一段短 prompt&lt;/td&gt;
&lt;td&gt;适合长 playbook（官方推荐 &amp;lt;2000 词）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组合&lt;/td&gt;
&lt;td&gt;一个命令可以调用 skill&lt;/td&gt;
&lt;td&gt;skill 可以调用其他 skill&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;简单说&lt;/strong&gt;：skill 是 command 的超集。新写东西默认选 skill，除非只是"给我贴一段固定 prompt"。&lt;/p&gt;

&lt;p&gt;但 &lt;code&gt;/blog new&lt;/code&gt; 这种形态&lt;strong&gt;必须&lt;/strong&gt;由 command 提供 —— skill 的 slash 名就是它自己的 &lt;code&gt;name&lt;/code&gt;（比如 &lt;code&gt;/blog-new&lt;/code&gt;），没法拆成 &lt;code&gt;/blog&lt;/code&gt; + 子命令。所以最终方案是 &lt;strong&gt;command 做路由 + 四个 skill 做实现&lt;/strong&gt;。&lt;/p&gt;

&lt;h2&gt;
  
  
  整体架构
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                      用户输入 /blog &amp;lt;sub&amp;gt; [args]
                                │
                                ▼
            ┌──────────────────────────────────────┐
            │ ~/.claude/commands/blog.md （路由）  │
            │  - 解析 $ARGUMENTS 第一个 token      │
            │  - 分派到对应 skill                  │
            └────────────────┬─────────────────────┘
                             │
       ┌──────────┬──────────┼──────────┬──────────┐
       ▼          ▼          ▼          ▼
   blog-new   blog-list  blog-edit  blog-delete
       │          │          │          │
       └──────────┴────┬─────┴──────────┘
                       │ 按需加载
                       ▼
       ~/.claude/skills/blog-shared/
         ├── templates/post.md
         ├── references/frontmatter-schema.md
         └── examples/reference-article.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;blog.md&lt;/code&gt;&lt;/strong&gt;（command）只做一件事：看 &lt;code&gt;$ARGUMENTS&lt;/code&gt; 第一个词，转发给对应 skill。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;四个 skill&lt;/strong&gt; 各自是独立的 playbook，互不干扰。&lt;code&gt;edit&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt; 在 slug 为空时会调用 &lt;code&gt;blog-list&lt;/code&gt;（skill 之间可以互相调）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;blog-shared&lt;/code&gt;&lt;/strong&gt; 不是 skill，而是&lt;strong&gt;共享资源目录&lt;/strong&gt;，放 frontmatter schema、正文模板、风格范文。所有 skill 都&lt;strong&gt;按需&lt;/strong&gt;读它，不常驻上下文。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  实现：command 薄壳
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/commands/blog.md&lt;/code&gt; 是一个带 YAML frontmatter 的 Markdown：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;new|list|edit|delete&amp;gt; [参数或自由文本]&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xtuul-blog 博客文章增删改查入口。按第一个词分派到对应 skill。&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Write, Edit, Bash, Glob, Grep&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# /blog 路由&lt;/span&gt;

用户输入：&lt;span class="sb"&gt;`$ARGUMENTS`&lt;/span&gt;

&lt;span class="gu"&gt;## 解析规则&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; 把 &lt;span class="sb"&gt;`$ARGUMENTS`&lt;/span&gt; 按&lt;span class="gs"&gt;**第一个空白**&lt;/span&gt;切分：
&lt;span class="p"&gt;   -&lt;/span&gt; 第一个 token → &lt;span class="gs"&gt;**子命令**&lt;/span&gt;（必须是 new/list/edit/delete 之一）
&lt;span class="p"&gt;   -&lt;/span&gt; 剩余全部文本 → &lt;span class="gs"&gt;**自由参数**&lt;/span&gt;，&lt;span class="gs"&gt;**原样**&lt;/span&gt;透传给对应 skill
&lt;span class="p"&gt;
2.&lt;/span&gt; 子命令和参数之间只用空格，不识别 &lt;span class="sb"&gt;`:`&lt;/span&gt; &lt;span class="sb"&gt;`|`&lt;/span&gt; &lt;span class="sb"&gt;`-`&lt;/span&gt; &lt;span class="sb"&gt;`#`&lt;/span&gt; 等分隔符。
   含空格的标题/slug 用双引号包裹。

&lt;span class="gu"&gt;## 执行&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; new    → 调用 Skill &lt;span class="sb"&gt;`blog-new`&lt;/span&gt;，透传剩余文本
&lt;span class="p"&gt;-&lt;/span&gt; list   → 调用 Skill &lt;span class="sb"&gt;`blog-list`&lt;/span&gt;，透传剩余文本
&lt;span class="p"&gt;-&lt;/span&gt; edit   → 调用 Skill &lt;span class="sb"&gt;`blog-edit`&lt;/span&gt;，透传剩余文本
&lt;span class="p"&gt;-&lt;/span&gt; delete → 调用 Skill &lt;span class="sb"&gt;`blog-delete`&lt;/span&gt;，透传剩余文本
&lt;span class="p"&gt;-&lt;/span&gt; 其他或空 → 输出用法说明，不自行推断
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：不自己设计任何分隔符，只用空格 + 引号 + 自然语言。Claude 自己解析。&lt;/p&gt;

&lt;h2&gt;
  
  
  实现：blog-new skill
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/skills/blog-new/SKILL.md&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blog-new&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This skill should be used when the user asks to&lt;/span&gt;
  &lt;span class="s"&gt;"write a new blog post", "draft an article", "新开一篇文章",&lt;/span&gt;
  &lt;span class="s"&gt;"起草博客", or invokes `/blog new`. Creates a new markdown&lt;/span&gt;
  &lt;span class="s"&gt;post under /Users/xtuul/projects/xtuul-blog/src/data/blog/&lt;/span&gt;
  &lt;span class="s"&gt;following xtuul-blog's AstroPaper frontmatter schema.&lt;/span&gt;
  &lt;span class="s"&gt;Does not touch git.&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[标题&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;或&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;主题描述]"&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Write, Edit, Bash, Glob, Grep&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# blog-new：起草一篇新文章&lt;/span&gt;

... 工作流 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;description 是 router 的命根子&lt;/strong&gt;。Claude 靠这段决定你说"新写一篇博客"要不要触发这个 skill。官方文档里强调三点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;第三人称&lt;/strong&gt;：&lt;code&gt;This skill should be used when...&lt;/code&gt;，不用 &lt;code&gt;You should...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;具体触发短语&lt;/strong&gt;：把用户会怎么说列出来&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;中英文并列&lt;/strong&gt;：触发短语中英文都写，命中率更高&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;body 里按&lt;strong&gt;祈使句&lt;/strong&gt;写步骤：确定标题 → 生成 slug → 写 frontmatter → 写正文 → 写入文件 → 汇报。参数为空时&lt;strong&gt;追问&lt;/strong&gt;用户"想写什么"，而不是自己编。&lt;/p&gt;

&lt;h2&gt;
  
  
  实现：blog-list / edit / delete
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;blog-list&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;Glob src/data/blog/**/*.md&lt;/code&gt;，读每个文件前 30 行 frontmatter，按 &lt;code&gt;pubDatetime&lt;/code&gt; 降序出表格。支持自然语言过滤（"只看草稿"、"标签 astro"）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;blog-edit&lt;/code&gt;&lt;/strong&gt;：第一个 token 作 slug，剩余作修改意图。slug 为空时&lt;strong&gt;调用 &lt;code&gt;blog-list&lt;/code&gt;&lt;/strong&gt; 让用户选 —— skill 之间互相调用是合法的，而且复用现成逻辑最干净。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;blog-delete&lt;/code&gt;&lt;/strong&gt;：&lt;strong&gt;强制二次确认&lt;/strong&gt;。用户必须原样打出 &lt;code&gt;确认删除 &amp;lt;slug&amp;gt;&lt;/code&gt; 才执行，回 &lt;code&gt;y&lt;/code&gt;/&lt;code&gt;是&lt;/code&gt;/&lt;code&gt;删&lt;/code&gt; 一概不算。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  为什么 delete 要这么死板
&lt;/h3&gt;

&lt;p&gt;删除不走 git（本仓库 push 之前都在本地）。文件一旦 &lt;code&gt;rm&lt;/code&gt; 就没了。&lt;br&gt;
&lt;code&gt;y&lt;/code&gt; 太容易手滑打出来，&lt;strong&gt;原样复述 slug&lt;/strong&gt; 才能保证用户真的看清了要删哪篇。&lt;/p&gt;
&lt;h2&gt;
  
  
  共享资源：blog-shared
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;blog-shared&lt;/code&gt; 不是 skill —— 没有 &lt;code&gt;SKILL.md&lt;/code&gt;，Claude 不会主动扫描它。它是普通文件夹：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/skills/blog-shared/
├── templates/post.md                # 带 {{...}} 占位符的正文骨架
├── references/frontmatter-schema.md # 字段硬规则（必填、格式、示例）
└── examples/reference-article.md    # draft: true 的风格范文
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;在各 skill 的 &lt;code&gt;## Additional Resources&lt;/code&gt; 小节里&lt;strong&gt;显式引用&lt;/strong&gt;绝对路径：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Additional Resources&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`~/.claude/skills/blog-shared/references/frontmatter-schema.md`&lt;/span&gt; — frontmatter 字段硬规则
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`~/.claude/skills/blog-shared/templates/post.md`&lt;/span&gt; — 正文骨架模板
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`~/.claude/skills/blog-shared/examples/reference-article.md`&lt;/span&gt; — 风格范文
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude 触发 skill 时，只加载 SKILL.md body；&lt;strong&gt;需要时&lt;/strong&gt;才 &lt;code&gt;Read&lt;/code&gt; 上面这三个资源。这就是官方说的 &lt;strong&gt;progressive disclosure&lt;/strong&gt;：主文件轻量化，详细内容按需加载。&lt;/p&gt;

&lt;p&gt;好处很实在：改 schema 只改一个文件，四个 skill 自动跟着更新；改风格范文也不会污染真实的 &lt;code&gt;src/data/blog/&lt;/code&gt;。&lt;/p&gt;

&lt;h2&gt;
  
  
  项目级 vs 用户级
&lt;/h2&gt;

&lt;p&gt;Claude Code 支持两个位置：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;th&gt;作用域&lt;/th&gt;
&lt;th&gt;何时用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;repo&amp;gt;/.claude/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅该仓库工作目录下生效&lt;/td&gt;
&lt;td&gt;和仓库强耦合的命令；团队共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;全局，任意目录都能用&lt;/td&gt;
&lt;td&gt;个人工具、跨仓库可复用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;两份同名会&lt;strong&gt;项目级覆盖用户级&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;我最开始放在项目级，结果发现两个问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;必须 &lt;code&gt;cd ~/projects/xtuul-blog &amp;amp;&amp;amp; claude&lt;/code&gt; 才能用 &lt;code&gt;/blog&lt;/code&gt;，在桌面/下载目录里想顺手开一篇博客就不行。&lt;/li&gt;
&lt;li&gt;GitHub 仓库里有 &lt;code&gt;.claude/&lt;/code&gt; 目录会被当成项目文件同步，别的设备或者协作者 clone 下来就自动启用了，不符合"这是我个人的工具"的定位。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以搬到了用户级 &lt;code&gt;~/.claude/&lt;/code&gt;，并把 SKILL.md 里所有 &lt;code&gt;src/data/blog/...&lt;/code&gt; 相对路径&lt;strong&gt;全部改成绝对路径&lt;/strong&gt; &lt;code&gt;/Users/xtuul/projects/xtuul-blog/src/data/blog/...&lt;/code&gt;。代价是这套 skill 只能我这台机器用，但对我来说就是自己用，刚好。&lt;/p&gt;

&lt;h2&gt;
  
  
  踩过的坑
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/blog&lt;/code&gt; 首次调用报 Unknown command&lt;/strong&gt;。Claude Code 只在&lt;strong&gt;会话启动时&lt;/strong&gt;扫描 commands 和 skills 目录。新建/搬迁文件后，&lt;strong&gt;必须重启 Claude Code&lt;/strong&gt; 才能识别，&lt;code&gt;/reload&lt;/code&gt; 之类的软重载不够。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;项目级和用户级同名冲突&lt;/strong&gt;。我忘了删项目级的旧版，新会话里仍然跑的是旧的相对路径 SKILL.md。删掉项目级的 &lt;code&gt;.claude/&lt;/code&gt; 后正常。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;description 写得像文档就失效&lt;/strong&gt;。第一版我写了 &lt;code&gt;用于管理 xtuul-blog 的博客文章&lt;/code&gt;，Claude 根本不匹配自然语言请求。改成 &lt;code&gt;This skill should be used when the user asks to "write a new blog post", "新开一篇文章", or invokes /blog new&lt;/code&gt; 之后命中率立马上去。&lt;strong&gt;description 是触发条件而不是文档&lt;/strong&gt;，越具体越好。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;skill body 超 2000 词后 Claude 抓不住重点&lt;/strong&gt;。第一版 &lt;code&gt;blog-new&lt;/code&gt; 把 schema 字段表、风格约束全塞进去，400 多行。后来按官方推荐拆成 &lt;code&gt;SKILL.md&lt;/code&gt;（只写流程）+ &lt;code&gt;references/frontmatter-schema.md&lt;/code&gt;（字段表）+ &lt;code&gt;templates/post.md&lt;/code&gt;（骨架），主文件压到 100 多行，生成质量明显好转。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;skill 内的路径必须是绝对路径&lt;/strong&gt;。只要没法保证用户一直在某个 CWD，&lt;strong&gt;相对路径都是坑&lt;/strong&gt;。我在 SKILL.md 里硬编码了 &lt;code&gt;/Users/xtuul/projects/xtuul-blog/&lt;/code&gt;，同时在每个 skill 开头加了一段"路径（硬编码绝对路径，不依赖 CWD）"说明 —— 未来换机器要改的地方集中在一处。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  小结
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;架构&lt;/strong&gt;：&lt;code&gt;commands/blog.md&lt;/code&gt; 做&lt;strong&gt;路由薄壳&lt;/strong&gt;，四个独立 skill 做&lt;strong&gt;实现&lt;/strong&gt;，一个 &lt;code&gt;blog-shared&lt;/code&gt; 目录放&lt;strong&gt;共享资源&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;描述&lt;/strong&gt;：skill 的 &lt;code&gt;description&lt;/code&gt; 用第三人称 + 具体中英文触发短语，直接决定 router 命中率&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;篇幅&lt;/strong&gt;：SKILL.md 控制在 &amp;lt;2000 词，详细内容走 &lt;code&gt;references/&lt;/code&gt; 和 &lt;code&gt;templates/&lt;/code&gt;，按需加载&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;路径&lt;/strong&gt;：全局 skill 里一律用&lt;strong&gt;绝对路径&lt;/strong&gt;，不依赖当前工作目录&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;不碰 git&lt;/strong&gt;：所有 skill 都禁 &lt;code&gt;git&lt;/code&gt; 命令，最终 push 前人工 review —— 博客的每一次提交都值得亲眼看一遍&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下一步想把 &lt;code&gt;blog-new&lt;/code&gt; 再增强一下：支持传入&lt;strong&gt;参考文章 URL&lt;/strong&gt; 或&lt;strong&gt;本地 markdown 路径&lt;/strong&gt;，Claude 读完后再写。这样写技术选型类文章时就不用我一段段复制粘贴上下文。&lt;/p&gt;

&lt;p&gt;至此系列三篇闭环了：&lt;strong&gt;篇一搭主站，篇二接分发，篇三管写作流&lt;/strong&gt;。以后写一篇博客就是打开任意终端敲 &lt;code&gt;/blog new 主题&lt;/code&gt; → Claude 起稿 → 我修 → &lt;code&gt;git push&lt;/code&gt; → workflow 自动同步到所有海外平台。整条链路零运维、按需生效，对一个人博客来说刚刚好。&lt;/p&gt;

</description>
      <category>blog</category>
      <category>claudecode</category>
      <category>skill</category>
      <category>automation</category>
    </item>
    <item>
      <title>从零搭建个人技术博客 · 篇二：跨平台自动分发</title>
      <dc:creator>lizhaopeng-cn</dc:creator>
      <pubDate>Fri, 24 Apr 2026 11:52:15 +0000</pubDate>
      <link>https://forem.com/lizhaopengcn/cong-ling-da-jian-ge-ren-ji-zhu-bo-ke-pian-er-kua-ping-tai-zi-dong-fen-fa-5e5h</link>
      <guid>https://forem.com/lizhaopengcn/cong-ling-da-jian-ge-ren-ji-zhu-bo-ke-pian-er-kua-ping-tai-zi-dong-fen-fa-5e5h</guid>
      <description>&lt;p&gt;篇一是主站（&lt;code&gt;blog.xtuul.com&lt;/code&gt;）本身的搭建，篇三是写作流（&lt;code&gt;/blog new&lt;/code&gt; 这套 skill）。这一篇填篇二：&lt;strong&gt;文章写完 push 上去之后，自动同步到其他平台&lt;/strong&gt;，不用我手动复制粘贴。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本篇讲什么&lt;/strong&gt;：一个具体的 GitHub Actions + TypeScript 小工具，push 到 main 就把 &lt;code&gt;src/data/blog/*.md&lt;/code&gt; 发到 dev.to、Hashnode、博客园，首发拿到平台 id，之后再推就是 update 而不是重发。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本篇不讲什么&lt;/strong&gt;：具体某个平台的 API 怎么接（RTFM 的活），以及为什么最后&lt;strong&gt;国内平台一个没留&lt;/strong&gt;。最后一节会单独讲这个。&lt;/p&gt;

&lt;h2&gt;
  
  
  目录
&lt;/h2&gt;

&lt;h2&gt;
  
  
  要解决的问题
&lt;/h2&gt;

&lt;p&gt;主站在 Cloudflare Pages 上，但一篇文章想被人看到，光靠 Google 搜索和主站 RSS 远远不够。常见做法有三种：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;只在主站写&lt;/strong&gt;，其他平台空着——SEO 和流量都吃亏&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;每篇写完手动复制粘贴到其他平台&lt;/strong&gt;——第 3 篇之后我就会放弃&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;写完一次 push，代码自动同步&lt;/strong&gt;——这篇要做的事&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;关键约束：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;主站是 canonical&lt;/strong&gt;：dev.to / Hashnode 都支持声明 &lt;code&gt;canonical_url&lt;/code&gt;，明确告诉搜索引擎"原文在 blog.xtuul.com"，这样即使文章被多平台收录，SEO 权重也不会被稀释&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;幂等&lt;/strong&gt;：同一篇文章推两次不能变成两篇。要走"首发→拿到 id→以后用 id update"的路径&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;一个平台挂不阻塞别的&lt;/strong&gt;：博客园风控拦了不能让 dev.to 也发不出去&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  整体架构
&lt;/h2&gt;

&lt;p&gt;先把地图画出来：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────────────────────────┐
│ 本地写作：/blog new → src/data/blog/&amp;lt;slug&amp;gt;.md             │
│                                                           │
│                    git push origin main                   │
│                            │                              │
└────────────────────────────┼──────────────────────────────┘
                             │
                             ▼
┌───────────────────────────────────────────────────────────┐
│ GitHub Actions: .github/workflows/syndicate.yml           │
│                                                           │
│   on.push.paths: [src/data/blog/**/*.md]                  │
│     │                                                     │
│     ▼                                                     │
│   pnpm dlx tsx scripts/crosspost/index.ts                 │
│     │                                                     │
│     │──► diff HEAD~1..HEAD 或 workflow_dispatch 入参     │
│     │──► 对每篇改动的 .md：                              │
│     │     ├─ load frontmatter                             │
│     │     ├─ if draft: skip                               │
│     │     ├─ for each platform (devto/hashnode/cnblogs): │
│     │     │    ├─ 已有 id → update                       │
│     │     │    └─ 无 id   → create, 拿 id+url           │
│     │     └─ 写回 frontmatter.crosspost.&amp;lt;platform&amp;gt;       │
│     └──► git add / commit / push [skip ci]               │
└───────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;代码都在 &lt;code&gt;scripts/crosspost/&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scripts/crosspost/
├── index.ts              # 入口，解析 diff，调度每个 publisher
├── lib/
│   ├── frontmatter.ts    # 手写的极简 YAML parse/stringify
│   └── types.ts          # Post / Publisher / PublishResult 接口
└── platforms/
    ├── devto.ts          # POST /api/articles
    ├── hashnode.ts       # GraphQL mutation publishPost / updatePost
    └── cnblogs.ts        # MetaWeblog XML-RPC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  设计决定与权衡
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 文件就是状态，没有外部数据库
&lt;/h3&gt;

&lt;p&gt;frontmatter 里直接加一个 &lt;code&gt;crosspost&lt;/code&gt; 字段：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;crosspost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;devto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3544836&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/lizhaopengcn/xxx"&lt;/span&gt;
  &lt;span class="na"&gt;hashnode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;69eb1b54bada4a44e9c589e2&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://xtuul.hashnode.dev/xxx"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第一次推文章：三个字段都不存在 → 脚本走 create → 拿到 id 和 url 写回 frontmatter → auto-commit 回 &lt;code&gt;main&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第二次推同一篇：frontmatter 里已经有 id → 脚本走 update，不会产生重复文章。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么不用 SQLite / KV&lt;/strong&gt;：这是个一人博客。加一个外部存储等于多一个要备份、要恢复、要对齐状态的东西。文章本身已经在 git 里了，id 就放旁边，&lt;strong&gt;状态和内容原子地一起 commit&lt;/strong&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  2. canonical URL 固定指向主站
&lt;/h3&gt;

&lt;p&gt;每次发文章时都显式带 &lt;code&gt;canonical_url: https://blog.xtuul.com/posts/&amp;lt;slug&amp;gt;/&lt;/code&gt;，哪怕主站还没上线那篇。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dev.to：支持 &lt;code&gt;canonical_url&lt;/code&gt; 字段，显示"Originally published at blog.xtuul.com"&lt;/li&gt;
&lt;li&gt;Hashnode：&lt;code&gt;originalArticleURL&lt;/code&gt; 字段，行为一样&lt;/li&gt;
&lt;li&gt;博客园：MetaWeblog 没 canonical 这个字段，只能在正文开头手动加一行"原文："（实际上最后没做，原因后面说）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. 每个 publisher 实现同一个接口
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Publisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hashnode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cnblogs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// 环境变量齐了就 enable&lt;/span&gt;
  &lt;span class="nl"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PublishResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;index.ts&lt;/code&gt; 不关心具体平台怎么调 API，只负责：挑出 enabled 的平台、串行跑、把 &lt;code&gt;PublishResult&lt;/code&gt; 写回 frontmatter、最后 git commit。每加一个平台只要写一个新的 &lt;code&gt;platforms/&amp;lt;name&amp;gt;.ts&lt;/code&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  4. per-post 串行、per-platform 独立
&lt;/h3&gt;

&lt;p&gt;一篇文章里三个平台&lt;strong&gt;串行&lt;/strong&gt;发（dev.to → Hashnode → 博客园），每个都单独 try/catch。任意一个失败不阻塞其他平台，也不阻塞下一篇文章。所有结果最后统一打印。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;writeback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✗ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  踩过的坑
&lt;/h2&gt;

&lt;p&gt;真写出来之后，前后一共掉坑里三次。记下来。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 1：Astro 的 &lt;code&gt;z.date()&lt;/code&gt; 不接受带引号的 ISO 字符串
&lt;/h3&gt;

&lt;p&gt;写完代码本地测试通过，push 到 main 之后 Cloudflare Pages &lt;strong&gt;构建挂了&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pubDatetime: Expected type "date", received "string"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;问题出在 "脚本回写 frontmatter" 这一步。Astro content collection 的 schema 里 &lt;code&gt;pubDatetime&lt;/code&gt; 是 &lt;code&gt;z.date()&lt;/code&gt;，它要求 YAML 里是&lt;strong&gt;裸的 timestamp&lt;/strong&gt;，不能是字符串：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 这样 Astro 能识别为 Date&lt;/span&gt;
&lt;span class="na"&gt;pubDatetime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-04-24T19:50:10+08:00&lt;/span&gt;

&lt;span class="c1"&gt;# 这样 Astro 会当 string，schema 直接挂&lt;/span&gt;
&lt;span class="na"&gt;pubDatetime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-24T19:50:10+08:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;我的 YAML stringify 函数 "遇到包含 &lt;code&gt;:&lt;/code&gt; 或其他特殊字符的字符串就加引号"，ISO 时间戳正好命中。修法是&lt;strong&gt;给日期类字段开白名单&lt;/strong&gt;，裸输出：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DATETIME_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pubDatetime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;modDatetime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DATETIME_KEYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 不加引号&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...其他走通用 stringify&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这种"主站和同步脚本之间有个看不见的契约"的坑，&lt;strong&gt;本地开发过程中完全不会碰到&lt;/strong&gt;，只有 push 之后 Cloudflare 那边才会炸。CI 流水线一定要&lt;strong&gt;在两边都跑一次&lt;/strong&gt;才能发现。&lt;/p&gt;

&lt;h3&gt;
  
  
  坑 2：自己写的 YAML parser 不支持嵌套对象
&lt;/h3&gt;

&lt;p&gt;上面那段 &lt;code&gt;crosspost.devto.id&lt;/code&gt; 是嵌套两层的。一开始我图省事，手写了个 20 行的 YAML parser，只支持"key: value"和列表。结果第二次推文章时：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ devto create: Canonical url has already been taken
✓ hashnode create → xxx-1-1     ← 注意 "-1-1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;脚本&lt;strong&gt;没读到已有的 id&lt;/strong&gt;，当成全新文章再发了一遍。dev.to 靠 canonical 查重、直接 422 拒绝；但 &lt;strong&gt;Hashnode 完全不查重&lt;/strong&gt;，默默给新文章一个 &lt;code&gt;-1&lt;/code&gt;、&lt;code&gt;-1-1&lt;/code&gt; 的递增 slug，看起来"发布成功"，&lt;strong&gt;实际上我的博客上重复文章越堆越多&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;后来把 parser 换成支持递归缩进的版本，才读得出嵌套结构。&lt;strong&gt;教训&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;不要在状态机关上图省事自己造轮子。要省就&lt;strong&gt;连嵌套都别用&lt;/strong&gt;（比如把 &lt;code&gt;crosspost_devto_id&lt;/code&gt; 拍平成一级 key），要嵌套就用 &lt;code&gt;js-yaml&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"默默成功"比"显式失败"可怕得多&lt;/strong&gt;。Hashnode 这种接口设计等于在地雷区里埋了一个脸朝下的地雷。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  坑 3：博客园把 GitHub Actions 的出站 IP 风控了
&lt;/h3&gt;

&lt;p&gt;这是让我最后放弃国内平台的直接原因，值得单拎一节。&lt;/p&gt;

&lt;h2&gt;
  
  
  为什么最后没做国内平台
&lt;/h2&gt;

&lt;p&gt;本来清单上是：&lt;strong&gt;dev.to + Hashnode + 博客园&lt;/strong&gt;。三个平台的 API 都接好了、secret 也配好了、本地 dry-run 全过。推上去跑 workflow：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ devto update → https://dev.to/...
✓ hashnode update → https://xtuul.hashnode.dev/...
✗ cnblogs create: HTTP 500 (empty body)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第一反应：鉴权有问题？XML-RPC 包错了？字段不全？&lt;/p&gt;

&lt;p&gt;挨个验证：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;在本地跑 curl&lt;/strong&gt;，同样的 token、同样的 username、同样的 endpoint，打一条最小的 &lt;code&gt;metaWeblog.newPost&lt;/code&gt; 过去。&lt;strong&gt;200，返回 postid，完美&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;在本地跑脚本&lt;/strong&gt;，用 &lt;code&gt;loadPost&lt;/code&gt; 读真实文章、构造和 Actions 里字节级完全相同的 XML，再 curl 发出去。&lt;strong&gt;又是 200，postid 正常返回&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;回到 Actions，一模一样的代码、一模一样的 secret。&lt;strong&gt;仍然 HTTP 500，body 空&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;排查逻辑很简单——如果鉴权或内容有问题，博客园会返回 &lt;code&gt;&amp;lt;fault&amp;gt;&lt;/code&gt; 带具体 &lt;code&gt;faultString&lt;/code&gt;；400、401、404 也都会有 body 说明原因。&lt;strong&gt;500 + 空 body&lt;/strong&gt; 是非常特殊的组合：请求根本没到业务层，而是在前置的网关/WAF 上就被掐掉了。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rpc.cnblogs.com&lt;/code&gt; 的网关对 &lt;strong&gt;Azure westus 的 IP 段&lt;/strong&gt;做了风控。GitHub Actions 的 runner 正好在那里。对 "Azure IP 对国内内容平台的出站连接" 这件事有过了解的人应该都见过类似剧情——十年来这类平台对海外云的 IP 越来越敏感。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;没有干净的解法&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 runner 上装代理 → 需要一台国内机器当出口，相当于给博客同步这件事凭空加一台要维护的 VPS，&lt;strong&gt;零运维的前提破了&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;换平台官方 SDK → 博客园官方 API 只有 MetaWeblog，没有别的公开协议&lt;/li&gt;
&lt;li&gt;用 Puppeteer 模拟浏览器 → CI 里跑无头 Chrome 要装 100MB 依赖、要在 CI 内完成扫码/验证码，&lt;strong&gt;越想越不值&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;调研了一下其他国内平台（掘金、CSDN、SegmentFault）：要么同样的 IP 风控，要么没有公开 API，抓包出来的接口几个月就会变一次&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我算了下账：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;搭建成本&lt;/th&gt;
&lt;th&gt;维护成本&lt;/th&gt;
&lt;th&gt;稳定性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;加一台国内中转机&lt;/td&gt;
&lt;td&gt;半天&lt;/td&gt;
&lt;td&gt;每月续费 + 偶尔抢救&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Puppeteer 方案&lt;/td&gt;
&lt;td&gt;2~3 天&lt;/td&gt;
&lt;td&gt;平台前端一改就挂，每平台每季度至少修一次&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览器插件（ArtiPub 之流）&lt;/td&gt;
&lt;td&gt;装一下&lt;/td&gt;
&lt;td&gt;低（但&lt;strong&gt;手动点击&lt;/strong&gt;，不算自动化）&lt;/td&gt;
&lt;td&gt;高但不自动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;全部放弃国内平台&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;对一个个人博客来说，&lt;strong&gt;我不做国内平台的"机会成本"&lt;/strong&gt; 是：国内读者来主站（或 dev.to / Hashnode 的英文版）的时候少看到一点入口。可以接受。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;继续做国内平台的"直接成本"&lt;/strong&gt; 是：一台新机器 or 一套会定期 rot 的 Puppeteer 代码。&lt;strong&gt;不能接受&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;所以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scripts/crosspost/platforms/cnblogs.ts&lt;/code&gt; 代码留着，以后哪天有国内机器了直接改 endpoint 就能用&lt;/li&gt;
&lt;li&gt;workflow 里留着 &lt;code&gt;CNBLOGS_*&lt;/code&gt; 的 secret 判断——没配就跳过，&lt;strong&gt;什么都不会打印&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;目前跑 workflow 只会看到 dev.to 和 Hashnode 成功&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  当前成品
&lt;/h2&gt;

&lt;h3&gt;
  
  
  workflow 配置
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/syndicate.yml&lt;/code&gt; 的骨架：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Syndicate&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/data/blog/**/*.md"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;要同步的文件路径（空格分隔），留空=diff"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;syndicate&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;crosspost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-24.04&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;# ... setup pnpm / node&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm dlx tsx scripts/crosspost/index.ts ${{ inputs.files }}&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEVTO_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;HASHNODE_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HASHNODE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;HASHNODE_PUBLICATION_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HASHNODE_PUBLICATION_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;SITE_BASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://blog.xtuul.com/&lt;/span&gt;
      &lt;span class="c1"&gt;# ... auto-commit if frontmatter changed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;关键点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;paths&lt;/code&gt; 过滤让"我只改了 README 或配置"的 push 不触发&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;concurrency&lt;/code&gt; 防止连续两次 push 引起竞态（前一次还没写回 id，后一次又 create 一遍）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch-depth: 2&lt;/code&gt; 才够算 &lt;code&gt;HEAD~1..HEAD&lt;/code&gt; 的 diff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;permissions: contents: write&lt;/code&gt; 才允许回写 commit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  每篇文章的状态
&lt;/h3&gt;

&lt;p&gt;前两篇文章现在的 frontmatter 尾部看起来是这样：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;crosspost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;devto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3544836&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/lizhaopengcn/..."&lt;/span&gt;
  &lt;span class="na"&gt;hashnode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;69eb1b54bada4a44e9c589e2&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://xtuul.hashnode.dev/..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;以后任何一篇我改了正文 push 上去，脚本看到已有 id，走 update 路径，&lt;strong&gt;平台上直接原地更新&lt;/strong&gt;，url 不变、评论不丢。&lt;/p&gt;

&lt;h2&gt;
  
  
  小结
&lt;/h2&gt;

&lt;p&gt;这篇的主题本来应该是"跨平台分发"，实际结果是"跨两个海外平台分发 + 一篇国内平台劝退录"。&lt;/p&gt;

&lt;p&gt;一些可以带走的结论：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;canonical URL 必须显式&lt;/strong&gt;。主站永远是 source of truth，分发只是副本。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;幂等靠 id，不靠 title/slug 做匹配&lt;/strong&gt;。id 写回 frontmatter，和文章原子地一起 commit，不要引入外部状态。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"默默成功"的平台要特别警惕&lt;/strong&gt;。dev.to 的 422 比 Hashnode 的 &lt;code&gt;-1-1&lt;/code&gt; slug 友好得多。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;零运维的门槛是"不需要我再额外养任何一台机器"&lt;/strong&gt;。一旦为了一个副功能要上国内 VPS 或浏览器自动化，整个系统的维护成本结构就变了。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;篇四还没想好，大概会写主站接入 Umami、或者 AstroPaper 主题的几处魔改。&lt;/p&gt;

</description>
      <category>blog</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>从零搭建个人技术博客 · 篇一：Astro + Cloudflare Pages + AstroPaper</title>
      <dc:creator>lizhaopeng-cn</dc:creator>
      <pubDate>Fri, 24 Apr 2026 07:27:15 +0000</pubDate>
      <link>https://forem.com/lizhaopengcn/cong-ling-da-jian-ge-ren-ji-zhu-bo-ke-pian-astro-cloudflare-pages-astropaper-4ib</link>
      <guid>https://forem.com/lizhaopengcn/cong-ling-da-jian-ge-ren-ji-zhu-bo-ke-pian-astro-cloudflare-pages-astropaper-4ib</guid>
      <description>&lt;p&gt;这是"从零搭建个人技术博客"系列的第一篇，记录我把 &lt;code&gt;blog.xtuul.com&lt;/code&gt; 从无到有跑起来的完整过程 —— 技术选型、架构、操作步骤、踩到的坑。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本篇目标&lt;/strong&gt;：用最小的维护成本搭一个&lt;strong&gt;快、干净、免费、有个人品牌感&lt;/strong&gt;的主站。&lt;br&gt;
&lt;strong&gt;不在本篇&lt;/strong&gt;：跨平台自动分发（dev.to / Hashnode / 掘金 / 公众号）—— 放到后续篇章。&lt;/p&gt;
&lt;h2&gt;
  
  
  目录
&lt;/h2&gt;
&lt;h2&gt;
  
  
  整体架构与选型
&lt;/h2&gt;
&lt;h3&gt;
  
  
  最终架构
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;         Claude Code（在本地生成 Markdown）
                       │
                       ▼
          src/data/blog/xxx.md（Git 跟踪）
                       │  push
                       ▼
           GitHub: lizhaopeng-cn/xtuul-blog
                       │  webhook
                       ▼
              Cloudflare Pages（自动构建）
                       │
                       ▼
          blog.xtuul.com（HTTPS + 全球 CDN）
             + Cloudflare Web Analytics（站长后台）
             + 不蒜子（页面可见计数）
             + Giscus（GitHub Discussions 评论）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;一个仓库、一次 push、全自动部署。没有服务器、没有数据库、没有后台。&lt;/p&gt;
&lt;h3&gt;
  
  
  为什么选这套组合
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;选型&lt;/th&gt;
&lt;th&gt;为什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;框架&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Astro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;静态输出、零 JS 默认、支持 MD/MDX、生态比 Hugo 现代、比 Next.js 轻&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主题&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AstroPaper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lighthouse 100、自带明暗主题/搜索/标签/RSS/sitemap、TypeScript + Tailwind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;托管&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;免费、自带 CDN 和 HTTPS、与 Cloudflare DNS 天然融合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;域名本来就在这里，一条 CNAME 都省了&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;包管理&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;pnpm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;磁盘效率和 monorepo 友好；AstroPaper 官方模板就是 pnpm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  排除项（为什么不选它们）
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt; —— 要维护数据库、安全补丁、主机账单。静态博客是一次性工作，没必要。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hexo / VuePress&lt;/strong&gt; —— 能用但生态趋冷，主题更新慢。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; —— 大炮打蚊子。博客不需要 SSR / Server Components。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; —— 也不错，但国内访问不如 Cloudflare 稳，且 DNS 不在一处。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Pages&lt;/strong&gt; —— 无 CDN、自定义域名要额外配置、国内访问慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  前置条件
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;一个顶级域名（我用的 &lt;code&gt;xtuul.com&lt;/code&gt;），&lt;strong&gt;已托管在 Cloudflare&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;本地装好：&lt;code&gt;node&lt;/code&gt; (20+)、&lt;code&gt;pnpm&lt;/code&gt;、&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;gh&lt;/code&gt;（GitHub CLI）&lt;/li&gt;
&lt;li&gt;一个 GitHub 账号，&lt;code&gt;gh auth login&lt;/code&gt; 登录过&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;验证：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;          &lt;span class="c"&gt;# v20 以上&lt;/span&gt;
pnpm &lt;span class="nt"&gt;-v&lt;/span&gt;
git &lt;span class="nt"&gt;--version&lt;/span&gt;
gh auth status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  第一步：用 pnpm 脚手架生成 AstroPaper
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;全程只用 pnpm&lt;/strong&gt;。AstroPaper 官方模板、本地开发、Cloudflare Pages 构建命令都统一到 pnpm，保证只有一份 &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;，后面构建稳定性会省很多事。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects
pnpm create astro@latest xtuul-blog &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template&lt;/span&gt; satnaing/astro-paper &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--skip-houston&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-git&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;参数含义：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--template satnaing/astro-paper&lt;/code&gt; 直接以 AstroPaper 作为起点&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--install&lt;/code&gt; 自动 &lt;code&gt;pnpm install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-git&lt;/code&gt; 先不初始化 git，后面手动推（避免 Astro 默认 commit 污染历史）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--skip-houston&lt;/code&gt; / &lt;code&gt;--yes&lt;/code&gt; 跳过交互式问答&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;完成后：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;xtuul-blog
pnpm dev
&lt;span class="c"&gt;# 浏览器访问 http://localhost:4321/ 应该能看到 AstroPaper 默认主题&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  第二步：品牌化与本地化
&lt;/h2&gt;

&lt;p&gt;AstroPaper 的&lt;strong&gt;全站配置&lt;/strong&gt;集中在 &lt;code&gt;src/config.ts&lt;/code&gt;。把站点信息改成自己的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;website&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://blog.xtuul.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Xtuul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/lizhaopeng-cn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;记录 AI、编程、自动化和个人项目。&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Xtuul Blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ogImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astropaper-og.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lightAndDarkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;postPerIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;postPerPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scheduledPostMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;showArchives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;showBackButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;editPost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dynamicOgImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ltr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Shanghai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;社交链接&lt;/strong&gt;在 &lt;code&gt;src/constants.ts&lt;/code&gt;，默认带了 X / LinkedIn / WhatsApp 等，按需裁剪。我只留 GitHub + 邮箱：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SOCIALS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Social&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GitHub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/lizhaopeng-cn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;linkTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; on GitHub`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IconGitHub&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mailto:xtuul@xtuul.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;linkTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Send an email to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IconMail&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  导航栏中文化
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/components/Header.astro&lt;/code&gt; 里把 &lt;code&gt;Posts / Tags / About / Archives / Search&lt;/code&gt; 改成 &lt;code&gt;文章 / 标签 / 关于 / 归档 / 搜索&lt;/code&gt;，&lt;code&gt;Skip to content&lt;/code&gt; 改成 &lt;code&gt;跳转到正文&lt;/code&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  默认深色模式
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/layouts/Layout.astro&lt;/code&gt; 里找到这段内联脚本，把空字符串改成 &lt;code&gt;"dark"&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialColorScheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 之前是 ""，现在强制首访深色&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lightAndDarkMode: true&lt;/code&gt; 保留，用户点顶部月亮/太阳按钮仍可切换。&lt;/p&gt;

&lt;h3&gt;
  
  
  验证本地构建
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;看到 &lt;code&gt;Finished in Xs&lt;/code&gt; 并且没有红色错误，就说明配置改动都是合法的。&lt;/p&gt;

&lt;h2&gt;
  
  
  第三步：推送到 GitHub
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/xtuul-blog
git init
git add &lt;span class="nt"&gt;-A&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"initial commit: Xtuul Blog on AstroPaper"&lt;/span&gt;

&lt;span class="c"&gt;# 用 gh 一条命令建 public 仓库 + 推送&lt;/span&gt;
gh repo create xtuul-blog &lt;span class="nt"&gt;--public&lt;/span&gt; &lt;span class="nt"&gt;--source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--remote&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;origin &lt;span class="nt"&gt;--push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;public 还是 private？&lt;/strong&gt; 博客内容公开，仓库也建议 public。public 仓库的 GitHub Actions 额度是&lt;strong&gt;无限&lt;/strong&gt;的，private 每月只有 2000 分钟，这在后续加自动分发时会成为实际差别。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  第四步：Cloudflare Pages 首次部署
&lt;/h2&gt;

&lt;p&gt;Dashboard → &lt;strong&gt;Workers &amp;amp; Pages&lt;/strong&gt; → Create → Pages → Connect to Git。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;选仓库 &lt;code&gt;lizhaopeng-cn/xtuul-blog&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Framework preset: &lt;code&gt;Astro&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build command: &lt;code&gt;pnpm install --frozen-lockfile &amp;amp;&amp;amp; pnpm build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build output directory: &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Environment variables（Production）: &lt;code&gt;NODE_VERSION = 22&lt;/code&gt;（纯文本）&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么显式写 pnpm 命令&lt;/strong&gt;：Cloudflare Pages 默认根据 lockfile 探测包管理器，大多数情况会对，但偶尔会 fallback 到 npm。显式写最稳。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么固定 Node 版本&lt;/strong&gt;：构建容器默认 Node 可能是 18，而新版 Astro 要求 20+。指定 22 是目前的 LTS，最保险。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;点 &lt;strong&gt;Save and Deploy&lt;/strong&gt;，1–3 分钟后应该看到绿色 Success。&lt;/p&gt;

&lt;h2&gt;
  
  
  第五步：绑定自定义域名
&lt;/h2&gt;

&lt;p&gt;Pages 项目 → &lt;strong&gt;Custom domains&lt;/strong&gt; → Set up a custom domain → 填 &lt;code&gt;blog.xtuul.com&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;因为域名本来就在 Cloudflare，它会&lt;strong&gt;自动在 DNS 区加一条 CNAME&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blog    CNAME    xtuul-blog.pages.dev    (Proxied)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;等几十秒证书签发完成，&lt;code&gt;https://blog.xtuul.com/&lt;/code&gt; 就活了。&lt;/p&gt;

&lt;h2&gt;
  
  
  以后发布新文章的流程
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;文章存放位置&lt;/strong&gt;：&lt;code&gt;src/data/blog/&lt;/code&gt;（文件名即 URL slug）。&lt;/p&gt;

&lt;h3&gt;
  
  
  标准 frontmatter 模板
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Xtuul&lt;/span&gt;
&lt;span class="na"&gt;pubDatetime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-04-22T10:30:00+08:00&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;文章标题"&lt;/span&gt;
&lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;article-slug&lt;/span&gt;
&lt;span class="na"&gt;featured&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;   &lt;span class="c1"&gt;# true 显示在首页"精选文章"&lt;/span&gt;
&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;      &lt;span class="c1"&gt;# true 不会发布&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tag1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tag2&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;一句话摘要（会出现在文章卡片和 SEO 描述里）&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="s"&gt;正文...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  三步发布
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. 写文章（放到 src/data/blog/xxx.md）&lt;/span&gt;
&lt;span class="c"&gt;# 2. 本地预览（可选）&lt;/span&gt;
pnpm dev

&lt;span class="c"&gt;# 3. 提交推送&lt;/span&gt;
git add src/data/blog/xxx.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"post: 标题"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare Pages 监听到 push，自动构建 + 部署，&lt;strong&gt;从 push 到线上可见通常 2 分钟内&lt;/strong&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  写作期间的小技巧
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;draft: true&lt;/code&gt; → 线上看不到但本地能预览，适合攒稿&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;featured: true&lt;/code&gt; → 首页置顶&lt;/li&gt;
&lt;li&gt;本地改 &lt;code&gt;src/config.ts&lt;/code&gt; / 组件时 &lt;code&gt;pnpm dev&lt;/code&gt; 会热更新&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  用 Claude Code 直接产出 Markdown
&lt;/h2&gt;

&lt;p&gt;上面的发布流程能跑，但&lt;strong&gt;从 0 到一篇成稿还是我自己打字&lt;/strong&gt;。博客更顺手的工作流是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;一句话需求 → Claude Code 写到 src/data/blog/xxx.md → 本地预览 → git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;我写博客的时候直接在仓库根目录开 Claude Code：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/xtuul-blog
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后一句话命题作文：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"写一篇新文章放到 &lt;code&gt;src/data/blog/setup-astro-cloudflare-astropaper.md&lt;/code&gt;，主题是从零搭建个人技术博客，选型是 Astro + Cloudflare Pages + AstroPaper，frontmatter 用 &lt;code&gt;author: Xtuul&lt;/code&gt;、&lt;code&gt;pubDatetime&lt;/code&gt; 用当前时间、&lt;code&gt;featured: true&lt;/code&gt;、tags 给 astro/cloudflare/astropaper/blog/devops。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code 直接落盘成 &lt;code&gt;.md&lt;/code&gt; 文件，本地 &lt;code&gt;pnpm dev&lt;/code&gt; 实时预览，不满意继续对话改（"第二部分太啰嗦，合并到第一部分"），满意了就 commit + push。&lt;/p&gt;

&lt;h3&gt;
  
  
  为什么要这样写，而不是甩一个"帮我写博客"
&lt;/h3&gt;

&lt;p&gt;几点经验：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;文件路径写死&lt;/strong&gt;。明确告诉 Claude 写到哪个文件、slug 是什么，它就会直接 &lt;code&gt;Write&lt;/code&gt; 工具落盘，不会先在对话里生成一坨让你复制。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;frontmatter 所有字段都写死&lt;/strong&gt;。作者、tags、是否 featured、是否 draft。Claude 对 AstroPaper 的 frontmatter schema 没先验知识，不写死它会漏字段或乱填。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;技术选型和事实由你给&lt;/strong&gt;，Claude 负责组织行文和代码块。让 Claude 自由发挥容易出"正确但不是你的"内容——你的博客是写给你自己的受众，口吻、技术偏好、踩坑细节都得自己提供。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;分多轮迭代&lt;/strong&gt;。先让它写大纲，再写某一节正文，再让它把某个代码块换成更简洁的版本。一次性让它写完 2000 字通常质量不如分块。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  工作流的好处
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;不离开编辑器&lt;/strong&gt;：写、改、预览、提交在一个 shell 里完成，不用在对话窗口和编辑器之间来回切。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;可追溯&lt;/strong&gt;：所有改动都是 &lt;code&gt;git diff&lt;/code&gt;，不好回滚就 &lt;code&gt;git checkout --&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;自动化友好&lt;/strong&gt;：以后可以把"发布到 dev.to / Hashnode / 掘金"也交给 Claude Code 跑 MCP 调用外部 API，这是篇二要做的事。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  第六步：接入访问统计与评论
&lt;/h2&gt;

&lt;p&gt;主站能跑之后，马上会想要两件事：&lt;strong&gt;这篇文章有多少人看过&lt;/strong&gt;、&lt;strong&gt;读者能不能留言&lt;/strong&gt;。我接了三个互不重叠的服务：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;需求&lt;/th&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;数据给谁看&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;站长看流量后台（趋势、地理分布、Referer）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Web Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;我，在 CF Dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;页面上可见的计数（页脚总 PV/UV、文章阅读量）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不蒜子&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;所有访客&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;评论系统&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Giscus&lt;/strong&gt;（基于 GitHub Discussions）&lt;/td&gt;
&lt;td&gt;所有访客，数据在我仓库&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;全部免费、零后端。&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Cloudflare Web Analytics
&lt;/h3&gt;

&lt;p&gt;Dashboard → Analytics &amp;amp; Logs → Web Analytics → Add a site → 选 Manual（手动 JS 片段），会拿到一段：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://static.cloudflareinsights.com/beacon.min.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-cf-beacon=&lt;/span&gt;&lt;span class="s"&gt;'{"token": "YOUR_TOKEN"}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;把它放到 &lt;code&gt;src/layouts/Layout.astro&lt;/code&gt; 的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 里（&lt;code&gt;&amp;lt;ClientRouter /&amp;gt;&lt;/code&gt; 之后），&lt;strong&gt;全站自动上报&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;注意：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;后台数据只有自己看&lt;/strong&gt;。访客看不到任何数字，不蒜子才是"前台展示"。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;每个 site 一个独立 token&lt;/strong&gt;。我 &lt;code&gt;xtuul.com&lt;/code&gt; 是橙云走自动统计，&lt;code&gt;blog.xtuul.com&lt;/code&gt; 是 Pages 自带的灰云（Pages 的自定义域都是灰云，这是正常的，不用改橙），走不到自动统计，所以单独为它生成了 token。&lt;/li&gt;
&lt;li&gt;如果你还想统计主站就再加一个 site、给主站页面嵌入对应 token 的片段即可，数据不会互相混。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.2 不蒜子：页面可见计数
&lt;/h3&gt;

&lt;p&gt;不蒜子是国内开发者博客最常用的极简计数器，一个 script + 三个约定的 span id：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- 站点总 PV --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"busuanzi_container_site_pv"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  总访问量 &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"busuanzi_value_site_pv"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt; 次
&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- 站点 UV --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"busuanzi_container_site_uv"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  访客数 &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"busuanzi_value_site_uv"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt; 人
&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- 当前文章 PV --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"busuanzi_container_page_pv"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  阅读量 &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"busuanzi_value_page_pv"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt; 次
&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;脚本只有三行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;三个关键细节&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;容器默认 &lt;code&gt;display:none&lt;/code&gt;&lt;/strong&gt;。不蒜子脚本拿到数据后会把 &lt;code&gt;busuanzi_container_*&lt;/code&gt; 显示出来。如果不默认隐藏，脚本返回前会看到 &lt;code&gt;访客数： 人&lt;/code&gt; 这种裸文案，很丑。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astro ClientRouter 切页脚本不会重跑&lt;/strong&gt;。AstroPaper 用 &lt;code&gt;&amp;lt;ClientRouter /&amp;gt;&lt;/code&gt; 做无刷新页面切换，&lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; 只在首次加载时执行一次，&lt;strong&gt;切换到下一篇文章的 page_pv 不会刷新&lt;/strong&gt;。解决：在 &lt;code&gt;astro:page-load&lt;/code&gt; 事件里重新注入脚本。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;只在 &lt;code&gt;astro:page-load&lt;/code&gt; 里注入，不要在 IIFE 里再调一次&lt;/strong&gt;。否则页面首次加载时首次 IIFE + &lt;code&gt;astro:page-load&lt;/code&gt; 事件会各发一次 JSONP，&lt;strong&gt;PV +2&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最后得到的是这样一段：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script is:inline&amp;gt;
  document.addEventListener("astro:page-load", () =&amp;gt; {
    // 清掉上次注入的脚本标签
    document.querySelectorAll("script[data-busuanzi]")
      .forEach(node =&amp;gt; node.remove());
    const s = document.createElement("script");
    s.src = "https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js";
    s.async = true;
    s.defer = true;
    s.setAttribute("data-busuanzi", "1");
    document.body.appendChild(s);
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;不蒜子的局限（被很多教程模糊带过）&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;只有 &lt;code&gt;page_pv&lt;/code&gt;，没有 &lt;code&gt;page_uv&lt;/code&gt;&lt;/strong&gt;。想知道某篇文章有多少独立访客——不蒜子做不到，只能自己上 Cloudflare Workers + KV。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;本地 &lt;code&gt;localhost:4321&lt;/code&gt; 的数字是全球所有本地开发者共享的&lt;/strong&gt;，动辄几万几十万，&lt;strong&gt;这是正常现象&lt;/strong&gt;，上线到真实域名后从 0 开始计数。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.3 Giscus：基于 GitHub Discussions 的评论
&lt;/h3&gt;

&lt;p&gt;Giscus 把评论直接挂在你仓库的 GitHub Discussions 上。好处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;评论数据在你自己仓库，跟着仓库迁移走&lt;/li&gt;
&lt;li&gt;访客用 GitHub 账号登录，天然过滤机器人&lt;/li&gt;
&lt;li&gt;支持 reaction、Markdown、代码块&lt;/li&gt;
&lt;li&gt;不要你跑任何后端&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;准备工作&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;仓库开启 Discussions：Settings → Features → Discussions ✅&lt;/li&gt;
&lt;li&gt;安装 &lt;a href="https://github.com/apps/giscus" rel="noopener noreferrer"&gt;giscus GitHub App&lt;/a&gt; 到仓库&lt;/li&gt;
&lt;li&gt;去 &lt;a href="https://giscus.app" rel="noopener noreferrer"&gt;giscus.app&lt;/a&gt; 配置向导，选 repo + discussion category（建议用 &lt;code&gt;Announcements&lt;/code&gt; 这种受限分类，避免无关讨论），拿到一段 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;在 Astro 里包装成组件&lt;/strong&gt;，关键要点有两个：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;主题要跟站内明暗同步&lt;/strong&gt;。Giscus 默认 &lt;code&gt;preferred_color_scheme&lt;/code&gt; 跟系统，但 AstroPaper 允许用户手动切换 &lt;code&gt;&amp;lt;html data-theme="..."&amp;gt;&lt;/code&gt;，两者会对不上。要用 &lt;code&gt;MutationObserver&lt;/code&gt; 监听 &lt;code&gt;data-theme&lt;/code&gt; 变化，通过 &lt;code&gt;postMessage&lt;/code&gt; 告诉 giscus iframe 换皮。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClientRouter 切页要重新挂载&lt;/strong&gt;。否则上一篇文章的评论残留在下一篇。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;简化后的组件（完整版在仓库 &lt;code&gt;src/components/Giscus.astro&lt;/code&gt;）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;section id="giscus-container"&amp;gt;
  &amp;lt;h2&amp;gt;评论&amp;lt;/h2&amp;gt;
  &amp;lt;div id="giscus"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;script is:inline data-astro-rerun&amp;gt;
  (function () {
    const container = document.getElementById("giscus");
    if (!container) return;
    container.innerHTML = ""; // 清理上一页残留

    function currentGiscusTheme() {
      return document.documentElement.getAttribute("data-theme") === "dark"
        ? "noborder_dark"
        : "noborder_light";
    }

    const s = document.createElement("script");
    s.src = "https://giscus.app/client.js";
    s.setAttribute("data-repo", "lizhaopeng-cn/xtuul-blog");
    s.setAttribute("data-repo-id", "...");
    s.setAttribute("data-category", "Announcements");
    s.setAttribute("data-category-id", "...");
    s.setAttribute("data-mapping", "pathname");
    s.setAttribute("data-theme", currentGiscusTheme());
    s.setAttribute("data-lang", "zh-CN");
    s.setAttribute("data-loading", "lazy");
    s.setAttribute("crossorigin", "anonymous");
    s.async = true;
    container.appendChild(s);

    // 站内主题切换时通知 iframe 同步
    new MutationObserver(() =&amp;gt; {
      const frame = document.querySelector("iframe.giscus-frame");
      frame?.contentWindow?.postMessage(
        { giscus: { setConfig: { theme: currentGiscusTheme() } } },
        "https://giscus.app"
      );
    }).observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["data-theme"],
    });
  })();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后在 &lt;code&gt;src/layouts/PostDetails.astro&lt;/code&gt; 里 &lt;code&gt;&amp;lt;Giscus /&amp;gt;&lt;/code&gt; 一行就挂上了。&lt;/p&gt;

&lt;h2&gt;
  
  
  踩过的坑
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloudflare Pages 不改构建命令&lt;/strong&gt;：默认的 &lt;code&gt;npm run build&lt;/code&gt; 在项目只有 &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; 时会因为缺 &lt;code&gt;package-lock.json&lt;/code&gt; 失败。&lt;strong&gt;必须显式改成&lt;/strong&gt; &lt;code&gt;pnpm install --frozen-lockfile &amp;amp;&amp;amp; pnpm build&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node 版本&lt;/strong&gt;：不指定 &lt;code&gt;NODE_VERSION&lt;/code&gt;，构建容器可能给你 18，新版 Astro 直接报错。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;默认主题探测&lt;/strong&gt;：AstroPaper 默认是跟随系统 &lt;code&gt;prefers-color-scheme&lt;/code&gt;。想强制首次深色，改 &lt;code&gt;Layout.astro&lt;/code&gt; 里的 &lt;code&gt;initialColorScheme = "dark"&lt;/code&gt;，别去动 &lt;code&gt;lightAndDarkMode&lt;/code&gt;（那个是&lt;strong&gt;是否允许&lt;/strong&gt;切换的开关）。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;不蒜子在 &lt;code&gt;localhost:4321&lt;/code&gt; 上数字离谱&lt;/strong&gt;：不蒜子按域名隔离，&lt;code&gt;localhost&lt;/code&gt; 被所有开发者共享，本地看到几万几十万是正常的。上线到 &lt;code&gt;blog.xtuul.com&lt;/code&gt; 才从 0 开始独立计数。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Giscus 不跟随 AstroPaper 主题切换&lt;/strong&gt;：Giscus 默认用 &lt;code&gt;preferred_color_scheme&lt;/code&gt; 跟系统，但 AstroPaper 允许用户点按钮切 &lt;code&gt;data-theme&lt;/code&gt;，两者会脱节。必须用 &lt;code&gt;MutationObserver&lt;/code&gt; 监听 &lt;code&gt;data-theme&lt;/code&gt;，通过 &lt;code&gt;postMessage&lt;/code&gt; 通知 iframe 换皮。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  下一篇预告
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;篇二：把一篇 Markdown 自动分发到 dev.to / Hashnode，canonical 指向主站。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;核心思路：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;统一 frontmatter 里加 &lt;code&gt;syndicate: { devto: true, hashnode: true }&lt;/code&gt; 开关&lt;/li&gt;
&lt;li&gt;GitHub Actions 监听 &lt;code&gt;src/data/blog/&lt;/code&gt; 变化，调用各平台 API 发布/更新&lt;/li&gt;
&lt;li&gt;首发后把平台返回的文章 ID 回写到 frontmatter，实现幂等更新&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;等系列跑完，最终目标是：&lt;strong&gt;在 Claude Code 里一句话生成 md → &lt;code&gt;git push&lt;/code&gt; → 全网同步更新&lt;/strong&gt;。&lt;/p&gt;

</description>
      <category>astro</category>
      <category>cloudflare</category>
      <category>astropaper</category>
      <category>blog</category>
    </item>
  </channel>
</rss>
