<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>YCX&apos;s Blog</title><description>记录与分享</description><link>https://ycxdev.com/</link><language>zh_CN</language><item><title>F12 抓包发现「视频伪装成图片」，以及 .ts 文件和 BOM 到底是什么</title><link>https://ycxdev.com/posts/quetta-f12-network-ts-and-bom/</link><guid isPermaLink="true">https://ycxdev.com/posts/quetta-f12-network-ts-and-bom/</guid><description>用 Quetta 逛某个站时没有弹出下载按钮，F12 一看，播放过程中刷了一堆图片请求。把 URL 的 .ts 改成 .jpg 再在文件头补上 JPEG 的 BOM 就能存下来。本文拆解这个观察背后的技术：MPEG-TS 分片、文件魔数，以及这招究竟骗过了谁。</description><pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;用 &lt;a href=&quot;https://www.quetta.net/&quot;&gt;Quetta&lt;/a&gt; 逛某个站的时候，遇到一件怪事。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页面上没有弹出下载按钮。&lt;/strong&gt; 但视频确实在播。&lt;/p&gt;
&lt;p&gt;于是我做了所有技术人都会做的那件事：&lt;strong&gt;F12 → Network。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后我看到了一个很有意思的东西。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、Network 面板里滚动的全是「图片」&lt;/h2&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
flowchart TD
A[&quot;打开页面播放视频&quot;] --&amp;gt; B[&quot;F12 → Network 面板&quot;]
B --&amp;gt; C{&quot;观察请求&quot;}
C --&amp;gt; D[&quot;大量 .jpg 请求&amp;lt;br/&amp;gt;随播放进度不断新增&quot;]
C --&amp;gt; E[&quot;没有任何 .mp4 / .m3u8 请求&quot;]
D --&amp;gt; F[&quot;视频明明在动&amp;lt;br/&amp;gt;但网络里全是图片？&quot;]
E --&amp;gt; G[&quot;视频是逐段加载的&amp;lt;br/&amp;gt;不是一次性一个文件&quot;]
F --&amp;gt; H[&quot;推断：服务端把视频分片&amp;lt;br/&amp;gt;伪装成了图片&quot;]
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;现象描述一下：播放过程中，Network 面板持续刷出&lt;strong&gt;大量 &lt;code&gt;.jpg&lt;/code&gt; 请求&lt;/strong&gt;，一个接一个；而列表里&lt;strong&gt;找不到任何 &lt;code&gt;.mp4&lt;/code&gt; 或 &lt;code&gt;.m3u8&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但视频确实在流畅播放。&lt;/p&gt;
&lt;p&gt;这个矛盾只有一个解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;视频不是以&quot;视频&quot;的形式传输的，而是被切成了很多小段，每一段以&quot;图片&quot;的名义加载。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再一深想就不对了 —— 一段视频帧，大小通常几十 KB 到几百 KB。CDN 上&quot;图片&quot;是极其便宜的资源类型（很多 CDN 对图片免流量费、有图片优化/尺寸变体、有独立的缓存规则）。把视频伪装成图片走，&lt;strong&gt;等于白嫖了图片托管的带宽和缓存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个思路和推理，问了下 AI，印证了：确实是这么干的。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、动手：改后缀 + 补 BOM&lt;/h2&gt;
&lt;p&gt;知道原理之后操作就很简单了。&lt;/p&gt;
&lt;p&gt;下载 F12 里抓到的那个&quot;图片&quot;，然后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;原始文件：segment-000123.ts      （内容其实是 MPEG-TS 分片）
改名  →：segment-000123.jpg
补头  →：文件开头插入 FF D8 FF
结果  →：能被当成 JPEG 处理
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 上的具体做法（PowerShell）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 读原始字节
$ts  = [IO.File]::ReadAllBytes(&quot;segment-000123.ts&quot;)

# JPEG 的 SOI（Start Of Image）标记：FF D8 FF
$bom = [byte[]](0xFF, 0xD8, 0xFF)

# 前面拼上 BOM
$jpg = New-Object byte[] ($bom.Length + $ts.Length)
[Array]::Copy($bom, 0, $jpg, 0, $bom.Length)
[Array]::Copy($ts,  0, $jpg, $bom.Length, $ts.Length)

[IO.File]::WriteAllBytes(&quot;segment-000123.jpg&quot;, $jpg)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改名 + 改文件头，搞定。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、&lt;code&gt;.ts&lt;/code&gt; 到底是个什么文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.ts&lt;/code&gt; 这个后缀有点误导人 —— 它&lt;strong&gt;不是&lt;/strong&gt; TypeScript。&lt;/p&gt;
&lt;p&gt;在这个上下文里，它指的是 &lt;strong&gt;MPEG-2 Transport Stream&lt;/strong&gt;（MPEG-2 传送流），也叫 &lt;strong&gt;M2TS&lt;/strong&gt;。这是直播和流媒体领域的标准容器格式。&lt;/p&gt;
&lt;h3&gt;它的核心特征：固定长度包 + 同步字节&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──────────┬──────────────────────────────────────┬──────────┐
│ 0x47     │  184 字节负载                          │ 0x47     │
│ 同步字节  │  （音频/视频/PES/PSI 数据）             │ 同步字节  │
└──────────┴──────────────────────────────────────┴──────────┘
      一个包 = 188 字节，且每个包的第一个字节都是 0x47
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;0x47&lt;/code&gt;（十进制 71）叫做&lt;strong&gt;同步字节（sync byte）&lt;/strong&gt;，它是 MPEG-TS 最显著的特征。&lt;/p&gt;
&lt;p&gt;设计成&quot;每 188 字节重复一个同步字节&quot;是有明确目的的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;抗错误 / 快速重同步。&lt;/strong&gt; 广播信号丢了一部分，接收端不用从头解析，只要扫到下一个 &lt;code&gt;0x47&lt;/code&gt; 就能重新对齐 —— 这是&quot;打一个同步标记&quot;，和硬盘扇区的同步区、CD 的同步信号是同一个思路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恒定码率、可交织。&lt;/strong&gt; 多路音频和视频可以交织在同一条流里，接收端按包头解复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不依赖文件可寻址。&lt;/strong&gt; 广播流不能 seek，所以设计上就是&quot;从头顺序读&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;流媒体里它在哪儿&lt;/h3&gt;
&lt;p&gt;HLS（HTTP Live Streaming）的经典做法就是把一段视频切成 10 秒左右的小块，每块一个 &lt;code&gt;.ts&lt;/code&gt; 文件，播放器按顺序拉：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;playlist.m3u8
  ├── seg001.ts   0s - 10s
  ├── seg002.ts   10s - 20s
  ├── seg003.ts   20s - 30s
  └── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（DASH 协议现在更多用 fMP4，但 HLS + &lt;code&gt;.ts&lt;/code&gt; 依然是兼容性最广的方案。）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以&quot;播放过程中不断产生新请求&quot;这个特征，本质上就是分片流媒体。&lt;/strong&gt; 它跟&quot;伪装成图片&quot;没有必然关系 —— 分片是正常行为，伪装是额外加的一层。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、BOM 是什么：先说狭义，再说广义&lt;/h2&gt;
&lt;h3&gt;狭义：BOM = Byte Order Mark&lt;/h3&gt;
&lt;p&gt;BOM 最原始的含义是&lt;strong&gt;字节序标记&lt;/strong&gt;，用来告诉程序&quot;这个文本文件是用哪种编码写的&quot;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编码&lt;/th&gt;
&lt;th&gt;文件开头的 BOM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EF BB BF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF-16 LE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FF FE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF-16 BE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FE FF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF-32 LE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FF FE 00 00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有了它，一个程序扫一眼开头三个字节就知道后面该怎么解码字节流，而不需要靠猜。&lt;/p&gt;
&lt;h3&gt;广义：BOM = 文件魔数（magic number / 文件签名）&lt;/h3&gt;
&lt;p&gt;在文件类型识别里，这个概念被泛化了 —— &lt;strong&gt;靠开头几个字节判断这是什么格式&lt;/strong&gt;，这在技术圈也被叫作 BOM。&lt;/p&gt;
&lt;p&gt;常见的一些：&lt;/p&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;JPEG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FF D8 FF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;89 50 4E 47 0D 0A 1A 0A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;47 49 46 38&lt;/code&gt;（就是 ASCII 的 &quot;GIF8&quot;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;25 50 44 46&lt;/code&gt;（&quot;%PDF&quot;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZIP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50 4B 03 04&lt;/code&gt;（&quot;PK\x03\x04&quot;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPEG-TS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;47&lt;/code&gt;&lt;/strong&gt;（每 188 字节一个）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;JPEG 的 &lt;code&gt;FF D8 FF&lt;/code&gt; 里：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FF D8&lt;/code&gt; 是 &lt;strong&gt;SOI（Start Of Image）&lt;/strong&gt; 标记&lt;/li&gt;
&lt;li&gt;后面的 &lt;code&gt;FF&lt;/code&gt; 是下一个标记的第一个字节。JPEG 的结构是&quot;一串标记&quot;，每个标记都以 &lt;code&gt;FF&lt;/code&gt; 开头、&lt;code&gt;FF&lt;/code&gt; 结尾。典型的 JPEG 紧接着是 APP0/JFIF 段：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;FF D8                     ← SOI
FF E0 00 10 4A 46 49 46 00   ← APP0 / JFIF 标识
01 01 00                  ← 版本、密度单位
00 01 00 01               ← X/Y 密度
00 00                     ← 缩略图（无）
...                       ← 后续帧数据
FF D9                     ← EOI (End Of Image)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、这招究竟骗过了谁 —— 一个需要诚实拆开的部分&lt;/h2&gt;
&lt;p&gt;这里有个特别容易想错的地方，我第一次也搞反了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;浏览器处理资源，看的是 HTTP 响应头 &lt;code&gt;Content-Type&lt;/code&gt;，不是文件后缀，也不是文件头。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;服务端返回 &lt;code&gt;Content-Type: image/jpeg&lt;/code&gt;，浏览器就把它当图片渲染，哪怕内容其实是 TS 流。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以在浏览器里，&lt;strong&gt;改后缀 + 改文件头本身不会改变任何行为&lt;/strong&gt; —— 决定行为的是服务器返回的头。&lt;/p&gt;
&lt;p&gt;那为什么这招有用？真正生效的地方是：&lt;/p&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;code&gt;Content-Type&lt;/code&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;&lt;strong&gt;URL 路径的后缀&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;有用&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN / 静态服务的类型分类&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;/td&gt;
&lt;td&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;JPEG 段结构&lt;/td&gt;
&lt;td&gt;❌ 解不出画面&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;strong&gt;最后一行是最重要的：加上 &lt;code&gt;FF D8 FF&lt;/code&gt; 之后，它骗过的是&quot;识别环节&quot;，不是&quot;解码环节&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;真正的图片解码器在读完 SOI 之后，会去找下一个合法标记（&lt;code&gt;FF C0&lt;/code&gt; 帧头、&lt;code&gt;FF E0&lt;/code&gt; APP0 等），结果读到的是 &lt;code&gt;47&lt;/code&gt; 开头的 TS 包 —— 解析直接失败。&lt;strong&gt;所以它不是一个&quot;能显示的图片&quot;，它只是一个&quot;被认为是图片的文件&quot;。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;那它到底解决了什么问题？通常是这两种：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 绕开站点自己的防下载检查。&lt;/strong&gt; 很多站点的&quot;禁止下载&quot;逻辑很朴素：拦截 URL 里出现 &lt;code&gt;/video/&lt;/code&gt;、&lt;code&gt;/media/&lt;/code&gt;、&lt;code&gt;.mp4&lt;/code&gt; 的请求，或者对已知播放器域名做限制。你改成 &lt;code&gt;.jpg&lt;/code&gt;，它的规则就不匹配了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 让文件能通过&quot;按扩展名分类&quot;的流程。&lt;/strong&gt; 比如上传到某个只接受图片的图床/网盘、或者粘贴进某个只认图片的地方。&lt;/p&gt;
&lt;p&gt;:::caution
&lt;strong&gt;顺便说清楚一件事&lt;/strong&gt;：改文件头并不会让你&quot;免费把视频存到图床上&quot;。如果你上传的是&lt;strong&gt;完整的视频内容&lt;/strong&gt;，图床不会因为前三个字节是 JPEG 就把它压缩、转码、或者长期保留 —— 多数图床会做内容检测。要真想在图床上传，那叫滥用，而且大部分服务条款明确禁止。
:::&lt;/p&gt;
&lt;h3&gt;那 Quetta 的下载按钮为什么没弹&lt;/h3&gt;
&lt;p&gt;Quetta 的下载器是&lt;strong&gt;识别&lt;/strong&gt;到了才弹按钮的（它支持 MP4 / WebM / M3U8 / HLS 这些格式）。伪装成图片的分片不在它的识别范围内，所以没有弹出。&lt;strong&gt;这不是 bug，是它没认出来。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;顺便说一个我在用的时候观察到的版本差异：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Quetta 在 &lt;strong&gt;官网（quetta.net）提供的安装包&lt;/strong&gt; 里可以下载 YouTube 视频，&lt;strong&gt;Google Play 商店版本不行&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同款应用、不同分发渠道、功能不同 —— 这在移动端很常见（渠道审核、功能分渠道灰度、区域策略都可能造成差异）。我没有查到官方对这个差异的明确说明，所以这里只记录我实测到的现象。官网版拿得到 Play 版拿不到的功能，Play 版用户可以考虑从官网获取。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、Quetta 本身值在哪&lt;/h2&gt;
&lt;p&gt;抛开这段小插曲，Quetta 这个浏览器我是真在用：&lt;/p&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;/td&gt;
&lt;td&gt;不需要装扩展（支持页面的部分除外）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;视频下载器&lt;/td&gt;
&lt;td&gt;支持 MP4 / WebM / M3U8 / HLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原生支持 Chrome 扩展&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;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;支持 Chrome 扩展&lt;/strong&gt;这一条是杀手级。移动端浏览器普遍不支持扩展，导致很多&quot;网页端能做、手机上做不了&quot;的事做不了。Quetta 直接把这个口子开了 —— 我装了个广告拦截扩展，规则和电脑上完全一致。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;七、总结这一个小结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;观察到矛盾&lt;/strong&gt;：视频在播，但 Network 里全是 &lt;code&gt;.jpg&lt;/code&gt;，没有视频请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推断&lt;/strong&gt;：视频被切成 MPEG-TS 分片，以&quot;图片&quot;的名义走 CDN —— 白嫖图片托管的带宽和缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.ts&lt;/code&gt; 是 MPEG-2 Transport Stream&lt;/strong&gt;：188 字节定长包，每包开头一个 &lt;code&gt;0x47&lt;/code&gt; 同步字节，为抗错和可复用而设计；HLS 分片流媒体的标准容器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BOM&lt;/strong&gt; 狭义是字节序标记（UTF-8 是 &lt;code&gt;EF BB BF&lt;/code&gt;），广义就是&lt;strong&gt;文件魔数&lt;/strong&gt; —— 靠开头几字节认类型。JPEG 的 SOI 是 &lt;code&gt;FF D8 FF&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这招骗的是&quot;识别&quot;不是&quot;解码&quot;&lt;/strong&gt;：浏览器看 &lt;code&gt;Content-Type&lt;/code&gt;，图片解码器看段结构；真正被绕过的是站点自己的防下载规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quetta 值在原生支持 Chrome 扩展&lt;/strong&gt;，官网版和 Play 版功能有差异。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整件事最值得记的一点，其实跟下载无关：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;遇到&quot;看到的现象&quot;和&quot;知道的事实&quot;矛盾时，F12 是最短的路。&lt;/strong&gt; 屏幕上的一切都是别人写好的结果，而 Network 面板里的是真实发生的请求。&lt;/p&gt;
&lt;p&gt;这次我只是觉得&quot;没弹出下载按钮有点奇怪&quot;，多按了一次 F12。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>别从零写博客：fork 一个 Astro 模板，再让 CI 替你把关</title><link>https://ycxdev.com/posts/github-actions-ci-and-fork-astro-template/</link><guid isPermaLink="true">https://ycxdev.com/posts/github-actions-ci-and-fork-astro-template/</guid><description>Astro 是 Cloudflare 官方推荐的框架，主题是现成的 MIT 项目。真正的工作量从来不在框架上，而在内容上。本文讲我怎么借力省下两个月，以及三条 CI 流水线各自在守什么、为什么不跑 build。</description><pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写博客最容易走的弯路，是从&quot;我要做一个博客&quot;开始。&lt;/p&gt;
&lt;p&gt;于是你开始：初始化项目、写 HTML 骨架、引入 CSS 框架、做亮/暗色主题切换、写文章列表分页、写目录导航、写 RSS、写搜索、写代码高亮、写 SEO meta……&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三个月后你有一个博客框架，和零篇文章。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我这次换了个思路。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、Astro 是 Cloudflare 官方推荐的框架&lt;/h2&gt;
&lt;p&gt;先把选型的成本降到零：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare 官方文档里有 &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/cloudflare/&quot;&gt;Astro on Cloudflare&lt;/a&gt; 的专门集成指南&lt;/li&gt;
&lt;li&gt;静态输出直接推到 Pages，&lt;strong&gt;连 adapter 都不需要&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;因为是官方推荐，社区里针对 Pages 的踩坑方案和 workaround 都是现成的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选型这一步没花我十分钟。&lt;/p&gt;
&lt;p&gt;主题也是现成的：&lt;a href=&quot;https://github.com/saicaca/fuwari&quot;&gt;Fuwari&lt;/a&gt;（MIT），一个完成度相当高的博客主题，开箱有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;亮色/暗色/跟随系统三态主题，带 localStorage 持久化&lt;/li&gt;
&lt;li&gt;标签页 / 分类聚合 / 归档 / 搜索&lt;/li&gt;
&lt;li&gt;文章目录（TOC）、上下篇导航、阅读时长&lt;/li&gt;
&lt;li&gt;KaTeX 数学公式、代码高亮、代码折叠&lt;/li&gt;
&lt;li&gt;页面转场动画（Swup）&lt;/li&gt;
&lt;li&gt;i18n（10 种语言）&lt;/li&gt;
&lt;li&gt;评论、站点地图、RSS&lt;/li&gt;
&lt;li&gt;图片懒加载 + 灯箱（PhotoSwipe）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些随便挑一项从零写，都够我写一个周末。&lt;strong&gt;所以我的第一个动作是 fork，不是 new。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/saicaca/fuwari.git my-blog
cd my-blog
rm -rf .git &amp;amp;&amp;amp; git init
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
这就是&quot;学会助力&quot;的意思。&lt;strong&gt;学生思维是&quot;这个东西我不会，我要去学&quot;。工程师思维是&quot;这个东西有人做好了，我要去用&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我学到的东西不会消失，借来的能力也不会。区别只在于我把时间花在了写文章上，而不是写 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 里的 meta 标签上。&lt;/p&gt;
&lt;p&gt;博客最重要的是&lt;strong&gt;博文&lt;/strong&gt;。框架能省的时间，就该省下来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、CI 只做三件事，每件都有明确边界&lt;/h2&gt;
&lt;p&gt;fork 完之后我加了三条 GitHub Actions 流水线。先说最重要的一个设计决策：&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;CI 里不跑 &lt;code&gt;astro build&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Cloudflare Pages 每次 push 都会完整构建一次。CI 再跑一遍是纯浪费 —— 同样的依赖安装、同样的构建，跑到两台不同的机器上。&lt;/p&gt;
&lt;p&gt;所以我做了分工：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cloudflare Pages  负责：这份源码能不能构建成一个能用的站点
GitHub Actions    负责：这份源码本身写对了没有
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建成功与否，Pages 会在 commit 上写 status，我没必要重复验证。&lt;strong&gt;CI 只补 Pages 不管的那部分检查。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;流水线 1：&lt;code&gt;astro check&lt;/code&gt; —— 类型检查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;name: Astro Check
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true      # 连推多次时取消旧的，省 Actions 分钟数

