<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>Hello World</title>
<link>https://hxling.top/</link>
<atom:link href="https://hxling.top/index.php/feed/" rel="self" type="application/rss+xml" />
<language>zh-CN</language>
<description>Your description here.</description>
<lastBuildDate>Mon, 02 Nov 2026 14:36:33 +0800</lastBuildDate>
<pubDate>Mon, 02 Nov 2026 14:36:33 +0800</pubDate>
<item>
<title>把大肥鱼接入QQ</title>
<link>https://hxling.top/index.php/archives/12/</link>
<guid>https://hxling.top/index.php/archives/12/</guid>
<pubDate>Mon, 02 Nov 2026 14:36:33 +0800</pubDate>
<dc:creator>鹤襄陵</dc:creator>
<description><![CDATA[把「大肥鱼」接进 QQ：一次从零到稳定的部署复盘环境：阿里云 ECS · Ubuntu 26.04 LTS · 2 vCPU / 1.7 GiB / 40 GB时间：2026-11-01 ~ 1...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>把「大肥鱼」接进 QQ：一次从零到稳定的部署复盘</h1><blockquote>环境：阿里云 ECS · Ubuntu 26.04 LTS · 2 vCPU / 1.7 GiB / 40 GB<br>时间：2026-11-01 ~ 11-02<br>说明：文中所有 IP、QQ 号、AppID、API Key、密码均已替换为占位符，请按自己的实际情况填写。</blockquote><hr><h2>0. TL;DR</h2><p>一句话：<strong>「让一个模型在 QQ 群里像人一样说话」，拆开是三件事</strong>——接一张嘴（消息通道）、写一份人设（提示词）、让它有记忆和情绪（插件）。</p><p>两条路线都跑通了：</p><table><thead><tr><th>路线</th><th>组成</th><th>特点</th><th>我的选择</th></tr></thead><tbody><tr><td>路线一</td><td>官方 QQ Bot + DeepSeek Harness（dsh）</td><td>官方通道、扫码即用、零封号风险、<strong>主动发言受限</strong></td><td>先跑通，后停用</td></tr><tr><td>路线三</td><td>NapCat + AstrBot + DeepSeek</td><td>个人号协议、人格化程度高、插件生态丰富、<strong>有封号风险</strong></td><td>最终在用</td></tr></tbody></table><p>如果只想「让鱼在群里正常说话」，路线一省心到离谱，二十分钟能搞定。<br>如果想要「会发表情包、会改签名、会记仇、会自己插话」，只有路线三。</p><blockquote>本文涉及的全部开源仓库（人设包、表情包素材、协议端、插件）整理在第 <strong>15 节</strong>，其中包含两个「已失效仓库」的记录，省得你顺着旧链接白找。</blockquote><hr><h2>1. 开始之前：先想清楚要什么</h2><p>动手前先把需求拆成四层，否则很容易在「装插件」里迷路：</p><ol><li><strong>通道</strong>：消息从哪来、回到哪去</li><li><strong>人格</strong>：它说话的味道</li><li><strong>能力</strong>：记忆、表情包、改资料、主动发言</li><li><strong>稳定性</strong>：掉线怎么办、封号怎么办、内存够不够</li></ol><p>这四层的优先级是<strong>倒过来</strong>的：稳定性 &gt; 人格 &gt; 能力 &gt; 通道。<br>因为通道挂了你会立刻知道，而「人格不像」「记忆错乱」往往要跑一天才发现，返工成本高得多。</p><hr><h2>2. 环境盘点与第一道坎</h2><h3>2.1 机器</h3><pre><code>Ubuntu 26.04.1 LTS
2 vCPU / 1735 MiB / 40 GB
Node.js v22.23.4（apt 自带版本太老，必须另行安装）</code></pre><h3>2.2 第一道坎：网络</h3><p>这台机器在国内，遇到的问题按顺序是：</p><table><thead><tr><th>目标</th><th>状态</th><th>解法</th></tr></thead><tbody><tr><td><code>deb.nodesource.com</code></td><td>✅ 通</td><td>直接装</td></tr><tr><td><code>registry.npmjs.org</code> / <code>github.com</code></td><td>✅ 通</td><td>直接 clone</td></tr><tr><td><code>registry-1.docker.io</code></td><td>❌ 超时</td><td>配置镜像加速</td></tr><tr><td>我这台本机的 <code>web_fetch</code></td><td>❌ 全部“解析到非公网 IP”</td><td>改用 Node 的 <code>https</code> 模块直连</td></tr></tbody></table><p><strong>顺手记一个排查技巧</strong>：<code>curl: (35) schannel: AcquireCredentialsHandle failed</code> 不是网络问题，是 TLS 凭据拿不到；而 <code>node -e &#039;require(&quot;https&quot;).get(...)&#039;</code> 往往能通——因为 Node 走自己的 OpenSSL，不依赖系统证书存储。</p><h3>2.3 Docker 镜像加速</h3><pre><code class="lang-json">// /etc/docker/daemon.json
{
  &quot;registry-mirrors&quot;: [
    &quot;https://docker.m.daocloud.io&quot;,
    &quot;https://docker.1ms.run&quot;,
    &quot;https://dockerproxy.net&quot;
  ],
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: { &quot;max-size&quot;: &quot;10m&quot;, &quot;max-file&quot;: &quot;3&quot; }
}</code></pre><p>实测 <code>docker.m.daocloud.io</code> 返回 401 是<strong>正常的</strong>（说明 Registry API 在响应），不代表不可用。</p><hr><h2>3. 路线一：官方 QQ Bot + dsh（20 分钟）</h2><h3>3.1 装</h3><pre><code class="lang-bash">curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
npm i -g pnpm @deepseek-ai/dsh      # pnpm 必须单独装，dsh 的插件管理依赖它

dsh --version                        # 0.2.1-rc.5</code></pre><h3>3.2 三步接入</h3><pre><code class="lang-bash">dsh plugin --profile qqbot add @tencent-connect/dsh-qqbot
dsh --profile qqbot</code></pre><p>首次启动会打印二维码，<strong>同时打印一条可点击的绑定链接</strong>：</p><pre><code>https://q.qq.com/qqbot/openclaw/connect.html?task_id=&lt;uuid&gt;&amp;source=dsh-qqbot&amp;_wv=2</code></pre><p>服务器上没有图形界面也没关系——把这条链接发到手机浏览器打开即可扫码。绑定成功后凭据自动写入：</p><pre><code>~/.dsh/profiles/qqbot/cordis.patch.yml</code></pre><p><strong>之后重启不再需要扫码</strong>（这一点我实测过三次）。</p><h3>3.3 一个关键认知</h3><p><code>dsh</code> 在 systemd 下<strong>不能扫码</strong>（无 TTY）。正确的顺序永远是：</p><pre><code>前台手动跑一次 → 扫码绑定 → 凭据落盘 → 再交给 systemd 托管</code></pre><p>反过来做（先写 unit 再启动）会一直卡在二维码界面。</p><h3>3.4 为什么最后停用了它</h3><p>官方通道的限制很硬：<strong>被动回复</strong>。用户不说话，它不能说话。<br>对「群里的活人感」这个目标来说，这条限制是致命的——它永远只能当客服，不能当群友。</p><p><code>systemctl disable --now dsh-qqbot</code> 之后配置全部保留，随时可以切回来。</p><hr><h2>4. 路线三：NapCat + AstrBot</h2><h3>4.1 架构</h3><pre><code>QQ 用户 ──▶ QQ 服务端 ──▶ NapCat（协议端）
                              │  反向 WebSocket
                              ▼
                          AstrBot（大脑）
                              │
                    ┌─────────┴─────────┐
                 DeepSeek          硅基流动
                (聊天/总结)        (记忆向量)</code></pre><p><strong>方向别搞反</strong>：AstrBot 的 aiocqhttp 适配器是<strong>反向 WS 服务端</strong>（监听 6199），NapCat 作为客户端连上去。<br>所以配置写在 NapCat 侧：</p><pre><code class="lang-json">&quot;websocketClients&quot;: [{
  &quot;name&quot;: &quot;astrbot&quot;,
  &quot;url&quot;: &quot;ws://astrbot:6199/ws&quot;,
  &quot;token&quot;: &quot;&lt;你的token&gt;&quot;,
  &quot;reconnectInterval&quot;: 3000
}]</code></pre><h3>4.2 docker-compose</h3><pre><code class="lang-yaml">services:
  napcat:
    image: mlikiowa/napcat-docker:latest
    container_name: napcat
    restart: unless-stopped
    environment:
      - NAPCAT_UID=1000
      - NAPCAT_GID=1000
      - ACCOUNT=&lt;小号QQ&gt;
      - NAPCAT_QUICK_PASSWORD=&lt;小号密码&gt;
      - TZ=Asia/Shanghai
    mem_limit: 800m
    volumes:
      - ./napcat/config:/app/napcat/config
      - ./napcat/qq:/app/.config/QQ
      - ./napcat/cache:/app/napcat/cache
    networks: [botnet]

  astrbot:
    image: soulter/astrbot:latest
    container_name: astrbot
    restart: unless-stopped
    depends_on: [napcat]
    environment:
      - TZ=Asia/Shanghai
    mem_limit: 700m
    ports:
      - &quot;127.0.0.1:6185:6185&quot;        # 面板只绑本机，走 SSH 隧道
    volumes:
      - ./astrbot/data:/AstrBot/data
    networks: [botnet]

networks:
  botnet:
    driver: bridge</code></pre><p>三个细节值得单独说：</p><ol><li><strong><code>ACCOUNT</code></strong>：不设它，NapCat 只会走扫码；设了才会尝试快速登录</li><li><strong><code>NAPCAT_QUICK_PASSWORD</code></strong>：token 失效时的兜底，能省掉无数次半夜扫码</li><li><strong><code>mem_limit</code></strong>：1.7 GiB 的机器不给容器设上限，一个内存泄漏就能把整机拖死</li></ol><h3>4.3 AstrBot 后台密码</h3><p>首次启动会生成随机强密码，默认的 <code>astrbot</code> 已经失效。密码只在启动日志里打印一次：</p><pre><code class="lang-bash">docker logs astrbot 2&gt;&amp;1 | grep &quot;Initial password&quot;</code></pre><p>忘了也没关系，重启一次就会重新打印。</p><h3>4.4 模型配置</h3><p>AstrBot v4 的 provider 结构是「连接」与「模型」分离的：</p><pre><code class="lang-json">{
  &quot;provider_sources&quot;: [{
    &quot;id&quot;: &quot;deepseek-flash_source&quot;,
    &quot;type&quot;: &quot;openai_chat_completion&quot;,
    &quot;key&quot;: [&quot;sk-****&quot;],
    &quot;api_base&quot;: &quot;https://api.deepseek.com/v1&quot;
  }],
  &quot;provider&quot;: [{
    &quot;id&quot;: &quot;deepseek-flash&quot;,
    &quot;model&quot;: &quot;deepseek-flash&quot;,
    &quot;provider_source_id&quot;: &quot;deepseek-flash_source&quot;,
    &quot;custom_extra_body&quot;: { &quot;thinking&quot;: { &quot;type&quot;: &quot;disabled&quot; } }
  }]
}</code></pre><p><code>thinking: disabled</code> 很重要——DeepSeek 的思考过程会拖慢首字延迟，而且对「短句聊天」毫无意义。</p><hr><h2>5. 人格：从 Markdown 到数据库</h2><h3>5.1 人设来源</h3><p>用了社区配置包 <strong><a href="https://github.com/zexuanw958-svg/dafeiyu-config-pack">zexuanw958-svg/dafeiyu-config-pack</a></strong> 的 <code>persona/大肥鱼.md</code>，里面有一个占位符 <code>{OWNER_QQ}</code>，必须替换成真实 QQ 号——<strong>它不只是装饰</strong>，人设里靠它识别「谁才是主人」，防止有人在群里冒充。</p><p>顺便说一句这个包的设计：它的 <code>settings/pack.json</code> 是一份「清单式」配置，把 6 个配套插件、两套表情包、语音/记忆/主动聊天的推荐参数都写在一处，再用 <code>tools/import_dafeiyu.py</code> 读清单改 AstrBot。这个思路值得借鉴——<strong>配置即文档</strong>，改哪项就改哪行，然后重跑一次导入。</p><p>我没有整包导入，而是按需取用（详见第 11 节）：语音链路需要额外的豆包/Breeze key，对 1.7 G 的机器也不友好；插件则挑了对「像人」帮助最大的几个。</p><h3>5.2 写进数据库</h3><p>AstrBot 的人格存在 SQLite 里，<code>personas</code> 表。第一次 INSERT 会连撞三个坑：</p><pre><code>sqlite3.IntegrityError: NOT NULL constraint failed: personas.created_at
sqlite3.IntegrityError: NOT NULL constraint failed: personas.sort_order</code></pre><p>正确写法：</p><pre><code class="lang-python">db.execute(&quot;&quot;&quot;INSERT INTO personas
  (created_at, updated_at, persona_id, system_prompt,
   begin_dialogs, tools, skills, custom_error_message, folder_id, sort_order)
  VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, &#039;大肥鱼&#039;, ?, &#039;[]&#039;, &#039;[]&#039;, &#039;[]&#039;, NULL, NULL, 0)&quot;&quot;&quot;, (prompt,))</code></pre><p>然后把 <code>agent_runner.config.persona.persona_id</code> 指向它。</p><h3>5.3 配套的基础设置</h3><pre><code class="lang-json">{
  &quot;platform_settings.segmented_reply.enable&quot;: true,
  &quot;platform_settings.segmented_reply.regex&quot;: &quot;.*?(?:\\r?\\n+|[。？！~…]+)|.+$&quot;,
  &quot;platform_settings.empty_mention_waiting&quot;: false,
  &quot;agent_runner.config.compression.max_turns&quot;: 30,
  &quot;provider_settings.datetime_system_prompt&quot;: true
}</code></pre><p>分段回复是「一条条发」的关键——不切分的话，模型会一次吐出 200 字，立刻暴露是机器人。</p><hr><h2>6. 表情包：把 100 张 GIF 变成插件资源包</h2><h3>6.1 素材</h3><p><code>diwer/ai-biaoqingbao</code> 提供 100 张 512×512 的中文 GIF，带 <code>catalog.json</code>（编号、含义、分类）。分类正好是 7 组：首批常用、日常回应、抽象反应、打工摸鱼、情绪状态、亲密陪伴、祝福庆祝。</p><h3>6.2 插件资源包的目录协议</h3><p><code>meme_manager</code> 认这个结构：</p><pre><code>packs/&lt;pack_id&gt;/
├── manifest.json
├── memes_data.json
└── memes/&lt;分类名&gt;/*.gif</code></pre><p><code>manifest.json</code>：</p><pre><code class="lang-json">{
  &quot;schema_version&quot;: 1,
  &quot;id&quot;: &quot;dafeiyu-biaoqingbao&quot;,
  &quot;name&quot;: &quot;大肥鱼表情包·100个中文GIF&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;categories&quot;: { &quot;抽象反应&quot;: { &quot;description&quot;: &quot;大肥鱼表情：抽象反应&quot; } }
}</code></pre><p>再把它登记进 <code>registry.json</code> 和 <code>selection_rules.json</code>（设成默认包）。</p><h3>6.3 打包时踩的坑</h3><p>用 PowerShell 的 <code>Compress-Archive</code> 打包，到 Linux 解压后<strong>所有目录变成了文件名的一部分</strong>：</p><pre><code>memes\抽象反应\001-不服气.gif     ← 一个带反斜杠的完整文件名</code></pre><p>因为 Windows 用 <code>\</code> 作分隔符，而 zip 规范要求 <code>/</code>。解决方法是<strong>用 Python 的 <code>zipfile</code> 显式写 POSIX 路径</strong>：</p><pre><code class="lang-python">rel = os.path.relpath(full, root).replace(&quot;\\&quot;, &quot;/&quot;)
z.write(full, rel)</code></pre><h3>6.4 为什么值得用插件而不是自己写</h3><p>我一开始自己写了个 80 行的插件，直接往回复链追加图片，能用。<br>但换成 <code>meme_manager</code> 之后才明白差别在哪：</p><blockquote>它把<strong>分类目录注入到模型提示词里</strong>，模型才知道「我有哪些图可以挑」，然后用 <code>&amp;&amp;抽象反应&amp;&amp;</code> 这种标记下单。</blockquote><p>自己写的插件只是「随机附一张图」，跟情绪无关；插件版是「有合适的才发」。这是「像人」和「像随机图片机器人」的区别。</p><hr><h2>7. 记忆：embedding 选型的成本账</h2><h3>7.1 三种方案</h3><table><thead><tr><th>方案</th><th>成本</th><th>可行性</th></tr></thead><tbody><tr><td>硅基流动 <code>BAAI/bge-m3</code></td><td>免费</td><td>✅ 选它</td></tr><tr><td>百炼 <code>text-embedding-v3</code></td><td>约 0.0005 元/千 token ≈ 1–2 元/月</td><td>✅ 也可</td></tr><tr><td>本机 ollama</td><td>免费</td><td>❌ 模型常驻 1.2 GB，1.7 G 机器跑不动</td></tr></tbody></table><p>结论：<strong>embedding 从来不是钱的问题</strong>，是内存问题。</p><h3>7.2 必踩的坑：维度</h3><p>装完 <code>livingmemory</code> 启动直接失败：</p><pre><code>ValueError: 无效的嵌入向量维度: 0。
请检查该知识库使用的 Embedding Provider 是否正确配置了 embedding_dimensions。</code></pre><p><code>openai_embedding</code> 适配器<strong>不会自动探测维度</strong>，必须在 provider 配置里显式写：</p><pre><code class="lang-json">{ &quot;embedding_model&quot;: &quot;BAAI/bge-m3&quot;, &quot;embedding_dimensions&quot;: 1024 }</code></pre><h3>7.3 记忆的机制</h3><pre><code class="lang-json">&quot;reflection_engine&quot;: { &quot;summary_trigger_rounds&quot;: 10 },
&quot;recall_engine&quot;: { &quot;top_k&quot;: 5, &quot;recent_memory_count&quot;: 2 }</code></pre><p>每 10 轮总结一次 → 存成带权重的记忆 → 回复时按语义召回 5 条 + 最近 2 条。<br>后台还有每日 00:05 的衰减/清理/备份调度。</p><hr><h2>8. 成本控制：两层概率</h2><h3>8.1 第一层：限流策略</h3><p>AstrBot 默认 <code>rate_limit.strategy = &quot;stall&quot;</code>，实现是：</p><pre><code class="lang-python">await asyncio.sleep(stall_duration)   # 睡到下一个窗口</code></pre><p>密集群聊时，超限的消息不是被丢弃，而是<strong>排队休眠</strong>——这是「延迟越堆越长」的元凶。</p><p>改成 <code>&quot;discard&quot;</code>：超限直接丢，绝不排队。</p><h3>8.2 第二层：回复概率</h3><p>最初我自己写了个 <code>reply_gate</code> 插件（<code>event.stop_event()</code> 可真正阻止模型调用，实测有效），按 DeepSeek 计费时段分档：</p><ul><li>工作日 09:00–12:00、14:00–18:00 → 40%</li><li>其余（含周末、节假日）→ 60%</li><li>@ / 引用 / 命令 → 100%</li></ul><p>后来装了 <code>group_chat_plus</code>，把概率交给它，<code>reply_gate</code> 停用。</p><h3>8.3 一个语义陷阱</h3><p><code>group_chat_plus</code> 的 <code>initial_probability</code> <strong>不是「回复概率」</strong>，而是「进入 AI 读空气判断的概率」。<br>所以配 0.6 ≠ 回 60%，实际回复率明显更低——这是它比自建方案更「像人」的代价。</p><h3>8.4 动态时段与平滑过渡</h3><pre><code class="lang-json">{
  &quot;enable_dynamic_reply_probability&quot;: true,
  &quot;reply_time_periods&quot;: &quot;[{\&quot;name\&quot;:\&quot;高峰(上午)\&quot;,\&quot;start\&quot;:\&quot;09:00\&quot;,\&quot;end\&quot;:\&quot;12:00\&quot;,\&quot;factor\&quot;:0.67}]&quot;,
  &quot;reply_time_transition_minutes&quot;: 30,
  &quot;reply_time_use_smooth_curve&quot;: true
}</code></pre><p>实测日志（14:00 峰值边界）：</p><pre><code>13:53:27  原始=0.60  系数=0.69  结果=0.4121
13:54:37  原始=0.80  系数=0.68  结果=0.5444
13:58:35  原始=0.60  系数=0.67  结果=0.4022
14:00:00  原始=0.60  系数=0.67  结果=0.4020</code></pre><p>系数从 0.69 平滑滑到 0.67——<strong>说明 30 分钟过渡是在峰值前的 30 分钟就开始爬坡的</strong>，不是整点突变。<br>好处是观感自然，代价是「中午的尾巴」被提前压掉了。想要硬边界就把过渡改成 10 分钟。</p><hr><h2>9. 稳定性：掉线、假在线与看门狗</h2><p>这是整个项目最花时间的部分，也是最值得写的部分。</p><h3>9.1 三次掉线，三种原因</h3><table><thead><tr><th>时间</th><th>现象</th><th>原因</th></tr></thead><tbody><tr><td>19:54</td><td><code>KickedOffLine 登录已失效</code> + 弹二维码</td><td>token 失效</td></tr><tr><td>22:38</td><td>同上</td><td>token 再次失效</td></tr><tr><td>11:09</td><td><strong>日志一片安静，什么错都没有</strong></td><td>会话静默失效（假在线）</td></tr></tbody></table><p>第三种最坑：容器 <code>Up</code>、WS 连接 <code>ESTABLISHED</code>、AstrBot 显示「适配器已连接」，但 QQ 侧早就不推消息了。从外部完全看不出来，只有「群里人不理它」这个现象。</p><h3>9.2 看门狗</h3><pre><code class="lang-bash"># /usr/local/bin/napcat-watchdog.sh（核心逻辑）
RECENT=$(docker logs napcat --since 5m 2&gt;&amp;1)

if ! echo &quot;$RECENT&quot; | grep -qE &quot;KickedOffLine|账号状态变更为离线|二维码已保存&quot;; then
  exit 0
fi

# 10 分钟冷却，避免循环重启
[ $((now - last)) -lt 600 ] &amp;&amp; exit 0

docker restart napcat</code></pre><p>配 systemd timer 每 2 分钟跑一次。重启后入口脚本会带着 <code>ACCOUNT</code> + <code>NAPCAT_QUICK_PASSWORD</code> 自动登录，35 秒恢复。</p><h3>9.3 看门狗的盲区</h3><p>它只能检测<strong>有日志的</strong>掉线。第三种「静默失效」一个关键字都不触发，所以看门狗全程没动作。</p><p>要覆盖它，需要<strong>主动健康检查</strong>：给 NapCat 开一个本地 HTTP API，让看门狗主动问 <code>get_status</code>。<br>我试过从容器外连它的 WS（3001），三次都是 <code>1006</code> 异常断开——这条路没走通，留作后续。</p><h3>9.4 一个非技术的根因</h3><p>前两次掉线间隔 2 小时 44 分，规律得太可疑。最后发现是<strong>手机端同时登录着这个小号</strong>——个人号协议下，手机端上线/刷新会把协议端踢掉。</p><p>让用户在手机上退出这个小号之后：<strong>12 小时内 <code>KickedOffLine</code> 次数 0</strong>。</p><blockquote>教训：掉线先问「这个号还在别处登陆吗」，比看一百行日志都快。</blockquote><h3>9.5 内存</h3><pre><code>1.7 GiB 总内存 / 加 2 GiB swap
napcat    ~240–290 MiB
astrbot   ~260–340 MiB（随插件增加）</code></pre><p>swap 是必须的：不加的话，一次长上下文 + 多发消息就可能 OOM。</p><hr><h2>10. 踩坑总表</h2><p>按「症状 → 原因 → 解法」整理，方便按图索骥。</p><table><thead><tr><th>#</th><th>症状</th><th>原因</th><th>解法</th></tr></thead><tbody><tr><td>1</td><td>所有 URL 抓取失败，报“非公网 IP”</td><td>本地网络策略把域名解析到内网</td><td>用 Node <code>https</code> 直连绕过</td></tr><tr><td>2</td><td><code>curl: (35) schannel: SEC_E_NO_CREDENTIALS</code></td><td>取不到系统 TLS 凭据</td><td>换 Node/OpenSSL 栈</td></tr><tr><td>3</td><td><code>docker pull</code> 一直超时</td><td>Docker Hub 不可达</td><td>配 <code>registry-mirrors</code></td></tr><tr><td>4</td><td>重启 NapCat 后要重新扫码</td><td>只有扫码 token，没有密码兜底</td><td><code>ACCOUNT</code> + <code>NAPCAT_QUICK_PASSWORD</code></td></tr><tr><td>5</td><td>被踢后容器一直卡在二维码</td><td>worker 内部重启不走入口脚本，<code>-q</code> 丢失</td><td>看门狗 <code>docker restart</code> 容器</td></tr><tr><td>6</td><td>日志无异常但群里不回</td><td>会话静默失效（假在线）</td><td>需要主动健康检查（未彻底解决）</td></tr><tr><td>7</td><td>每 2 小时掉一次线</td><td>手机端同时登录互踢</td><td>手机端退出该账号</td></tr><tr><td>8</td><td>zip 解压后目录变成文件名</td><td>Windows <code>\</code> vs zip 规范 <code>/</code></td><td>Python <code>zipfile</code> 写 POSIX 路径</td></tr><tr><td>9</td><td><code>Unexpected UTF-8 BOM</code></td><td>JSON 带 BOM（AstrBot 写的配置就是）</td><td>读写都用 <code>utf-8-sig</code></td></tr><tr><td>10</td><td><code>NOT NULL constraint failed: personas.created_at</code></td><td>表有非空列没给值</td><td>INSERT 补齐 <code>created_at/updated_at/sort_order</code></td></tr><tr><td>11</td><td><code>无效的嵌入向量维度: 0</code></td><td>适配器不自动探测维度</td><td>显式配 <code>embedding_dimensions: 1024</code></td></tr><tr><td>12</td><td>启动时刷 <code>faiss.swigfaiss_avx512</code> 报错</td><td>faiss 探测可选加速模块</td><td>无害，忽略</td></tr><tr><td>13</td><td>登录后台一直失败</td><td>默认密码被替换成随机强密码</td><td>`docker logs</td><td>grep "Initial password"`</td></tr><tr><td>14</td><td>市场接口解析出 0 个插件</td><td>返回的是 dict 不是 list</td><td>按 dict 遍历</td></tr><tr><td>15</td><td><code>git clone</code> 报 <code>could not read Username</code></td><td>其实是 404（仓库没了）</td><td>换源或用 codeload 直下 zip</td></tr><tr><td>16</td><td>插件仓库 404</td><td>市场条目过期（作者删库/改名）</td><td>用替代品（如下）</td></tr><tr><td>17</td><td>裸 @ 不回，且无任何日志</td><td><code>empty_mention_waiting: false</code></td><td>置 true，或在插件里自己处理</td></tr><tr><td>18</td><td>偶发 <code>IndexError: list index out of range</code></td><td>空 <code>wake_prefix</code> 让 <code>messages[0]</code> 越界</td><td>已知副作用，事件本身无内容，可忽略</td></tr><tr><td>19</td><td>两套概率插件互相打架</td><td>概率相乘（0.4 × 0.6 = 0.24）</td><td>只保留一个概率决策方</td></tr><tr><td>20</td><td>1.7 G 内存跑浏览器自动化</td><td>Chromium 常驻 300–600 MB</td><td>换 4 G 以上机器再考虑</td></tr></tbody></table><p><strong>关于 #16 的替代品</strong>（2026-11 实测）：</p><table><thead><tr><th>原仓库</th><th>状态</th><th>替代</th></tr></thead><tbody><tr><td><code>Zhalslar/astrbot_plugin_qqprofile</code></td><td>404</td><td><code>Akusative/astrbot_plugin_qqprofile_ex</code></td></tr><tr><td><code>Zhalslar/astrbot_plugin_qzone</code></td><td>404</td><td><code>sujingyi1/astrbot_plugin_qzone</code></td></tr></tbody></table><hr><h2>11. 最终形态</h2><h3>11.1 插件清单</h3><table><thead><tr><th>插件</th><th>作用</th><th>自主行为</th></tr></thead><tbody><tr><td><code>meme_manager</code></td><td>表情包（100 张 / 7 分类）</td><td>回复 35% 概率配图</td></tr><tr><td><code>group_chat_plus</code></td><td>群聊读空气、打字模拟、错字</td><td>非 @ 消息按概率接话</td></tr><tr><td><code>livingmemory</code></td><td>长期记忆（bge-m3 向量）</td><td>每 10 轮总结</td></tr><tr><td><code>dafeiyu_clean</code></td><td>过滤模型漏出的内部标签</td><td>无</td></tr><tr><td><code>dafeiyu_signature</code></td><td>情绪触发改签名</td><td>2 次/天</td></tr><tr><td><code>qqprofile_ex</code></td><td>改签名/状态/昵称</td><td>签名 240min、状态 60min 冷却；昵称已关</td></tr><tr><td><code>qzone</code></td><td>发/看说说</td><td><strong>全手动</strong>（cron 全空）</td></tr><tr><td><code>reply_gate</code></td><td>自建概率门（已停用）</td><td>—</td></tr></tbody></table><h3>11.2 行为分层</h3><p><strong>纯定时（没人说话也会跑）</strong>：记忆衰减/备份、索引重建 —— 都在后台，用户无感。</p><p><strong>对话中自主（有人说话时自己加戏）</strong>：接话、配图、改签名、改状态、总结记忆。</p><p><strong>完全被动（必须指令）</strong>：私聊/群聊回复、发说说、看说说、改昵称。</p><hr><h2>12. 运维手册</h2><pre><code class="lang-bash"># 服务
cd /opt/napcat-bot &amp;&amp; docker compose ps
docker compose restart astrbot      # 改配置后
docker compose restart napcat       # 会走密码自动登录

# 日志
docker logs astrbot --since 10m 2&gt;&amp;1 | grep -Ei &quot;error|ERRO&quot;
docker logs napcat --since 10m 2&gt;&amp;1 | grep -E &quot;接收|发送&quot;

# 概率判定
docker logs astrbot --since 1h 2&gt;&amp;1 | grep &quot;动态时间调整&quot;

# 掉线排查
docker logs napcat --since 6h 2&gt;&amp;1 | grep -c KickedOffLine
journalctl -u napcat-watchdog --since -6h | tail

# 面板（都只绑本机）
ssh -L 6185:127.0.0.1:6185 root@&lt;服务器IP&gt;   # AstrBot WebUI</code></pre><hr><h2>13. 没做的事，以及原因</h2><table><thead><tr><th>项目</th><th>状态</th><th>原因</th></tr></thead><tbody><tr><td>语音输出（TTS）</td><td>不做</td><td>明确不需要；四条语音链路都要额外 key 或显卡</td></tr><tr><td>唱歌插件</td><td>不做</td><td>语音衍生功能</td></tr><tr><td>定时发说说</td><td>不做</td><td>主动行为最容易被风控，手动触发足够</td></tr><tr><td>主动找人聊天</td><td>不做</td><td>同上；且会打扰人</td></tr><tr><td>抖音群聊</td><td>不做</td><td>无官方群机器人 API，只能浏览器自动化；1.7 G 跑 Chromium 会拖垮现有服务</td></tr><tr><td>微信个人号</td><td>暂不做</td><td>只能靠逆向/托管协议，封号风险比 QQ 高一档</td></tr><tr><td>后两个表情包（官方市场）</td><td>未装</td><td>用 <a href="https://github.com/diwer/ai-biaoqingbao">ai-biaoqingbao</a> 的 100 张替代</td></tr></tbody></table><hr><h2>14. 方法论：这次学到的东西</h2><ol><li><strong>先打通最小链路，再叠功能。</strong><br>通道 → 模型 → 人格 → 插件，每一步都验证过再往上加。跳过任何一环，后面出问题都定位不到。</li><li><strong>把「以为」变成「日志」。</strong><br>「它好像不回我」和「<code>reply_gate</code> 判定 p=0.40 roll=0.65 → skip」是两件事。前者需要猜，后者只需要看。</li><li><strong>区分「主动」和「被动」是设计的基础。</strong><br>同一个机器人，哪些行为会自己冒出来、哪些必须被叫，必须能一句话说清。说不清就说明设计还没定型。</li><li><strong>国内服务器部署，网络问题占一半工作量。</strong><br>Docker Hub、GitHub、npm，三条链路的状态各不相同，先测再动手。</li><li><strong>复杂度要能被一句话解释。</strong><br>最后稳定下来的形态其实很简单：一个协议端、一个大脑、两个模型、几个插件。中间所有的折腾，都是在排除「不必要」。</li><li><strong>风控是产品约束，不是技术 bug。</strong><br>个人号协议天然有封号风险。接受它，然后通过「用小号、降频率、别主动」把风险压到可接受——而不是幻想把它降到零。</li></ol><hr><h2>15. 项目与素材来源</h2><p>这次用到的全部仓库，按用途归类。<strong>注意：社区插件变动极快，链接随时可能 404，请以实际可达为准。</strong></p><h3>15.1 人设与配置包</h3><table><thead><tr><th>项目</th><th>链接</th><th>说明</th></tr></thead><tbody><tr><td>大肥鱼配置包</td><td><a href="https://github.com/zexuanw958-svg/dafeiyu-config-pack">zexuanw958-svg/dafeiyu-config-pack</a></td><td>人设 Markdown + 6 个配套插件清单 + 语音/记忆参数 + 一键导入脚本</td></tr></tbody></table><p>这个包是整个项目里信息密度最高的一个，值得单独读一遍——它不只是给了一份人设，而是把「一个能跑的大肥鱼需要哪些部件」列成了清单。</p><h3>15.2 表情包素材</h3><table><thead><tr><th>项目</th><th>链接</th><th>说明</th></tr></thead><tbody><tr><td>大肥鱼表情包</td><td><a href="https://github.com/diwer/ai-biaoqingbao">diwer/ai-biaoqingbao</a></td><td>100 张中文 GIF（240/512 两档），带 <code>catalog.json</code> 分类目录</td></tr></tbody></table><p>仓库里还有 <code>NOTICE.txt</code> 写明了二创来源与使用边界（<strong>不提供商用授权</strong>），如果要商用请先看那份说明。</p><h3>15.3 平台与协议端</h3><table><thead><tr><th>项目</th><th>链接</th><th>说明</th></tr></thead><tbody><tr><td>NapCat</td><td><a href="https://github.com/NapNeko/NapCatQQ">NapNeko/NapCatQQ</a></td><td>QQ NT 协议端，输出 OneBot v11</td></tr><tr><td>NapCat Docker</td><td><a href="https://github.com/mlikiowa/napcat-docker">mlikiowa/napcat-docker</a></td><td>本文使用的部署形态</td></tr><tr><td>AstrBot</td><td><a href="https://github.com/AstrBotDevs/AstrBot">AstrBotDevs/AstrBot</a></td><td>机器人框架（大脑）</td></tr><tr><td>DeepSeek Harness</td><td><a href="https://github.com/deepseek-ai/deepseek-harness">deepseek-ai/deepseek-harness</a></td><td>路线一的官方 Agent 运行时</td></tr></tbody></table><h3>15.4 插件</h3><table><thead><tr><th>插件</th><th>链接</th><th>用途</th></tr></thead><tbody><tr><td><code>meme_manager</code></td><td><a href="https://github.com/anka-afk/astrbot_plugin_meme_manager">anka-afk/astrbot_plugin_meme_manager</a></td><td>表情包管理 + 语境发送</td></tr><tr><td><code>group_chat_plus</code></td><td><a href="https://github.com/Him666233/astrbot_plugin_group_chat_plus">Him666233/astrbot_plugin_group_chat_plus</a></td><td>群聊读空气、打字拟态</td></tr><tr><td><code>livingmemory</code></td><td><a href="https://github.com/lxfight/astrbot_plugin_livingmemory">lxfight/astrbot_plugin_livingmemory</a></td><td>长期记忆（向量检索）</td></tr><tr><td><code>qqprofile_ex</code></td><td><a href="https://github.com/Akusative/astrbot_plugin_qqprofile_ex">Akusative/astrbot_plugin_qqprofile_ex</a></td><td>改签名/状态/昵称</td></tr><tr><td><code>qzone</code></td><td><a href="https://github.com/sujingyi1/astrbot_plugin_qzone">sujingyi1/astrbot_plugin_qzone</a></td><td>QQ 空间说说</td></tr><tr><td><code>dsh-qqbot</code></td><td><a href="https://github.com/tencent-connect/dsh-qqbot">tencent-connect/dsh-qqbot</a></td><td>路线一的官方 QQ Bot 通道</td></tr></tbody></table><h3>15.5 已失效的仓库（记录在此，省得后来人再踩）</h3><table><thead><tr><th>仓库</th><th>状态</th><th>替代</th></tr></thead><tbody><tr><td><code>Zhalslar/astrbot_plugin_qqprofile</code></td><td>404</td><td><a href="https://github.com/Akusative/astrbot_plugin_qqprofile_ex">Akusative/astrbot_plugin_qqprofile_ex</a></td></tr><tr><td><code>Zhalslar/astrbot_plugin_qzone</code></td><td>404</td><td><a href="https://github.com/sujingyi1/astrbot_plugin_qzone">sujingyi1/astrbot_plugin_qzone</a></td></tr></tbody></table><p><code>qqprofile_ex</code> 是基于原版改的，多了「AI 自主改签名/状态/昵称 + 定时状态切换」，工具名换成 <code>change_my_*</code> 后反倒更不容易撞名。装完记得检查：它默认三个 <code>auto_*</code> 全开，其中<strong>改昵称建议关掉</strong>——群里突然换了名字，成员会一脸问号。</p><h3>15.6 一点使用提醒</h3><ol><li><strong>插件的协议要看清</strong>。<code>group_chat_plus</code> 是 AGPL-3.0，二次分发有传染性要求。</li><li><strong>第三方插件等于把宿主机权限交给别人的代码</strong>。它们能读配置文件、能发网络请求。装之前至少扫一眼 <code>main.py</code> 有没有可疑的 <code>subprocess</code>。</li><li><strong>表情包素材的版权边界比插件更麻烦</strong>。二创角色、AI 改绘、原图来源，这三层权利关系在素材仓库的 <code>NOTICE.txt</code> 里写得很清楚，商用前务必确认。</li></ol><hr><h2>附录：几个可以直接复用的片段</h2><h3>A. 看门狗 systemd 单元</h3><pre><code class="lang-ini"># /etc/systemd/system/napcat-watchdog.service
[Unit]
Description=NapCat offline watchdog
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/napcat-watchdog.sh</code></pre><pre><code class="lang-ini"># /etc/systemd/system/napcat-watchdog.timer
[Unit]
Description=Run NapCat watchdog every 2 minutes

[Timer]
OnBootSec=3min
OnUnitActiveSec=2min
Unit=napcat-watchdog.service

[Install]
WantedBy=timers.target</code></pre><h3>B. 概率门插件的核心（AstrBot）</h3><pre><code class="lang-python">@filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE)
async def gate(self, event: AstrMessageEvent) -&gt; None:
    text = (event.message_str or &quot;&quot;).strip()
    if text.startswith(&quot;/&quot;):
        return
    # @ / 引用 必回
    for seg in event.get_messages():
        if isinstance(seg, Comp.At) and str(seg.qq) == str(event.get_self_id()):
            return
    prob = 0.4 if is_peak(datetime.now()) else 0.6
    if random.random() &gt;= prob:
        event.stop_event()      # 关键：真正阻止模型调用</code></pre><p><code>event.stop_event()</code> 有效的原因：管道调度器在阶段之间检查 <code>is_stopped()</code>，命中就 <code>break</code>，<br>所以模型根本不会被调用——省的是真金白银。</p><h3>C. 两份必须收好权限的文件</h3><pre><code class="lang-bash">chmod 600 /opt/napcat-bot/docker-compose.yml            # 含小号密码
chmod 600 /path/to/astrbot/data/cmd_config.json        # 含所有 API Key
chmod 600 /path/to/astrbot/data/plugin_data/*/config.json</code></pre><hr><p><em>（全文完。如果这篇对你有用，欢迎在评论区交流；有踩到别的坑也欢迎补充。）</em></p>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://hxling.top/index.php/archives/12/#comments</comments>
<wfw:commentRss>https://hxling.top/index.php/feed/</wfw:commentRss>
</item>
<item>
<title>CC Switch 安装教程：从下载到一键切换 API 全流程图解</title>
<link>https://hxling.top/index.php/archives/7/</link>
<guid>https://hxling.top/index.php/archives/7/</guid>
<pubDate>Tue, 22 Sep 2026 20:49:00 +0800</pubDate>
<dc:creator>鹤襄陵</dc:creator>
<description><![CDATA[简介： CC Switch 是一款免费开源的跨平台 AI 编程工具配置管理器，支持 Windows / macOS / Linux。它统一管理 Claude、Codex、Gemini 等工具的 ...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<p><strong>简介：</strong> CC Switch 是一款免费开源的跨平台 AI 编程工具配置管理器，支持 Windows / macOS / Linux。它统一管理 Claude、Codex、Gemini 等工具的 API 供应商，通过可视化界面一键切换，免去手动编辑 JSON/TOML 配置的繁琐操作。</p><p>CC Switch 是一款免费、开源的桌面应用，作用是统一管理 AI 编程工具的 API 供应商，Windows、macOS、Linux 全平台支持。</p><p>用 Claude Code、Codex、Gemini CLI、OpenCode 这类 AI 编程工具时，经常要在不同供应商（官方、中转、自建等）之间切换，手动改配置文件很麻烦。CC Switch 把这些工具的供应商配置集中到一个图形界面里，切换时点一下就行，不用再复制粘贴配置内容。</p><h2>一、获取 CC Switch</h2><p>CC Switch 安装包：<a href="https://xiecoding.cn/drive.php?id=252">CC Switch安装包（源自 GitHub，Windows + macOS + Linux）</a></p><p>CC Switch 官方提供了各平台的安装包：</p><table><thead><tr><th>安装包名称</th><th>对应平台（适用场景）</th><th>格式说明</th><th>文件大小</th></tr></thead><tbody><tr><td>CC-Switch-v3.19.2-Windows.msi</td><td>Windows x64（主流 Windows 电脑，推荐）</td><td>MSI 安装包，双击安装</td><td>12.5 MB</td></tr><tr><td>CC-Switch-v3.19.2-Windows-Portable.zip</td><td>Windows x64（免安装便携使用）</td><td>压缩包，解压即用</td><td>12.5 MB</td></tr><tr><td>CC-Switch-v3.19.2-Windows-arm64.msi</td><td>Windows Arm 64（骁龙等 ARM 芯片电脑）</td><td>MSI 安装包，双击安装</td><td>11.8 MB</td></tr><tr><td>CC-Switch-v3.19.2-Windows-arm64-Portable.zip</td><td>Windows Arm 64（免安装便携使用）</td><td>压缩包，解压即用</td><td>11.9 MB</td></tr><tr><td>CC-Switch-v3.19.2-macOS.dmg</td><td>macOS（Apple Silicon 和 Intel 芯片均可，推荐）</td><td>DMG 安装包，双击拖入应用程序</td><td>25.9 MB</td></tr><tr><td>CC-Switch-v3.19.2-Linux-x86_64.AppImage</td><td>Linux x64（主流 Linux 发行版，推荐）</td><td>AppImage 单文件，赋予执行权限即可运行</td><td>87.6 MB</td></tr><tr><td>CC-Switch-v3.19.2-Linux-x86_64.deb</td><td>Linux x64（Debian/Ubuntu 系发行版）</td><td>Debian 包，用 dpkg/apt 安装</td><td>12.5 MB</td></tr><tr><td>CC-Switch-v3.19.2-Linux-x86_64.rpm</td><td>Linux x64（RedHat/CentOS/Fedora 系发行版）</td><td>RPM 包，用 rpm/yum 安装</td><td>12.5 MB</td></tr><tr><td>CC-Switch-v3.19.2-Linux-arm64.AppImage</td><td>Linux Arm 64（ARM 架构设备）</td><td>AppImage 单文件，赋予执行权限即可运行</td><td>85.3 MB</td></tr><tr><td>CC-Switch-v3.19.2-Linux-arm64.deb</td><td>Linux Arm 64（Debian/Ubuntu 系发行版）</td><td>Debian 包，用 dpkg/apt 安装</td><td>12 MB</td></tr><tr><td>CC-Switch-v3.19.2-Linux-arm64.rpm</td><td>Linux Arm 64（RedHat/CentOS/Fedora 系发行版）</td><td>RPM 包，用 rpm/yum 安装</td><td>12 MB</td></tr></tbody></table><h2>二、安装 CC Switch</h2><h3>1. Windows 平台安装</h3><p>有了 .msi 文件后双击启动，点击 Next：</p><p><img src="https://hxling.top/images/01-windows-install-next.jpeg" alt="CC Switch Windows 安装：点击 Next" title="CC Switch Windows 安装：点击 Next"></p><p>选择安装路径。默认装在 C 盘，想省系统盘空间可以改到 D 盘：</p><p><img src="https://hxling.top/images/02-windows-install-path.jpeg" alt="CC Switch Windows 安装：选择安装路径" title="CC Switch Windows 安装：选择安装路径"></p><p>点击 Install，开始安装：</p><p><img src="https://hxling.top/images/03-windows-install-progress.jpeg" alt="CC Switch Windows 安装：点击 Install" title="CC Switch Windows 安装：点击 Install"></p><p>等待安装完成，完成后桌面会出现 CC Switch 快捷方式，双击就能打开它：</p><p><img src="https://hxling.top/images/04-windows-install-done.jpeg" alt="CC Switch Windows 安装完成" title="CC Switch Windows 安装完成"></p><p>如果不想写注册表，下载 Portable 便携版，解压后直接运行 exe 文件：</p><p><img src="https://hxling.top/images/05-windows-portable.jpeg" alt="CC Switch Windows 便携版" title="CC Switch Windows 便携版"></p><blockquote><strong>注意：</strong> Windows 版本已禁用「一键安装」功能，如果需要安装 Claude Code 等工具，请手动安装后再通过 CC Switch 来管理。</blockquote><h3>2. macOS 平台安装</h3><p>有了 .dmg 文件后双击打开，把 CC Switch 图标拖入「应用程序」文件夹，应用就能打开了。</p><h3>3. Linux 平台安装</h3><p>Ubuntu/Debian/Mint 用户获得 .deb 文件，执行：</p><pre><code class="lang-bash">sudo apt install ./CC-Switch-*.deb</code></pre><p>Fedora/RHEL 用户获得 .rpm 文件，执行：</p><pre><code class="lang-bash">sudo dnf install ./CC-Switch-*.rpm</code></pre><p>其他发行版可以直接获得 .AppImage 文件，加执行权限后运行即可：</p><pre><code class="lang-bash">chmod +x CC-Switch-*.AppImage
./CC-Switch-*.AppImage</code></pre><h2>三、CC Switch 基础使用</h2><p>CC Switch 的核心功能是管理和切换供应商，下面依次给大家讲清楚。</p><h3>1. 认识主界面</h3><p>打开 CC Switch 后，顶部是导航栏，可以选择当前管理的 AI 编程工具（Claude、Claude Desktop、Codex、Gemini、OpenCode、OpenClaw、Hermes），切换后下方列表就显示对应应用的供应商配置：</p><p><img src="images/06-main-interface.jpeg" alt="CC Switch 主界面" title="CC Switch 主界面"></p><p>导航栏最右侧是添加按钮（+）。界面主体是供应商卡片列表，每个供应商以一张卡片展示：</p><p><img src="https://hxling.top/images/07-provider-cards.jpeg" alt="CC Switch 供应商卡片列表" title="CC Switch 供应商卡片列表"></p><h3>2. 添加供应商</h3><p>点击主界面右上角的 + 按钮，在弹出窗口的「预设供应商」框里选择你的供应商（常用预设包括智谱 GLM、MiniMax、DeepSeek、Kimi 等），预设会自动填好端点地址，你只需要填写 API Key，点击「添加」即可。</p><p><img src="https://hxling.top/images/08-add-provider.jpeg" alt="CC Switch 添加供应商" title="CC Switch 添加供应商"></p><p>如果你的供应商不在预设里，选择「自定义」手动配置接口地址和模型信息。</p><h3>3. 切换供应商</h3><p>添加完成后供应商出现在列表中，切换有两种方式：</p><p>一是主界面切换，把鼠标悬停在目标供应商卡片上，点击卡片上的「启用」按钮（▶）：</p><p><img src="https://hxling.top/images/09-switch-provider.jpeg" alt="CC Switch 主界面切换供应商" title="CC Switch 主界面切换供应商"></p><p>二是托盘快速切换，右键系统托盘里的 CC Switch 图标，在「Claude」「Codex」等子菜单里直接点供应商名称，当前启用的会带勾选标记，不用打开主界面就能切。</p><p><img src="https://hxling.top/images/10-tray-switch.jpeg" alt="CC Switch 系统托盘快速切换" title="CC Switch 系统托盘快速切换"></p><h3>4. 健康检查</h3><p>添加完供应商后，可以点击旁边的「健康检查」按钮，发送一个测试请求来验证 API Key 和网络连通性。这个功能很实用，能帮你快速排除是 API Key 的问题还是网络的问题。</p><p><img src="https://hxling.top/images/11-health-check.jpeg" alt="CC Switch 健康检查" title="CC Switch 健康检查"></p><p>配置正常的话，CC Switch 会给出「运行正常」的提示：</p><p><img src="https://hxling.top/images/12-health-check-ok.jpeg" alt="CC Switch 健康检查通过" title="CC Switch 健康检查通过"></p><h3>5. 管理供应商卡片</h3><p>把鼠标悬停在卡片上，会显示一排操作按钮：启用（使用中）、编辑、复制、检测连通、配置用量查询、删除：</p><ul><li>编辑可以改供应商配置；</li><li>复制能快速创建副本；</li><li>检测连通可以测试模型连通性和响应速度；</li><li>删除可以移除不需要的供应商（当前正在用的供应商要先切换到别的才能删除）。</li></ul><p>按住卡片左侧的拖拽手柄（≡）可以上下拖动调整供应商顺序。</p><h2>四、新手注意事项</h2><ol><li>CC Switch 是管理工具，本身不包含任何 AI 服务。添加供应商时需要填入自己的 API 密钥或配置信息，密钥由各服务商提供。首次使用先添加一个供应商测试连通性，确认能正常调用再批量添加其他供应商。</li><li>切换供应商后，已打开的 AI 编程工具（如 Claude Code）需要重启或新开会话才能生效。这是正常现象，不是 CC Switch 的问题——配置是启动时读取的，切换后记得重启对应工具。</li><li>不同 AI 编程工具的配置格式不一样，CC Switch 为每个工具提供了对应的供应商添加入口。添加时先选对工具类型（Claude Code、Codex、Gemini CLI 等），再填对应配置，填错位置会导致切换无效。</li></ol>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://hxling.top/index.php/archives/7/#comments</comments>
<wfw:commentRss>https://hxling.top/index.php/feed/</wfw:commentRss>
</item>
<item>
<title>益诗误删文件复盘</title>
<link>https://hxling.top/index.php/archives/3/</link>
<guid>https://hxling.top/index.php/archives/3/</guid>
<pubDate>Sat, 19 Sep 2026 21:46:36 +0800</pubDate>
<dc:creator>鹤襄陵</dc:creator>
<description><![CDATA[九月十九日下午，我在服务器上整理文件，敲了一条删除命令，删错了一个目录。益诗上线四个月，所有用户上传的图片都放在那里。包括 571 个人的头像，124 张诗歌封面，50 个诗集封面，79 张 A...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<p>九月十九日下午，我在服务器上整理文件，敲了一条删除命令，删错了一个目录。</p><p>益诗上线四个月，所有用户上传的图片都放在那里。包括 571 个人的头像，124 张诗歌封面，50 个诗集封面，79 张 AI 海报。</p><p>这篇是复盘。我把它完整写下来，因为里面有几步值得记住，也有两个我自己犯的错误值得你绕开。</p><h2>一、先清点：什么还在，什么没了</h2><p>先要做的事是把边界划清楚。</p><p>数据库完好无损。用户 1770 个，诗歌 15332 首（未删除部分），评论 2368 条，通知 34896 条，还有点赞收藏、关注关系、诗集、漂流瓶、成长记录——全部安好。这些都在 MySQL 里，误删的目录跟它无关。</p><p>后端程序也活着。<code>yishi.jar</code> 在另一个目录，进程已经连续跑了 48 天。systemd 单元文件在 <code>/etc/systemd/system/</code>，不在 <code>wwwroot</code> 下面，所以数据库密码、Redis 密码、JWT 密钥、通义千问的 API Key、MinIO 凭据、QQ 互联的 AppID 和 AppKey，一个都没丢。这一点后来省了我很多事。</p><p>丢的是这些：</p><table><thead><tr><th>位置</th><th>内容</th></tr></thead><tbody><tr><td><code>/www/wwwroot/poem-client/</code></td><td>用户端 H5 静态文件</td></tr><tr><td><code>/www/wwwroot/poem-admin/</code></td><td>管理端静态文件</td></tr><tr><td><code>/www/wwwroot/yishi-server/logs/</code></td><td>日志目录</td></tr><tr><td><code>/www/wwwroot/yishi-server/minio/minio</code></td><td>MinIO 可执行文件本身</td></tr><tr><td><code>/www/wwwroot/yishi-server/minio/data/</code></td><td>对象存储数据，全部图片</td></tr></tbody></table><p>Nginx 配置在宝塔的 vhost 目录里，不在 <code>wwwroot</code> 下，也活着。</p><h2>二、让站点先活过来</h2><p>前端最容易补，本地构建产物都在。H5 有 82 个资源文件，管理端 85 个，打包压缩后 scp 上去解包，改属主权限就好。</p><p>后端 jar 完全不用重传。我比对了两边的 MD5：</p><pre><code>线上  f160342af2d74778653aab791676de8f
本地  f160342af2d74778653aab791676de8f</code></pre><p>一致，说明线上跑的就是我本地这份构建，省掉一次重新打包和上传。</p><p>APK 也在本地，<code>yishi-0.11.0.apk</code>，20,340,809 字节，放进 <code>/apk/</code> 目录。Nginx 没有单独的 <code>/apk/</code> location，走的是 SPA 兜底的静态服务，能正常下载。</p><p><strong>真正麻烦的是 MinIO。</strong> 它的可执行文件被删了，而且 —— <code>dl.min.io</code> 这个官方下载域已经停用，所有路径都返回 <code>410 Gone</code>：</p><pre><code>https://dl.min.io/server/minio/release/linux-amd64/minio            → 410
https://dl.min.io/server/minio/release/linux-amd64/archive/...      → 410
https://dl.min.io/server/minio/release/linux-amd64/minio.asc        → 410</code></pre><p>GitHub Releases 也是 404。最后在官方中文源拿到了：</p><pre><code class="lang-bash">curl -sL --retry 3 -o minio \
  https://dl.minio.org.cn/server/minio/release/linux-amd64/minio</code></pre><p>版本 RELEASE.2025-07-23T15-54-02Z，110 MB，仍然支持 <code>--console-address</code>，systemd 单元一个字不用改。</p><p>装回去之后还有一层坑。数据目录当时只剩一个 <code>.minio.sys</code>，MinIO 启动时报的是：</p><pre><code>Unable to load healing tracker: unformatted drive found, re-initializing..
Error: Read failed. Insufficient number of drives online</code></pre><p>它认不出这个盘，于是 <code>mc mb</code> 建桶直接报 <code>Resource requested is unwritable</code>。重启一次，让它把空盘重新格式化，然后建桶、设公共读：</p><pre><code class="lang-bash">mc mb --ignore-existing local/yishi
mc anonymous set download local/yishi</code></pre><p>到这一步站点是活的，但 824 张图片真的没了。磁盘上、备份里、回收站里都没有。</p><h2>三、意外的出路</h2><p>那天下午我想通一件事：这些图片在我的服务器上没了，但它们在你手机里。</p><p><code>/img/</code> 这个路径的响应头是这样的：</p><pre><code>expires: Sun, 19 Sep 2027 09:39:02 GMT
cache-control: max-age=31536000
cache-control: public, immutable</code></pre><p>一年有效期，<code>immutable</code> 意味着浏览器可以直接复用，不必回服务器校验。MinIO 的文件名是 UUID，永不变更，当初这么配就是为了让图片秒开。这个配置让图片在别人手机上留了一份完整的字节副本，而且源站返回 404 也不会触发重新拉取。</p><p>同源页面上的 JavaScript 可以把它读出来，再传回服务器。因为写回的是<strong>完全相同的 object key</strong>，数据库里那 824 条 URL 一个字都不用改。</p><p>我写了一个临时页面干这件事，跑完就撤了。技术路线值得记下来。</p><h3>第一次尝试，失败了</h3><p>最初我用两种方式去读缓存：<code>fetch(url, {cache: &#039;force-cache&#039;})</code>，以及一个游离的 <code>new Image()</code> 对象。</p><p>结果 nginx 访问日志里是一整片 404 —— 每一次请求都打到了服务器。浏览器渲染图片时用的是另一条缓存通道。这一版只捞回 93 张，而且命中大多来自别的原因。</p><h3>换成真实渲染</h3><p>第二版换了思路：把图片真实地 <code>&lt;img&gt;</code> 渲染进 DOM，再从已经渲染出来的元素上取像素。</p><pre><code class="lang-js">// 图片既然能显示，就一定能从它身上取到像素
ctx.drawImage(img, 0, 0, cw, ch);
const blob = await new Promise(res =&gt; canvas.toBlob(res, type, 0.94));</code></pre><p>因为要经过 canvas 重新编码，还得配一条降级链：<code>toBlob</code> 编码不出来就退回 <code>toDataURL</code>，标准尺寸失败就把图缩到 60%、35% 再试。</p><p>这一版数字从 93 涨到 272。</p><h3>然后页面崩了</h3><p>头像组有 571 张。第二版一次性把整组缩略图都挂进 DOM，移动端浏览器的内存直接爆掉了。</p><p>第三版改成<strong>固定槽位</strong>：只保留 N 个 <code>&lt;img&gt;</code> 元素循环复用（默认 3 个，可调 2/3/4），每张判完状态立刻 <code>removeAttribute(&#039;src&#039;)</code> 释放，再换下一张。</p><pre><code class="lang-js">// DOM 节点数和内存占用恒定，跟图片总数无关
function release(slot) {
  try { slot.img.removeAttribute(&#039;src&#039;); } catch (e) { slot.img.src = &quot;&quot;; }
}</code></pre><p>跑 50 张和跑 571 张，占用完全一样。命中的图用 88×88 的小画布裁切收进网格，上限 400 张，单张约 31KB。</p><p>顺带还发现一件事：<code>/img/</code> 的响应带 <code>vary: Origin</code>，而站内「生成海报」功能是用 <code>crossOrigin=&quot;anonymous&quot;</code> 读图的 —— 那是另一条缓存条目。所以补了一条 <code>crossorigin</code> 备用路径。</p><h2>四、三分之一的墙</h2><p>最终停在 <strong>272 / 824</strong>，约 33%。</p><p>为什么不是更多？我拿一个指标去验证：<strong>每张图片最后一次被服务器成功取到的时间</strong>。</p><ul><li><strong>已经找回来的头像</strong>：98 张里 97 张的最后一次成功加载在九月</li><li><strong>仍然缺失的诗歌封面</strong>：44 张停在六月，25 张停在五月，只有 18 张落在九月</li></ul><p>规律很清楚了。<code>immutable</code> 和 <code>expires</code> 管的是<strong>新鲜度</strong>：浏览器愿意直接复用，不必回服务器校验。至于<strong>留存</strong>，移动浏览器和 iOS Safari 依然会按最近最少使用（LRU）清理长期不碰的条目。</p><p>头像出现在每一个页面上，被反复读取，缓存条目一直是热的。诗歌封面只在特定卡片上出现，而 15333 首诗里只有 127 首带封面 —— 出现频率太低，条目早就凉了。</p><p>我做了一个决定性的验证：把用户当天实际打开过的每一首诗的封面逐一对照。结果是 <strong>封面仍缺失的数量为 0</strong>。也就是说，他们能在手机上看到的那些诗歌图片，正是已经接回来的那 47 张。剩下那些，确实不在任何设备上留下过副本。</p><p>这条路走到头了。</p><h2>五、然后我犯了第二个错误</h2><p>我没有立刻收手，转而去试磁盘取证 —— 图片被删掉才两个多小时，服务器还有 16G 空闲空间，我猜数据块可能还躺在原地。</p><p>前半段是有收获的。我摸清了 MinIO 在磁盘上的真实布局：<strong>每个对象是一个以对象名为名的目录</strong>，里面有：</p><ul><li><code>xl.meta</code>，约 350 字节，含 etag 和 content-type</li><li><code>DDir-&lt;uuid&gt;/part.1</code>，实际数据，约 656KB 一片</li></ul><p>这意味着目录名本身就是文件名，理论上可以整目录找回，比缓存那条路完整得多。</p><p>然后我跑了这条命令：</p><pre><code class="lang-bash">dd if=/dev/vda3 bs=8M | LC_ALL=C grep -aob -F -f patterns.txt</code></pre><p><code>patterns.txt</code> 有 555 个模式 —— 552 个缺失文件名加上几个图片魔数。目标是遍历 40GB 磁盘找残留。</p><p>这是那次事故里最糟的一个决定。它把生产盘的 I/O 占满，内存也被吃掉，最终拖垮了整台服务器，我只能在阿里云控制台强制重启。</p><p>正确的做法是在<strong>离线快照</strong>上做，或者用 <code>debugfs</code> 定点读取 inode。任何长耗时的扫描都应该加 <code>nice -n 19</code> 和 <code>ionice -c3</code>，并且预先设好超时。判断数据能否恢复，优先用轻量证据：看一个 inode 的 extent 记录、做小范围采样，都足够下结论。</p><p>代价是我为了多救几张图片，让整个站点又断了一次。</p><h2>六、重启之后</h2><p>服务器重启后所有服务都自动起来了：yishi、minio、nginx、mysql、redis、cron 全部 active。桶内 272 个对象完好，数据库行数与重启前一致。</p><p>我做了收尾：</p><ul><li>删除临时恢复页面、接收程序、分组清单、nginx 反代配置、8099 端口</li><li>还原 nginx 配置并删除部署时产生的备份文件</li><li>移除 mc 的 <code>local</code> 别名</li><li>清理 <code>/tmp</code> 和本地脚本目录</li><li>复查确认站点根目录只剩 <code>index.html</code> / <code>assets</code> / <code>static</code> / <code>apk</code></li></ul><p>站内公告也改写了一版，去掉了所有引导用户参与的步骤 —— 找回通道关了，不该再让任何人点进去。</p><h2>七、值得记住的几件事</h2><p><strong>在生产目录里删东西之前，先确认它有没有备份。</strong> 这次的全部损失，源头就是这一条。我有数据库的每日备份，但对象存储目录不在任何备份计划里。</p><p><strong><code>immutable</code> 管的是新鲜度。</strong> 这个响应头让浏览器愿意直接复用、不回源校验。条目能留多久，由最近最少使用策略决定 —— 低频访问的资源，缓存待不了太久。</p><p><strong>顺手配的缓存策略，在灾难时会变成救命的东西。</strong> 那 272 张图片能回来，靠的就是当初为了让页面快一点而加的 <code>immutable</code>。当时做这个决定，只想到性能这一层。</p><p><strong>读浏览器缓存要用渲染。</strong> <code>fetch(cache: &#039;force-cache&#039;)</code> 和游离的 <code>new Image()</code> 都读不到渲染通道那份缓存，实测每一次请求都会打到服务器。</p><p><strong>长耗时的扫描要放在离线快照上做。</strong> 第二个错误比第一个更不可原谅，因为它是我在已经出事之后，又主动制造的一次故障。</p><h2>八、接下来</h2><p>已经加上了对象存储目录的异地备份，跟数据库放在同一套计划任务里。</p><p>找不回的 552 张图片，位置会留空。头像随时可以重新上传，新图会被正常保存。</p><p>那 272 张能回来，是一台台设备上那些看不见的副本替我保住的。写这篇文章的时候我在想，如果当初没有顺手开那个缓存头，现在的数字会是 0。</p><hr><p><em>写于 2026-09-19，服务器重启之后。</em></p>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://hxling.top/index.php/archives/3/#comments</comments>
<wfw:commentRss>https://hxling.top/index.php/feed/</wfw:commentRss>
</item>
</channel>
</rss>