permissions:
  contents: read                # 最小权限，默认 token 权限更大

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
        with:
          node-version: 22
      - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
        with:
          run_install: false
      - run: pnpm install --frozen-lockfile
      - run: pnpm astro check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;@11bd7190...&lt;/code&gt; 这种写法 —— &lt;strong&gt;action 版本钉死在 commit SHA 上&lt;/strong&gt;，注释里写上对应的 tag。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ❌ 别人改了 tag 指向，你下次构建跑的就是新代码
uses: actions/checkout@v4

# ✅ 供应链攻击改不了你的构建产物
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是洁癖。&lt;code&gt;actions/checkout&lt;/code&gt; 这种被所有仓库引用的 action，一旦 tag 被劫持就是全网级的供应链事件。个人博客用 SHA 钉死，零成本。&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;--frozen-lockfile&lt;/code&gt; 是 pnpm 对应 &lt;code&gt;npm ci&lt;/code&gt; 的严格模式：&lt;strong&gt;lockfile 和 package.json 不一致就直接失败&lt;/strong&gt;。CI 里必须用它，否则本地偷偷改了依赖但没更新 lockfile，CI 会装出一套和本地不一样的依赖，bug 现场极难复现。
:::&lt;/p&gt;
&lt;h3&gt;流水线 2：Biome 代码质量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;name: Code quality
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71...
      - uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
        with:
          version: latest
      - run: biome ci ./src --reporter=github
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--reporter=github&lt;/code&gt; 让问题直接显示在 PR 的 Files Changed 里，标注到具体行。省掉&quot;CI 红了 → 本地跑一遍 → 对照行号&quot;这一套。&lt;/p&gt;
&lt;p&gt;:::warning
&lt;strong&gt;这条流水线在 Windows 本地有个大坑。&lt;/strong&gt; 我的工作区是 CRLF，仓库里存的是 LF。本地跑 &lt;code&gt;biome ci ./src&lt;/code&gt; 会误报几十个格式错误，但 CI 是 Linux，是绿的。&lt;/p&gt;
&lt;p&gt;也就是说：&lt;strong&gt;本地红了不代表代码有问题，但你不能因此就无视它。&lt;/strong&gt; 要在本地验证，得先把文件转成 LF：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get-ChildItem -Recurse -Path .\src -Include *.ts,*.astro,*.css,*.styl |
  ForEach-Object {
    $c = [IO.File]::ReadAllText($_.FullName)
    [IO.File]::WriteAllText($_.FullName, $c.Replace(&quot;`r`n&quot;, &quot;`n&quot;), (New-Object Text.UTF8Encoding $false))
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 Windows 开发最没劲的地方之一：&lt;strong&gt;你没做错，但工具链因为换行符给你制造了一个不存在的问题。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h3&gt;流水线 3：IndexNow —— 主动推送给搜索引擎&lt;/h3&gt;
&lt;p&gt;这个是我折腾得最值的一条。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;/strong&gt;：Google 在 2023 年下线了 sitemap ping 接口。所以&quot;发布完主动告诉搜索引擎&quot;这件事，主流方案里只剩 IndexNow 还活着。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;IndexNow&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要注册账号吗&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不需要&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;参与方&lt;/td&gt;
&lt;td&gt;Bing、Yandex、Seznam、Naver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;❌ 不参与（走 Search Console sitemap 提交）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;归属验证&lt;/td&gt;
&lt;td&gt;根目录放一个 &lt;code&gt;&amp;lt;key&amp;gt;.txt&lt;/code&gt;，内容等于文件名&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;归属验证那一招很妙 —— 不需要 DNS TXT、不需要账号，只要你的站点上有一个文件。&lt;strong&gt;换域名也不用重新验证&lt;/strong&gt;，把文件一起部署就行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: IndexNow
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  submit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71...

      # ① 等 Pages 把新版本部署上去
      - name: Wait for the new Pages deployment to go live
        run: |
          set -u
          SITE=&quot;https://ycxdev.com&quot;
          for i in $(seq 1 30); do
            if curl -fsS &quot;$SITE/robots.txt&quot; | grep -q &quot;Sitemap&quot;; then
              echo &quot;Site is serving: $SITE&quot;
              exit 0
            fi
            echo &quot;waiting for deployment... ($i/30)&quot;
            sleep 10
          done
          echo &quot;Site did not come up in time, continuing anyway&quot;

      # ② 推送
      - name: Submit URLs to IndexNow
        continue-on-error: true      # 推送失败不该让 CI 变红
        run: |
          URLS=$(curl -fsS &quot;$SITE/sitemap-0.xml&quot; \
            | grep -o &apos;&amp;lt;loc&amp;gt;[^&amp;lt;]*&amp;lt;/loc&amp;gt;&apos; | sed -e &apos;s|&amp;lt;loc&amp;gt;||&apos; -e &apos;s|&amp;lt;/loc&amp;gt;||&apos; \
            | head -100 | jq -R . | jq -s .)

          jq -n --arg host &quot;ycxdev.com&quot; \
                --arg key &quot;$KEY&quot; \
                --arg keyLocation &quot;$SITE/$KEY.txt&quot; \
                --argjson urlList &quot;$URLS&quot; \
            &apos;{host:$host,key:$key,keyLocation:$keyLocation,urlList:$urlList}&apos; &amp;gt; payload.json

          curl -sS -X POST &quot;https://api.indexnow.org/indexnow&quot; \
            -H &quot;Content-Type: application/json; charset=utf-8&quot; \
            --data-binary @payload.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步里有&lt;strong&gt;两个细节是我后来才补上的，都是踩出来的&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;:::caution
&lt;strong&gt;① 必须等部署生效再推送。&lt;/strong&gt; 否则你告诉搜索引擎&quot;这个 URL 有新内容了&quot;，它立刻来抓，结果页面 404 —— 这是最糟糕的信号。Cloudflare Pages 构建要一两分钟，所以要轮询等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② &lt;code&gt;continue-on-error: true&lt;/code&gt;。&lt;/strong&gt; 搜索引擎推送失败是网络波动类的错误，&lt;strong&gt;不应该让 CI 变红&lt;/strong&gt;。变红了就会养成&quot;CI 红了就 rerun&quot;的习惯，真正的构建失败会被淹没。
:::&lt;/p&gt;
&lt;h3&gt;附赠：Dependabot&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;version: 2
updates:
  - package-ecosystem: &quot;npm&quot;
    directory: &quot;/&quot;
    schedule:
      interval: &quot;daily&quot;
    groups:
      patch-updates:
        patterns: [&quot;*&quot;]
        update-types: [&quot;patch&quot;]
      minor-updates:
        patterns: [&quot;*&quot;]
        update-types: [&quot;minor&quot;]
    ignore:
      - dependency-name: &quot;*&quot;
        update-types: [&quot;version-update:semver-major&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖更新全自动开 PR。patch 和 minor 分成两组（一次提一堆，不会让你收到几十个 PR），major 忽略 —— &lt;strong&gt;大版本变更必须人工判断，不能自动合&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、整条链路跑起来的样子&lt;/h2&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant ME as 我
participant GH as GitHub
participant CI as GitHub Actions
participant CF as Cloudflare Pages
participant IX as IndexNow
participant SE as Bing / Yandex
ME-&amp;gt;&amp;gt;GH: git push main
par 两条流水线并发跑
GH-&amp;gt;&amp;gt;CI: Astro Check + Biome
CI-&amp;gt;&amp;gt;CI: pnpm install → astro check → biome ci
CI--&amp;gt;&amp;gt;GH: 状态写回 commit
end
GH-&amp;gt;&amp;gt;CF: Webhook
CF-&amp;gt;&amp;gt;GH: 拉源码
CF-&amp;gt;&amp;gt;CF: pnpm build → dist
Note over CF: 构建失败则不动线上版本（原子部署）
CF--&amp;gt;&amp;gt;SE: 线上已更新
GH-&amp;gt;&amp;gt;CI: IndexNow 流水线启动
CI-&amp;gt;&amp;gt;CF: 轮询 robots.txt，等新版本真的上线
CF--&amp;gt;&amp;gt;CI: 就绪
CI-&amp;gt;&amp;gt;CF: 抓 sitemap-0.xml
CI-&amp;gt;&amp;gt;IX: POST {host, key, keyLocation, urlList}
IX-&amp;gt;&amp;gt;SE: 通知参与方
SE-&amp;gt;&amp;gt;CF: 立刻重新抓取新文章
Note over CI: continue-on-error，推送失败不影响 CI 状态
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、我在这个过程中做的减法&lt;/h2&gt;
&lt;p&gt;一开始我的 CI 是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strategy:
  matrix:
    node-version: [18, 20, 22]
steps:
  - run: pnpm astro check
  - run: pnpm astro build      # ← 和 Pages 重复
  - run: pnpm test             # ← 项目里根本没有测试
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跑了几次之后删到只剩一个 job、一个 step。原因很简单：&lt;/p&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;Node 18/20/22 矩阵&lt;/td&gt;
&lt;td&gt;Pages 只用一个版本，构建矩阵没有意义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;astro build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pages 每次 push 都构建，重复且更慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pnpm test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;没有测试文件，红的&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;CI 的成本不是分钟数，是维护成本。&lt;/strong&gt; 一个总是红的 CI 会被忽略，一个没人看的 CI 等于没有。每加一条流水线，就多一份将来会过期、会误报、会让你养成 rerun 习惯的负担。
:::&lt;/p&gt;
&lt;p&gt;删掉冗余检查之后，CI 变成了它该有的样子：&lt;strong&gt;一个纯粹的、信号明确的红绿灯。&lt;/strong&gt; 红了就是真出问题了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小int小结&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;选型    → 官方推荐的框架（Cloudflare 文档里有 Astro 集成）
起步    → fork 一个完成度高的 MIT 主题，别从 &amp;lt;head&amp;gt; 开始
CI 1    → astro check：类型检查（Pages 不管这个）
CI 2    → biome ci：代码质量，reporter 直出 PR
CI 3    → IndexNow：免费、无需账号、主动推送搜索引擎
不做    → 不跑 build（Pages 已经跑了）、不跑矩阵（就一个版本）、不跑没用的 test
记得    → action 钉 SHA、--frozen-lockfile、推送失败 continue-on-error、
          push 之后先等部署上线再推索引
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用别人的框架 + 让 CI 守住底线，我在这个博客上的&quot;工程投入&quot;大概是&lt;strong&gt;一个周末&lt;/strong&gt;。剩下的全是写文章。&lt;/p&gt;
&lt;p&gt;这才是正确的比例。&lt;/p&gt;
</content:encoded></item><item><title>GitHub + Cloudflare Pages：零成本、无备案、全球 CDN 的个人博客</title><link>https://ycxdev.com/posts/github-cloudflare-pages-free-blog/</link><guid isPermaLink="true">https://ycxdev.com/posts/github-cloudflare-pages-free-blog/</guid><description>GitHub 存源码、Cloudflare Pages 跑构建、全球 CDN 直接白嫖 —— 不用服务器、不用绑卡、不用备案。本文讲清整条部署链路、免费额度有多少、换自有域名时必须同步改哪四个地方，以及几个会让你构建失败静默上线的坑。</description><pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;个人博客这件事最容易犯的错，是一上来就想&quot;我要做个什么&quot;。&lt;/p&gt;
&lt;p&gt;我这次反过来做：&lt;strong&gt;先确定&quot;最省事的方案能长什么样&quot;，再倒推技术选型。&lt;/strong&gt; 结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;成本       ¥0（Cloudflare 侧；域名另算，见下）
域名       可以完全不买（用免费的 *.pages.dev）
备案       不用（原因后面讲清）
服务器     不用（Pages 是静态托管）
CDN        Cloudflare 全球 anycast，默认就有
HTTPS      自动签发，自动续期
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整套东西的核心就一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GitHub 存源码，Cloudflare Pages 跑构建，两边一个 push 打通。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;一、整条链路长什么样&lt;/h2&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant ME as 我（本地）
participant GH as GitHub 仓库
participant CF as Cloudflare Pages
participant CFCDN as Cloudflare 全球 CDN
participant U as 读者
ME-&amp;gt;&amp;gt;ME: 本地 pnpm build 验证
ME-&amp;gt;&amp;gt;GH: git push main
GH-&amp;gt;&amp;gt;CF: Webhook 通知（仓库变更）
CF-&amp;gt;&amp;gt;GH: 拉取源码（按 commit SHA）
CF-&amp;gt;&amp;gt;CF: 按 .nvmrc / package.json 装环境
CF-&amp;gt;&amp;gt;CF: pnpm install → pnpm build
CF-&amp;gt;&amp;gt;CF: 产出 dist/ 静态文件
CF-&amp;gt;&amp;gt;CF: 原子替换上线（发布 Production 部署）
CF--&amp;gt;&amp;gt;GH: 部署状态回写 commit status
U-&amp;gt;&amp;gt;CFCDN: 访问 https://ycxdev.com
CFCDN-&amp;gt;&amp;gt;CFCDN: 就近边缘节点命中缓存
CFCDN--&amp;gt;&amp;gt;U: 返回 HTML/CSS/JS（自动 HTTPS）
Note over U,CFCDN: 读者不需要回源，成本也不按流量计费
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;几个值得注意的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare 主动拉代码&lt;/strong&gt;，不是 GitHub 推产物。所以你的密钥不需要交给 GitHub Actions，Cloudflare 用的是它自己跟 GitHub 的 OAuth 授权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署是原子的&lt;/strong&gt;：新构建完整成功后才切换，失败不影响线上版本。这一点比很多自建方案强 —— 你 push 了一个坏 commit，线上还是好的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每次 push 都有独立的部署 URL&lt;/strong&gt;，除了 production 还有一堆 preview 部署，24 小时后自动清理。想看草稿效果随便推。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、免费额度到底有多少&lt;/h2&gt;
&lt;p&gt;Cloudflare Pages 免费版（Free plan）的关键数字：&lt;/p&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;/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;500 次 / 月&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;20,000&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单文件大小&lt;/td&gt;
&lt;td&gt;25 MiB&lt;/td&gt;
&lt;td&gt;所以图片别往仓库里塞特大的原图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构建超时&lt;/td&gt;
&lt;td&gt;20 分钟&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;&lt;strong&gt;不限流量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pages 不按流量计费，这是它最大的优势&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义域名&lt;/td&gt;
&lt;td&gt;100 个 / 项目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
&quot;不限流量&quot;这一条要重点说一下。很多静态托管（Vercel Hobby、Netlify 免费版）都有流量/构建额度限制，超了就限速或停服。Cloudflare Pages 免费版不按流量收费 —— 这也是我最后选它的主要原因之一。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键约束：注册时不要绑信用卡。&lt;/strong&gt; 绑卡后会进入需要验证的账户体系，一堆原本免费的功能会变成&quot;需要开通付费&quot;。这个博客从头到尾只在免费额度里跑。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、为什么不用备案&lt;/h2&gt;
&lt;p&gt;这个是很多国内用户的顾虑，讲清楚原理就不神秘了。&lt;/p&gt;
&lt;p&gt;ICP 备案的触发条件是&lt;strong&gt;两个同时成立&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 网站内容/服务器 面向中国大陆境内用户提供服务
② 服务器 部署在中国大陆境内（境内 IP 的机房）

备案才需要。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 Pages 的机器在 &lt;strong&gt;Cloudflare 的全球 anycast 网络上&lt;/strong&gt;（境外节点）。你的域名解析过去，落到的是境外机房的 IP，&lt;strong&gt;不满足第 ② 条&lt;/strong&gt;，所以不需要备案。&lt;/p&gt;
&lt;p&gt;:::warning
需要避开的坑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要开 Cloudflare 中国网络（China Network）&lt;/strong&gt; —— 那个是企业版付费功能，明确要求域名已完成 ICP 备案。开了反而会要求你有备案。&lt;/li&gt;
&lt;li&gt;如果你买的是 &lt;code&gt;.cn&lt;/code&gt; 域名并做实名认证，注意 &lt;code&gt;.cn&lt;/code&gt; 域名本身在境内就有实名要求，这跟站点备案是两件事。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我的站点现在跑在 &lt;code&gt;ycxdev.com&lt;/code&gt; 上，走的就是海外 anycast。国内访问速度取决于运营商和跨境链路，但这不是&quot;不合规&quot;的问题，只是延迟问题。&lt;/p&gt;
&lt;h3&gt;那域名要不要买&lt;/h3&gt;
&lt;p&gt;分两种情况：&lt;/p&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;code&gt;xxx.pages.dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ycxdev.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;花钱&lt;/td&gt;
&lt;td&gt;¥0&lt;/td&gt;
&lt;td&gt;域名年费（&lt;code&gt;.com&lt;/code&gt; 通常几十块/年）&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;strong&gt;仍然不需要&lt;/strong&gt;（托管仍在境外）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;换托管商&lt;/td&gt;
&lt;td&gt;只能改 Cloudflare 项目名&lt;/td&gt;
&lt;td&gt;改 NS 记录，随时搬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邮件 / 其他服务&lt;/td&gt;
&lt;td&gt;用不上&lt;/td&gt;
&lt;td&gt;可以拿去做邮箱、Redirect 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Graph 观感&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;p&gt;&lt;strong&gt;先用 &lt;code&gt;*.pages.dev&lt;/code&gt; 把内容跑起来，等真的有内容了再买域名。&lt;/strong&gt; 我一开始也是纯 &lt;code&gt;pages.dev&lt;/code&gt;，后来才换的 —— 换域名只是加一条 DNS 记录 + 在 Pages 里绑一次，站点内容一个字都不用动。&lt;/p&gt;
&lt;p&gt;:::note
换域名之后有件事&lt;strong&gt;必须手动改&lt;/strong&gt;，否则线上会静默出问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;astro.config.mjs  的 site:          → canonical / sitemap / RSS 的绝对地址
public/admin/config.yml 的 site_url → CMS 里生成的链接
.github/workflows/indexnow.yml      → 推给搜索引擎的 host 和站点地址
CF 上绑定的 Web Analytics 站点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些地方都拿&quot;站点绝对地址&quot;来拼 URL。改域名时漏改一个，canonical 就会指向旧域名，搜索引擎会当成两个站处理。
:::&lt;/p&gt;
&lt;p&gt;:::warning
需要避开的坑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要开 Cloudflare 中国网络（China Network）&lt;/strong&gt; —— 那个是企业版付费功能，明确要求域名已完成 ICP 备案。开了反而要求你有备案。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.cn&lt;/code&gt; / &lt;code&gt;.com&lt;/code&gt; 在境内注册商买域名要做&lt;strong&gt;域名实名认证&lt;/strong&gt;，但这是&quot;域名实名&quot;，跟&quot;网站 ICP 备案&quot;是两件不同的事，别混。&lt;/li&gt;
&lt;li&gt;域名买了不等于站点备案了，也不等于需要备案 —— 取决于你的服务器在哪。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;四、顺手白嫖的 Cloudflare 设施&lt;/h2&gt;
&lt;p&gt;Pages 只是入口。同一个账号下免费额度里还有一堆能直接用的东西，我都用上了：&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
flowchart LR
A[&quot;Cloudflare 免费账号&quot;] --&amp;gt; B[&quot;Pages&amp;lt;br/&amp;gt;静态托管 + CDN&quot;]
A --&amp;gt; C[&quot;Pages Functions&amp;lt;br/&amp;gt;10万请求/天&quot;]
A --&amp;gt; D[&quot;KV&amp;lt;br/&amp;gt;1000 写入/天&quot;]
A --&amp;gt; E[&quot;D1 (SQLite)&amp;lt;br/&amp;gt;500万行读/天&quot;]
A --&amp;gt; F[&quot;Workers&amp;lt;br/&amp;gt;10万请求/天&quot;]
A --&amp;gt; G[&quot;Web Analytics&amp;lt;br/&amp;gt;无 Cookie 无采样&quot;]
A --&amp;gt; H[&quot;Turnstile&amp;lt;br/&amp;gt;人机验证&quot;]
B --&amp;gt; C
C --&amp;gt; D
C --&amp;gt; G
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;具体我这个博客用到的：&lt;/p&gt;
&lt;h3&gt;1. Pages Functions + KV 做阅读量&lt;/h3&gt;
&lt;p&gt;纯静态站点没法记&quot;这篇文章被看了多少次&quot;，因为静态文件没有后端状态。用 Pages Functions（本质是 Workers）当后端，数据丢 KV：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// functions/api/views.ts （简化）
export async function onRequestGet({ request, env }) {
  const path = new URL(request.url).searchParams.get(&quot;path&quot;);
  const key = `v:${path}`;
  const n = parseInt((await env.VIEW_COUNTS.get(key)) ?? &quot;0&quot;) + 1;
  await env.VIEW_COUNTS.put(key, String(n));
  return Response.json({ views: n });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;strong&gt;KV 免费额度只有 1000 次写入 / 天。&lt;/strong&gt; 一个写入对应一次自增。所以这个计数接口必须做防重 —— 我在前端用 &lt;code&gt;lastCountedPath&lt;/code&gt; 保证同一次浏览只计一次。超额了 KV 会直接拒绝写入（不会扣费，但会报错）。这也是为什么阅读量要&quot;点开才显示&quot;而不是放在首屏阻塞渲染。
:::&lt;/p&gt;
&lt;h3&gt;2. Web Analytics&lt;/h3&gt;
&lt;p&gt;Cloudflare 的 Web Analytics &lt;strong&gt;免费、无 Cookie、不采样&lt;/strong&gt;，而且关键是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;它不要求站点托管在 Cloudflare。&lt;/strong&gt; &lt;code&gt;*.pages.dev&lt;/code&gt; 这种不属于你自己 zone 的域名也能用（拿不到 zone 级功能，但 RUM 可以）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;打开方式就两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Cloudflare 自动注入的 beacon --&amp;gt;
&amp;lt;script defer src=&quot;https://static.cloudflareinsights.com/beacon.min.js&quot;
        data-cf-beacon=&apos;{&quot;token&quot;:&quot;&amp;lt;你的 32 位 token&amp;gt;&quot;}&apos;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
那个 token 本身就是公开的 —— 它必须出现在前端 JS 里才能工作。所以看到别人页面上有这个 token 不构成安全问题，不用当成密钥来藏。
:::&lt;/p&gt;
&lt;h3&gt;3. OAuth 中转 Worker&lt;/h3&gt;
&lt;p&gt;我给博客配了一个 Sveltia CMS，需要 GitHub OAuth 登录。GitHub OAuth 的回调地址必须是&lt;strong&gt;注册过的绝对 URL&lt;/strong&gt;，用 localhost 开发时很麻烦。我用一个免费 Worker 做中转，把回调先落到 Worker 上再 302 回本地。&lt;/p&gt;
&lt;p&gt;这是 Workers 免费额度（10 万请求/天）最典型的用法。&lt;/p&gt;
&lt;h3&gt;4. 用不到的（因为要绑卡）&lt;/h3&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;Cloudflare R2&lt;/td&gt;
&lt;td&gt;需要绑卡开通&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Images&lt;/td&gt;
&lt;td&gt;需要绑卡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Stream&lt;/td&gt;
&lt;td&gt;需要绑卡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workers Paid / Pages 付费增强&lt;/td&gt;
&lt;td&gt;需要绑卡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;China Network&lt;/td&gt;
&lt;td&gt;需要备案 + 企业合同&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;五、Cloudflare Pages 部署 Astro 的几个坑&lt;/h2&gt;
&lt;p&gt;Astro 是 Cloudflare 官方推荐的框架之一（他们有专门的 Pages 集成文档），但纯静态输出用 Pages 时&lt;strong&gt;不需要 adapter&lt;/strong&gt;。我踩过的坑：&lt;/p&gt;
&lt;h3&gt;坑 1：Node 版本不一致导致构建失败&lt;/h3&gt;
&lt;p&gt;Cloudflare 默认用自己固定的 Node 大版本。本地 &lt;code&gt;node -v&lt;/code&gt; 是 20，构建机上可能是 22，某些原生依赖会挂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：加 &lt;code&gt;.nvmrc&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;坑 2：包管理器必须一致&lt;/h3&gt;
&lt;p&gt;用 pnpm 就要让 Cloudflare 也用 pnpm。有两种做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 仓库根目录加一个 corepack 声明
&quot;packageManager&quot;: &quot;pnpm@9.14.4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;坑 3：自定义响应头（CSP）用错文件名&lt;/h3&gt;
&lt;p&gt;Cloudflare Pages 的自定义 header 文件名必须是 &lt;strong&gt;&lt;code&gt;_headers&lt;/code&gt;&lt;/strong&gt;，放在 &lt;code&gt;public/&lt;/code&gt; 下，会被原样拷到 &lt;code&gt;dist/&lt;/code&gt; 根目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public/_headers  →  dist/_headers  →  边缘节点生效
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
CSP 一开始我是开着的，结果被内联的 analytics beacon 和几个第三方资源全部挡掉。&lt;strong&gt;先上 &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; 观察一段时间，确认没有违规再切成强制模式。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h3&gt;坑 4：构建失败时线上是完好的&lt;/h3&gt;
&lt;p&gt;这不是坑，是优点，值得单独强调：&lt;strong&gt;Cloudflare Pages 的部署是原子的&lt;/strong&gt;。构建失败就停在原版本，线上不受影响。我有一次 commit 写崩了 markdown 配置，GitHub 上 CI 红了，站点还是好的 —— 改完再推就恢复了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、为什么源码一定放 GitHub&lt;/h2&gt;
&lt;p&gt;这一点我一开始想错过，后来发现是整个方案的地基。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cloudflare Pages  ┐
                  ├──→ 内容以 GitHub 仓库为唯一来源（single source of truth）
Sveltia CMS       ┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pages 只是构建产物&lt;/strong&gt;。删了重建一条命令，不丢任何东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMS 也只是编辑器&lt;/strong&gt;，它提交的变更最终是往仓库里 push Markdown 文件。所以 CMS 挂了不影响发布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有完整历史&lt;/strong&gt;。哪天想回退到三个月前的某个版本，&lt;code&gt;git revert&lt;/code&gt; 一下，Cloudflare 自动重新构建。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移成本低&lt;/strong&gt;。以后想换别的静态托管（Vercel / Netlify / 自建），换的是部署配置，内容文件一个字都不用改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
我的仓库是 &lt;strong&gt;Private&lt;/strong&gt;。内容里有私人记录，而且 Cloudflare 的 GitHub 集成支持私有仓库 —— 构建机上拿的是只读的短期凭据，源码不会因为部署而泄露。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构就一句话&lt;/strong&gt;：GitHub 是唯一真源，Cloudflare Pages 负责构建和 CDN，中间一个 webhook 打通。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本 ¥0&lt;/strong&gt;：不绑卡、不用服务器、不备案（域名可选，先白嫖 &lt;code&gt;*.pages.dev&lt;/code&gt; 也行）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不限流量&lt;/strong&gt;是 Pages 免费版最大的卖点，500 次构建/月对个人博客远远够用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不备案的原因&lt;/strong&gt;：备案要求&quot;境内服务器&quot;，Pages 在境外 anycast，两者不同时成立。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顺手白嫖&lt;/strong&gt;：KV + Functions 做阅读量、Web Analytics 做统计、Workers 做 OAuth 中转、Turnstile 防机器人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避开要绑卡的&lt;/strong&gt;：R2 / Images / Stream / Workers Paid / China Network。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小心 Node 版本、包管理器、&lt;code&gt;_headers&lt;/code&gt; 文件名&lt;/strong&gt;这三个构建坑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在这个博客从推代码到全球可访问，全程不超过两分钟，成本为零。剩下的时间全用来写东西 —— 这才是博客该有的样子。&lt;/p&gt;
</content:encoded></item><item><title>在已经部署好的博客上写博客，然后自动流回 GitHub</title><link>https://ycxdev.com/posts/sveltia-cms-reverse-sync-github/</link><guid isPermaLink="true">https://ycxdev.com/posts/sveltia-cms-reverse-sync-github/</guid><description>博客已经是一堆静态 HTML 了，怎么还能在网页上直接编辑？答案是 CMS 本身就跟着站点一起部署，而它的写入目标是 GitHub 仓库。本文拆解 Sveltia CMS + GitHub OAuth 中转 Worker 的完整回路，附可复制的配置和一个把人卡两天的路径坑。</description><pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一篇讲了怎么把博客部署上去。部署完有个很朴素的困惑：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;站点已经是 Cloudflare 上的静态 HTML 了，我怎么在这个网页上直接写新文章？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总不能每次都 &lt;code&gt;vim&lt;/code&gt; 一遍、&lt;code&gt;git add&lt;/code&gt;、&lt;code&gt;git commit&lt;/code&gt;、&lt;code&gt;git push&lt;/code&gt; 吧 —— 虽然我实际上就是这么干的（因为要给 AI 写 AGENTS.md 协作）。&lt;/p&gt;
&lt;p&gt;答案是一个很漂亮的闭环设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ┌──────────────────────────────────────────────┐
        │                                              │
        ▼                                              │
   Cloudflare Pages（静态站点）                          │
        │                                              │
        │  /admin/ 下跟着站点一起部署了 CMS 的 index.html  │
        │                                              │
        └────────────┬─────────────────────┘            │
                     │                                  │
                     │  保存文章                          │
                     ▼                                  │
              GitHub 仓库（唯一真源）                    │
                     │                                  │
                     │  git push                         │
                     └────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CMS 跟着站点走，但写入目标是仓库。&lt;/strong&gt; 站点既是发布产物，也是编辑器。这条回路走通之后，写文章就真的变成&quot;打开网址 → 写 → 点保存&quot;了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、为什么选 Sveltia 而不是自己写后台&lt;/h2&gt;
&lt;p&gt;先说清楚我&lt;strong&gt;不&lt;/strong&gt;想干什么：&lt;/p&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;/td&gt;
&lt;td&gt;那就不是&quot;静态博客&quot;了，要维护服务、要备份数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion / 飞书文档直接同步&lt;/td&gt;
&lt;td&gt;内容不在 Git 里，仓库不再是唯一真源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直接在服务器上编辑文件&lt;/td&gt;
&lt;td&gt;Pages 没有文件系统给你编辑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我要的是：&lt;strong&gt;一个纯前端、零后端、直接调用 GitHub API 的 CMS&lt;/strong&gt;。GitHub 就是数据库，commit 就是历史，PR 就是预览。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/sveltia/sveltia-cms&quot;&gt;Sveltia CMS&lt;/a&gt;（Decap CMS 的活跃 fork）正好干这个。它是一个单文件 JS，加载就跑，所有读写都走 GitHub API。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、挂进站点：它只是一个静态文件&lt;/h2&gt;
&lt;p&gt;CMS 不需要单独部署，把它当普通文件丢进 &lt;code&gt;public/admin/&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- public/admin/index.html --&amp;gt;
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;meta name=&quot;robots&quot; content=&quot;noindex&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Blog Admin&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就这些。Cloudflare 构建时把它原样拷到 &lt;code&gt;dist/admin/index.html&lt;/code&gt;，于是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://ycxdev.com/admin/         →  CMS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;meta robots: noindex&lt;/code&gt; 别忘了 —— 否则搜索引擎会收录你的后台。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、配置：告诉它写到哪去&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;public/admin/config.yml&lt;/code&gt;（这是我博客的真实配置）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;backend:
  name: github
  repo: devycx/MyBlog
  branch: main
  base_url: https://sveltia-cms-auth.&amp;lt;你的-workers-子域&amp;gt;.workers.dev

site_url: https://ycxdev.com

# 顶层媒体目录（供资源库 / Asset Library 使用）
media_folder: src/content/posts/images
public_folder: /src/content/posts/images

collections:
  - name: posts
    label: Posts
    folder: src/content/posts
    media_folder: images        # ← 集合级，相对路径
    public_folder: images       # ← 集合级，相对路径
    create: true
    slug: &quot;{{slug}}&quot;
    extension: md
    summary: &quot;{{title}} - {{published}}&quot;
    fields:
      - { name: title, label: Title, widget: string }
      - name: published
        label: Published
        widget: datetime
        date_format: &quot;YYYY-MM-DD&quot;
        time_format: false
        format: &quot;YYYY-MM-DD&quot;
      - { name: description, label: Description, widget: text, required: false }
      - { name: image, label: Cover Image, widget: image, required: false }
      - { name: tags, label: Tags, widget: list, required: false, default: [] }
      - { name: category, label: Category, widget: string, required: false }
      - { name: draft, label: Draft, widget: boolean, default: false }
      - { name: body, label: Body, widget: markdown }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;folder: src/content/posts&lt;/code&gt; 正好是 Astro content collection 的目录。所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CMS 保存  →  GitHub 上多一个 .md 文件
Astro 构建 →  读同一个目录 → 生成一个页面
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;中间没有任何转换层。&lt;/strong&gt; 这是整套设计最舒服的地方。&lt;/p&gt;
&lt;p&gt;:::note
那个 &lt;code&gt;draft&lt;/code&gt; 开关是真的有用的。站点里过滤草稿的逻辑就一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/utils/content-utils.ts
getCollection(&quot;posts&quot;, ({ data }) =&amp;gt;
  import.meta.env.PROD ? data.draft !== true : true
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PROD&lt;/code&gt; 下过滤草稿，本地 &lt;code&gt;dev&lt;/code&gt; 时不过滤。所以草稿能在本地预览、发出去也不会上线 —— 我现在手上这批文章就全是 &lt;code&gt;draft: false&lt;/code&gt; 状态。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、OAuth 中转 Worker：为什么必须搞一个&lt;/h2&gt;
&lt;p&gt;GitHub backend 需要 OAuth 登录。麻烦在于 &lt;strong&gt;GitHub OAuth App 的 callback URL 是注册时写死的绝对地址&lt;/strong&gt;，而你现在是通过 &lt;code&gt;https://ycxdev.com/admin/&lt;/code&gt; 访问的。&lt;/p&gt;
&lt;p&gt;回调被 GitHub 打到一个不是 CMS 的 URL，Sveltia 拿不到 code，整个流程断掉。&lt;/p&gt;
&lt;p&gt;解决办法是插一个 Worker 在中间：&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant B as 浏览器（CMS）
participant W as OAuth 中转 Worker
participant GH as GitHub
participant U as 我（浏览器）
B-&amp;gt;&amp;gt;B: 点 &quot;Login with GitHub&quot;
B-&amp;gt;&amp;gt;W: GET /auth （带 redirect_uri 与 state）
Note over W: state 存 KV / 签名 Cookie，防 CSRF
W-&amp;gt;&amp;gt;GH: 302 → github.com/login/oauth/authorize&amp;lt;br/&amp;gt;（client_id + redirect_uri = Worker）
GH--&amp;gt;&amp;gt;U: 用户授权
GH-&amp;gt;&amp;gt;W: 302 回调 ?code=...
W-&amp;gt;&amp;gt;GH: POST /login/oauth/access_token（code 换 token）
GH--&amp;gt;&amp;gt;W: access_token
W-&amp;gt;&amp;gt;B: 302 回 /admin/，把 token 交给 CMS
Note over B: token 存 localStorage
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Worker 的职责就三件事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 提供一个固定的、已注册的 callback URL
2. 完成 code → token 的交换（这一步必须在服务端做，
   因为要把 client_secret 藏起来）
3. 把 token 安全地交回浏览器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Worker &lt;strong&gt;必须&lt;/strong&gt;持有 &lt;code&gt;client_secret&lt;/code&gt; 并在服务端完成 code 交换。
如果把 secret 放进 &lt;code&gt;config.yml&lt;/code&gt;，那等于公开了这个 OAuth App —— 任何人拿到 secret 就能冒充你的应用换 token。
:::&lt;/p&gt;
&lt;p&gt;这个 Worker 跑在 Cloudflare Workers 免费额度里（10 万请求/天），完全够用。命名上我用的是 &lt;code&gt;sveltia-cms-auth-&amp;lt;Pages 项目名&amp;gt;&lt;/code&gt;，和站点同名，纯粹为了好记 —— 你随便起，只要和 GitHub OAuth App 里登记的 callback URL 一致就行。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、保存一篇文章的完整时序&lt;/h2&gt;
&lt;p&gt;现在把 OAuth 之后的部分补完 —— 这是&quot;反向同步&quot;的核心：&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant B as 浏览器 CMS
participant API as GitHub API
participant R as GitHub 仓库
participant P as Cloudflare Pages
B-&amp;gt;&amp;gt;API: GET /repos/devycx/MyBlog/contents/src/content/posts
API--&amp;gt;&amp;gt;B: 文件列表 + 每个文件的 sha
Note over B: sha 是乐观锁，用来防覆盖
B-&amp;gt;&amp;gt;B: 我在编辑器里写 Markdown&amp;lt;br/&amp;gt;（正文字段 → 文件正文）
B-&amp;gt;&amp;gt;API: POST /media（上传图片）
API--&amp;gt;&amp;gt;B: 图片入库，写回 frontmatter
Note over API: 图片直接进仓库 src/content/posts/images/
B-&amp;gt;&amp;gt;API: PUT .../contents/src/content/posts/xxx.md&amp;lt;br/&amp;gt;{ message, content(base64), sha }
API-&amp;gt;&amp;gt;R: 创建 commit（branch: main）
API--&amp;gt;&amp;gt;B: 201 Created，新 sha
R--&amp;gt;&amp;gt;P: Webhook（push 事件）
P-&amp;gt;&amp;gt;R: 拉取该 commit
P-&amp;gt;&amp;gt;P: pnpm install &amp;amp;&amp;amp; pnpm build
P--&amp;gt;&amp;gt;U: 原子切换到新版本
Note over B,U: 整个过程无感，URL 没变
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;几个关键理解：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 乐观锁靠 &lt;code&gt;sha&lt;/code&gt;。&lt;/strong&gt; GitHub 的 contents API 要求更新文件时带上当前的 blob sha。不带就报 422。这样两台设备同时编辑同一篇时会明确冲突，而不是静默覆盖。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 图片进仓库，不是图床。&lt;/strong&gt; CMS 上传图片走的是 &lt;code&gt;POST /repos/.../contents/&amp;lt;path&amp;gt;&lt;/code&gt;，落到 &lt;code&gt;src/content/posts/images/&lt;/code&gt;。这正好和我的约定一致 —— 内容以仓库为唯一来源，构建产物可随时重建。&lt;/p&gt;
&lt;p&gt;:::caution
图片进仓库的代价是&lt;strong&gt;仓库体积不可逆地增长&lt;/strong&gt;。GitHub 单文件 100MB、仓库建议 &amp;lt;1GB。放原图的话几十篇文章就能顶满。正确做法是上传前压缩（WebP / AVIF，或长边限制在 1600px）。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;public_folder&lt;/code&gt; 的相对/绝对是个真坑。&lt;/strong&gt; 见下节。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、卡了我两天的那个坑&lt;/h2&gt;
&lt;p&gt;当时上传的封面图全都 404。文章在 &lt;code&gt;src/content/posts/&lt;/code&gt;，frontmatter 里写的是 &lt;code&gt;images/xxx.jpg&lt;/code&gt;，Fuwari 主题里是这样解析的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/pages/posts/[...slug].astro
&amp;lt;ImageWrapper src={entry.data.image}
              basePath={path.join(&quot;content/posts/&quot;, getDir(entry.id))} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说它把 &lt;code&gt;images/xxx.jpg&lt;/code&gt; 拼成 &lt;code&gt;content/posts/&amp;lt;目录&amp;gt;/images/xxx.jpg&lt;/code&gt;，然后通过 Astro 的图片管线去引用。&lt;/p&gt;
&lt;p&gt;但问题是：&lt;strong&gt;如果 frontmatter 里写的是 &lt;code&gt;/images/xxx.jpg&lt;/code&gt;（带前导斜杠），Astro 会当成 public 目录下的资源去找&lt;/strong&gt;，当然找不到。&lt;/p&gt;
&lt;p&gt;所以关键是 &lt;code&gt;config.yml&lt;/code&gt; 里 &lt;strong&gt;集合级&lt;/strong&gt;的 &lt;code&gt;media_folder&lt;/code&gt; / &lt;code&gt;public_folder&lt;/code&gt; 要用&lt;strong&gt;相对路径&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ❌ 顶层用绝对路径 → frontmatter 被写成 /src/content/posts/images/xxx.jpg
media_folder: src/content/posts/images
public_folder: /src/content/posts/images

# ✅ 集合级用相对路径 → frontmatter 被写成 images/xxx.jpg
collections:
  - name: posts
    media_folder: images
    public_folder: images
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
顺便提醒一个 API 名字的坑：Sveltia / Decap CMS 里这个选项叫 &lt;strong&gt;&lt;code&gt;public_folder&lt;/code&gt;&lt;/strong&gt;，&lt;strong&gt;不是&lt;/strong&gt; &lt;code&gt;public_path&lt;/code&gt;。写错了不报错，只是 frontmatter 里路径不对，然后你在主题里死活找不到文件 —— 我就是在这上面浪费了一个下午。
:::&lt;/p&gt;
&lt;p&gt;我在 &lt;code&gt;config.yml&lt;/code&gt; 里直接把原因写成注释了，因为半年后我一定会忘：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 集合级【相对路径】是关键：
#   media_folder: images  -&amp;gt; 图片实际存到 src/content/posts/images/
#   public_folder: images -&amp;gt; frontmatter 写成相对路径 images/xxx.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、这套方案的边界&lt;/h2&gt;
&lt;p&gt;说优点也要说缺点，不然是软文。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;零后端成本，CMS 就是一个静态文件&lt;/li&gt;
&lt;li&gt;内容、代码、历史全在一个仓库里，&lt;code&gt;git log&lt;/code&gt; 就是写作历史&lt;/li&gt;
&lt;li&gt;CMS 挂了不影响发布（&lt;code&gt;git push&lt;/code&gt; 照样能上线）&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;ul&gt;
&lt;li&gt;&lt;strong&gt;写长文体验一般。&lt;/strong&gt; Markdown 编辑器就是个文本框，没有大纲导航、没有实时预览、没有类似 Typora 的所见即所得。写代码文章够用，写散文就一般。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖第三方 CDN。&lt;/strong&gt; &lt;code&gt;unpkg.com&lt;/code&gt; 挂了后台就白屏。可以自己 host 一份 &lt;code&gt;sveltia-cms.js&lt;/code&gt; 到 &lt;code&gt;public/admin/&lt;/code&gt;，我暂时没这么做。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鉴权粒度粗。&lt;/strong&gt; 一个 GitHub PAT/OAuth 就是全部权限。多人协作得靠 GitHub 仓库权限管。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是移动端优先。&lt;/strong&gt; 手机上写长文体验很差。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
我的实际用法是混合的：&lt;strong&gt;长文用编辑器/AI 写好 Markdown，push 上去；短文、需要图和快速发布的用 CMS。&lt;/strong&gt; CMS 的价值不是&quot;取代编辑器&quot;，是&quot;把发布这一步的摩擦降到零&quot;。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMS 跟着静态站点一起部署&lt;/strong&gt;，一个 &lt;code&gt;index.html&lt;/code&gt; + 一个 &lt;code&gt;config.yml&lt;/code&gt; 就完事&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入目标是 GitHub 仓库&lt;/strong&gt;，靠 &lt;code&gt;folder&lt;/code&gt; 直接对上 Astro 的 content collection，中间零转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth 中转 Worker&lt;/strong&gt; 解决 callback URL 固定的问题，并且必须把 &lt;code&gt;client_secret&lt;/code&gt; 留在服务端&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每次保存 = 一个 commit = 一次 Pages 重新构建&lt;/strong&gt;，这就是&quot;反向同步&quot;的全部含义&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;public_folder&lt;/code&gt; 必须是集合级相对路径&lt;/strong&gt;，顶层绝对路径会让图片 404&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片进仓库&lt;/strong&gt;，记得压缩，仓库体积不可逆&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个系统的本质就一句话：&lt;strong&gt;GitHub 是数据库，commit 是历史，CMS 是个前端，Pages 是个编译器。&lt;/strong&gt; 四个组件各司其职，谁都不是数据库，但组合起来就是一个完整的 CMS。&lt;/p&gt;
</content:encoded></item><item><title>智谱新用户到底送了多少 token？我把资源包页面拉出来数了一遍</title><link>https://ycxdev.com/posts/zhipu-20m-token-gift/</link><guid isPermaLink="true">https://ycxdev.com/posts/zhipu-20m-token-gift/</guid><description>注册 bigmodel.cn 登录页写着「送 2000 万 tokens」。我把自己账户里的资源包明细导出来，实际是 2500 万 tokens，分 6 个包。更值得看的是：注册 4 天，额度全部用完。</description><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;打开 &lt;a href=&quot;https://bigmodel.cn/&quot;&gt;https://bigmodel.cn/&lt;/a&gt; 的登录页，右上角挂着一行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新用户免费赠送专享 2000 万 tokens 体验包！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不用绑卡，进去就有。这个数字听起来很实在，但&lt;strong&gt;登录页说的是&quot;约数&quot;&lt;/strong&gt; —— 我把自己控制台里 &lt;code&gt;财务 → 资源包 → 我的资源包&lt;/code&gt; 的明细拉出来看了一遍，实际结构比一行宣传语复杂得多，也更有意思。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、实际发了几个包&lt;/h2&gt;
&lt;p&gt;我的注册时间是 &lt;strong&gt;2026-10-11&lt;/strong&gt;，控制台里躺着 6 个包：&lt;/p&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;【实名认证】500 万 GLM-5.3-Flash 体验包&lt;/td&gt;
&lt;td&gt;500 万 tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;已用完&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2026-11-11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;【新用户专享】1500 万 GLM-5.3-Flash 资源包&lt;/td&gt;
&lt;td&gt;1500 万 tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;已用完&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2026-11-11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;【新用户专享】300 万 GLM-5.3-FlashX 资源包&lt;/td&gt;
&lt;td&gt;300 万 tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;已用完&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2026-11-11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;【新用户专享】20 次图片/视频生成资源包&lt;/td&gt;
&lt;td&gt;20 次&lt;/td&gt;
&lt;td&gt;生效中&lt;/td&gt;
&lt;td&gt;2026-11-11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;【新用户专享】200 万通用模型推理资源包&lt;/td&gt;
&lt;td&gt;200 万 tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;已用完&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2027-01-11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;【新用户专享】100 次搜索资源包&lt;/td&gt;
&lt;td&gt;100 次&lt;/td&gt;
&lt;td&gt;生效中&lt;/td&gt;
&lt;td&gt;2027-01-11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;加起来是 &lt;strong&gt;2500 万 tokens + 120 次按次计费的调用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
pie showData
title 新用户 token 额度构成（单位：万 tokens）
&quot;GLM-5.3-Flash 1500万&quot; : 1500
&quot;GLM-5.3-Flash 实名 500万&quot; : 500
&quot;GLM-5.3-FlashX 300万&quot; : 300
&quot;通用模型推理 200万&quot; : 200
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;登录页说的&quot;2000 万&quot;，是前两个包的和（1500 + 500）。&lt;strong&gt;另外 500 万是拆开给的&lt;/strong&gt;，而且生效范围不同 —— 这是接下来最关键的细节。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/zhipu-resource-packages.png&quot; alt=&quot;智谱新用户资源包：发放明细与消耗状态&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;上图是根据控制台数据整理的数据图，不是界面截图。想看原界面的话，登录 bigmodel.cn 走「财务 → 资源包 → 我的资源包」。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、这 6 个包不是&quot;通用货币&quot;&lt;/h2&gt;
&lt;p&gt;第一眼看到 2500 万，很容易以为&quot;随便怎么用都行&quot;。&lt;strong&gt;不是。&lt;/strong&gt;「适用场景」那一列才是真正的约束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1500 万 + 500 万  →  只适用于 glm-5.3-flash 的推理
  300 万         →  只适用于 glm-5.3-flashx 的推理
  200 万         →  适用于所有按 tokens 计费的基础模型  ← 只有这个是万能的
  20 次          →  只适用于图片 / 视频生成
 100 次          →  只适用于 search-std / search-pro / search-pro-quark / search-pro-sogou
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;真正&quot;通用&quot;的额度只有 200 万 tokens，占总额度的 8%。剩下 92% 都被绑死在具体模型上。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这直接改变了一套使用策略。你如果拿 GLM-5.3-Flash 之外的模型去跑 agent，能烧的其实只有那 200 万。&lt;/p&gt;
&lt;p&gt;:::note
包是按&lt;strong&gt;模型粒度&lt;/strong&gt;拆分发放的，这在国产大模型平台的体验包设计中很常见 —— 既是资源隔离，也是引导你去试它主推的那个型号。理解这一点，才不会在&quot;我明明有 2500 万额度&quot;的时候发现调用报余额不足。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、4 天烧完&lt;/h2&gt;
&lt;p&gt;真正让我意外的不是额度大小，是速度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注册时间 2026-10-11，我今天（10-15）再看，四个 token 包全部「已用完」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
xychart-beta
title &quot;新用户 token 额度消耗&quot;
x-axis [&quot;10/11 注册&quot;, &quot;10-15 现在&quot;]
y-axis &quot;剩余 token（万）&quot; 0 --&amp;gt; 2500
bar [2500, 0]
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;也就是说：&lt;strong&gt;2500 万 tokens，4 天。&lt;/strong&gt; 平均一天六百万。&lt;/p&gt;
&lt;p&gt;而且我并不是高强度使用者 —— 主要是拿它在 opencode / 各种 agent 里跑日常的代码任务和文档处理。所以这里有个很反直觉的结论：&lt;/p&gt;
&lt;p&gt;:::warning
&lt;strong&gt;&quot;2000 万 tokens&quot;听起来像能用几个月的数字，实际上是几天的量级。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因在 agent 场景的 token 结构，而不是你的问题难度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一次普通问答              ≈  2k tokens
一次读文件改代码          ≈  15k ~ 60k tokens（含反复读取）
一次完整的 agent 任务     ≈  100k ~ 500k tokens（含多轮工具调用）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个&quot;就改一行&quot;的任务，模型可能读 8 次文件、调 15 次工具，每次工具结果都回灌进上下文。&lt;strong&gt;你看到的是行数，它烧的是上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按这个量级反推：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2500 万 tokens  ≈  agent 任务 × 50~250 次
                ≈  纯聊天     × 1 万次以上
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;纯聊天能撑很久；带工具调用的 agent，几天就见底。&lt;strong&gt;差异是几百倍。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;顺便给一个换算是很有用的：控制台的&quot;购买资源包&quot;页里，&lt;strong&gt;1000 万 tokens 的新客专享包标价 29.9 元&lt;/strong&gt;（3 个月有效）。也就是说这 2500 万额度，按购买价折算大概值 &lt;strong&gt;70 多元&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;70 块钱，4 天。这个数字比任何宣传语都能说明白 agent 到底有多吃 token。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、怎么用才不亏&lt;/h2&gt;
&lt;p&gt;基于上面这几点，我总结出四条：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 先看「适用场景」，别只看总额&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;知道通用额度只有 200 万之后，我的做法是：&lt;strong&gt;把模型档位分两层&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;日常对话 / 简单任务        →  免费模型（不消耗额度）
写代码 / 长上下文 / 复杂推理 →  token 包（优先用绑定的 GLM-5.3-Flash）
                       →  通用额度当&quot;预算外资金&quot;，留给别的模型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 别拿它测 prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;新手最容易犯的错是拿 2500 万去调&quot;这个 prompt 写得好不好&quot;。&lt;strong&gt;测 prompt 用免费模型，不要用包。&lt;/strong&gt; 包是一次性资源，用完就没了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 长上下文别拆碎&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同一份文档&quot;问三次&quot;和&quot;一次问清再追问&quot;，后者便宜得多 —— 因为前缀被缓存了。碎片化的提问会让缓存命中率崩掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 盯住到期日&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是最亏的死法：包还在，过期了，一分没花。这批包的有效期分两档：&lt;strong&gt;GLM 模型包 1 个月（到 11-11），通用包 3 个月（到 2027-01-11）&lt;/strong&gt;。领到之后先看一眼日期，然后安排用掉。&lt;/p&gt;
&lt;p&gt;:::tip
注册入口就一个：&lt;a href=&quot;https://bigmodel.cn/&quot;&gt;https://bigmodel.cn/&lt;/a&gt;。进去后在控制台创建 API Key，&lt;strong&gt;第一次就把 key 存进密码管理器&lt;/strong&gt; —— 平台明确说了&quot;API Key 仅在创建时完整展示一次，后续无法查看或恢复&quot;。&lt;/p&gt;
&lt;p&gt;顺带一句：平台的资源包页面现在支持按「模型 / 功能 / 工具插件」分类筛选，还有&quot;仅展示可用资源包&quot;的开关。查余额之前记得关掉过滤，不然会以为还有额度。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、一个额外发现：他们卖的是&quot;尝鲜包&quot;&lt;/h2&gt;
&lt;p&gt;翻资源包商店的时候，我注意到一个挺有意思的产品设计：&lt;strong&gt;绝大部分 SKU 是&quot;尝鲜包&quot;，价格在 2.9 元 ~ 19.9 元之间。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GLM-5.3-Flash 尝鲜包     9.9 元
GLM-5.3 尝鲜包           39.9 元
GLM-OCR 3 折尝鲜包       2.9 元
GLM-Image 3 折尝鲜包     5.9 元
GLM-4.6V 3 折尝鲜包      5.9 元
GLM-4.5-Air 3 折尝鲜包   19.9 元
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是随便定的价。&lt;strong&gt;2.9 元这个价位，意图是让你&quot;无脑点一下&quot;&lt;/strong&gt;，而不是让你算性价比。平台赌的是你用完尝鲜包之后直接订阅/充值，而不是回头比价。&lt;/p&gt;
&lt;p&gt;从用户角度，这反而是好事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如果你想试某个特定模型（OCR、图像生成、视觉推理），先买它对应的尝鲜包，比直接充值划算得多。&lt;/strong&gt; 因为尝鲜包是折扣价，而且范围清晰、不需要自己在账单里对账。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这跟前面那个&quot;资源包按模型粒度拆分&quot;是同一个思路的两面：&lt;strong&gt;把额度切成小份、绑定到具体能力、用低价降低尝试门槛。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;登录页写&quot;送 2000 万 tokens&quot;，&lt;strong&gt;实际是 2500 万 tokens + 120 次按次调用&lt;/strong&gt;，分 6 个包&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有 200 万（8%）是通用额度&lt;/strong&gt;，其余都被绑死在 GLM-5.3-Flash / FlashX 上 ——「适用场景」列比总额重要&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注册 4 天全部用完&lt;/strong&gt;。agent 场景的 token 消耗是纯聊天的几百倍，别拿聊天量级去估&lt;/li&gt;
&lt;li&gt;按购买价折算，这 2500 万大概值 &lt;strong&gt;70 多元&lt;/strong&gt;，四天烧完 —— 这就是 agent 的真实成本&lt;/li&gt;
&lt;li&gt;有效期分两档（1 个月 / 3 个月），&lt;strong&gt;过期未用是最亏的&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;想试特定模型，&lt;strong&gt;先买对应的&quot;尝鲜包&quot;&lt;/strong&gt;，比直接充值划算&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;薅来的东西要有个用完的计划，不然它就是个躺在后台慢慢归零的数字。这次我倒是用完了 —— 只是没想到这么快。&lt;/p&gt;
</content:encoded></item><item><title>连上 Wi-Fi 才启动 VPN：Windows 计划任务的两个默认陷阱</title><link>https://ycxdev.com/posts/windows-scheduled-task-vpn-on-network-up/</link><guid isPermaLink="true">https://ycxdev.com/posts/windows-scheduled-task-vpn-on-network-up/</guid><description>TUN 模式的 VPN 在图书馆把网断掉了。根因是它抢占了默认路由 + 认证还没完成。解法是用计划任务监听网络连接事件再启动 —— 但任务计划程序有两个默认值会让你一周后莫名其妙掉线，本文附我本机的真实 XML 和完整注册脚本。</description><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我的笔记本在学校图书馆连 Wi-Fi 会断网。手机在同一个 Wi-Fi 下好好的。&lt;/p&gt;
&lt;p&gt;排查了大半天，最后定位到：&lt;strong&gt;是我的 VPN（Clash 系，TUN 模式）启动太早了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这台笔记本开机后我如果不开 VPN，一切正常。开了 VPN 去图书馆，必断。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、为什么 TUN 模式的 VPN 会让图书馆连不上网&lt;/h2&gt;
&lt;h3&gt;TUN 模式在干什么&lt;/h3&gt;
&lt;p&gt;普通代理模式（系统代理 / HTTP 代理）只接管&lt;strong&gt;明确配置了走代理的应用&lt;/strong&gt;。TUN 模式不一样 —— 它在系统里创建一张虚拟网卡，然后把&lt;strong&gt;默认路由&lt;/strong&gt;抢过来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;正常状态：
  0.0.0.0/0  →  192.168.1.1  via  WLAN      (metric 25)  ✓ 生效
  0.0.0.0/0  →  10.0.0.1     via  Clash-TUN (metric 5)   ✗ 被压制

TUN 启动后：
  0.0.0.0/0  →  10.0.0.1     via  Clash-TUN (metric 5)   ✓ 现在生效
  0.0.0.0/0  →  192.168.1.1  via  WLAN      (metric 25)  ✗ 还在表里，但不承载流量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 同时只会用一条默认路由（多条时按 metric 取最小）。&lt;strong&gt;TUN 的目标就是把这条唯一的名额抢走&lt;/strong&gt;，这样才能做到&quot;全局代理&quot;。&lt;/p&gt;
&lt;h3&gt;但图书馆的 WLAN 有一个特殊之处&lt;/h3&gt;
&lt;p&gt;家里的 Wi-Fi 连上就是通的。学校图书馆的 WLAN 不是 —— 它通常要过 &lt;strong&gt;802.1X（企业级认证）&lt;/strong&gt; 或 &lt;strong&gt;Portal（网页认证）&lt;/strong&gt;。这个过程有几个硬性要求：&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
flowchart TD
A[&quot;网卡关联 AP&amp;lt;br/&amp;gt;(物理层已连接)&quot;] --&amp;gt; B[&quot;4 次握手&amp;lt;br/&amp;gt;拿到 IP / 网关 / DNS&quot;]
B --&amp;gt; C{&quot;需要认证吗?&quot;}
C --&amp;gt;|家用WiFi| H[&quot;可以直接上网 ✓&quot;]
C --&amp;gt;|图书馆 802.1X / Portal| D[&quot;必须能发出&amp;lt;br/&amp;gt;未认证的 EAPOL / Portal 流量&quot;]
D --&amp;gt; E{&quot;TUN 已接管默认路由?&quot;}
E --&amp;gt;|否 未启动| F[&quot;认证包正常走 WLAN ✓&quot;]
E --&amp;gt;|是 已启动| G[&quot;认证包被路由进 TUN&amp;lt;br/&amp;gt;或被 kill-switch 丢弃 ✗&quot;]
G --&amp;gt; I[&quot;认证一直不完成&amp;lt;br/&amp;gt;= 连上了 Wi-Fi 但没网&quot;]
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;问题的本质：&lt;strong&gt;&quot;物理上关联成功&quot;和&quot;能上网&quot;之间，还隔着一个必须依赖默认路由的认证阶段。&lt;/strong&gt; 而 TUN 恰好在这个阶段把默认路由拿走了。&lt;/p&gt;
&lt;h3&gt;具体的死法有几种，通常几个一起&lt;/h3&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant W as 开机/唤醒
participant OS as Windows 网络栈
participant AP as 图书馆 AP + 认证服务器
participant V as VPN 客户端 (TUN)
participant NIC as 物理网卡 WLAN
W-&amp;gt;&amp;gt;V: 用户开机，VPN 自启
V-&amp;gt;&amp;gt;OS: 创建虚拟网卡，注入 0.0.0.0/0 (低 metric)
Note over OS: 默认路由此刻已被 TUN 占据
W-&amp;gt;&amp;gt;OS: 开始连接 Wi-Fi
OS-&amp;gt;&amp;gt;AP: 关联 + 4 次握手
AP--&amp;gt;&amp;gt;OS: DHCP Offer（网关 192.168.x.1）
Note over OS: 路由表已有默认路由（TUN 的），&amp;lt;br/&amp;gt;无法改走物理网卡
OS-&amp;gt;&amp;gt;AP: 发起 802.1X 认证 / Portal 探测
alt strict-route / kill-switch 开启
AP--xOS: 认证响应被丢弃
Note over OS,AP: ✗ 永远认证不通过
else 认证包被路由进 TUN
OS-&amp;gt;&amp;gt;V: 认证包进了隧道
V--xAP: 出不去，或回包进不来
Note over OS,AP: ✗ 同样卡死
end
V-&amp;gt;&amp;gt;OS: DNS 已劫持到隧道内 (1.1.1.1 等)
Note over OS,V: 要解析出口域名才能建隧道，&amp;lt;br/&amp;gt;但 DNS 在隧道里 → 先有鸡还是先有蛋
Note over OS: 重试 → 认证失败 → 认证不通过 → 系统拿不到 IP
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;把上面压缩成一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TUN 模式假设&quot;你已经能上网了&quot;。而图书馆的 WLAN 假设&quot;你还没认证完&quot;。两个假设互相打架。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;还有一个更隐蔽的：&lt;strong&gt;DNS 死锁&lt;/strong&gt;。TUN 模式通常会把 DNS 也劫持到隧道内的公共 DNS（1.1.1.1 / 8.8.8.8）。而隧道要建连，得先能解析出口服务器的域名 —— 解析请求被送进一个还没建好的隧道。配置不讲究的客户端就会一直卡在这里反复重试。&lt;/p&gt;
&lt;p&gt;:::warning
成熟客户端一般会维护一份 &lt;strong&gt;bypass 列表&lt;/strong&gt;，把出口服务器 IP 和内网网段排除在隧道之外，避免自噬。这份列表配得好不好，直接决定你会不会遇到&quot;自己把自己套进去&quot;。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、解法：把 VPN 启动推迟到&quot;网络就绪之后&quot;&lt;/h2&gt;
&lt;p&gt;不要在开机时启动 VPN。等系统告诉你&quot;网络连接已经建立&quot;，再启动。&lt;/p&gt;
&lt;p&gt;Windows 里这件事的官方触发器就是 &lt;strong&gt;Microsoft-Windows-NetworkProfile/Operational 事件日志的 EventID 10000（网络连接建立）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;任务计划程序 → 创建基本任务 → 触发器 → &lt;strong&gt;自定义&lt;/strong&gt; → 事件触发器：&lt;/p&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;/td&gt;
&lt;td&gt;&lt;code&gt;Microsoft-Windows-NetworkProfile/Operational&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件 ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务&lt;/td&gt;
&lt;td&gt;启动 VPN 客户端&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我本机跑的任务实际长这样（&lt;code&gt;\用户程序库\Clash Network Start&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-16&quot;?&amp;gt;
&amp;lt;Task version=&quot;1.3&quot; xmlns=&quot;http://schemas.microsoft.com/windows/2004/02/mit/task&quot;&amp;gt;
  &amp;lt;Principals&amp;gt;
    &amp;lt;Principal id=&quot;Author&quot;&amp;gt;
      &amp;lt;UserId&amp;gt;S-1-5-21-xxxxxxxx-1001&amp;lt;/UserId&amp;gt;
      &amp;lt;LogonType&amp;gt;InteractiveToken&amp;lt;/LogonType&amp;gt;
    &amp;lt;/Principal&amp;gt;
  &amp;lt;/Principals&amp;gt;
  &amp;lt;Settings&amp;gt;
    &amp;lt;DisallowStartIfOnBatteries&amp;gt;false&amp;lt;/DisallowStartIfOnBatteries&amp;gt;
    &amp;lt;StopIfGoingOnBatteries&amp;gt;false&amp;lt;/StopIfGoingOnBatteries&amp;gt;
    &amp;lt;ExecutionTimeLimit&amp;gt;PT0S&amp;lt;/ExecutionTimeLimit&amp;gt;
    &amp;lt;MultipleInstancesPolicy&amp;gt;IgnoreNew&amp;lt;/MultipleInstancesPolicy&amp;gt;
    &amp;lt;UseUnifiedSchedulingEngine&amp;gt;true&amp;lt;/UseUnifiedSchedulingEngine&amp;gt;
  &amp;lt;/Settings&amp;gt;
  &amp;lt;Triggers&amp;gt;
    &amp;lt;EventTrigger&amp;gt;
      &amp;lt;Subscription&amp;gt;&amp;amp;lt;QueryList&amp;amp;gt;&amp;amp;lt;Query Id=&quot;0&quot; Path=&quot;Microsoft-Windows-NetworkProfile/Operational&quot;&amp;amp;gt;&amp;amp;lt;Select Path=&quot;Microsoft-Windows-NetworkProfile/Operational&quot;&amp;amp;gt;*[System[Provider[@Name=&apos;Microsoft-Windows-NetworkProfile&apos;] and EventID=10000]]&amp;amp;lt;/Select&amp;amp;gt;&amp;amp;lt;/Query&amp;amp;gt;&amp;amp;lt;/QueryList&amp;amp;gt;&amp;lt;/Subscription&amp;gt;
    &amp;lt;/EventTrigger&amp;gt;
  &amp;lt;/Triggers&amp;gt;
  &amp;lt;Actions Context=&quot;Author&quot;&amp;gt;
    &amp;lt;Exec&amp;gt;
      &amp;lt;Command&amp;gt;&quot;C:\Program Files\YourVPN\YourVPN.exe&quot;&amp;lt;/Command&amp;gt;
    &amp;lt;/Exec&amp;gt;
  &amp;lt;/Actions&amp;gt;
&amp;lt;/Task&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;一键注册脚本（PowerShell，管理员）&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$xml = @&apos;
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-16&quot;?&amp;gt;
&amp;lt;Task version=&quot;1.3&quot; xmlns=&quot;http://schemas.microsoft.com/windows/2004/02/mit/task&quot;&amp;gt;
  &amp;lt;RegistrationInfo&amp;gt;&amp;lt;Author&amp;gt;me&amp;lt;/Author&amp;gt;&amp;lt;/RegistrationInfo&amp;gt;
  &amp;lt;Principals&amp;gt;
    &amp;lt;Principal id=&quot;Author&quot;&amp;gt;
      &amp;lt;UserId&amp;gt;DOMAIN\user&amp;lt;/UserId&amp;gt;
      &amp;lt;LogonType&amp;gt;InteractiveToken&amp;lt;/LogonType&amp;gt;
    &amp;lt;/Principal&amp;gt;
  &amp;lt;/Principals&amp;gt;
  &amp;lt;Settings&amp;gt;
    &amp;lt;DisallowStartIfOnBatteries&amp;gt;false&amp;lt;/DisallowStartIfOnBatteries&amp;gt;
    &amp;lt;StopIfGoingOnBatteries&amp;gt;false&amp;lt;/StopIfGoingOnBatteries&amp;gt;
    &amp;lt;ExecutionTimeLimit&amp;gt;PT0S&amp;lt;/ExecutionTimeLimit&amp;gt;
    &amp;lt;MultipleInstancesPolicy&amp;gt;IgnoreNew&amp;lt;/MultipleInstancesPolicy&amp;gt;
  &amp;lt;/Settings&amp;gt;
  &amp;lt;Triggers&amp;gt;
    &amp;lt;EventTrigger&amp;gt;
      &amp;lt;Subscription&amp;gt;&amp;amp;lt;QueryList&amp;amp;gt;&amp;amp;lt;Query Id=&quot;0&quot; Path=&quot;Microsoft-Windows-NetworkProfile/Operational&quot;&amp;amp;gt;&amp;amp;lt;Select Path=&quot;Microsoft-Windows-NetworkProfile/Operational&quot;&amp;amp;gt;*[System[Provider[@Name=&apos;Microsoft-Windows-NetworkProfile&apos;] and EventID=10000]]&amp;amp;lt;/Select&amp;amp;gt;&amp;amp;lt;/Query&amp;amp;gt;&amp;amp;lt;/QueryList&amp;amp;gt;&amp;lt;/Subscription&amp;gt;
    &amp;lt;/EventTrigger&amp;gt;
  &amp;lt;/Triggers&amp;gt;
  &amp;lt;Actions Context=&quot;Author&quot;&amp;gt;
    &amp;lt;Exec&amp;gt;&amp;lt;Command&amp;gt;&quot;C:\path\to\vpn-client.exe&quot;&amp;lt;/Command&amp;gt;&amp;lt;/Exec&amp;gt;
  &amp;lt;/Actions&amp;gt;
&amp;lt;/Task&amp;gt;
&apos;@

Register-ScheduledTask -TaskName &quot;VPN On Network Up&quot; -Xml $xml -Force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get-ScheduledTask -TaskName &quot;VPN On Network Up&quot; | Select-Object TaskName, State
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、你问的那两个坑：你的理解是对的，而且很常见&lt;/h2&gt;
&lt;h3&gt;坑 1：默认 72 小时后任务被杀掉&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;是真的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Task Scheduler 的 &lt;code&gt;ExecutionTimeLimit&lt;/code&gt;（&quot;如果任务运行时间超过此时间，则停止任务&quot;）默认值是 &lt;strong&gt;&lt;code&gt;PT72H&lt;/code&gt;&lt;/strong&gt;，也就是 72 小时。文档里的原话是&quot;如果未指定该值，默认使用 72 小时&quot;。&lt;/p&gt;
&lt;p&gt;一个&lt;strong&gt;开机常驻&lt;/strong&gt;的任务 —— 比如 VPN 客户端 —— 跑满三天就会被系统主动干掉，而且：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日志里只记一句&quot;任务因超出时间限制而终止（任务已完成）&quot;&lt;/li&gt;
&lt;li&gt;不会弹任何提示&lt;/li&gt;
&lt;li&gt;你不会意识到是计划任务干的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;改法&lt;/strong&gt;：&lt;code&gt;PT0S&lt;/code&gt; = 不限时。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ExecutionTimeLimit&amp;gt;PT0S&amp;lt;/ExecutionTimeLimit&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;坑 2：拔电源会停任务&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;也是真的，而且是两个独立的开关。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;任务计划程序创建任务时，GUI 里默认勾着「&lt;strong&gt;如果计算机使用电池电源，则停止任务&lt;/strong&gt;」。这个勾选会写出两个 XML 节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;DisallowStartIfOnBatteries&amp;gt;true&amp;lt;/DisallowStartIfOnBatteries&amp;gt;  &amp;lt;!-- 电池上干脆不启动 --&amp;gt;
&amp;lt;StopIfGoingOnBatteries&amp;gt;true&amp;lt;/StopIfGoingOnBatteries&amp;gt;          &amp;lt;!-- 运行中拔电就杀掉 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个默认都是 &lt;code&gt;true&lt;/code&gt;。表现是：笔记本拔掉电源线的那一瞬间，VPN 客户端进程被计划任务管理器直接结束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;改法&lt;/strong&gt;：两个都置 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;DisallowStartIfOnBatteries&amp;gt;false&amp;lt;/DisallowStartIfOnBatteries&amp;gt;
&amp;lt;StopIfGoingOnBatteries&amp;gt;false&amp;lt;/StopIfGoingOnBatteries&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价的 cmdlet 写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;New-ScheduledTaskSettingsSet `
  -ExecutionTimeLimit ([TimeSpan]::Zero) `
  -AllowStartIfOnBatteries `
  -DontStopIfGoingOnBatteries `
  -MultipleInstances IgnoreNew
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
我本机这个任务的 XML 里这两个坑&lt;strong&gt;都已经改过来了&lt;/strong&gt;（&lt;code&gt;PT0S&lt;/code&gt; + 两个 &lt;code&gt;false&lt;/code&gt;）。所以如果你现在遇到的是我之前那个症状 —— 图书馆连不上网但别的地方正常 —— 那就不是这两个默认值的问题，回到第一节的路由抢占去看。
:::&lt;/p&gt;
&lt;h3&gt;顺带第三个坑：IdleSettings&lt;/h3&gt;
&lt;p&gt;有些任务里还会看到这一段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;IdleSettings&amp;gt;
  &amp;lt;Duration&amp;gt;PT10M&amp;lt;/Duration&amp;gt;
  &amp;lt;WaitTimeout&amp;gt;PT1H&amp;lt;/WaitTimeout&amp;gt;
  &amp;lt;StopOnIdleEnd&amp;gt;true&amp;lt;/StopOnIdleEnd&amp;gt;
  &amp;lt;RestartOnIdle&amp;gt;false&amp;lt;/RestartOnIdle&amp;gt;
&amp;lt;/IdleSettings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的含义是「&lt;strong&gt;仅当计算机空闲至少 10 分钟时才启动任务&lt;/strong&gt;」，加上「&lt;strong&gt;一旦不空闲就停止&lt;/strong&gt;」。只有 &lt;code&gt;&amp;lt;RunOnlyIfIdle&amp;gt;true&amp;lt;/RunOnlyIfIdle&amp;gt;&lt;/code&gt; 时才生效 —— 默认是 false，所以一般无害。&lt;/p&gt;
&lt;p&gt;但&lt;strong&gt;如果你哪天在 GUI 里手滑勾上了「仅当计算机空闲时才启动任务」，拔电源断网这种玄学问题就又能重现了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、EventID 10000 的两个副作用&lt;/h2&gt;
&lt;h3&gt;它不只对 Wi-Fi 触发&lt;/h3&gt;
&lt;p&gt;EventID 10000 是&lt;strong&gt;任何网络配置建立&lt;/strong&gt;都会触发，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有线网&lt;/li&gt;
&lt;li&gt;手机 USB 网络共享&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VPN 自己那张虚拟网卡重连&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Docker / WSL2 虚拟交换机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以完全可能出现&quot;VPN 自己断了 → 触发 10000 → 计划任务又去启动 VPN → 反复重启&quot;。&lt;/p&gt;
&lt;p&gt;想严格只认 Wi-Fi，把动作改成调一个脚本做判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# start-vpn-if-wifi.ps1
$wifi = Get-NetAdapter | Where-Object { $_.Status -eq &apos;Up&apos; -and $_.NdisPhysicalMedium -eq 9 }
if ($wifi) { Start-Process &apos;C:\Program Files\YourVPN\YourVPN.exe&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NdisPhysicalMedium -eq 9&lt;/code&gt; 就是 &lt;code&gt;Native 802.11&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;事件日志通道必须处于启用状态&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Microsoft-Windows-NetworkProfile/Operational&lt;/code&gt; 默认是开的。但如果被组策略关掉或者日志被清空过，触发器会&lt;strong&gt;静默失效&lt;/strong&gt; —— 任务状态显示 &lt;code&gt;Ready&lt;/code&gt;，永远不触发，也没有任何错误提示。&lt;/p&gt;
&lt;p&gt;验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wevtutil gl Microsoft-Windows-NetworkProfile/Operational | Select-String enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、排查这类问题的通用顺序&lt;/h2&gt;
&lt;p&gt;以后再遇到&quot;某个网络下连不上&quot;，按这个顺序查，能少走很多弯路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 关掉 VPN 试试
   └─ 好了 → 是路由/DNS/防火墙问题，不是 Wi-Fi 的问题
2. route print 看 0.0.0.0/0 有几条、哪条 metric 最小
   └─ TUN 的那条生效 → 就是本文说的抢占
3. nslookup 域名 + ping 网关
   └─ 网关通、域名不通 → DNS 劫持/死锁
4. ipconfig /all 看 IPv4 是不是 169.254.x.x
   └─ 是 → DHCP 续租失败，认证阶段就被卡住了
5. netsh wlan show interfaces 看 认证状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
第 2 步是分水岭。&lt;strong&gt;如果你看到一个软件的虚拟网卡出现在默认路由里且 metric 最小，那基本可以确定问题出在它身上&lt;/strong&gt;，不用再往 Wi-Fi 配置上找。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;图书馆连不上网的根因&lt;/strong&gt;：TUN 抢占了默认路由，而 802.1X / Portal 认证必须在认证完成前使用默认路由。TUN 假设&quot;你已能上网&quot;，图书馆 WLAN 假设&quot;你还没认证&quot;，直接对撞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解法&lt;/strong&gt;：用计划任务监听 &lt;code&gt;Microsoft-Windows-NetworkProfile/Operational&lt;/code&gt; 的 &lt;strong&gt;EventID 10000&lt;/strong&gt;，等网络建立后再启动 VPN。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;你记得的两个默认坑都是真的&lt;/strong&gt;：&lt;code&gt;ExecutionTimeLimit&lt;/code&gt; 默认 &lt;code&gt;PT72H&lt;/code&gt;（改成 &lt;code&gt;PT0S&lt;/code&gt;）；&lt;code&gt;DisallowStartIfOnBatteries&lt;/code&gt; / &lt;code&gt;StopIfGoingOnBatteries&lt;/code&gt; 默认 &lt;code&gt;true&lt;/code&gt;（改成 &lt;code&gt;false&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;还有第三个&lt;/strong&gt;：&lt;code&gt;IdleSettings&lt;/code&gt; 的 &lt;code&gt;StopOnIdleEnd&lt;/code&gt;，勾上「仅空闲时运行」就会重现玄学掉线。&lt;/li&gt;
&lt;li&gt;EventID 10000 不区分网络类型，严谨点要加 Wi-Fi 判断，否则 VPN 重连会自触发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Windows 计划任务这玩意儿的默认值都是&lt;strong&gt;为&quot;跑一次就完事的短任务&quot;设计的&lt;/strong&gt;。你一旦用它跑常驻服务，就得把这几个默认一个个翻过来 —— 症状还都是&quot;过几天突然不好使了&quot;，不查根本想不到是它。&lt;/p&gt;
</content:encoded></item><item><title>DeepSeek 注册送 0 元？去它自家 Harness 领 6 块</title><link>https://ycxdev.com/posts/deepseek-harness-6-yuan-freebie/</link><guid isPermaLink="true">https://ycxdev.com/posts/deepseek-harness-6-yuan-freebie/</guid><description>DeepSeek 开放平台注册后已经不再赠送任何余额。但官方另一个入口——开源的 DeepSeek Harness 桌面端——安装登录就送 6 元体验金。本文说清入口在哪、能用多久、值不值得薅。</description><pubDate>Sun, 11 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;先说结论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DeepSeek 开放平台注册，确实一分钱都不送。&lt;/strong&gt;
&lt;strong&gt;但 DeepSeek 自家的开源 agent harness —— DeepSeek Harness 桌面端 —— 安装后登录，送 6 元体验金。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这两件事在同一个品牌下，但几乎没有互相导流。不专门找的话根本注意不到。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、先确认一下：开放平台确实没赠送了&lt;/h2&gt;
&lt;p&gt;很多人（包括之前的我）是 2024 年那批用户，那时候注册送 14 元。后来政策改了，新用户注册打开就是 0 余额：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;platform.deepseek.com  →  注册 →  余额：¥0.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是个例。GitHub 上有一堆 issue 和博客在专门讨论&quot;怎么找回消失的新用户额度&quot;，给出的方案清一色是绕路 —— GitHub Models、Cloudflare Workers AI、各种第三方中转。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但那些方案有一个共同问题：折扣、限速、稳定性全都不如直连。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以我更愿意找官方自己的口子。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Harness 是什么&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.deepseek.com/zh/harness/&quot;&gt;DeepSeek Harness&lt;/a&gt; 是 DeepSeek 官方开源的 agent harness，仓库在 &lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness&quot;&gt;deepseek-ai/deepseek-harness&lt;/a&gt;（MIT 协议）。它一句话的自我描述是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DeepSeek Harness: Everything is a Plugin.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;架构上是一个 &lt;strong&gt;everything-is-a-plugin&lt;/strong&gt; 的插件化 agent 框架，底座是 &lt;a href=&quot;https://github.com/cordiverse/cordis&quot;&gt;Cordis&lt;/a&gt;，设计思路来自论文《&lt;em&gt;A Programming Paradigm for Spatiotemporal Composability&lt;/em&gt;》。&lt;/p&gt;
&lt;p&gt;它能干什么（官方列的四类）：&lt;/p&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;/td&gt;
&lt;td&gt;整理资料、分析表格、撰写文档、做汇报&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&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;标注来源&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;处理耗时任务&lt;/td&gt;
&lt;td&gt;跑脚本、批量处理文件，后台执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;外加自动化任务插件（按计划执行重复工作）和一个执行轨迹视图（能展开看每次工具调用的细节，方便排查）。&lt;/p&gt;
&lt;p&gt;:::note
&quot;开发者预览版&quot;，官方明确警告会有不兼容变更。MIT 开源，249k star。定位上它更像&quot;个人助理 + 可编程 agent 底座&quot;，而不是单纯的代码补全工具。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、6 元怎么领&lt;/h2&gt;
&lt;p&gt;就三步，没有隐藏条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 &lt;a href=&quot;https://www.deepseek.com/zh/harness/&quot;&gt;https://www.deepseek.com/zh/harness/&lt;/a&gt;，点**「下载桌面端」**&lt;/li&gt;
&lt;li&gt;下载安装（官网提供 &lt;strong&gt;macOS（Apple 芯片）&lt;/strong&gt; 和 &lt;strong&gt;Windows（64 位）&lt;/strong&gt; 版本）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 DeepSeek 账号登录&lt;/strong&gt; → 到账 6 元体验金&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant U as 你
participant W as deepseek.com/harness
participant A as Harness 桌面端
participant B as DeepSeek 账户余额
U-&amp;gt;&amp;gt;W: 访问 Harness 官网
W--&amp;gt;&amp;gt;U: 下载桌面端 (macOS Apple 芯片 / Win x64)
U-&amp;gt;&amp;gt;A: 安装并启动
A-&amp;gt;&amp;gt;B: 登录（复用 DeepSeek 账号）
B--&amp;gt;&amp;gt;A: ★ 到账 6 元体验金
Note over A,B: 无需绑卡、无需充值、无需邀请码
U-&amp;gt;&amp;gt;A: 直接开聊 / 跑任务
A-&amp;gt;&amp;gt;B: 按 token 计费扣减
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;:::warning
&lt;strong&gt;别去开放平台找。&lt;/strong&gt; 那个入口不给。这 6 块是在 Harness 客户端里登录之后才发到同一个账户余额的，我在开放平台后台是看不到&quot;这笔钱从哪来&quot;的标注的，所以很多人以为没到账。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、6 块能用多久&lt;/h2&gt;
&lt;p&gt;这取决于你干什么。DeepSeek 的定价是按 token 计费的，V4 系列的价格可以对照&lt;a href=&quot;https://api-docs.deepseek.com/quick_start/pricing/&quot;&gt;官方定价页&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;粗略给个体感：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;纯聊天 / 读文档      → 几十到上百小时
日常改代码、写脚本    → 几周
长上下文 agent 任务   → 可能一周就没了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
活动是&lt;strong&gt;限时的&lt;/strong&gt;。我看到的信息是这个体验金&lt;strong&gt;有效期到 10 月 6 日&lt;/strong&gt;。所以如果你现在去装发现没送，大概率是这一轮结束了 —— 但 DeepSeek 这种活动通常会周期性续，盯一下官网就行。
:::&lt;/p&gt;
&lt;p&gt;关键还是那句老话：&lt;strong&gt;薅来的额度只够让你&quot;试出好不好用&quot;。&lt;/strong&gt; 觉得好用再自己充值，别一开始就往里塞钱。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、一个让我更在意的点：Harness 本身&lt;/h2&gt;
&lt;p&gt;说实话，6 块只是顺手的事。真正让我花时间试的是 Harness 这个项目本身。&lt;/p&gt;
&lt;p&gt;它开源的定位不是&quot;我的客户端&quot;，而是**&quot;你也可以写一个自己的 agent&quot;**。&lt;/p&gt;
&lt;h3&gt;一切皆插件&lt;/h3&gt;
&lt;p&gt;官方插件列表之外，你可以在「创造模式」里&lt;strong&gt;直接跟它对话，让它帮你生成插件&lt;/strong&gt;。也就是说扩展能力的方式不是去 GitHub 抄一个别人的插件，而是把需求说出来，让它给你造一个。&lt;/p&gt;
&lt;h3&gt;三种跑法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 桌面端（那个送 6 块的）
#    macOS(Apple 芯片) / Windows(64 位)

# 2. npx 起 Web UI —— 默认 http://127.0.0.1:3080
npx @deepseek-ai/dsh web

# 3. 源码跑
git clone https://github.com/deepseek-ai/deepseek-harness
cd deepseek-harness
pnpm install &amp;amp;&amp;amp; pnpm run build &amp;amp;&amp;amp; pnpm dsh web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 2 种特别有意思：&lt;strong&gt;它默认只听 127.0.0.1:3080，跟 opencode 的安全模型一样&lt;/strong&gt; —— 默认本机，需要暴露到网络时你得自己显式开。&lt;/p&gt;
&lt;p&gt;插件生态有统一的 topic 标记（&lt;code&gt;dsh-plugin&lt;/code&gt;），可以按这个标记搜到所有第三方插件。&lt;/p&gt;
&lt;p&gt;:::tip
如果我某天想给 opencode 加个 DeepSeek 后端，或者想把 Harness 的某个插件思路搬过去，&lt;code&gt;dsh&lt;/code&gt; 开源这点就很关键 —— 至少架构是可以读的。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、Harness 和 opencode 怎么选&lt;/h2&gt;
&lt;p&gt;这两个东西形态很像但取向不同。我目前的判断：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DeepSeek Harness&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;定位&lt;/td&gt;
&lt;td&gt;个人助理 + 可编程 agent 底座&lt;/td&gt;
&lt;td&gt;终端优先的 AI 编程助手&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;架构&lt;/td&gt;
&lt;td&gt;一切皆插件（Cordis）&lt;/td&gt;
&lt;td&gt;插件 + MCP + LSP + ACP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认模型&lt;/td&gt;
&lt;td&gt;DeepSeek 自家（V4.1 Flash 等）&lt;/td&gt;
&lt;td&gt;多 provider，模型自由切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开源&lt;/td&gt;
&lt;td&gt;MIT，249k star&lt;/td&gt;
&lt;td&gt;MIT，官方主推&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上手&lt;/td&gt;
&lt;td&gt;装客户端就能用，零配置&lt;/td&gt;
&lt;td&gt;装个 CLI，要配 provider&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;p&gt;我的用法是&lt;strong&gt;两个都用&lt;/strong&gt;：写代码还是 opencode 顺手（多模型可切换，终端工作流顺），日常文档/表格/杂活丢给 Harness（开箱即用）。&lt;/p&gt;
&lt;p&gt;:::warning
一个务实的提醒：&lt;strong&gt;Harness 目前是开发者预览版，官方明说不兼容变更会来。&lt;/strong&gt; 当主力生产工具之前，做好心理准备。送的那 6 块反正也不心疼。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;开放平台注册 0 赠额是事实，不用找了&lt;/li&gt;
&lt;li&gt;官方另一个入口在 &lt;strong&gt;deepseek.com/zh/harness&lt;/strong&gt; —— 装桌面端、登录、送 6 元&lt;/li&gt;
&lt;li&gt;但更重要的是：Harness 是个 MIT 开源的 everything-is-a-plugin agent 框架，值得单独看&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npx @deepseek-ai/dsh web&lt;/code&gt; 一行命令起本地 Web UI，默认只监听 127.0.0.1&lt;/li&gt;
&lt;li&gt;额度只够试，别当成长期方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DeepSeek 这个牌子在开源这块其实相当舍得 —— 一个 249k star 的 MIT 项目，白送给你 6 块钱让你用它的模型去跑这个项目。算下来自己都不用花钱试了。&lt;/p&gt;
</content:encoded></item><item><title>Tailscale 是怎么做到&quot;像在同一个局域网&quot;的</title><link>https://ycxdev.com/posts/tailscale-p2p-how-it-works/</link><guid isPermaLink="true">https://ycxdev.com/posts/tailscale-p2p-how-it-works/</guid><description>不用配置路由器、不用端口转发、不用公网 IP。Tailscale 用 WireGuard + 协调服务器 + NAT 打洞，把一堆设备拼成一个加密的扁平网络。本文拆解它的控制平面、数据平面、密钥体系和 DERP 兜底机制。</description><pubDate>Sun, 11 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;用了大半年 Tailscale，最大的感受不是&quot;网速快&quot;，而是它&lt;strong&gt;把一整类问题从根上消掉了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统组网你得处理：公网 IP、端口转发、DDNS、路由器防火墙规则、两边能不能互通、运营商 CGNAT……每一个都是单独的坑。Tailscale 的做法是：这些全都不需要你想。&lt;/p&gt;
&lt;p&gt;它到底怎么做到的？&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、先说结论：这是&quot;控制平面 / 数据平面&quot;分离的经典范式&lt;/h2&gt;
&lt;p&gt;理解 Tailscale 只要抓住一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;协调服务器只负责&quot;介绍大家认识&quot;，数据从来不经过它。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│  控制平面（Control Plane）—— 只交换元数据，永不见明文      │
│                                                          │
│    coordination server                                  │
│    ├─ 认证：这个设备是谁？                               │
│    ├─ 分发：把 peer 的公钥 和 当前可达 endpoint 告诉 everyone│
│    └─ 策略：ACL / 子网路由 / DNS 记录                     │
└─────────────────────────────────────────────────────────┘
                            │
              交换公钥 + endpoint 地址（就这些）
                            │
┌───────────────────────────┴─────────────────────────────┐
│  数据平面（Data Plane）—— 设备之间直连，端到端加密        │
│                                                          │
│    笔记本 100.x.y.z  ◄──── WireGuard (UDP) ────►  家里 PC  │
│                                                          │
│    打洞失败时降级：                                       │
│    笔记本  ◄── WireGuard over DERP(TLS 443) ──►  家里 PC  │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个切分带来的直接后果：&lt;strong&gt;协调服务器被入侵，攻击者拿到的是一堆公钥和 IP 地址，不是你的文件内容。&lt;/strong&gt; 数据平面的加密密钥在设备本地，服务器没有。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、阶段 A：把设备加进同一个 tailnet&lt;/h2&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant D as 新设备（笔记本）
participant C as 协调服务器&amp;lt;br/&amp;gt;controlplane.tailscale.com
participant A as 已在线的设备&amp;lt;br/&amp;gt;(家里 PC)
D-&amp;gt;&amp;gt;D: 生成设备密钥 (curve25519)
Note over D: 私钥永不离开本机，只上传公钥
D-&amp;gt;&amp;gt;C: 发起登录 (SSO / auth key)
C-&amp;gt;&amp;gt;C: 验证身份，绑定到你的 tailnet
C--&amp;gt;&amp;gt;D: 下发节点密钥 + 网络策略(ACL) + DNS 配置
D-&amp;gt;&amp;gt;C: 上报本机 endpoint&amp;lt;br/&amp;gt;(公网 IP:随机端口列表)
C--&amp;gt;&amp;gt;A: 推送 peer 信息：新设备的公钥 + endpoint
C--&amp;gt;&amp;gt;D: 推送 peer 列表：家里 PC 的公钥 + endpoint
Note over D,A: 两端现在互相知道对方&quot;是谁&quot;和&quot;在哪&quot;
Note over D,A: 但还没有任何一包数据通过
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;这里有个容易被忽略的点：&lt;strong&gt;协调服务器给的是公钥，不是私钥。&lt;/strong&gt; 所以它从架构上就不可能解密你的流量。&lt;/p&gt;
&lt;p&gt;身份体系是另一层东西。Tailscale 的设备身份有三类密钥：&lt;/p&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;strong&gt;设备密钥（machine key）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;标识&quot;这是哪台设备&quot;，设备身份的生命周期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;节点密钥（node key）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WireGuard 会话密钥，每次连接协商&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;预授权密钥（auth key / preauth key）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;给无头设备（服务器、树莓派、容器）用，&lt;strong&gt;免交互加入&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第三类在自动化场景里非常有用。我 NAS 上跑的 Tailscale 就是用预授权密钥直接加入的，没有弹过任何登录页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale up --authkey=tskey-auth-xxxxx --advertise-routes=192.168.1.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
预授权密钥的泄露风险比密码高 —— 它是&lt;strong&gt;可重复使用&lt;/strong&gt;的，能直接往你的 tailnet 里加设备。所以一般签发一次性（ephemeral）版本，且定期轮转。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、阶段 B：NAT 打洞，然后直连&lt;/h2&gt;
&lt;p&gt;拿到对方 endpoint 之后，两台设备&lt;strong&gt;同时&lt;/strong&gt;开始发包，目标是让中间的 NAT 设备在状态表里打开一个洞。&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant L as 笔记本 (NAT 后)
participant D as DERP 中继&amp;lt;br/&amp;gt;(协调服务器附属)
participant P as 家里 PC (NAT 后)
Note over L,P: 第一步：先经 DERP 完成真正的握手
L-&amp;gt;&amp;gt;D: 中继包裹（内含 WireGuard 握手发起）
D-&amp;gt;&amp;gt;P: 转发给 P
P-&amp;gt;&amp;gt;D: 中继包裹（WireGuard 握手响应）
D-&amp;gt;&amp;gt;L: 转发给 L
Note over L,P: ★ 隧道已建立，且双方都知道了&amp;lt;br/&amp;gt;对方的 NAT 映射地址:端口
par 同时打洞
L-&amp;gt;&amp;gt;P: UDP 直发（经对方 NAT 映射地址）
and
P-&amp;gt;&amp;gt;L: UDP 直发（经对方 NAT 映射地址）
end
Note over L,P: 双向流量都通过了 → NAT 表已打开
L-&amp;gt;&amp;gt;P: ⚡ WireGuard 直连，绕开 DERP
P-&amp;gt;&amp;gt;L: ⚡ WireGuard 直连，绕开 DERP
Note over D: DERP 此后只在心跳阶段参与，不再承载数据
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;这是整个设计里最漂亮的一步。&lt;strong&gt;DERP 在这里的作用不是&quot;中转流量&quot;，而是&quot;牵个线&quot;&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;NAT 穿越的难点在于：你不知道对方在 NAT 后的映射地址，而且通常只有一方能主动建连（对称型 NAT 双方都难）。DERP 让双方先通过中继完成握手，握手过程中双方互相通告了自己的&lt;strong&gt;源地址:源端口&lt;/strong&gt;，然后同时向这个地址发包。&lt;/p&gt;
&lt;p&gt;NAT 设备看到&quot;有出站流量到 X:Y&quot;，就会为返回流量在状态表里开一个洞。&lt;strong&gt;两边都打，就都开了。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;打洞失败怎么办&lt;/h3&gt;
&lt;p&gt;不是所有网络都允许 UDP —— 有些企业防火墙、有些酒店 Wi-Fi、有些移动网络会直接封 UDP。这时候自动降级到 &lt;strong&gt;DERP 中继&lt;/strong&gt;，数据走 WireGuard over TLS 443：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale status
# 100.x.y.z   laptop   linux   -   直接连接
# 100.x.y.z   nas      linux   relay &quot;iad&quot;   走 DERP 中继
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;功能完全正常，只是延迟高一点。&lt;code&gt;tailscale netcheck&lt;/code&gt; 可以看当前网络的 NAT 类型和 UDP 可用性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
这也是为什么 Tailscale 在受限网络里&quot;总能用&quot;—— 它有一个永远能通的降级路径。中继走 443/TLS，几乎不可能被企业防火墙完全拦死。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;四、拿到&quot;局域网体验&quot;的三层能力&lt;/h2&gt;
&lt;p&gt;直连只是基础。让人觉得&quot;跟在一个局域网里&quot;的其实是这三样：&lt;/p&gt;
&lt;h3&gt;1. MagicDNS —— 不用记 IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tailscale status
# 100.101.102.103  my-nas      linux   -
# 100.104.105.106  my-laptop   windows -
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ssh my-nas
ping my-laptop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;背后是 Tailscale 内置的 DNS resolver，把 &lt;code&gt;my-nas&lt;/code&gt; 解析到 &lt;code&gt;100.x.y.z&lt;/code&gt;，再由 netfilter 把这个网段的流量送进 WireGuard 隧道。&lt;/p&gt;
&lt;p&gt;:::warning
&lt;code&gt;100.64.0.0/10&lt;/code&gt; 是 CGNAT 保留段（RFC 6598）。Tailscale 选它是为了&lt;strong&gt;几乎不可能和你的真实局域网冲突&lt;/strong&gt; —— 家宽和运营商内网正好也用这个段，所以反向的情况（你的设备真实地址就在 100.x 里）才是要小心的那个。Tailscale 也确实因此和某些 CGNAT 场景下的本地路由撞车，官方文档里明确列了这个已知冲突。
:::&lt;/p&gt;
&lt;h3&gt;2. 子网路由 —— 访问&quot;根本不在 tailnet 里&quot;的机器&lt;/h3&gt;
&lt;p&gt;如果家里有一台没装 Tailscale 的打印机或摄像头，可以让&lt;strong&gt;装了 Tailscale 的那台 PC 当路由器&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在家里 PC 上
tailscale up --advertise-routes=192.168.1.0/24

# 在管理后台批准这条路由（必须手动批准）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;批准后，其他 tailnet 设备就能直接访问 &lt;code&gt;192.168.1.0/24&lt;/code&gt;，&lt;strong&gt;只有一台设备需要装 Tailscale&lt;/strong&gt;。IP 转发和 SNAT 由 tailscaled 自动开好。&lt;/p&gt;
&lt;h3&gt;3. Exit Node —— 把整个出口搬到别处&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tailscale up --exit-node=my-nas
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有设备的默认流量都从 NAS 出去。典型用途是：人在国外想让国内设备走回国出口，或者反过来。&lt;/p&gt;
&lt;p&gt;:::caution
Exit Node 和子网路由&lt;strong&gt;别同时无脑开&lt;/strong&gt;。Exit Node 会接管默认路由，容易和本地 VPN（Clash 之类）打架 —— 这也是 Tailscale 和 Clash 共存时最常见的冲突点。配合使用时，一般要么让 Clash 跑在 Tailscale 之上（子网路由/Exit Node 出口），要么反过来明确指定 &lt;code&gt;--exit-node&lt;/code&gt; 只处理特定流量。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、和 OpenTunnel 怎么选&lt;/h2&gt;
&lt;p&gt;上一篇写了用 OpenTunnel 把 opencode 暴露到公网。这里对比一下，因为两者经常被放在一起问：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tailscale&lt;/th&gt;
&lt;th&gt;OpenTunnel&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;私有网络&lt;/strong&gt;，设备间互联&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;单点公网入口&lt;/strong&gt;，一个 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;暴露面&lt;/td&gt;
&lt;td&gt;只有 tailnet 内的设备&lt;/td&gt;
&lt;td&gt;任何人拿到 URL 都能访问（要自己加鉴权）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加密位置&lt;/td&gt;
&lt;td&gt;WireGuard，&lt;strong&gt;内核态&lt;/strong&gt;，端到端&lt;/td&gt;
&lt;td&gt;TLS 在本地终结，&lt;strong&gt;盲转发&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移动端&lt;/td&gt;
&lt;td&gt;装 App&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;/td&gt;
&lt;td&gt;临时把本地 web 服务甩到公网&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际用法是组合拳：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tailscale  ──▶  手机安全地回到家里内网
OpenTunnel ──▶  把某个服务单独甩到公网（带密码）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
如果你只是想让手机访问家里几个服务，&lt;strong&gt;只用 Tailscale 就够了&lt;/strong&gt;，别开 OpenTunnel —— 前者暴露面是&quot;你的设备列表&quot;，后者是&quot;一个公网 URL&quot;。安全边界完全不同。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Tailscale 之所以能用得这么舒服，是因为它把三个本该由用户操心的问题各自交给了正确的层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;身份问题&lt;/strong&gt; → 协调服务器（但只管公钥，不管数据）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可达性问题&lt;/strong&gt; → DERP 中继做握手辅助 + NAT 打洞，失败自动降级中继&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加密问题&lt;/strong&gt; → WireGuard 端到端，密钥在设备本地，服务端物理上无法解密&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;学 networking 的时候，&quot;VPN = 加密隧道&quot;这个印象会束缚你很久。Tailscale 告诉你的是：&lt;strong&gt;加密隧道只是数据平面，前面还需要一个不碰数据的控制平面；而可达性问题，可以用&quot;先借道、再打洞、最后直连&quot;的时序来解决。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>OpenCode + OpenTunnel：让手机直接和家里电脑上的 AI 编程助手对话</title><link>https://ycxdev.com/posts/opencode-opentunnel-remote-from-phone/</link><guid isPermaLink="true">https://ycxdev.com/posts/opencode-opentunnel-remote-from-phone/</guid><description>不用 VPN、不用公网 IP、不用 frp。用 OpenCode 官方生态的 OpenTunnel 把 localhost 变成一个 https 地址，手机浏览器直接打开就能和本机 opencode 对话。本文拆解 blind TLS 隧道的原理，附完整时序图。</description><pubDate>Sat, 10 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我一直想在通勤路上给家里那台电脑上的 opencode 派活儿 —— 不是远程桌面那种&quot;看着屏幕点点&quot;，是&lt;strong&gt;真的把指令发过去，让它自己跑&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;试过几条路：&lt;/p&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;frp 自建服务器&lt;/td&gt;
&lt;td&gt;要有台有公网 IP 的 VPS，还得自己运维&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Tunnel&lt;/td&gt;
&lt;td&gt;要绑自己的域名，配置一堆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale + 客户端 App&lt;/td&gt;
&lt;td&gt;得装 App，网页端不一定好用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH 端口转发 + 客户端&lt;/td&gt;
&lt;td&gt;手机上没法开 SSH 隧道&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTunnel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;一条命令，一个 https 网址，手机浏览器直接用&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后这个是 OpenCode 联合创始人 Dax Raad 做的开源项目：&lt;a href=&quot;https://github.com/anomalyco/opentunnel&quot;&gt;anomalyco/opentunnel&lt;/a&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、它是什么：一个&quot;盲&quot;TLS 隧道&lt;/h2&gt;
&lt;p&gt;一句话概括它的设计哲学：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;服务端&lt;strong&gt;只读 TLS 握手的第一个包（ClientHello）&lt;/strong&gt;，拿到 SNI 就知道该转给谁，剩下的一概不看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是个很反常规的设计。传统内网穿透（ngrok、frp）的模型是：客户端和服务端之间建立一条隧道，&lt;strong&gt;所有流量都经过服务端&lt;/strong&gt;。所以服务端能看到你的明文 —— 包括密码、cookie、API Key。&lt;/p&gt;
&lt;p&gt;OpenTunnel 换了个思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                     ┌─────────────────────────────┐
  浏览器  ──HTTPS──▶  │  OpenTunnel Server (443)    │
  （以为在直连）      │  只读 ClientHello 里的 SNI   │
                     └──────────────┬──────────────┘
                                    │ 按 SNI 找到对应隧道
                                    │ （剩余内容原样转发，加密状态不变）
                                    ▼
                     ┌─────────────────────────────┐
                     │  本地 opentunnel client      │
                     │  ★ 持有证书私钥，在这里终结 TLS │
                     │  ★ 服务端从来看不到明文       │
                     └──────────────┬──────────────┘
                                    │ 明文 HTTP
                                    ▼
                     ┌─────────────────────────────┐
                     │  opencode server 127.0.0.1:4096 │
                     └─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点：&lt;strong&gt;证书私钥在你自己电脑上，服务端从头到尾没有碰过。&lt;/strong&gt; 它就像一个只认门牌号、然后原样把包裹送过去的快递员 —— 它知道包裹要送给谁，但包裹里是什么它拆不开。&lt;/p&gt;
&lt;p&gt;它管这叫 &lt;strong&gt;blind TLS tunnel（盲 TLS 隧道）&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、完整时序图&lt;/h2&gt;
&lt;p&gt;:::note
下图里的 &lt;code&gt;ROUTE&lt;/code&gt; 是默认随机生成的 16 位路由名，&lt;code&gt;ID&lt;/code&gt; 是服务端分配给本机的隧道子域名，两者拼起来就是一个完整地址。Mermaid 的标签里不能用尖括号，所以这里用大写占位。
:::&lt;/p&gt;
&lt;h3&gt;阶段 A：建隧道（一次性）&lt;/h3&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant CLI as 你的终端
participant CLI2 as opentunnel client
participant SRV as OpenTunnel Server&amp;lt;br/&amp;gt;(Fly.io :443)
participant ACME as ZeroSSL (ACME)
participant CF as Cloudflare DNS
CLI-&amp;gt;&amp;gt;CLI2: opentunnel route add 4096
CLI2-&amp;gt;&amp;gt;SRV: POST /api/routes (申请隧道)
SRV--&amp;gt;&amp;gt;CLI2: 返回 id，例如 3f2a...
Note over CLI2,SRV: 分配唯一子域名 ID.opentunnel.xyz
CLI2-&amp;gt;&amp;gt;ACME: 申请证书 (ACME DNS-01)
ACME-&amp;gt;&amp;gt;CF: 验证 _acme-challenge TXT 记录
CF--&amp;gt;&amp;gt;ACME: 验证通过
ACME--&amp;gt;&amp;gt;CLI2: 下发证书 + 私钥（只给客户端）
Note over CLI2: ★ 私钥永不离开本机
CLI2-&amp;gt;&amp;gt;SRV: 建立 bridge WebSocket（携带密文通道）
SRV--&amp;gt;&amp;gt;CLI2: 就绪，开始接收转发
CLI2--&amp;gt;&amp;gt;CLI: 打印 https://ROUTE(随机16位).ID.opentunnel.xyz
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;注意 DNS-01 这一步：&lt;strong&gt;它不验证端口 80，不要求你有公网 IP，只验证 DNS 权限。&lt;/strong&gt; 这是没有固定 IP 的家庭宽带能拿到正规证书的唯一途径。&lt;/p&gt;
&lt;p&gt;路由名默认是 &lt;strong&gt;16 位随机字符&lt;/strong&gt;，而且&lt;strong&gt;不会出现在公开的证书透明度（CT）日志里&lt;/strong&gt; —— 意味着没人能通过查 CT 日志反查出你的隧道地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opentunnel route add 4096
# Added route 21992cc9713e5fc5 → 127.0.0.1:4096
# https://21992cc9713e5fc5.3f2a91c4.opentunnel.xyz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;安装&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://opentunnel.xyz/install | sh
brew install anomalyco/tap/opentunnel
yay -S opentunnel-bin              # Arch
npm install -g opentunnel
cargo install opentunnel-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;阶段 B：手机发一条消息（每次）&lt;/h3&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
sequenceDiagram
autonumber
participant M as 手机浏览器
participant S as OpenTunnel Server
participant C as 本地 opentunnel client
participant O as opencode server&amp;lt;br/&amp;gt;127.0.0.1:4096
M-&amp;gt;&amp;gt;S: TCP 连接 443，TLS ClientHello&amp;lt;br/&amp;gt;SNI = ROUTE.ID.opentunnel.xyz
Note over S: 只解析 SNI，其余字节看不懂也不看
S-&amp;gt;&amp;gt;C: 经 bridge WebSocket 转发原始 ClientHello 密文
C-&amp;gt;&amp;gt;C: ★ 用本地私钥终结 TLS，拿到明文 HTTP
Note over C: 校验证书域名，握手完成
C-&amp;gt;&amp;gt;O: HTTP 请求&amp;lt;br/&amp;gt;GET / (Authorization: Basic ...)
Note over C,O: 应用看到的是 loopback 对端
O--&amp;gt;&amp;gt;C: 200 + OpenCode Web 界面
C-&amp;gt;&amp;gt;S: 用会话密钥加密后回传
S--&amp;gt;&amp;gt;M: 原样转发（浏览器全程以为在直连服务端）
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;:::note
最后一步是最舒服的地方：&lt;strong&gt;对浏览器来说，它就是访问一个普通的 https 站点&lt;/strong&gt;。没有证书警告，没有客户端 App，没有 VPN 状态栏。它甚至不知道中间有个隧道。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、最要命的一步：那个随机 24 位密码&lt;/h2&gt;
&lt;p&gt;隧道跑通之后我差点裸奔到公网上。然后我意识到一个很要命的事实：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;README 里写得很清楚：
&lt;strong&gt;&quot;Route names never appear in public certificate logs, so the URL can&apos;t be guessed, but anyone you share it with can reach the service: it is not authentication.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;地址猜不出来 ≠ 有鉴权。&lt;strong&gt;地址泄露了 = 完全门户大开。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而 opencode 官方文档也直说了：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If &lt;code&gt;OPENCODE_SERVER_PASSWORD&lt;/code&gt; is not set, the server will be &lt;strong&gt;unsecured&lt;/strong&gt;. This is fine for local use but &lt;strong&gt;should be set for network access&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以第一步就是打开 HTTP Basic 认证。用户名默认是 &lt;code&gt;opencode&lt;/code&gt;，可以用 &lt;code&gt;OPENCODE_SERVER_USERNAME&lt;/code&gt; 改。&lt;/p&gt;
&lt;h3&gt;密码怎么生成&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;密码强度必须对得起&quot;这个地址现在挂在公网上&quot;这件事。&lt;/strong&gt; 别用 &lt;code&gt;123456&lt;/code&gt;、别用手机号生日、别用你其他服务复用过的密码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Linux / macOS / WSL —— openssl 生成，18 字节 = base64 后正好 24 个字符
export OPENCODE_SERVER_PASSWORD=$(openssl rand -base64 18)

# 顺手看一眼长度，确认是 24
echo -n &quot;$OPENCODE_SERVER_PASSWORD&quot; | wc -c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;18 字节 = 144 bit 熵，&lt;code&gt;base64&lt;/code&gt; 编码后是 &lt;strong&gt;24 个字符，且没有 &lt;code&gt;=&lt;/code&gt; 补位&lt;/strong&gt;。这个长度刚好能在密码管理器里一眼输完，又足够长到爆破无望。&lt;/p&gt;
&lt;p&gt;:::warning
Windows 的 PowerShell 默认没有 &lt;code&gt;openssl.exe&lt;/code&gt;（Windows 自带的是 OpenSSH，不是 OpenSSL）。用这个等效实现，长度同样是 24：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$b = New-Object byte[] 18
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b)
$env:OPENCODE_SERVER_PASSWORD = [Convert]::ToBase64String($b).TrimEnd(&apos;=&apos;).Replace(&apos;+&apos;,&apos;-&apos;).Replace(&apos;/&apos;,&apos;_&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-&lt;/code&gt; 和 &lt;code&gt;_&lt;/code&gt; 的替换是为了避开 &lt;code&gt;+&lt;/code&gt; &lt;code&gt;/&lt;/code&gt;，这样密码在 URL、配置文件、shell 历史里都不需要转义。
:::&lt;/p&gt;
&lt;h3&gt;启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 只监听 localhost —— 有隧道就够了，不要在局域网里裸奔
OPENCODE_SERVER_PASSWORD=&quot;$OPENCODE_SERVER_PASSWORD&quot; opencode serve --port 4096 --hostname 127.0.0.1

# 手机打开那个 https 网址，弹 Basic Auth
# 用户名：opencode
# 密码：你刚生成的那 24 位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--hostname&lt;/code&gt; 保持 &lt;code&gt;127.0.0.1&lt;/code&gt; 是个原则性问题：&lt;strong&gt;暴露面只留隧道这一个口&lt;/strong&gt;。你用隧道纯粹是为了穿越 NAT，不是为了让同 Wi-Fi 下的邻居也能连你。&lt;/p&gt;
&lt;h3&gt;还有一层：&lt;code&gt;--proxy-protocol&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;默认情况下你的应用看到的是 &lt;strong&gt;loopback 对端&lt;/strong&gt;，看不到真实来访 IP。OpenTunnel 官方专门写了份 trust-boundary 文档提醒这件事 —— 意思是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你的应用里有任何基于 &lt;code&gt;127.0.0.1&lt;/code&gt; 判断&quot;这是本地可信请求&quot;的逻辑，&lt;strong&gt;它在隧道后面是失效的&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要传递真实来访者地址的话，客户端加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opentunnel route add 4096 --proxy-protocol v2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;只在你的应用确实支持 PROXY protocol 时才开。&lt;/strong&gt; 乱开会往应用里塞它不认识的请求头。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、串起来的完整命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 1. 生成 24 位密码并记住它
export OPENCODE_SERVER_PASSWORD=$(openssl rand -base64 18)

# 2. 起 opencode，只听 localhost，带 Basic Auth
OPENCODE_SERVER_PASSWORD=&quot;$OPENCODE_SERVER_PASSWORD&quot; \
  opencode serve --port 4096 --hostname 127.0.0.1 &amp;amp;

# 3. 开隧道
opentunnel route add 4096
# https://21992cc9713e5fc5.3f2a91c4.opentunnel.xyz

# 4. 手机浏览器打开它，用 opencode / 你的密码登录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想两边同时用（电脑上看 TUI，手机上发消息），加一个终端 attach：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opencode attach http://localhost:4096
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;两个客户端共享同一份 session 状态&lt;/strong&gt;，不是两个独立实例。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、几个值得记的点&lt;/h2&gt;
&lt;h3&gt;为什么是 blind TLS 而不是普通反代&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;普通反代（ngrok 类）&lt;/th&gt;
&lt;th&gt;Blind TLS（OpenTunnel）&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;/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;tr&gt;
&lt;td&gt;DNS-01 签发证书&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;/td&gt;
&lt;td&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;/td&gt;
&lt;td&gt;只读 SNI，开销极小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这本质上是把信任边界画在了正确的地方&lt;/strong&gt; —— 中转节点只负责&quot;路由&quot;，不负责&quot;内容&quot;。&lt;/p&gt;
&lt;h3&gt;它不是什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不是 VPN&lt;/strong&gt;。没有全局流量接管，你手机上只有这一个网址是&quot;远程&quot;的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是端口转发&lt;/strong&gt;。只支持 HTTP 应用（&quot;任何支持 TLS 的现代浏览器/设备&quot;），不是任意 TCP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是零配置的安全方案&lt;/strong&gt;。地址保密性来自不可猜测，不来自鉴权。密码必须设。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
既然是&quot;限时/白嫖性质&quot;的第三方服务，我的取舍是：&lt;strong&gt;可以用它做个人开发工具，但不要把生产系统的面板挂在上面。&lt;/strong&gt; 它本质上是一个公共的流量中转，服务端不做任何端到端的身份认证 —— 信任链完全建立在你那 24 位密码上。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;把 localhost 变成一个公网 https 地址，难点从来不是&quot;穿透&quot;，而是&lt;strong&gt;在不牺牲加密强度的前提下穿透&lt;/strong&gt;。OpenTunnel 的答案是：让服务端看不懂数据，只让它认门牌号。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blind TLS&lt;/strong&gt;：服务端只读 SNI，证书私钥和明文永远留在本地&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS-01 签发&lt;/strong&gt;：不需要公网 IP、不需要域名、80 端口也不用开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;16 位随机路由名 + 不进 CT 日志&lt;/strong&gt;：地址不可枚举&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但仍需鉴权&lt;/strong&gt;：&lt;code&gt;OPENCODE_SERVER_PASSWORD&lt;/code&gt; + 24 位随机密码，是这套方案成立的前提&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--hostname 127.0.0.1&lt;/code&gt;&lt;/strong&gt;：暴露面只留隧道一个入口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在我出门在地铁上，动动手指就能给家里的 opencode 派活儿了。写完这段话，我马上试一次。&lt;/p&gt;
</content:encoded></item><item><title>从 OpenCode 数据页捡漏：我怎么在 30 秒内判断一个免费模型值不值得用</title><link>https://ycxdev.com/posts/opencode-data-pick-a-model/</link><guid isPermaLink="true">https://ycxdev.com/posts/opencode-data-pick-a-model/</guid><description>opencode.ai/data 把全平台的模型用量摊开给你看。绝大多数人只看排行榜第一，我告诉你哪三个数字才是真正决定手感的 —— 以及为什么 space-bunny 的留存率比它的 token 量更值得看。</description><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;刷 opencode.ai/data 的时候我发现一件事：这个页面远比&quot;谁最火&quot;有用得多。&lt;/p&gt;
&lt;p&gt;它的定位是 OpenCode 官方公开的&lt;strong&gt;全平台遥测&lt;/strong&gt;——每小时聚合一次，把每个模型的 token 量、独立用户数、周留存、单会话成本、缓存命中率全部摊开。这个东西平时不显山露水，但它其实是一份&lt;strong&gt;公开的、由真实负载产生的模型口碑数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我看这张表的方式，和大多数人不太一样。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、大多数人只看第一眼&lt;/h2&gt;
&lt;p&gt;排行榜第一是 DeepSeek V4.1 Flash，过去 7 天 50T token。第二是 Muse Spark 1.3 Contributor，37T。第三是 space-bunny，21T。&lt;/p&gt;
&lt;p&gt;所以&quot;细心如我&quot;注意到的是——&lt;strong&gt;一个没听过的名字，怎么能排到第三？&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;/li&gt;
&lt;li&gt;也可能是因为&lt;strong&gt;少数重度用户刷出了大量 token&lt;/strong&gt;，而非普通用户真的爱用&lt;/li&gt;
&lt;li&gt;榜单是&quot;token 总量&quot;，而 token 总量 ≈ 使用人数 × 人均用量。人均用量差异可以到几十倍&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以关键问题是：&lt;strong&gt;这 21T 是一万个人的日常，还是一百个重度用户的肝？&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、真正该看的是留存率&lt;/h2&gt;
&lt;p&gt;页面底部有一张 &lt;strong&gt;Weekly retention&lt;/strong&gt; 表，定义是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;某模型本周的用户中，下周还在用它的比例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这才是&quot;好不好用&quot;的直接度量。&lt;strong&gt;一个新工具的留存高，说明它进了工作流；留存低，说明大家只是来试新鲜的。&lt;/strong&gt;&lt;/p&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;1&lt;/td&gt;
&lt;td&gt;MiMo-V2.6-Flash&lt;/td&gt;
&lt;td&gt;80.3%&lt;/td&gt;
&lt;td&gt;2.8K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;space-bunny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.2K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Muse Spark 1.2 Contributor&lt;/td&gt;
&lt;td&gt;78.3%&lt;/td&gt;
&lt;td&gt;9.1K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Kimi K2.5&lt;/td&gt;
&lt;td&gt;77.1%&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;DeepSeek V4 Flash&lt;/td&gt;
&lt;td&gt;69.3%&lt;/td&gt;
&lt;td&gt;272K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Muse Spark 1.3 Contributor&lt;/td&gt;
&lt;td&gt;67.0%&lt;/td&gt;
&lt;td&gt;41K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;GPT-6 Luna&lt;/td&gt;
&lt;td&gt;65.2%&lt;/td&gt;
&lt;td&gt;3.5K&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里有两层信息，第二层更值钱：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;space-bunny 留存 80%，排第二&lt;/strong&gt; —— 一个&quot;限时免费&quot;的模型，能拿到 80% 的周留存，说明它不是薅完就走。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但排行榜冠军 DeepSeek V4 Flash 只有 69.3%&lt;/strong&gt; —— token 量是 space-bunny 的两倍多，留存却低了 11 个百分点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意样本量那一列。DeepSeek 有 27 万个有效用户周，space-bunny 只有 8200。&lt;strong&gt;大样本的低留存，比小样本的高留存更可信。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
榜单告诉你&quot;谁在被用&quot;，留存告诉你&quot;谁被留住了&quot;。前者是流量，后者是产品。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、把两条曲线叠起来看，才有意思&lt;/h2&gt;
&lt;p&gt;我把 space-bunny 上线以来（约 9/23）的每日数据拉出来画了一遍：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token 量（单位 T）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
xychart-beta
title &quot;space-bunny 每日 token 量（单位 T）&quot;
x-axis [&quot;9/23&quot;,&quot;9/30&quot;,&quot;10/7&quot;,&quot;10/13&quot;]
y-axis &quot;T tokens&quot; 0 --&amp;gt; 11
bar [1.5, 10, 3.9, 3.1]
line [1.5, 10, 3.9, 3.1]
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;日活用户（单位 K）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;mermaid&quot;&amp;gt;
xychart-beta
title &quot;space-bunny 日活独立用户（单位 K）&quot;
x-axis [&quot;9/23&quot;,&quot;9/30&quot;,&quot;10/7&quot;,&quot;10/13&quot;]
y-axis &quot;K users&quot; 0 --&amp;gt; 55
bar [6.9, 50, 25, 19]
line [6.9, 50, 25, 19]
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;把这两张图放一起看，出现了一个很有意思的现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10 月初的 10T/天 是&lt;strong&gt;发布红利&lt;/strong&gt;，峰值日活 5 万&lt;/li&gt;
&lt;li&gt;到了 10/13，token 掉到 3.1T（约 -70%），但&lt;strong&gt;日活只掉到 1.9 万（约 -62%）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;同期大盘 rank 从 #3 一路掉下来（周环比 -62%）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;token 跌幅&lt;strong&gt;大于&lt;/strong&gt;用户跌幅。这意味着：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;掉走的是重度用户，留下的是日常用户。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个结论和 80% 的周留存互相印证 —— 产品本身没垮，只是最初那批薅得最狠的人额度用完了。&lt;/p&gt;
&lt;p&gt;读数据到这个程度，才算真的看懂了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、还有两个数字：成本和缓存&lt;/h2&gt;
&lt;p&gt;页面上还有一张 &lt;strong&gt;Session cost&lt;/strong&gt; 表，把&quot;平均每个会话花多少钱&quot;和&quot;平均每个会话多少 token&quot;放在一起：&lt;/p&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;平均单会话 token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;space-bunny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0005&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;778K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mimo-v2.6-flash&lt;/td&gt;
&lt;td&gt;$0.0022&lt;/td&gt;
&lt;td&gt;1.2M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;muse-spark-1.3-contributor&lt;/td&gt;
&lt;td&gt;$0.0032&lt;/td&gt;
&lt;td&gt;2.2M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;$0.0286&lt;/td&gt;
&lt;td&gt;3.1M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4.1-flash&lt;/td&gt;
&lt;td&gt;$0.0500&lt;/td&gt;
&lt;td&gt;3.7M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glm-5.3-flash&lt;/td&gt;
&lt;td&gt;$0.0851&lt;/td&gt;
&lt;td&gt;2.1M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;space-bunny 的单会话成本是 DeepSeek V4.1 Flash 的 &lt;strong&gt;1/100&lt;/strong&gt;，而单会话 token 只有它的 1/5 左右。&lt;/p&gt;
&lt;p&gt;这直接解释了我体感上的一个疑问：&lt;strong&gt;为什么用 space-bunny 感觉更省 token？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再看 &lt;strong&gt;Cache ratio&lt;/strong&gt;：&lt;/p&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;缓存 token&lt;/th&gt;
&lt;th&gt;未缓存 token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;space-bunny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20T&lt;/td&gt;
&lt;td&gt;709B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-v4.1-flash&lt;/td&gt;
&lt;td&gt;96.1%&lt;/td&gt;
&lt;td&gt;48T&lt;/td&gt;
&lt;td&gt;1.9T&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;muse-spark-1.3-contributor&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;td&gt;35T&lt;/td&gt;
&lt;td&gt;1.7T&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;全平台都在 90% 以上，说明 opencode 的 prompt cache 机制对所有模型都生效。但 space-bunny 的 &lt;strong&gt;未缓存输入只有 709B&lt;/strong&gt;，是所有模型里最低的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
在 agent 场景里，缓存命中率才是 token 账单的主控变量。一次多轮工具调用的会话，真正的&quot;新信息&quot;往往只有几千 token，其余全是重复的上下文。缓存命中率高 = 你少付 90% 的钱。&lt;/p&gt;
&lt;p&gt;这就是为什么我现在觉得 opencode 这套 harness 体感上比 deepseek harness 省 token —— 底层模型差不多，&lt;strong&gt;harness 怎么组织 prompt、怎么触发缓存，差出好几倍&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至于&quot;智力&quot;，OpenCode 官方的描述是：space-bunny 是个&lt;strong&gt;stealth model&lt;/strong&gt;（未公布身份的模型），1M 上下文、支持多模态、&lt;strong&gt;Zero Data Retention&lt;/strong&gt;（零数据保留），通过 OpenCode Zen 限时免费提供。&lt;/p&gt;
&lt;p&gt;零数据保留这一点对把代码喂给模型的人很重要 —— 官方承诺不留存，我在写一些内部逻辑的时候顾虑就少很多。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、这套方法可以复用到任何&quot;选型&quot;决策上&lt;/h2&gt;
&lt;p&gt;把这张表的读法抽出来，它其实是四个通用动作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 看绝对量  →  这个东西有多少人在用？      （看规模，不看排名）
2. 看留存    →  用过的人还回来吗？          （看是否进了工作流）
3. 看单会话  →  一次典型任务花多少钱？      （看真实成本，不是标价）
4. 看分母    →  样本量够不够支撑这个结论？  （防止被小样本骗）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 4 步是最容易被跳过、也最值钱的一步。前面那张留存榜里，&lt;code&gt;Kimi K2.5&lt;/code&gt; 的留存是 77.1%，比 DeepSeek 高出 8 个点 —— 但它的有效样本只有 &lt;strong&gt;214 个用户周&lt;/strong&gt;。这跟&quot;272K 用户周下的 69.3%&quot;是完全不同量级的证据。&lt;/p&gt;
&lt;p&gt;:::warning
这张表是 OpenCode 平台的遥测，&lt;strong&gt;只代表 opencode 用户的使用模式&lt;/strong&gt;。你在别的客户端（比如 Claude Code）用同一个模型，结论可能不一样。它的价值在于样本量大、更新快、免费公开 —— 而不是&quot;官方认证的排行榜&quot;。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;DeepSeek 现在的模型确实强得离谱，50T token 的周总量和断档式领先的性价比是实打实的。但这次翻数据页最大的收获不是&quot;哪个模型第一&quot;，而是意识到：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判断一个模型好不好用，看榜单是不作数的，要看留存、单会话成本和缓存命中。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;space-bunny 是个限时免费的模型，随时可能收费。作为免费轮，它的体感确实是目前 OpenCode 里第一档的 —— 响应快、脑子够用、还零留存。等它收费了，我大概还是会回到 DeepSeek。&lt;/p&gt;
&lt;p&gt;但至少现在，我知道自己用的是不是真的好，而不是被热度推着走。&lt;/p&gt;
</content:encoded></item><item><title>把知乎收藏变成知识图谱：导出 → AI 整理 → GitHub 存档</title><link>https://ycxdev.com/posts/zhihu-collection-to-knowledge-graph/</link><guid isPermaLink="true">https://ycxdev.com/posts/zhihu-collection-to-knowledge-graph/</guid><description>知乎收藏夹是我最脏也最舍不得删的资产。用开源工具批量导出成 Obsidian 友好的 Markdown，再让 AI Agent 生成知识图谱，最后推到 GitHub 做版本化存档。</description><pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;知乎收藏夹是我用得最狠的功能，也是最容易出事的地方：&lt;strong&gt;收藏的时候爽，三个月后再也找不到。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;平台不给导出，收藏夹里的文章随时可能被作者删除，链接会 404。所以我这两年一直在做一件事：把它变成自己的资产。&lt;/p&gt;
&lt;p&gt;最近把整条链路跑通了，简单说就是三步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;知乎收藏夹  ──导出──&amp;gt;  本地 Markdown  ──AI 整理──&amp;gt;  知识图谱  ──git push──&amp;gt;  GitHub
   （随时会消失）      （Obsidian 直接能看）      （关系被显式化）        （有历史、可回滚）
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;一、找导出工具：知乎没有官方出口&lt;/h2&gt;
&lt;p&gt;知乎至今没有给用户任何官方导出接口。收藏夹只是渲染出来给你看的页面数据，所以工具只能走两条路：&lt;/p&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;strong&gt;抓 HTML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;请求文章页，用 CSS 选择器抠正文&lt;/td&gt;
&lt;td&gt;实现简单&lt;/td&gt;
&lt;td&gt;选择器一改就全废；知乎反爬会 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;走 API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;直接打 &lt;code&gt;/api/v4/...&lt;/code&gt; 这类内部接口&lt;/td&gt;
&lt;td&gt;快、结构化、字段干净&lt;/td&gt;
&lt;td&gt;需要 Cookie，容易被风控&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我用的这个项目把两条路都做了兜底 —— &lt;a href=&quot;https://github.com/JasonJarvan/Zhihu-Collections-MCP&quot;&gt;Export-Zhihu-Collections&lt;/a&gt;（&lt;code&gt;JasonJarvan/Zhihu-Collections-MCP&lt;/code&gt;）。它后来干脆把整套能力包成了一个 &lt;strong&gt;MCP Server&lt;/strong&gt;，这一点后面会用到。&lt;/p&gt;
&lt;p&gt;它的关键设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;输出结构是给 Obsidian 看的&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;downloads/
├── 编程/                      ← 一个收藏夹一个目录
│   ├── xxx.md                 ← 一篇一个文件
│   └── assets/
│       ├── 20240101120000.jpg ← 图片本地化，不再依赖知乎 CDN
│       └── ...
├── logs/
└── debug/                     ← 解析失败的页面 HTML 存这里，方便复盘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图片&lt;strong&gt;必须本地化&lt;/strong&gt;。这不是洁癖：知乎的图片 URL 带鉴权参数且会过期，你把 md 存到本地，半年后打开是一堆裂图，等于没存。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;私密收藏夹靠 Cookie&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[
  {&quot;name&quot;: &quot;z_c0&quot;, &quot;value&quot;: &quot;你的值&quot;}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存成 &lt;code&gt;cookies.json&lt;/code&gt; 放项目根目录。这个文件等同于你的知乎登录态，&lt;strong&gt;别提交到公开仓库&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;403 自动降级&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;HTML 被拦了，自动改走 API；专栏文章走流式下载（几万字带图的长文一次性拉容易断），再加 3 次重试。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;去重&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;跑第二遍不会重复下载。它会检查目标文件是否已存在。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;安装与运行&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/JasonJarvan/Zhihu-Collections-MCP
cd Zhihu-Collections-MCP
pip install -r requirements.txt

# 先让脚本自动扫出你的所有收藏夹，写回 config.json
python fetch_collections.py

# 编辑 config.json：确认收藏夹列表 + 输出路径
python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 路径两种写法都认：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &quot;outputPath&quot;: &quot;D:\\Documents\\ZhihuExports&quot; }
{ &quot;outputPath&quot;: &quot;D:/Documents/ZhihuExports&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;跑完的体感&lt;/h3&gt;
&lt;p&gt;几百篇下来，真正&quot;抓不到&quot;的只有两类：&lt;strong&gt;已删除&lt;/strong&gt;和&lt;strong&gt;需要登录才能看的盐选&lt;/strong&gt;。剩下 95% 以上都干净落盘了。剩下那些会在 &lt;code&gt;debug/&lt;/code&gt; 里留下现场 HTML，你 F12 一比就知道是 404 还是权限问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、光有 Markdown 不够&lt;/h2&gt;
&lt;p&gt;导出完成那一刻会有一种错觉：搞定了。&lt;/p&gt;
&lt;p&gt;其实没有。你得到的是&lt;strong&gt;一千个孤立的文件&lt;/strong&gt;，而不是知识。它们之间没有任何关系，你收藏过的&quot;什么是 Raft 协议&quot;和&quot;Paxos 论文精读&quot;之间可能存在很强的关联，但 Markdown 不会告诉你。&lt;/p&gt;
&lt;p&gt;Obsidian 的图谱视图为什么让人上瘾，就是因为它把这些隐式关系画出来了。但它需要你先手动插链接 —— 一千篇手动插？不可能。&lt;/p&gt;
&lt;p&gt;所以第二步是：&lt;strong&gt;让 AI 读一遍，生成关系。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我把这个导出工具以 &lt;strong&gt;MCP Server&lt;/strong&gt; 的形式挂给了 Hermes：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;zhihu-collections&quot;: {
      &quot;command&quot;: &quot;python&quot;,
      &quot;args&quot;: [&quot;mcp_server.py&quot;],
      &quot;cwd&quot;: &quot;你的项目路径&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;暴露出来四个工具：&lt;/p&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_collections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出所有收藏夹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_collection_info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;收藏夹基本信息（文章数等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_collections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按关键词搜收藏夹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export_collection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;导出指定收藏夹为 Markdown&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这一步的意义在于：Agent 拿到的不再是&quot;一堆待处理的文件&quot;，而是&quot;一个能按需调用的知识源&quot;。它可以边读边决定下一批导出哪几个收藏夹。&lt;/p&gt;
&lt;p&gt;导出完之后，让 Hermes 扫全量 Markdown，产出三样东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主题聚类&lt;/strong&gt; —— 把零散文章归到有限几个主题下&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实体与关系&lt;/strong&gt; —— 抽概念、建 &lt;code&gt;[[Obsidian 双链]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一张入口笔记&lt;/strong&gt; —— 每个主题一个 hub 笔记，链到具体文章&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;产出的知识图谱长这样（结构示意）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              ┌─────────────┐
              │ 分布式共识  │  ← hub 笔记
              └──────┬──────┘
       ┌─────────────┼─────────────┐
       ▼             ▼             ▼
  ┌────────┐   ┌──────────┐   ┌────────┐
  │ Raft   │◄─►│ Paxos    │◄─►│ 线性一致│
  └────┬───┘   └──────────┘   └────┬───┘
       │                            │
       ▼                            ▼
  ┌──────────┐              ┌────────────┐
  │ 领导选举  │─────────────►│ CAP 定理    │
  └──────────┘              └────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有个反直觉的点，值得单独说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI 生成的知识图谱质量，取决于它读到的是不是全文。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果导出的是摘要或者列表页文本，图谱就是&quot;标题聚类&quot;，毫无价值。正是因为工具把正文完整落地了，Agent 才能读懂内容、发现文章之间真正的语义关联。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;三、推上 GitHub：给知识加时间维度&lt;/h2&gt;
&lt;p&gt;最后一步是很多人会跳过的，也是我认为&lt;strong&gt;最值钱的&lt;/strong&gt;一步。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ZhihuExports
git init
git add .
git commit -m &quot;sync: 2026-10-08 知乎收藏快照&quot;
git push -u origin git@github.com:devycx/my-zhihu-library.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitHub 在这里承担三件事：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 版本化 = 知识快照的时间轴&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每条 commit 是一次快照。你能精确回答&quot;我什么时候读过 Raft 那篇论文&quot;&quot;三个月前我到底收藏了什么&quot;。知乎只会告诉你&quot;你收藏了 800 条&quot;，没有时间维度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 可回滚&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;知乎文章被作者删了，但你本地有。导出工具某次改坏了格式，重新 clone 一下就回来了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 二次加工的入口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;知识图谱是可再生的，但&lt;strong&gt;原始 Markdown 不是&lt;/strong&gt;。有了 GitHub 仓库，我随时可以换一个 Agent、换一套 schema 重新生成图谱，而不用重新抓一遍知乎 —— 知乎的反爬策略是会变的，下次可能就抓不动了。&lt;/p&gt;
&lt;h3&gt;仓库结构建议&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;my-zhihu-library/
├── exports/              # 原始导出，尽量不动
│   └── 2026-10-08/      # 按快照日期分目录
├── graph/                # AI 产出的知识图谱
│   ├── 分布式共识.md
│   └── index.md
├── README.md
└── .gitignore
    └── cookies.json      # 关键
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把原始数据和加工结果分开，是让图谱能被反复重做的前提。&lt;/p&gt;
&lt;h3&gt;几个安全上的注意点&lt;/h3&gt;
&lt;p&gt;:::warning
导出的 Markdown 里通常带作者名、原文链接，部分还有你自己加的批注。如果仓库设为 &lt;strong&gt;Public&lt;/strong&gt;，等于把个人知识库和阅读偏好公开了。我的做法是设成 &lt;strong&gt;Private&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;p&gt;:::tip
&lt;code&gt;cookies.json&lt;/code&gt; 一定要写进 &lt;code&gt;.gitignore&lt;/code&gt;。一次 &lt;code&gt;git add .&lt;/code&gt; 手滑，它就变成永久历史了 —— 删文件是不够的，要 &lt;code&gt;git filter-repo&lt;/code&gt; 或者干脆重建仓库。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、这套流程其实可以复用&lt;/h2&gt;
&lt;p&gt;抽象一下，这套东西跟&quot;知乎&quot;没有任何关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;任意内容源  →  结构化落地  →  LLM 抽取关系  →  版本化仓库
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;内容源换成 Notion 导出、微信读书书签、YouTube 播放列表、Pocket&lt;/li&gt;
&lt;li&gt;结构化落地换成任意导出脚本&lt;/li&gt;
&lt;li&gt;后两步一模一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正决定产出质量的是&lt;strong&gt;第一步落下来的东西够不够完整&lt;/strong&gt;。抓取的时候多花点心思，别让清洗的活儿留给后面的 AI。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;知乎没有官方导出，工具必须自己解决 &lt;strong&gt;HTML / API 双通道 + Cookie + 图片本地化 + 断点续传&lt;/strong&gt; 这四件事&lt;/li&gt;
&lt;li&gt;MCP Server 的意义是让 AI &lt;strong&gt;按需调用&lt;/strong&gt;知识源，而不是一次性喂一堆文件&lt;/li&gt;
&lt;li&gt;图谱的价值来自全文，不是标题 —— &lt;strong&gt;抓取质量决定图谱质量&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;推 GitHub 不只是备份，是给知识库&lt;strong&gt;装上时间轴&lt;/strong&gt;，并保留重新加工的自由&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;收藏的价值不在于存，在于&lt;strong&gt;还能被再次找到&lt;/strong&gt;。而&quot;再次找到&quot;这件事，本质上就是给散落的碎片补上关系。&lt;/p&gt;
</content:encoded></item></channel></rss>