<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Better’s study fairyland</title>
    <subtitle>Better（中文署名廖小新）的前端与 AI 博客：React、Vue、Node.js、npm、Chrome 插件与 AI Agent 工具链的长期记录，开源实测多附可复现命令，自 2016 年更新。</subtitle>
    <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/"/>
    <link rel="self" type="application/atom+xml" href="https://liaolongdong.github.io/better-blog/feed.xml"/>
    <id>https://liaolongdong.github.io/better-blog/</id>
    <updated>2026-11-03T14:52:20+08:00</updated>
    <author>
        <name>Better</name>
    </author>

    <entry>
        <title>RTK 压缩命令输出喂给 AI 编程助手：8 条命令实测，省了 token 也丢了什么</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/28/rtk-command-output-compression.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/28/rtk-command-output-compression.html</id>
        <updated>2026-10-28T00:00:00+08:00</updated>

        <summary>同一套过滤器，git status 省 65%，日志省 95%，Vitest 5 却只剩 passthrough 提示：省下的比例和还能用的信息不是一回事</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="AI编程" scheme="tag"/>

        <category term="工具" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="开源项目" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/28/rtk-command-output-compression.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/rtk-command-output-compression/banner.webp&quot; alt=&quot;RTK 把命令输出过滤后交给 AI 编程助手&quot; /&gt;&lt;/p&gt;

&lt;p&gt;让 AI 编程助手修一个失败的测试，它通常会先跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;，再翻日志。每一步的原始输出都会进上下文。一个 34 条用例的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:test&lt;/code&gt; 运行约 4.4KB，一份 120 条订单的 JSON 有 17.8KB，一段 168 行的日志有 13.7KB。模型真正需要的，往往只是三个失败用例名和一行超时错误。&lt;/p&gt;

&lt;p&gt;RTK 就卡在这条链路上。它是一个用 Rust 写的单二进制 CLI 代理，README 里列了 100 多条命令，支持 Claude Code、Codex、Cursor 等 18 个 AI 编程工具。它把它认识的 Bash 命令改写成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk&lt;/code&gt; 版本，按命令类型过滤、分组、截断、去重，再把压缩后的结果交给 agent。最新版本是 2026 年 10 月 25 日发布的 1.1.0。&lt;/p&gt;

&lt;p&gt;但省下的 token 和 agent 还能不能修好测试，不是同一个数。我在一个隔离的虚构项目里量了 8 条命令，下面把压缩率和信息损失放在一起看。&lt;/p&gt;

&lt;h2 id=&quot;它卡在-shell-和-agent-之间&quot;&gt;它卡在 shell 和 agent 之间&lt;/h2&gt;

&lt;p&gt;RTK 不是模型，也不是代码索引。它不理解业务逻辑，只根据命令类型决定怎么处理输出。README 把策略分成四类：过滤噪声、把同类结果分组、截断过长的上下文、合并重复日志。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt; 去掉提示语，只留分支和文件状态；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; 去掉部分上下文，保留改动；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt; 变成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk read&lt;/code&gt;；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log&lt;/code&gt; 只留错误和警告；测试输出只留失败项。&lt;/p&gt;

&lt;p&gt;这个位置决定了它能省哪一块。agent 自带的长输出处理通常按长度截断，不知道哪一行是失败用例。手动过滤要求每条命令都写成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status --short&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rg --max-count&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jq&lt;/code&gt; 的形式，脚本能维护，但 agent 不一定按你的写法执行。RTK 的假设是：命令类型本身包含了“该留什么”的信息。&lt;/p&gt;

&lt;p&gt;RTK 不回答代码在哪，它只处理命令输出。代码检索是另一层工具，后面再比。&lt;/p&gt;

&lt;h2 id=&quot;接进-agent-之前先看清-hook-管到哪&quot;&gt;接进 agent 之前，先看清 hook 管到哪&lt;/h2&gt;

&lt;p&gt;本文实测的是 OSS 版，许可证为 Apache-2.0。完整版多一个 BUSL-1.1 的压缩 engine；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk-proxy&lt;/code&gt; 是另一个 BUSL-1.1 的守护进程，不在这次测试范围内。安装可以走 release 包，也可以用 README 里的 brew、winget 或安装脚本。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# macOS x86_64，v1.1.0 的 OSS 包；Apple Silicon 换 aarch64&lt;/span&gt;
gh release download v1.1.0 &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; rtk-ai/rtk &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;rtk-oss-x86_64-apple-darwin.tar.gz&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;xzf rtk-oss-x86_64-apple-darwin.tar.gz
./rtk &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# rtk 1.1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;让 hook 生效靠 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk init&lt;/code&gt;。我用隔离的 HOME 跑过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk init -g --dry-run&lt;/code&gt;：它会创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude/RTK.md&lt;/code&gt;，往 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; 里加引用，并询问是否把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreToolUse&lt;/code&gt;、匹配 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bash&lt;/code&gt; 的 hook 写进 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settings.json&lt;/code&gt;。非交互模式下它默认不写这个文件。安装二进制和让 hook 生效是两步，别把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk --version&lt;/code&gt; 能跑当成 agent 已经在用。&lt;/p&gt;

&lt;p&gt;hook 的覆盖范围比名字看起来窄。它只处理 Bash 工具调用，agent 内建的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Read&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Grep&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Glob&lt;/code&gt; 不经过它。命令改写也不全：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk rewrite &quot;npm test&quot;&lt;/code&gt; 返回退出码 1，等于没有改写；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run test&lt;/code&gt; 才会被改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk npm run test&lt;/code&gt;；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat file&lt;/code&gt; 会被改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk read file&lt;/code&gt;，但不带压缩参数。装了 hook 不等于所有输出都会变小，具体能省哪一块，要看你常用的命令有没有被覆盖。&lt;/p&gt;

&lt;p&gt;本文只测 CLI 层，没有把 hook 写进本机的 Claude Code 配置。文中出现的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk&lt;/code&gt; 命令，就是 hook 生效后 agent 会看到的那一层输出；hook 改写规则通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk rewrite&lt;/code&gt; 和 dry-run 观察。&lt;/p&gt;

&lt;h2 id=&quot;8-条命令的实测从-0-到-99&quot;&gt;8 条命令的实测：从 0 到 99%&lt;/h2&gt;

&lt;p&gt;演示项目是虚构的：28 个 JavaScript 源文件，34 条 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node:test&lt;/code&gt; 用例（3 条失败），25 条 Vitest 用例（3 条失败），一份 120 条订单的 JSON，一段 168 行的应用日志。测试环境是 macOS 13.1、Intel x86_64、Node 24.22.0、RTK OSS 1.1.0、Vitest 5.0.5。token 数用 GPT-4o 分词器（o200k_base）计算，不是 RTK 面板里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bytes / 4&lt;/code&gt; 估算。&lt;/p&gt;

&lt;p&gt;下表只对比字节和 token，信息是否够用放在后面的小节里。第一列是 RTK 命令；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json&lt;/code&gt; 的原始命令是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vitest&lt;/code&gt; 那行的原始命令是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx vitest run&lt;/code&gt;，其余行的原始命令就是去掉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk&lt;/code&gt; 的版本。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;RTK 命令&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;原始 → RTK（字节 / token）&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk git status&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;522 / 147 → 154 / 51（-65.3%）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk git diff&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,965 / 642 → 1,463 / 463（-27.9%）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk read --max-lines 20&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1,741 / 487 → 486 / 129（-73.5%）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk log&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;13,716 / 4,984 → 604 / 238（-95.2%）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk json&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;17,857 / 6,049 → 159 / 55（-99.1%）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk npm test&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4,447 / 1,377 → 4,400 / 1,361（-1.2%）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk vitest run&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2,681 / 1,065 → 236 / 83（-92.2%*）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk grep&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;187 / 62 → 187 / 62（0%）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt; Vitest 这一行的 -92.2% 是把测试输出换成 passthrough 消息后的字节差，不算有效压缩，下面单独说。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; 那组只有三条匹配，压缩前后完全一样；小结果集不是它的战场。&lt;/p&gt;

&lt;h3 id=&quot;git-status-和-git-diff压掉提示留下名字&quot;&gt;git status 和 git diff：压掉提示，留下名字&lt;/h3&gt;

&lt;p&gt;这两条最接近“装了就能用”。原始 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt; 会把“On branch”“Changes not staged for commit”和提示语都带进来；RTK 只留一行分支和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;M&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;??&lt;/code&gt; 状态，文件数不多时，agent 仍能知道改了哪些文件。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; 去掉了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; 行和部分上下文，但每个文件的改动、hunk 头和新增删除统计都还在。&lt;/p&gt;

&lt;p&gt;代价是少了改动的完整上下文。对人来说，几行上下文有助于判断改动位置；对 agent 来说，只要 hunk 头在，通常还能定位。遇到跨文件重命名或大段移动时，仍需看原始 diff。&lt;/p&gt;

&lt;h3 id=&quot;readlogjson压得狠但只剩摘要&quot;&gt;read、log、json：压得狠，但只剩摘要&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk read&lt;/code&gt; 的默认行为仍是全文。同一个 1.7KB 文件，不加参数时输出 1,741 字节；加上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--max-lines 20&lt;/code&gt; 后变成 486 字节。留下的内容是这样（节选）：&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TAX_RATE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.09&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;57&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;more&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这适合让 agent 先摸清文件结构，不适合让它据此修改业务逻辑。函数名和签名还在，后面的函数体没了，agent 只能再读一次全文。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk log&lt;/code&gt; 也一样：它把 168 行日志压成错误清单和计数，8 条超时错误都在，160 条 info 被折叠。排错时很有用，追踪某个请求的完整路径时就不够。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk json&lt;/code&gt; 的压缩率最高，但它给出的是预览，不是 JSON。键没有引号，数组只留一个样例，尾部是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;... +119 more&lt;/code&gt;。把它交给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jq&lt;/code&gt; 会直接失败。这个输出适合让 agent 知道“有哪些字段、第一条长什么样”，不适合做数据计算。&lt;/p&gt;

&lt;h3 id=&quot;测试输出三种结果最需要逐条验收&quot;&gt;测试输出：三种结果，最需要逐条验收&lt;/h3&gt;

&lt;p&gt;测试是 agent 最需要精确信息的场景，也是这次测试里最不稳定的一块。直接跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk npm test&lt;/code&gt;，34 条用例的输出几乎原样保留，只省了 1.2%。换成通用过滤器 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk test npm test&lt;/code&gt;，输出变成最后 5 行加一个 recall 号：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;OUTPUT (last 5 lines):
      actual: 12,
      expected: 13,
      operator: &apos;strictEqual&apos;,
      diff: &apos;simple&apos;
    }
[full output: rtk recall &amp;lt;hash&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这时 4,447 字节的原始输出只剩 162 字节，但三个失败用例名都没了。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk recall &amp;lt;hash&amp;gt; --full&lt;/code&gt; 能把 4,446 字节的原输出取回来，代价是 agent 再花一次调用。省下的 token 在 recall 时又还了回去，除非 agent 只需要知道“测试挂了”，不需要知道哪三条。&lt;/p&gt;

&lt;p&gt;Vitest 这条更直接。用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk vitest run&lt;/code&gt; 跑 Vitest 5.0.5，agent 收到的是：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;JSON report written to &amp;lt;...&amp;gt;/.vitest/json/output.json
[RTK:PASSTHROUGH] vitest parser: All parsing tiers failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;原始 2,681 字节的测试输出没有出现，失败用例名也没有。RTK 把 JSON 报告写到了磁盘，agent 只看到路径，得自己决定去读。相比之下，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk npx vitest run&lt;/code&gt; 会原样传递 2,661 字节的输出，没有压缩，至少信息还在。&lt;/p&gt;

&lt;h2 id=&quot;压得越狠越要检查剩下的是什么&quot;&gt;压得越狠，越要检查剩下的是什么&lt;/h2&gt;

&lt;p&gt;RTK 自带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk gain&lt;/code&gt; 面板。把上面 8 条命令在干净 HOME 里各跑一次，面板显示 8 条命令，输入 10.2K token，输出 1.9K token，节省 8.3K，节省率 81.2%。其中 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json&lt;/code&gt; 99.1%、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log&lt;/code&gt; 95.6%、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read&lt;/code&gt; 72.0%、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt; 70.2%、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; 25.6%、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt; 1.1%、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; 0%。Vitest 那一条也被记成 43.0% 节省。&lt;/p&gt;

&lt;p&gt;这里有两个口径要分开看。第一，面板用的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bytes / 4&lt;/code&gt; 估算，不是真实分词器；我把 8 条命令的原始输出用 GPT-4o 分词器相加，得到 14,813 token，压缩后 2,442 token，整体节省 83.5%。方向一致，绝对值不同，单条命令的差距在几个百分点。第二，面板算的是“输出字节变少了多少”，不是“agent 还能不能完成任务”。Vitest 那条只回了两行 passthrough，实际测试信息全丢了，面板仍把它记成节省；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk gain -F&lt;/code&gt; 这次也没有记录解析失败。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk gain&lt;/code&gt; 适合看趋势和发现哪类命令在贡献节省，不适合单独当正确性证明。要判断能不能用，得把 agent 实际看到的输出拿出来，和原始输出逐行比：失败用例名在不在、错误行在不在、JSON 还能不能解析、函数体是不是被清空。&lt;/p&gt;

&lt;h2 id=&quot;和手动过滤mcp-代码检索比它该放在哪&quot;&gt;和手动过滤、MCP 代码检索比，它该放在哪&lt;/h2&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;原样交给 agent&lt;/td&gt;
      &lt;td&gt;靠上下文策略丢内容；不增加成本，先丢哪行不可控&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;手动过滤&lt;/td&gt;
      &lt;td&gt;你写规则，每条命令都要改；agent 不一定照做&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RTK&lt;/td&gt;
      &lt;td&gt;按命令类型过滤；覆盖不全、有损，解析失败可能不显眼&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MCP 代码检索&lt;/td&gt;
      &lt;td&gt;建索引回答“代码在哪”；不压命令输出&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;接入成本也不同：RTK 要装二进制并接 hook，MCP 检索要起 server、建索引，手动过滤最轻，但每条命令都要写。RTK 的独特之处是“透明”和“按命令类型”。它不要求 agent 改工作流，也不要求你为每条命令写正则；只要命令被覆盖，输出就会变短。代价也同样明显：覆盖有限，过滤器有损，hook 只看到 Bash，内建 Read/Grep 不经过它。&lt;/p&gt;

&lt;p&gt;MCP 代码检索工具解决另一层问题。像 code-review-graph 这类工具会把代码库建成可查询的图，让 agent 只在需要时读相关文件；RTK 则让 agent 读到的每条命令输出更短。两者组合是合理的：先用代码检索定位，再用 RTK 压 shell 输出。但如果你的 agent 主要靠内建 Read 和 Grep 工作，RTK 的收益会小很多，因为那些调用根本不进 hook。&lt;/p&gt;

&lt;h2 id=&quot;该不该用先让它在-git-和日志上证明自己&quot;&gt;该不该用：先让它在 git 和日志上证明自己&lt;/h2&gt;

&lt;p&gt;我的判断：RTK 值得试，但把它当“输出过滤器”，不要当“上下文管理器”。它的价值不在“最多省 90%”这个数字，而在“这条命令的输出里，哪些信息可以安全地丢”。如果是我用，git status 和 git diff 可以进日常，log 和 json 只在侦察时开，测试输出先验收再决定。&lt;/p&gt;

&lt;p&gt;如果你的 agent 每天在 Bash 里跑大量 git、构建、日志和 JSON 预览命令，又愿意保留 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk recall&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk bypass&lt;/code&gt; 作为退路，可以先上。先别急着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk init -g&lt;/code&gt;，在一个仓库副本上跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk git status&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk log&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk git diff&lt;/code&gt;，把输出和原始版本逐行比一遍，再用自己的分词器算一次 token。测试链路单独试：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk test npm test&lt;/code&gt; 能不能 recall 回失败用例，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtk vitest&lt;/code&gt; 在你的 Vitest 版本上是否解析成功。都过了，再把 hook 接进日常。&lt;/p&gt;

&lt;p&gt;反过来，如果 agent 主要用内建 Read/Grep/Glob，或者你必须把 JSON 原样交给下游程序、依赖完整函数体来改代码，RTK 要么帮不上，要么会把关键信息藏起来，装上去反而多一层排查成本。&lt;/p&gt;

&lt;p&gt;许可证也影响采用方式。OSS 版是 Apache-2.0；完整版多出的 engine 是 BUSL-1.1，生产使用免费，但不能把 engine 或衍生物作为收费、托管服务转卖。团队要把 RTK 包进对外产品，先看清这一层，不要只看“开源”两个字。&lt;/p&gt;

&lt;p&gt;RTK 的压缩率越高，越要检查它丢掉的是不是 agent 下一步要用的那一行。先让它在 git 和日志上跑通一遍，再决定要不要让它碰测试输出。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/rtk-ai/rtk&quot;&gt;https://github.com/rtk-ai/rtk&lt;/a&gt;&lt;br /&gt;
版本与许可证：&lt;a href=&quot;https://github.com/rtk-ai/rtk/releases/tag/v1.1.0&quot;&gt;v1.1.0 release&lt;/a&gt; · &lt;a href=&quot;https://github.com/rtk-ai/rtk#readme&quot;&gt;README&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>同一份代码，构建出 Chrome 和 Firefox 两套扩展：WXT 上手实测</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/27/wxt-cross-browser-extension.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/27/wxt-cross-browser-extension.html</id>
        <updated>2026-10-27T00:00:00+08:00</updated>

        <summary>入口文件生成 manifest、Shadow DOM 隔离徽章、storage 跨上下文共享，一个阅读时间扩展走完从初始化到打包</summary>


        <category term="前端工程化"/>

        <category term="开源实测"/>


        <category term="Chrome扩展" scheme="tag"/>

        <category term="浏览器扩展" scheme="tag"/>

        <category term="前端工程化" scheme="tag"/>

        <category term="开源项目" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/27/wxt-cross-browser-extension.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/wxt-cross-browser-extension/banner.webp&quot; alt=&quot;WXT 封面：一份代码入口，生成多个浏览器扩展产物&quot; /&gt;&lt;/p&gt;

&lt;p&gt;给内容站加一个“阅读时间”提示，页面里的代码可能不到一百行。把它做成浏览器扩展，麻烦才刚开始：manifest 里要声明 content script 的匹配范围、popup 的入口、background 的脚本；Chrome 侧是 MV3，WXT 给 Firefox 的默认产物还是 MV2；改一行样式，得去扩展管理页手动刷新；等你想同时发两个商店，又要把同一套逻辑复制成两份构建配置。&lt;/p&gt;

&lt;p&gt;WXT 想接住的就是这一层。它是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt-dev&lt;/code&gt; 维护的 MIT 开源框架，2023 年开始，最新版 0.21.5 发布于 2026-10-26。官方把它比作“扩展界的 Nuxt”：底层仍是 Vite，但用文件路径决定入口，再由入口生成 manifest。你继续写普通前端代码，扩展的样板交给它。&lt;/p&gt;

&lt;p&gt;这篇文章用一个能跑的“阅读时间”扩展，把 WXT 的入口、内容脚本 UI、storage 同步和跨浏览器构建串一遍；最后给出它和 extension.js、CRXJS、手写 Vite 方案的选择建议。实测只覆盖 Chrome 的加载与交互，Firefox、Safari 的边界会单独说明。&lt;/p&gt;

&lt;h2 id=&quot;它先替你收走了-manifest-和入口约定&quot;&gt;它先替你收走了 manifest 和入口约定&lt;/h2&gt;

&lt;p&gt;WXT 的核心不是一套新的扩展 API，而是一套构建约定。浏览器扩展的 API 仍然是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;browser.*&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chrome.*&lt;/code&gt;；WXT 负责把文件找出来、生成 manifest、打包，并在 dev 时把改动送进浏览器。它不替你做界面设计和权限决策，也不负责商店审核；这些仍然是产品层面的事。&lt;/p&gt;

&lt;p&gt;一个最小项目长这样：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;reading-time/
├── entrypoints/
│   ├── background.ts
│   ├── content.ts
│   └── popup/
│       └── index.html
├── utils/settings.ts
├── assets/content.css
└── wxt.config.ts
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;文件名决定入口类型。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content.ts&lt;/code&gt; 会成为 content script，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popup/index.html&lt;/code&gt; 成为点击图标打开的弹窗，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background.ts&lt;/code&gt; 在 Chrome MV3 里是 service worker，在 Firefox MV2 里是 background scripts。入口需要的 manifest 选项就近写在入口文件里：&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;defineContentScript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;matches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;http://127.0.0.1/*&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里故意只匹配 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1&lt;/code&gt;；如果你用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; 或自己的域名，记得改这一行。端口不进 match pattern，但 host 会。这行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;matches&lt;/code&gt; 会自动进入生成的 manifest，不需要再维护一份独立的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;manifest.json&lt;/code&gt;。popup 的标题、图标、默认弹窗也可以在 HTML 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; 里配置；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt.config.ts&lt;/code&gt; 则放全局的权限、名称、描述。入口和 manifest 在同一个地方，改动不容易两处漂移。&lt;/p&gt;

&lt;p&gt;除了入口发现，WXT 还自带几件常用工具：内容脚本的 Shadow DOM UI、带默认值和迁移能力的 storage 封装、i18n 与 analytics 模块，以及一个可以把构建产物打包成 ZIP、再提交到 Chrome/Edge/Firefox 商店的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt submit&lt;/code&gt;。这些能力不一定每个扩展都用得上，但它们决定了你不需要在每个新扩展里重写一遍同样的胶水。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/wxt-cross-browser-extension/01-pipeline.webp&quot; alt=&quot;WXT 构建流程：入口文件经 WXT + Vite 生成三套浏览器产物&quot; /&gt;&lt;/p&gt;

&lt;p&gt;代价是约定。入口必须放在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entrypoints/&lt;/code&gt; 下，而且只支持零层或一层深度；把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popup.ts&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popup.css&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popup.html&lt;/code&gt; 平铺在入口目录里，会被当成三个入口。这个限制不难适应，但你得先知道它存在。&lt;/p&gt;

&lt;h2 id=&quot;用一个阅读时间扩展走通全流程&quot;&gt;用一个阅读时间扩展走通全流程&lt;/h2&gt;

&lt;p&gt;目标很小：打开一篇文章时，右下角显示“约 X 分钟 · Y 字”；点扩展图标，可以关闭徽章、调整阅读速度。设置要在 popup 和页面之间共享，但不需要后端。&lt;/p&gt;

&lt;p&gt;“字”在这里是一个近似单位：中文按汉字计数，英文和数字按连续串计数，标点不计。徽章上写的“字”就是这套近似算法的单位。只统计 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;article&lt;/code&gt; 元素；如果没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;article&lt;/code&gt;，退回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;，再退回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt;。这样在有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;article&lt;/code&gt; 的页面上，导航、侧栏和页脚不会把阅读时间撑大。&lt;/p&gt;

&lt;h3 id=&quot;初始化与依赖&quot;&gt;初始化与依赖&lt;/h3&gt;

&lt;p&gt;WXT 要求 Node 22 以上。本文用的是 Node 24.22.0，初始化命令：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npx wxt@latest init reading-time &lt;span class=&quot;nt&quot;&gt;--template&lt;/span&gt; vanilla &lt;span class=&quot;nt&quot;&gt;--pm&lt;/span&gt; npm
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;reading-time
npm &lt;span class=&quot;nb&quot;&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; 会触发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt prepare&lt;/code&gt;，根据入口生成类型。实际测试时我在一个隔离目录里加了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--no-git&lt;/code&gt;，避免脚手架初始化一个新的 git 仓库；后面的构建和加载都基于这个目录。&lt;/p&gt;

&lt;h3 id=&quot;内容脚本计数徽章同步&quot;&gt;内容脚本：计数、徽章、同步&lt;/h3&gt;

&lt;p&gt;先定义共享的 storage 项：&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;storage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#imports&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;readingSpeed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;defineItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;local:readingSpeed&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showBadge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;defineItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;local:showBadge&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReadingStats&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lastStats&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;defineItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ReadingStats&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;local:lastStats&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage.defineItem&lt;/code&gt; 把 key、类型和默认值放在一起；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fallback&lt;/code&gt; 让第一次读取就有值，不需要自己判断 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;undefined&lt;/code&gt;。key 必须带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local:&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session:&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync:&lt;/code&gt; 或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;managed:&lt;/code&gt; 前缀，这是 WXT 对浏览器 storage API 的一层约束。这份 demo 用的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local:&lt;/code&gt;，只在本机浏览器内共享，不做跨设备同步。&lt;/p&gt;

&lt;p&gt;内容脚本负责统计和渲染：&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@/assets/content.css&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lastStats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;readingSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showBadge&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@/utils/settings&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countArticleText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;article&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;innerText&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cjk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;[\u&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;4e00-&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\u&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;9fff&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;words&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;[\u&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;4e00-&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\u&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;9fff&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;A-Za-z0-9&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cjk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;words&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;defineContentScript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;matches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;http://127.0.0.1/*&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;cssInjectionMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ui&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ui&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createShadowRootUi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;reading-time-badge&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;overlay&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;alignment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;bottom-right&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;anchor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;onMount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;badge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;badge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;className&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;badge&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;badge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showBadge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;badge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;display&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;badge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;display&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;inline-flex&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

          &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;speed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;readingSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;units&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countArticleText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;units&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;speed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;badge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;textContent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`约 &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; 分钟 · &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toLocaleString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;zh-CN&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; 字`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

          &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lastStats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;800&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;unwatchShow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showBadge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;unwatchSpeed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;readingSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onInvalidated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;unwatchShow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;unwatchSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里有三个 WXT 的便利点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defineContentScript&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createShadowRootUi&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt; 都是自动导入的，不需要手写 import；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run compile&lt;/code&gt; 能通过，说明类型生成正常。想显式导入也可以，从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#imports&lt;/code&gt; 取。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cssInjectionMode: &apos;ui&apos;&lt;/code&gt; 配合 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createShadowRootUi&lt;/code&gt;，徽章和样式都挂在 Shadow DOM 里。页面的全局 CSS 不会污染徽章，徽章也不会反过来影响页面。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctx.setTimeout&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctx.onInvalidated&lt;/code&gt; 绑定这次内容脚本的生命周期。扩展被禁用或更新时，定时器和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage.watch&lt;/code&gt; 会一起清掉；不清理时，这类回调容易在扩展上下文失效后继续跑。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一个和入口约定有关的构建细节：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content.css&lt;/code&gt; 不能直接放在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entrypoints/&lt;/code&gt; 根目录，否则 WXT 会把它识别成名为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content&lt;/code&gt; 的第二个入口，报“Multiple entrypoints with the same name”。把它放到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets/&lt;/code&gt; 再 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt; 进来，构建就正常了。&lt;/p&gt;

&lt;p&gt;popup 只做两件事：读同一份 storage，改同一份 storage。下面是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popup/main.ts&lt;/code&gt; 的核心片段，省略了 DOM 查询和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;renderStats&lt;/code&gt; 的定义：&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;showInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checked&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showBadge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;speedInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;readingSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;showInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showBadge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;speedInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;change&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;speed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speedInput&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;isFinite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;speed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;readingSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;speed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;lastStats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;renderStats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;内容脚本写入 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lastStats&lt;/code&gt;，popup 通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lastStats.watch&lt;/code&gt; 更新“最近一次统计”；popup 改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;readingSpeed&lt;/code&gt;，内容脚本通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;readingSpeed.watch&lt;/code&gt; 重新计算。两个上下文没有直接互相调用，只共享 storage。popup 的 HTML 只需两个控件（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#show&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#speed&lt;/code&gt;）和一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#stats&lt;/code&gt; 文本节点；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt.config.ts&lt;/code&gt; 给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;manifest.permissions&lt;/code&gt; 加 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt;；background 只在安装时设置 action title；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets/content.css&lt;/code&gt; 负责徽章的定位与配色。这些是常规扩展代码，没有逐行展开。&lt;/p&gt;

&lt;h3 id=&quot;构建与实测结果&quot;&gt;构建与实测结果&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run build          &lt;span class=&quot;c&quot;&gt;# chrome-mv3&lt;/span&gt;
npm run build:firefox  &lt;span class=&quot;c&quot;&gt;# firefox-mv2&lt;/span&gt;
npm run zip            &lt;span class=&quot;c&quot;&gt;# 生成可上传的 zip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Chrome 构建产物在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.output/chrome-mv3&lt;/code&gt;。实际生成的 manifest 里，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permissions&lt;/code&gt; 只有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storage&lt;/code&gt;；host 访问只来自 content script 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;matches&lt;/code&gt;，范围是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://127.0.0.1/*&lt;/code&gt;，没有声明额外的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;host_permissions&lt;/code&gt; 或网络权限。这是有意收窄的：真实扩展应该只匹配自己的站点，而不是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;我用 Playwright 以解包方式把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.output/chrome-mv3&lt;/code&gt; 加载进 Chrome for Testing 156.0.8078.47，测试页是一篇本地虚构的书店文章。普通版 Chrome 从 137 起不再接受 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--load-extension&lt;/code&gt;（&lt;a href=&quot;https://developer.chrome.com/blog/extension-news-june-2025&quot;&gt;Chrome 扩展新闻&lt;/a&gt;），所以没有用日常浏览器；这不影响扩展本身的构建，但做自动化验收时要准备 Chrome for Testing 或 Chromium。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/wxt-cross-browser-extension/02-article-badge.webp&quot; alt=&quot;实测：虚构文章页右下角的阅读时间徽章显示“约 4 分钟 · 947 字”&quot; /&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;947 个单位；徽章数字与页面内同一算法的复算一致（等价性检查，不是独立人工计数）；导航与侧栏未计入&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;默认 300 字/分钟&lt;/td&gt;
      &lt;td&gt;约 4 分钟&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;改成 200 字/分钟&lt;/td&gt;
      &lt;td&gt;约 5 分钟，弹窗与页面一起更新&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.badge&lt;/code&gt; 样式（背景红、字号 40px，均带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;!important&lt;/code&gt;）&lt;/td&gt;
      &lt;td&gt;徽章仍是深灰底、13px 字，Shadow DOM 隔离生效&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;非匹配 host（localhost）&lt;/td&gt;
      &lt;td&gt;页面正常打开，徽章数量为 0&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;947 这一项是用同一套统计函数在页面里复算的，验证的是徽章与函数一致，不是人工逐个数字符。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/wxt-cross-browser-extension/03-popup.webp&quot; alt=&quot;实测：扩展弹窗读取同一份 storage，显示最近一次统计并提供开关与速度选择&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这些数字只说明这个 demo 在本地虚构页面上按预期工作，不代表真实站点的性能或稳定性。demo 在页面加载后 800 毫秒补算一次，没有监听 SPA 路由变化，也没有用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MutationObserver&lt;/code&gt; 持续跟踪 DOM；如果文章是异步渲染的，或者站点是单页应用，还需要在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; 里加上路由监听或节流后的 DOM 观察。&lt;/p&gt;

&lt;h2 id=&quot;同一份代码三套-manifest&quot;&gt;同一份代码，三套 manifest&lt;/h2&gt;

&lt;p&gt;同一个项目继续跑：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run build:firefox
npx wxt build &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt; safari
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;会得到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.output/firefox-mv2&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.output/safari-mv2&lt;/code&gt;。同一个项目除了 Chrome 和 Firefox，还能再生成 Safari 目标。WXT 默认给 Chrome 生成 MV3，给 Firefox 和 Safari 生成 MV2；也可以用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--mv2&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--mv3&lt;/code&gt; 覆盖。Chrome 产物（节选）：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;manifest_version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;permissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;storage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;background&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;service_worker&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;background.js&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;default_popup&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;popup.html&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;content_scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;matches&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://127.0.0.1/*&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;js&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;content-scripts/content.js&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Firefox 产物（节选）：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;manifest_version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;permissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;storage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;background&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;background.js&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;browser_action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;default_popup&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;popup.html&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;content_scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;matches&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://127.0.0.1/*&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;js&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;content-scripts/content.js&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;不是把一份 manifest 复制三份，而是按目标改结构：MV3 的 background 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;service_worker&lt;/code&gt;，MV2 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts&lt;/code&gt;；MV3 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt; 在 MV2 里叫 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;browser_action&lt;/code&gt;；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;web_accessible_resources&lt;/code&gt; 的形态也不同。WXT 还提供了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import.meta.env.BROWSER&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import.meta.env.MANIFEST_VERSION&lt;/code&gt;，可以在代码里按目标分支。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run zip&lt;/code&gt; 会把 Chrome 产物打成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.output/reading-time-0.0.0-chrome.zip&lt;/code&gt;，这次是 25.93 kB、13 个文件。ZIP 不是“发布”本身，但省掉了手动挑文件、排除源码和映射的步骤。&lt;/p&gt;

&lt;p&gt;边界也要说清：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Firefox 只做了构建验证，没有运行。&lt;/strong&gt; 本机没有安装 Firefox；WXT 的构建日志还提示，新扩展从 2025-11-03 起需要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data_collection_permissions&lt;/code&gt;，MV3 还需要扩展 ID。真要在 Firefox 上发布，这些商店要求要单独处理。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Safari 也只做了构建验证。&lt;/strong&gt; WXT 能生成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;safari-mv2&lt;/code&gt;，但 Safari 扩展还需要 Xcode 的原生包装与签名，WXT 文档明确说不自动完成发布。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;dev 模式的 HMR 没有实测。&lt;/strong&gt; 官方文档把 HMR 和快速重载列为主要卖点，但这次只验证了生产构建和真实加载；如果你最在意的是 dev 体验，建议先在自己的项目里跑一遍 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt dev&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;复杂的 SPA 与跨域 iframe 没有覆盖。&lt;/strong&gt; demo 是一个静态文章页，不涉及路由切换、动态 DOM、iframe 注入。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;和-extensionjscrxjs手写方案怎么选&quot;&gt;和 extension.js、CRXJS、手写方案怎么选&lt;/h2&gt;

&lt;p&gt;把问题限定在“我要做一个浏览器扩展，前端栈是 Vite/TypeScript”，可以这样比较：&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;a href=&quot;https://github.com/wxt-dev/wxt&quot;&gt;WXT&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;框架 + Vite；生成 Chrome、Firefox、Safari 目标；文件入口、自动 manifest、内置 storage/UI。适合想用约定减少样板的前端团队。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/extension-js/extension.js&quot;&gt;extension.js&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;零配置框架；支持 Chrome、Edge、Firefox、Safari；CLI + 托管浏览器二进制。适合想一条命令跑起来、少碰构建配置的人。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/crxjs/chrome-extension-tools&quot;&gt;CRXJS&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;Vite 插件；面向 Chromium（MV3）；在现有 Vite 配置上加插件。适合已有 Vite 应用、只想加扩展入口的人。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;手写 Vite + MV3&lt;/td&gt;
      &lt;td&gt;自建；自己写 manifest 和构建脚本，平台取决于实现。适合构建链特殊、需要完全控制的人。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;本轮验证情况：WXT 在 Chrome for Testing 156 里加载并测试；Firefox 和 Safari 只构建、没运行；extension.js 和 CRXJS 没有运行。表格里的能力来自各自 README 和 npm 元数据。&lt;/p&gt;

&lt;p&gt;WXT 的取舍是约定换样板，extension.js 是零配置换控制权，CRXJS 是插件换更小的接入面：它不是完整框架，入口约定和 storage 封装不像 WXT 那样属于框架能力。extension.js 的 npm 最新版是 4.1.35（2026-10-22），CRXJS 是 3.0.0（2026-09-24），两者都在更新。&lt;/p&gt;

&lt;p&gt;如果你正在看旧教程，&lt;a href=&quot;https://www.npmjs.com/package/plasmo&quot;&gt;Plasmo&lt;/a&gt; 值得先确认一下时间：它的 npm 最新版 0.90.5 发布于 2025-05-17。这个事实本身不等于项目不能用，但意味着新项目要自己承担后续 Vite 和浏览器 API 的兼容风险。&lt;/p&gt;

&lt;h2 id=&quot;现在该不该用&quot;&gt;现在该不该用&lt;/h2&gt;

&lt;p&gt;WXT 现在仍是 0.x。0.21.5 发布于 2026-10-26，升级前应该看官方的 &lt;a href=&quot;https://wxt.dev/guide/resources/upgrading.html&quot;&gt;Upgrading 文档&lt;/a&gt;；它不是那种“装上去三年不用管”的稳定层。把它放进项目之前，至少要接受两点：入口要遵守 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entrypoints/&lt;/code&gt; 的约定；扩展的构建输出和 manifest 由框架生成，遇到特殊需求时要确认能不能在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wxt.config.ts&lt;/code&gt; 或 hook 里表达。&lt;/p&gt;

&lt;p&gt;demo 本身只做到最小验证：统计逻辑只认 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;article&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;，并且只在加载后补算一次。真实项目里，内容脚本要考虑异步内容、路由切换、页面隐藏时的清理，以及更细的权限范围。WXT 提供的是生命周期工具和构建约定，不会替你决定这些产品行为。&lt;/p&gt;

&lt;p&gt;我的建议分三种情况：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;你已经在维护一个跨 Chrome/Firefox 的扩展，入口不止一个，每次改 manifest 都要对照文档：WXT 值得用一个下午做一次迁移试验，先量一下入口发现、跨浏览器构建和 storage 封装到底省了多少。&lt;/li&gt;
  &lt;li&gt;你只是给现有 Vite 应用加一个小 popup，路由和权限都简单：CRXJS 或手写 Vite 插件更轻，不必为了一个入口引入整套框架。&lt;/li&gt;
  &lt;li&gt;你想要零配置、托管浏览器二进制、少碰构建链：extension.js 值得对比；它的路线和 WXT 不完全相同，最好用同一个最小扩展各做一遍。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WXT 的价值不是让扩展开发变得神奇，而是把重复的构建工作收进一个可读的约定。先用最小扩展把入口、manifest、内容脚本 UI 和 storage 跑通，再决定要不要把主扩展交给它。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/wxt-dev/wxt&quot;&gt;wxt-dev/wxt&lt;/a&gt;；文档：&lt;a href=&quot;https://wxt.dev/guide/installation.html&quot;&gt;wxt.dev&lt;/a&gt;。&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>README 四条示例我逐字跑了：两条成立，两条不成立</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/27/sandbox-runtime-ssh-readable.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/27/sandbox-runtime-ssh-readable.html</id>
        <updated>2026-10-27T00:00:00+08:00</updated>

        <summary>srt 默认读全放、写全拦，~/.ssh/id_rsa 在沙箱里 open() 得动</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="AI" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="Node" scheme="tag"/>

        <category term="前端" scheme="tag"/>

        <category term="安全" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/27/sandbox-runtime-ssh-readable.html">&lt;h1 id=&quot;readme-四条示例我逐字跑了两条成立两条不成立&quot;&gt;README 四条示例我逐字跑了：两条成立，两条不成立&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;一句话结论：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srt&lt;/code&gt; 装完直接跑，README 顶部四条示例有两条不成立，读侧默认比示例宽得多。
现在该做什么：把它当成默认只锁写的沙箱，凭据目录自己写进 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denyRead&lt;/code&gt; 再验一次。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;curl-anthropiccom-在沙箱里拿到-403&quot;&gt;curl anthropic.com 在沙箱里拿到 403&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm i @anthropic-ai/sandbox-runtime&lt;/code&gt; 十六秒装完。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx srt --version&lt;/code&gt; 给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.81&lt;/code&gt;，本机没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.srt-settings.json&lt;/code&gt;，走内置默认。然后照 README 第 19 行那个代码块逐字跑。&lt;/p&gt;

&lt;p&gt;成立的两条：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl example.com&lt;/code&gt; 确实被拦，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat README.md&lt;/code&gt; 确实能读当前目录。&lt;/p&gt;

&lt;p&gt;不成立的两条：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;沙箱内 curl anthropic.com (GET) → 403 Connection blocked by…
沙箱外 同一条命令(对照)         → 301
沙箱内 cat ~/.ssh/id_rsa        → READ-OK
沙箱外 同一路径(对照)           → READ-OK
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;README 写的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;html&amp;gt;...&amp;lt;/html&amp;gt; # Request succeeds&lt;/code&gt;，另一条写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Operation not permitted&lt;/code&gt;。前者默认配置下拿不到，后者要你自己写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denyRead: [&quot;~/.ssh&quot;]&lt;/code&gt; 才会出现。&lt;/p&gt;

&lt;h2 id=&quot;11-项探针的分布读放行写全拒&quot;&gt;11 项探针的分布：读放行、写全拒&lt;/h2&gt;

&lt;p&gt;403 只是网络侧。文件侧我做了 11 项探针，分布很干净。&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;读 cwd 文件&lt;/td&gt;
      &lt;td&gt;放行&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$HOME&lt;/code&gt; 自建文件&lt;/td&gt;
      &lt;td&gt;放行&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh/id_rsa&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;放行&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/hosts&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;放行&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;写 cwd 文件&lt;/td&gt;
      &lt;td&gt;被拒&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;被拒&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cwd 建子目录&lt;/td&gt;
      &lt;td&gt;被拒&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这不是我推出来的规律，是它自己写的。README 第 41 行同一屏里，先说 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;processes start with minimal access&lt;/code&gt;，紧接着的能力清单又说读是 deny-then-allow、默认到处可读，写是 allow-only、默认处处拒绝。&lt;/p&gt;

&lt;p&gt;两句话不矛盾。但印象是相反的。默认锁的是外联和落盘，不是你能看见什么。&lt;/p&gt;

&lt;h2 id=&quot;域名解析被切ip-字面量也走同一条判决&quot;&gt;域名解析被切，IP 字面量也走同一条判决&lt;/h2&gt;

&lt;p&gt;上一节的 403 是代理回的。网络这块才是我认为它做得硬的地方。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;沙箱内 gethostbyname(&apos;anthropic.com&apos;) → socket.gaierror [Errno 8]
沙箱外 同一条                          → 160.79.104.10
沙箱内 curl https://1.1.1.1/           → 403 host is not on the allow list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;解析直接被切，进程只能把连接交给它那两个代理，代理再按 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allowedDomains&lt;/code&gt; 判。IP 字面量走同一条判决，没因为不是域名就绕过去。&lt;/p&gt;

&lt;p&gt;沙箱内 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;env&lt;/code&gt; 能看到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http_proxy&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NO_PROXY&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GIT_CONFIG_PARAMETERS&lt;/code&gt; 这些注入项（我只打变量名）。这解释了为什么 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; 在沙箱里被自然接管，不需要包装命令。&lt;/p&gt;

&lt;h2 id=&quot;我的-test--r-和-curl--si-都判错过东西&quot;&gt;我的 test -r 和 curl -sI 都判错过东西&lt;/h2&gt;

&lt;p&gt;11 项探针里有两项是我自己测错的。&lt;/p&gt;

&lt;p&gt;第一项：为了省时间，我把 README 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl anthropic.com&lt;/code&gt; 写成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl -sI&lt;/code&gt;。HEAD 与 GET 响应不同，我差点把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;403 Forbidden&lt;/code&gt; 当成代理的判决。逐字改回 GET、再在沙箱外跑同一条，才看清沙箱外是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;301&lt;/code&gt;、沙箱内那个 403 的正文写着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Connection blocked by network allowlist&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第二项更要紧。我用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test -r ~/.ssh/id_rsa&lt;/code&gt; 判可读性，它返回可读——可 Seatbelt 是在真正 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;open()&lt;/code&gt; 时才拒绝，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test -r&lt;/code&gt; 查的是权限位，量的是另一件事。换成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt; 到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/null&lt;/code&gt;（内容丢弃）之后结论才站住：默认配置下这文件就是读得到。&lt;/p&gt;

&lt;p&gt;两处都不是被测仓库的问题，是我的判据不对。测沙箱，量具错了会得出方向相反的结论。&lt;/p&gt;

&lt;h2 id=&quot;1288-行-readme-与第-85-行的-denyread&quot;&gt;1,288 行 README 与第 85 行的 denyRead&lt;/h2&gt;

&lt;p&gt;这个包的 README 有 156,873 字节。&lt;/p&gt;

&lt;p&gt;1,288 行，第 9 行挂着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;research preview&lt;/code&gt; 横幅。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denyRead&lt;/code&gt; 第一次出现在第 85 行，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.srt-settings.json&lt;/code&gt; 在第 80 行，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;## Configuration&lt;/code&gt; 一节要到第 407 行。&lt;/p&gt;

&lt;p&gt;顶部那段 Basic Usage 是「配好之后长这样」，但它没在那一段说这件事。一个从上往下敲的人会以为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh&lt;/code&gt; 默认就被保护了。我第一轮就是这么以为的。&lt;/p&gt;

&lt;p&gt;公平地说它没说谎。能力清单那两句把默认模型写得很准。问题只有顺序：示例在前，口径在后。&lt;/p&gt;

&lt;h2 id=&quot;real-path-尾斜杠那条-pr-我下次先看&quot;&gt;real-path 尾斜杠那条 PR 我下次先看&lt;/h2&gt;

&lt;p&gt;三件事会让我回头重测。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denyRead&lt;/code&gt; 的默认在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v0.0.81&lt;/code&gt; 之后收进 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh&lt;/code&gt;，我就重跑这 11 项探针。&lt;/li&gt;
  &lt;li&gt;如果 Basic Usage 补一句「示例假设已有配置」，文档顺序这条就消失。&lt;/li&gt;
  &lt;li&gt;如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropics/sandbox-runtime#775&lt;/code&gt; 合了，我重测路径规范化的边界。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;符号链接那条路我没验到：测它的时候连 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ln -sf&lt;/code&gt; 都被写策略拒了。&lt;/p&gt;

&lt;p&gt;没跑到的写清楚。只测 macOS 默认配置，Linux 与 Windows 没测——包里注释自己承认 Linux 不能像 macOS 那样在内核按域名强制。配好 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denyRead&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allowedDomains&lt;/code&gt; 之后的矩阵没做，那测的是我的配置、不是它的默认。LD_PRELOAD、ptrace 这类逃逸我没试。&lt;/p&gt;

&lt;p&gt;它是给 agent 用的研究预览。不是通用 OS 沙箱的替代品。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/anthropics/sandbox-runtime&quot;&gt;https://github.com/anthropics/sandbox-runtime&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>Kimi K3.1 Pro 分成两档卖：贵一倍的钱，在什么负载下花不到两倍</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/25/kimi-k3-1-pro-two-tiers.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/25/kimi-k3-1-pro-two-tiers.html</id>
        <updated>2026-10-25T00:00:00+08:00</updated>

        <summary>标准档与 Highspeed 的差价覆盖输入、输出和缓存写入，唯独缓存命中那一档两价相同</summary>


        <category term="AI"/>


        <category term="AI" scheme="tag"/>

        <category term="Agent" scheme="tag"/>

        <category term="评测" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="性能优化" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/25/kimi-k3-1-pro-two-tiers.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/kimi-k3-1-pro-two-tiers/banner.webp&quot; alt=&quot;封面：同一条输出纸带从左侧分叉成上下两条轨道，稀疏的一条挂着 1× 价签，密集带速度线的一条挂着 2× 价签&quot; /&gt;&lt;/p&gt;

&lt;p&gt;给产品接一个会写长答案的模型，最疼的往往不是它答得对不对，而是两件事：一个字一个字往外吐的时候用户在不在等，以及月底那张按 token 算出来的账单。&lt;/p&gt;

&lt;p&gt;10 月 24 日凌晨（北京时间），月之暗面发布 &lt;strong&gt;Kimi K3.1 Pro&lt;/strong&gt;，同时给出一个高速档 &lt;strong&gt;K3.1 Pro Highspeed&lt;/strong&gt;。官方的说法是两者智能相同，高速档输出速度约 2~3 倍；价格是常规输入、输出、缓存写入全部翻倍，但缓存命中价一分没涨。&lt;/p&gt;

&lt;p&gt;这篇讲三件会被“更强了、降价了”盖过去的事：两档的差价实际长什么样；单价降了三成为什么独立评测算出的每任务成本几乎没降；以及“11 月 4 日前开放权重”这句话里，哪部分还没到手。&lt;/p&gt;

&lt;p&gt;先说清我的取材边界，后面的判断才好称重：&lt;strong&gt;我没有跑通任何真实调用。&lt;/strong&gt; Kimi 开放平台的快速开始文档写明要先充值（最低 10 元）才解锁调用，本轮不擅自消费，所以文中所有金额都是按公开费率的推演，第三方读数都注明出处。&lt;/p&gt;

&lt;h2 id=&quot;现在能调什么还要等什么&quot;&gt;现在能调什么，还要等什么&lt;/h2&gt;

&lt;p&gt;技术博客是英文的，介绍第一款模型的原话是 “We’re introducing Kimi K3.1 Pro, the first model in our new K3.1 series and our most capable 3T-class open-weight model to date, with a 1-million-token context window”，紧接着 “We are also releasing Kimi K3.1 Pro Highspeed, which delivers 2-3× the speed of K3.1 Pro”。（译文：K3.1 系列的第一个模型，迄今最强的 3T 级开放权重模型，支持 100 万 token 上下文；同时放出 Highspeed，速度是标准版的 2~3 倍。）&lt;/p&gt;

&lt;p&gt;同一篇里还有一句更关键的说明：K3.1 Pro 是 &lt;strong&gt;Kimi K3 的后训练版本&lt;/strong&gt;，用了多得多的后训练算力。也就是说，这不是一次新的预训练，而是在已经开源的 K3（国内平台模型页标为“全球首个开源的 3 万亿级模型，2.8 万亿参数”）之上，用更大规模的强化学习再推一层。Artificial Analysis 给它的规格读数是 2.8T 总参数、104B 激活参数——官方口径只到“3 万亿级”，精确数字目前只有第三方给出。&lt;/p&gt;

&lt;p&gt;把“已到手 / 还没到手”摊开：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;已经能用&lt;/strong&gt;：Kimi 网页版与移动端、Kimi Work、Kimi Code，以及 API 上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kimi-k3.1-pro&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kimi-k3.1-pro-highspeed&lt;/code&gt;。API 兼容 OpenAI 格式，国内端点是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://api.moonshot.cn/v1&lt;/code&gt;，两款模型的上下文上限都写作 1,048,576 token。官方定价页还提到 API 客户可选 Zero Data Retention，即按约定不保留请求数据。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;企业环境&lt;/strong&gt;：需要管理员主动开启，发布时默认关闭。如果你的公司准备接入，先去后台确认开关，不要假设别的组织已经打开。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;还没到手&lt;/strong&gt;：完整权重。原文两处写“将于 11 月 4 日前发布”，这是路线图书诺，不是今天的交付物；许可证条款没有出现在我读过的任何一份官方材料里。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“全球最大开源模型上新”这类标题把两件事压成了一件事：今天上新的是模型和它的付费入口，开源那半边还压着一个日期。&lt;/p&gt;

&lt;h2 id=&quot;两档的差价落在哪一类-token-上&quot;&gt;两档的差价，落在哪一类 token 上&lt;/h2&gt;

&lt;p&gt;对打算接 API 的人来说，这次最能马上用上的部分是计费规则，因为官方把公式写明了。一行输入 token 只会属于下面四类之一，各按自己的费率计，最后加上输出：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;未缓存的常规输入，按输入费率；&lt;/li&gt;
  &lt;li&gt;命中已有缓存的前缀，按缓存命中费率；&lt;/li&gt;
  &lt;li&gt;第一次写进缓存的前缀，按缓存写入费率，分 5 分钟和 1 小时两种 TTL（缓存存活时间，过期就要重新写入）；&lt;/li&gt;
  &lt;li&gt;输出 token，按输出费率，&lt;strong&gt;包含模型思考用的 token&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;两档的费率表（每百万 token，美元，税前，官方定价页核对日期标注为 2026-10-24）：&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;5 分钟写入&lt;/th&gt;
      &lt;th&gt;1 小时写入&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;kimi-k3.1-pro&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;$2.00&lt;/td&gt;
      &lt;td&gt;$0.20&lt;/td&gt;
      &lt;td&gt;$2.00&lt;/td&gt;
      &lt;td&gt;$4.00&lt;/td&gt;
      &lt;td&gt;$10.00&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kimi-k3.1-pro-highspeed&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;$4.00&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$0.20&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;$4.00&lt;/td&gt;
      &lt;td&gt;$8.00&lt;/td&gt;
      &lt;td&gt;$20.00&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;官方中文定价页对这一点的解释比表格直白：“Highspeed 的常规输入、输出和缓存写入标价均为标准版的两倍。其缓存输入费率仍为每百万 token $0.20，与标准模型相同。因此，包含大量缓存上下文的请求需要单独计算成本，不能简单地将标准模型账单翻倍。”&lt;/p&gt;

&lt;p&gt;国内开放平台首页给的是人民币一套：标准档输入 ¥13.00、输出 ¥68.00、缓存写入 ¥13.00、命中 ¥1.30；高速档 ¥26.00 / ¥136.00 / ¥26.00，命中同样是 ¥1.30；上一代 K3 是 ¥20.00 / ¥100.00 / ¥20.00 / ¥2.00。倍数关系和美元那套一致。&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;th&gt;输出 token&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;A 编码 agent 首轮&lt;/td&gt;
      &lt;td&gt;4K 新输入 + 36K 缓存写入（5 分钟档）&lt;/td&gt;
      &lt;td&gt;6K&lt;/td&gt;
      &lt;td&gt;$0.1400&lt;/td&gt;
      &lt;td&gt;$0.2800&lt;/td&gt;
      &lt;td&gt;2.00×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;B 同一会话后续轮&lt;/td&gt;
      &lt;td&gt;36K 命中 + 4K 新输入&lt;/td&gt;
      &lt;td&gt;6K&lt;/td&gt;
      &lt;td&gt;$0.0752&lt;/td&gt;
      &lt;td&gt;$0.1432&lt;/td&gt;
      &lt;td&gt;1.90×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;C 长文档问答&lt;/td&gt;
      &lt;td&gt;100K 命中 + 2K 新输入&lt;/td&gt;
      &lt;td&gt;500&lt;/td&gt;
      &lt;td&gt;$0.0290&lt;/td&gt;
      &lt;td&gt;$0.0380&lt;/td&gt;
      &lt;td&gt;1.31×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;D 重输出任务&lt;/td&gt;
      &lt;td&gt;8K 命中 + 8K 新输入&lt;/td&gt;
      &lt;td&gt;24K&lt;/td&gt;
      &lt;td&gt;$0.2576&lt;/td&gt;
      &lt;td&gt;$0.5136&lt;/td&gt;
      &lt;td&gt;1.99×&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;规律是：&lt;strong&gt;溢价跟着输出占比走。&lt;/strong&gt; 输出是整倍差价里最大的一项，所以任务越像“写很多字”，高速档越接近两倍价；反过来，把长前缀缓存好、只让它回几百个 token，溢价能被压到 1.3 倍附近。极端情况下——所有 token 都命中缓存、没有输出计费——两档价会完全相同，只是那种请求本身不存在。首轮是另一头：缓存写入按 5 分钟档也和常规输入同价（$2.00 / $4.00），所以首轮是干净的两倍。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/kimi-k3-1-pro-two-tiers/01-request-cost-anatomy.webp&quot; alt=&quot;示意图：首轮请求的三段 token 全部按两倍价计，后续轮只有未缓存输入和输出两段翻倍，缓存命中段两档同价&quot; /&gt;&lt;/p&gt;

&lt;p&gt;把 Claude 两档放进来对照更直观。标价出自 Anthropic 定价页（我 2026-10-25 查阅，不是下面 Artificial Analysis 那张表的换算）：Claude Sonnet 5.5 输入 $2 / 输出 $10 / 缓存读 $0.10，Claude Opus 5.5 输入 $4 / 输出 $20 / 缓存读 $0.20。对得上的是：&lt;strong&gt;标准档 K3.1 Pro 的账单结构贴着 Sonnet 5.5，高速档的账单结构则和 Opus 5.5 重合&lt;/strong&gt;——场景 B 里高速档和 Opus 5.5 都是 $0.1432，场景 D 里都是 $0.5136。买高速档花的钱，在 Claude 那边对应的是旗舰档价目。&lt;/p&gt;

&lt;h2 id=&quot;单价降了三分之一每个任务的账却没怎么变&quot;&gt;单价降了三分之一，每个任务的账却没怎么变&lt;/h2&gt;

&lt;p&gt;厂商给的三项单价都比上一代低：K3 是 $3.00 输入、$15.00 输出、$0.30 缓存命中，K3.1 Pro 统一低三分之一。但 Artificial Analysis 在 10 月 23 日的发布评测里写的是“每个任务的成本与 K3 大致相同，约 $1.98”，并给出一个原因：K3.1 Pro 每个智能指数任务用掉 55K 输出 token，比 K3 的 48K 多 14%。&lt;/p&gt;

&lt;p&gt;这里我要指出一个算不平的地方，因为它比“降价”两个字更值得注意。只看输出那一侧：单价乘以 0.67，用量乘以 1.14，输出成本应该低四分之一左右。要让整个任务的账单“大致相同”，持平只能发生在输入和缓存那一侧，而 Artificial Analysis 没有拆开讲，也没给 K3 的每任务读数。到 10 月 25 日我查它的 leaderboard 时，同一个“每任务成本”显示为 &lt;strong&gt;$2.11&lt;/strong&gt;——和 $1.98 是两个不同时间、不同加权口径的读数，不该当成同一个数用。&lt;/p&gt;

&lt;p&gt;结论不是“这次没降价”，而是：&lt;strong&gt;每任务成本是个需要你自己按业务量测的东西，公告的单价和评测站的每任务成本都不能直接替你算。&lt;/strong&gt; 模型这边能推动账单的两个因素都写在官方规则里：它是个话痨——在整个智能指数评测里生成了 150M 输出 token，所有模型中位数是 87M，被 Artificial Analysis 标为“极其冗长”（这一单项它排 92/116，即 116 个模型里第 92 名，越靠后越冗长）；而且思考关不掉，只能调 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reasoning_effort&lt;/code&gt;（low / high / max，默认 max）。所以真要省钱，第一个该动的旋钮是 effort 和任务拆分，而不是等下一版降价。&lt;/p&gt;

&lt;h2 id=&quot;第-4-名的智能141-名的出字速度&quot;&gt;第 4 名的智能，141 名的出字速度&lt;/h2&gt;

&lt;p&gt;厂商的 benchmark 表很长，我挑三个跟决策有关的，然后立刻换成第三方口径，因为两套数并不完全对得上。&lt;/p&gt;

&lt;p&gt;官方自测：Terminal-Bench 4.0 得 65.7%，同一行注明 GPT-6 Astra 58.2%、Claude Opus 5.5 66.4%；内部长程编程基准 Kimi Code Bench 3.0 Long-horizon 从 K3 的 55.95% 提到 61.2%；BenchCAD 带工具 92.7%（Opus 5.5 为 96.2%）。还有一个内部基准 Kimi Hallu Bench Hard，claim-level 幻觉率从 K3 的 23.4% 降到 8.3%——官方紧跟了一句免责：该评测是事实准确性的压力测试，所报告的比率不代表日常使用中的整体错误率。&lt;/p&gt;

&lt;p&gt;Artificial Analysis 在 10 月 23 日的发布评测里，同一个 Terminal-Bench 4.0 给出 67%（它自己的 harness，并称这一项是全场最高），而 K3 在这一项只有 13%。&lt;strong&gt;同一个基准名字，两套数字，跑法不同&lt;/strong&gt;：官方那张表里至少一部分对手的分数，是直接取自 OpenAI、Anthropic、z.ai 的公布页。所以我读公告分数时会先确认是谁跑的、用什么环境跑的。&lt;/p&gt;

&lt;p&gt;同一家 leaderboard 把几个决定性维度放在一张表里（我 2026-10-25 的整表读数；这几行的上下文窗口都是 1M，所以省掉那一列）：&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;中位 tok/s&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;Claude Opus 5.5（max）&lt;/td&gt;
      &lt;td&gt;58&lt;/td&gt;
      &lt;td&gt;$5.98&lt;/td&gt;
      &lt;td&gt;96&lt;/td&gt;
      &lt;td&gt;652.61s&lt;/td&gt;
      &lt;td&gt;657.81s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Claude Sonnet 5.5（max）&lt;/td&gt;
      &lt;td&gt;56&lt;/td&gt;
      &lt;td&gt;$5.46&lt;/td&gt;
      &lt;td&gt;134&lt;/td&gt;
      &lt;td&gt;371.62s&lt;/td&gt;
      &lt;td&gt;375.34s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Claude Opus 5.5（xhigh）&lt;/td&gt;
      &lt;td&gt;56&lt;/td&gt;
      &lt;td&gt;$3.46&lt;/td&gt;
      &lt;td&gt;82&lt;/td&gt;
      &lt;td&gt;109.62s&lt;/td&gt;
      &lt;td&gt;115.71s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Kimi K3.1 Pro&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;55&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;$2.11&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;58&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;3.07s&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;46.53s&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GPT-6 Astra（max）&lt;/td&gt;
      &lt;td&gt;53&lt;/td&gt;
      &lt;td&gt;$3.26&lt;/td&gt;
      &lt;td&gt;66&lt;/td&gt;
      &lt;td&gt;278.25s&lt;/td&gt;
      &lt;td&gt;285.78s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Gemini 4 Argon（high）&lt;/td&gt;
      &lt;td&gt;53&lt;/td&gt;
      &lt;td&gt;$1.99&lt;/td&gt;
      &lt;td&gt;81&lt;/td&gt;
      &lt;td&gt;57.01s&lt;/td&gt;
      &lt;td&gt;63.20s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GLM-5.3（max）&lt;/td&gt;
      &lt;td&gt;45&lt;/td&gt;
      &lt;td&gt;$2.01&lt;/td&gt;
      &lt;td&gt;98&lt;/td&gt;
      &lt;td&gt;2.39s&lt;/td&gt;
      &lt;td&gt;27.86s&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这张表要先读三件事，否则会歪。各家是按不同推理强度拆成多行的，K3.1 Pro 这一行是它的推理档（该站模型页写明这是 reasoning 版本）；“每任务成本”是它按自己那套智能指数任务加权算出来的钱，跟你业务的 token 结构无关；“中位 tok/s”是输出流速度，和“首块延迟”“总响应”是三个不同的东西——表里那行 K3.1 Pro 的 58，与它模型页上的 57.5 是同一次测量的两种舍入。&lt;/p&gt;

&lt;p&gt;在这三个维度上，K3.1 Pro 的位置很别扭也很好用：&lt;strong&gt;智能 55，这张表里排第四；每任务成本 $2.11，是智能 55 分及以上那几行里最低的（$1.99 的 Gemini 4 Argon 智能低两分，属于下一档）；中位输出速度 58 tok/s，在它的模型页上排 141/186，被标为“明显偏慢”。&lt;/strong&gt; 首块延迟 3.07 秒，是这张表里 53 分以上模型最低的一个——Anthropic 那两个 max 行的首块要等上几分钟；唯一比它更快的首块来自 45 分的 GLM-5.3（2.39 秒）。&lt;/p&gt;

&lt;p&gt;这组数恰好解释了高速档存在的理由，也解释了它的边界。把“快”拆成三个可买的量：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;什么时候开始出字&lt;/strong&gt;（首块延迟）：3.07 秒，这一档标准档就做到了，高速档解决不了更多。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;字出得多快&lt;/strong&gt;（tok/s）：58 tok/s 是短板。按这个数推，一个 6,000 token 的回答要打 104 秒；如果官方 2~3 倍成立，高速档落在 35~52 秒。这是我把厂商声称拿来做除法，&lt;strong&gt;不是实测&lt;/strong&gt;——Artificial Analysis 的高速档页面此刻还是 404。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;整件事多久结束&lt;/strong&gt;（总响应）：表里那个 46.53 秒是 Artificial Analysis 在它自己的标准任务上测的完整耗时，和上面按 6,000 token 折算的 104 秒不是同一件事，只能用来判断量级，不能互相验算。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;顺带给个参照：国内平台模型列表里，上一代编码模型的高速档 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kimi-k2.7-code-highspeed&lt;/code&gt; 标的是“输出速度约 180 Tokens/s，短上下文场景可达 260 Token/s”。所以“速度档”不是这次的新实验，是把已有做法搬到了旗舰模型上。&lt;/p&gt;

&lt;h2 id=&quot;为开放权重而削掉的能力而且把代价量化了&quot;&gt;为开放权重而削掉的能力，而且把代价量化了&lt;/h2&gt;

&lt;p&gt;这次发布里我认为最值得工程师细读的一段是安全部分，因为它罕见地写出了“削了什么、在哪两个阶段削、削完少了多少分”。&lt;/p&gt;

&lt;p&gt;英文原话是这样：随着后训练规模变大，漏洞挖掘表现稳步上升；团队追踪来源，发现 cyber-related content 混在通用 mid-training 语料里，RL 环境组合里也有 cyber-related tasks；接着那句是关键——“For the open-weight release of K3.1 Pro, we identified and removed cyber-related data from both training stages, while retaining the general-purpose coding tasks needed for model usefulness.”（为开放权重这次发布，我们在两个训练阶段识别并移除了网络攻击相关数据，同时保留模型有用性所需的一般编程任务。）&lt;/p&gt;

&lt;p&gt;然后他们给了一个可核算的对照：在内部基准 xdev-bench-hard（专家构造、基于真实源码的漏洞发现与利用任务，多数复现 Linux 内核、Node.js、JavaScriptCore、Exim、VirGL 等近期披露的漏洞并开启缓解措施）上，没有做这些削减的检查点 pass@3（每个任务允许 3 次尝试）解出 90.5%，最终的 K3.1 Pro 是 81.0%。官方还补了一句：K3.1 Pro 的网络攻击能力仍落后于已公开至少三个月的前沿专有模型。&lt;/p&gt;

&lt;p&gt;我的读法，这部分是判断不是官方结论：削减是做在这个模型本身上的，评测对象就叫“final K3.1 Pro”，而动机写在“开放权重原则”那一节里。所以&lt;strong&gt;今天你在 API 里调到的这个版本，应该已经是削过的&lt;/strong&gt;，而不是“API 满血、权重打折”。官方也没有把两者描述成能力不同的两个制品。实际含义有两层：一是准备用模型做防御性代码审计的人，别指望它顶替专门的漏洞挖掘工具；二是“为了将来能开放权重，现在就把能力压住”是一种新的产品约束形式——它让你今天用的模型和 11 月 4 日那批权重服从同一套限制。&lt;/p&gt;

&lt;p&gt;讨论区对这个取舍反应两极。那条 1,067 分、665 条评论的 Hacker News 帖子里，有人说希望不受限的网络能力扩散得慢一点、世界需要准备；也有人反驳说现在被削的只会让政府受益，防御方同样需要强模型；还有人直接抱怨给普通用户的版本在网络安全上被削弱了，跟美国模型一样。这些只是社区观察，不构成能力证据，但它们说明这个决定被相当多人当作影响自己的事在读。&lt;/p&gt;

&lt;p&gt;至于 11 月 4 日那批权重本身，现在还判断不了：许可证条款、权重体积与分片、量化和部署支持都没给。关于上一代 K3 用什么许可，我只找到二手记载（有页面写“Modified MIT”，有页面写“Kimi K3 License”），在 Hugging Face 上没能取到一手页面，所以我不把 K3 的许可结论写进本文的事实里。&lt;/p&gt;

&lt;h2 id=&quot;现在值得做什么还要等什么&quot;&gt;现在值得做什么，还要等什么&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;已经在一个模型上跑生产的团队。&lt;/strong&gt; 把这次发布当“档位变化”评估，而不是“版本升级”。按顺序做三件事：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;拿你自己真实的输入/输出比例套上表算两档差价，看它落在 1.3 倍还是 2 倍。决定成本形态的是“这个任务花多少钱在写字上”。&lt;/li&gt;
  &lt;li&gt;把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reasoning_effort&lt;/code&gt; 从默认 max 往低调一档，用同一批任务比一次账单和答对率——输出 token 是这个模型账单的主要变量。&lt;/li&gt;
  &lt;li&gt;重新检查缓存前缀的设计。官方明确说，措辞相似并不保证命中：前缀靠前的部分一旦改动，它后面的内容就不再被复用。命中率和缓存写入次数是你能改的部分，单价不是。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;独立开发者，主要在意响应体感。&lt;/strong&gt; 高智能模型的首块延迟普遍难看的时期（Anthropic 的 max 行是几百秒级），K3.1 Pro 的 3.07 秒很有吸引力；但“很快开始出字、然后慢慢打印”对交互式产品是另一种难受。我的建议是先别为了流式体验直接上高速档 API：高速档的速度只有厂商声称，没有独立读数。可以先用会员产品感受——官方定价页写 Pro、Max、Ultra 三档都在网页版、Kimi Work 和 Kimi Code 里包含 K3.1 Pro Highspeed，而 API 是按量单独计费，两条花钱路径不互相抵扣。顺带一提，100 万 token 的超长对话容量只给到 Max 和 Ultra，Plus 档没有。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;写安全相关工具的人。&lt;/strong&gt; 先接受“它是被削过的”这个前提，再决定用不用。真要评估就照官方口径自己跑一批样本，别把 xdev-bench-hard 的 81.0% 当成你的通过率。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;还在用 K2 系列的人。&lt;/strong&gt; 平台文档另有两个日期：K2 系列 2026 年 10 月 31 日起停止向新注册用户开放，计划在 12 月底下线全部 K2 系列模型。这和这次发布无关，但和迁移排期有关。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;还要等的&lt;/strong&gt;：11 月 4 日前权重的实际放出与许可证文本；高速档的第三方速度实测；以及官方“逐步提高开放权重里的网络能力”这句话会不会变成有日期的东西——原文用的是 “we intend to”，那是意图。&lt;/p&gt;

&lt;h2 id=&quot;判断速度成了可买的档位账单成了用量问题&quot;&gt;判断：速度成了可买的档位，账单成了用量问题&lt;/h2&gt;

&lt;p&gt;前两节已经把数字摊开了，这一节只说我的位置和它的代价。&lt;/p&gt;

&lt;p&gt;这次 K3.1 Pro 真正改变的东西比它的排名更具体：&lt;strong&gt;它把输出速度变成了一个可以单独购买的价格档，同时把每个任务的账单从“单价问题”变成了“用量问题”。&lt;/strong&gt; 前者对做交互式产品的人是好事——以前只能在“聪明”和“快”之间挑一家，现在可以在同一家里挑档位；后者对做预算的人是坏事——你需要自己测，公告里的“降价三分之一”基本没有预算意义。&lt;/p&gt;

&lt;p&gt;这个档位划分也有它不好的一面：把速度切成两档，意味着同一套 prompt 现在要多维护一条路由、两套预算口径和一个“什么时候切档”的判断逻辑。Anthropic 用 fast mode（同样的 2 倍价、官方称 2.5 倍速）走的是同一条路，所以这不是月之暗面一家的怪做法，而是这个行业正在把“快”从模型属性变成商品规格。我的判断是：接下来一年，选型的第一个问题可能不再是“用哪家”，而是“这个请求值不值得让它跑快点”。&lt;/p&gt;

&lt;p&gt;至于那 3 分。Artificial Analysis 说权重放出后，开放权重最强与专有前沿之间的智能指数差距会从 12 分缩到 3 分（它 10 月 23 日的口径，当时开放权重最强是 46 分的 MiMo-V2.6-Pro，专有最强是 58 分的 Opus 5.5 max）。这个说法有个容易被跳过去的前提：缩到 3 分的那个模型，是为了能够开放而削过网络攻击能力的同一个模型。所以“开源追上前沿”这句话，条件不在分数上，在你想用它做哪件事上。只是写代码、做研究、跑智能体流程，11 月 4 日之后你会多一个便宜选择；如果你的场景包含漏洞挖掘与利用，那 3 分之间的距离可能比表上看起来更长。&lt;/p&gt;

&lt;p&gt;参考与来源（均于 2026-10-25 查阅）：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Kimi 官方技术博客：&lt;a href=&quot;https://www.kimi.ai/blog/kimi-k3-1-pro&quot;&gt;kimi.ai/blog/kimi-k3-1-pro&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Kimi 开放平台模型列表：&lt;a href=&quot;https://platform.kimi.com/docs/models&quot;&gt;platform.kimi.com/docs/models&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Kimi 快速开始（端点与充值门槛）：&lt;a href=&quot;https://platform.kimi.com/docs/guide/kimi-k3-1-pro-quickstart&quot;&gt;platform.kimi.com/docs/guide/kimi-k3-1-pro-quickstart&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Kimi K3.1 Pro 定价页（页面自标费率核对日期 2026-10-24）：&lt;a href=&quot;https://www.kimi.ai/zh-hans/resources/kimi-k3-1-pro-pricing&quot;&gt;kimi.ai/zh-hans/resources/kimi-k3-1-pro-pricing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Artificial Analysis 发布评测（2026-10-23）：&lt;a href=&quot;https://artificialanalysis.ai/articles/kimi-k3-1-pro-launch-benchmarks&quot;&gt;artificialanalysis.ai/articles/kimi-k3-1-pro-launch-benchmarks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Artificial Analysis 模型页与 leaderboard：&lt;a href=&quot;https://artificialanalysis.ai/models/kimi-k3-1-pro&quot;&gt;模型页&lt;/a&gt;、&lt;a href=&quot;https://artificialanalysis.ai/leaderboards/models&quot;&gt;leaderboard&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Anthropic 定价页（单价对照）：&lt;a href=&quot;https://www.anthropic.com/pricing&quot;&gt;anthropic.com/pricing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Hacker News 讨论串（1,067 分 / 665 评论，仅作社区观察）：&lt;a href=&quot;https://news.ycombinator.com/item?id=50212065&quot;&gt;news.ycombinator.com/item?id=50212065&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>

    <entry>
        <title>FreeLLMAPI 说 635 个免费端点：我免 key 拿到的那份是 483</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/25/freellmapi-635-vs-483.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/25/freellmapi-635-vs-483.html</id>
        <updated>2026-10-25T00:00:00+08:00</updated>

        <summary>同一个问题我数了四处，得到 34、38、41、76 四个答案</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="AI" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="Node" scheme="tag"/>

        <category term="前端" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/25/freellmapi-635-vs-483.html">&lt;h1 id=&quot;freellmapi-说-635-个免费端点我免-key-拿到的那份是-483&quot;&gt;FreeLLMAPI 说 635 个免费端点：我免 key 拿到的那份是 483&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/freellmapi-635-vs-483/banner.webp&quot; alt=&quot;封面：同一份模型目录在四个地方印出四个数字，只有中间那条对得上实物&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;一句话结论：它宣传的免费端点数，和免 key 就能拉到的那份目录不是同一个东西，差 152 个。
现在该做什么：装之前先把那份签名目录拉下来数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.counts&lt;/code&gt;，别拿 README 的数当容量。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;免-key-拉一次就知道是-483&quot;&gt;免 key 拉一次就知道是 483&lt;/h2&gt;

&lt;p&gt;先跑一条命令。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tashfeenahmed/freellmapi&lt;/code&gt; 这个把几十家免费额度收进一个 OpenAI 兼容 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1&lt;/code&gt; 的自托管网关，模型目录是一份签名 JSON，网关启动时自己去拉。拉它不要 key：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ curl -s https://api.freellmapi.co/v1/latest | jq &apos;.counts&apos;
{ &quot;platforms&quot;: 41, &quot;models&quot;: 442, &quot;embeddings&quot;: 30,
  &quot;transcriptionModels&quot;: 10, &quot;videoModels&quot;: 1, &quot;searchEngines&quot;: 4 }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;442 行 chat、30 个 embedding、10 个转写、1 个视频，四类合计 483。快照的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version&lt;/code&gt; 是 2026.10.24，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;generatedAt&lt;/code&gt; 落在我抓它的前一天。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/freellmapi-635-vs-483/01-screenshot-catalog-counts.webp&quot; alt=&quot;终端实录：免 key 拉到的目录快照，counts 逐项列出，四类端点合计 483&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;635-加得平逐项却对不上-483&quot;&gt;635 加得平，逐项却对不上 483&lt;/h2&gt;

&lt;p&gt;483 是拉出来的，635 是读来的。同一个 README 里两处给了它。&lt;/p&gt;

&lt;p&gt;第 5 行是这么开的：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7.4 billion tokens per month. 34 free LLM providers. 635 free model endpoints.&lt;/code&gt; 第 237 行再给分解：584 chat、41 embeddings、7 transcription、3 video。&lt;/p&gt;

&lt;p&gt;加起来正好 635。它自己的算术没毛病。&lt;/p&gt;

&lt;p&gt;把 483 那份快照按项摊开对：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;项&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;README&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;快照&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;chat&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;584&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;442&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;embeddings&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;41&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;transcription&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;7&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;video&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;合计&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;635&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;483&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;方向不一致这点值得停一下。前三项 README 偏高，转写和视频反而偏低。如果是缓存过期，五项应该同向。所以更像是两份东西量的不是同一层。&lt;/p&gt;

&lt;h2 id=&quot;四处四个数34384176&quot;&gt;四处四个数：34、38、41、76&lt;/h2&gt;

&lt;p&gt;483 与 635 之间，我还撞到另外两个数。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;出处&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;provider 数&lt;/th&gt;
      &lt;th&gt;模型 / 端点数&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;README 头部&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;34&lt;/td&gt;
      &lt;td&gt;474 族 / 635 端点&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;models.html 的 meta&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;38&lt;/td&gt;
      &lt;td&gt;395 模型&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;models.html 正文&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;未标&lt;/td&gt;
      &lt;td&gt;335 模型 / 438 端点&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;签名目录快照&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;41&lt;/td&gt;
      &lt;td&gt;442 行 / 483 端点&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;源码 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;register()&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;76&lt;/td&gt;
      &lt;td&gt;不数模型&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;公开页那份更妙：同一份 HTML，meta description 写着 395 models across 38 providers，往下滚的计数行写着 335 of 335 models · 438 free endpoints。它跟自己也没对齐。&lt;/p&gt;

&lt;p&gt;差这么多，代码里给了路径。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared/types.ts&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Platform&lt;/code&gt; 联合有 77 个成员，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;providers/index.ts&lt;/code&gt; 里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;register()&lt;/code&gt; 调了 76 次——其中 32 家不是具名适配器，是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new OpenAICompatProvider({ platform: ... })&lt;/code&gt; 内联注册的。我一开始数 import 语句，只数出 44，差点把 44 当成源码侧的真数。文档表 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docs/en/providers/01-supported-platforms.md&lt;/code&gt; 也是 44 行，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;groq&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mistral&lt;/code&gt; 都在里面，但联合类型里那 33 个没有文档行的名字（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aclide&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plugsky&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;router9&lt;/code&gt; 这类）同样在跑。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/freellmapi-635-vs-483/03-diagram-six-counters.webp&quot; alt=&quot;结构图：同一个「有多少模型」在类型、注册、文档、快照、页面、README 六层各给一个数&quot; /&gt;&lt;/p&gt;

&lt;p&gt;一层层往上都在做减法：类型 77、注册 76、文档表 44、快照 41、真有 chat 行的 36、README 的 34。减法本身站得住，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search-only.ts&lt;/code&gt; 那 8 家只出不进 chat 路由，注释写得清清楚楚。问题是这些数最后都被叫作 providers。&lt;/p&gt;

&lt;h2 id=&quot;一把废-key-也返回-200&quot;&gt;一把废 key 也返回 200&lt;/h2&gt;

&lt;p&gt;483 与 635 之间那层是什么，我用假 key 试了三次。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ curl -s -o /dev/null -w &apos;%{http_code} %{size_download}\n&apos; \
    -H &apos;Authorization: Bearer sk-bogus-for-test&apos; \
    https://api.freellmapi.co/v1/latest
200 576073
$ curl -s &apos;https://api.freellmapi.co/v1/latest?tier=live&apos; | jq &apos;.tier&apos;
monthly
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;字节数和匿名那次一模一样。没信号。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/freellmapi-635-vs-483/02-screenshot-bogus-key-still-200.webp&quot; alt=&quot;终端实录：废 key 与 tier=live 都拿不到另一份目录，v1/live 直接 404&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;catalog-sync.ts&lt;/code&gt; 的注释解释了为什么：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A valid Premium license key (Bearer) gets the live tier&lt;/code&gt;。换层靠的是有效 key，不是查询串。&lt;/p&gt;

&lt;p&gt;这条对付费的人是实用信息：如果你以为自己的 key 生效了，这个端点不会给你任何反馈——不 401、不降级提示，只安静地返回免费那份。要确认，只能比对返回体里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;counts&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;635-住在库里不在-git-里&quot;&gt;635 住在库里，不在 git 里&lt;/h2&gt;

&lt;p&gt;上周我写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;thesysdev/openui&lt;/code&gt;，也是拿 README 的数字开刀。差别就在这：openui 把 benchmark 的样本和脚本都提交了，我抓下来重跑就能对账。FreeLLMAPI 的 635 住在运行时数据库里，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server/src/scripts/export-catalog.ts&lt;/code&gt; 的注释写着这份产物从 live 库导出，源码树里没有一份静态文件能数出 635。&lt;/p&gt;

&lt;p&gt;所以这不是数字造假那种便宜结论。它是另一件事：目录型产品的宣传数，可复算性取决于作者有没有把那份 JSON 留在仓库里。留了，读者能自己数；没留，读者只能信，或者像我一样去 curl 一个公开端点。&lt;/p&gt;

&lt;p&gt;给同样在挑网关的人一条判别：先看仓库里有没有 catalog 的静态产物，再看公开端点免不免 key。两个都满足，README 那个数才有意义。&lt;/p&gt;

&lt;p&gt;顺便给个规模参照：这个仓库 5 天发了 6 个补丁版本（v0.16.4 到 v0.16.9），10 月 24 日一天合了 8 个 fix 类 PR，npm 上那个 CLI 包上周被下载 6,049 次。项目不是没人管，是数字管不住数字。&lt;/p&gt;

&lt;h2 id=&quot;盯-readme-第-237-行有没有-tier&quot;&gt;盯 README 第 237 行有没有 tier&lt;/h2&gt;

&lt;p&gt;三条，各带触发条件。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果第 237 行补上 live 与 monthly 两份各给一个数，主峰作废。判据：那一行出现 tier 限定词。&lt;/li&gt;
  &lt;li&gt;如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export-catalog.ts&lt;/code&gt; 开始往仓库里落一份月度 JSON，我就能离线复算，不用 curl。&lt;/li&gt;
  &lt;li&gt;如果 models.html 的 meta 与正文计数对上（现在差 60 个模型），说明那页是生成的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;没验到的那一半也写清楚：付费 live 层我没 key、没验，所以本文不说 635 是错的，只说免 key 拿到的是 483。本地起 server 数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/models&lt;/code&gt; 我没做，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server/package.json&lt;/code&gt; 十二个依赖里没有数据库驱动，怀疑走 Node 22 内置的 sqlite，建库加迁移这轮时间不够。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/tashfeenahmed/freellmapi&quot;&gt;https://github.com/tashfeenahmed/freellmapi&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>AI 做的网页都长一个样？我试了 10 万 Star 的 taste-skill</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/24/taste-skill-contact-sheet.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/24/taste-skill-contact-sheet.html</id>
        <updated>2026-10-24T00:00:00+08:00</updated>

        <summary>从 247 个站点提炼方向，再让浏览器验收：我用一页博客索引做了前后对照。</summary>


        <category term="AI"/>

        <category term="前端工程化"/>


        <category term="AI" scheme="tag"/>

        <category term="Agent" scheme="tag"/>

        <category term="前端" scheme="tag"/>

        <category term="开源项目" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/24/taste-skill-contact-sheet.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/taste-skill-contact-sheet/banner.webp&quot; alt=&quot;一张被放大镜查看的网页联系表，旁边是一台显示界面草图的笔记本&quot; /&gt;&lt;/p&gt;

&lt;p&gt;让 Agent 做一个落地页，它交回来的东西往往有同一套表情：标题上面一行小号大写字母，紫色渐变从左上铺到右下，三张图标卡排成一行，数字用 01/02/03 标好，最后一句 “Get started”。每一块都眼熟，合起来却不知道是谁的页面。&lt;/p&gt;

&lt;p&gt;我拿这套模板做了一个故意很“AI”的对照页，又用 taste-skill 重做了一版。同一批博客封面，检查器在三个宽度下量出的 FAIL 从 11 个降到 0 个。但更有意思的不是这个数字，而是它怎么逼一个 Agent 先做决定，再去看自己的截图。&lt;/p&gt;

&lt;h2 id=&quot;先说结论它治的是没做决定不是不好看&quot;&gt;先说结论：它治的是“没做决定”，不是“不好看”&lt;/h2&gt;

&lt;p&gt;taste-skill 不是一套组件库，也不是一个更会写 CSS 的模型。它是一份给编码 Agent 的流程：先盘点这个对象独有的材料，再从研究过的方向里抽几个，选一个，写一句能把材料和版式连起来的概念，然后才动 HTML。&lt;/p&gt;

&lt;p&gt;它也不承诺把页面变漂亮。README 里有一句很清醒的话：把装饰拿掉只是一半，另一半是内容、图片和真实信息；一个“没有套路”的空页面并不比一个堆满卡片的页面更好。&lt;/p&gt;

&lt;p&gt;所以我把这次测试理解成一次“决策实验”，而不是“审美评分”。检查器只负责量出被禁用的套路，量不出一个页面是否值得读。0 个 FAIL 是起点，不是结论。&lt;/p&gt;

&lt;h2 id=&quot;10-万-star-的到底是什么一套会验收的设计流程&quot;&gt;10 万 Star 的到底是什么：一套会验收的设计流程&lt;/h2&gt;

&lt;p&gt;截至 2026-10-24 抓取，Leonxlnx/taste-skill 在 GitHub 上是 101,508 Star、6,852 Fork，MIT 协议，2026-02-19 创建。README 里写的版本是 v2.1，定位是 Agent Skill，不是独立应用。&lt;/p&gt;

&lt;p&gt;它的研究底稿来自 247 个精选站点：One Page Love 的最新页面，以及 Awwwards 的 Sites of the Day。作者把这些页面逐页测量，提炼出 20 个可以复用的方向。每次构建，脚本从 20 个里抽 3 个，Agent 按对象的材料选一个，而不是按“哪个最熟”选。&lt;/p&gt;

&lt;p&gt;一次完整构建大概这样走：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;读 brief，盘点只有这个对象才有的东西：名字、地点、材料、限制、真实图片。&lt;/li&gt;
  &lt;li&gt;抽三个方向，选一个；再抽这个方向的字体、配色、首屏和区块结构。&lt;/li&gt;
  &lt;li&gt;写一句概念，用最近的竞品做替换测试：如果换一个对象也成立，这个概念就还是品类的，不是这个对象的。&lt;/li&gt;
  &lt;li&gt;先写 claims 和文案，再决定 section；没有 claim 的 section 删掉。&lt;/li&gt;
  &lt;li&gt;在真实浏览器里按 1440、768、390 三个宽度验收：标题行数、字号、颜色、边距、空白、图片、溢出。&lt;/li&gt;
  &lt;li&gt;打开每个宽度的截图，修最明显的三个问题，再跑一次；最多三轮。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这六步里，真正和“提示词合集”拉开差距的是第 5、6 步。它不检查源码里有没有某个词，而是打开页面，量最终渲染出来的东西。源码看起来再干净，标题在 390px 下折成四行，检查器照样会报。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/taste-skill-contact-sheet/02-flowchart-six-steps.webp&quot; alt=&quot;taste-skill 的六步构建流程，以及 247 个站点、20 个方向和三轮验收的数字&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;三个动作把审美变成可执行的选择&quot;&gt;三个动作，把“审美”变成可执行的选择&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;第一个动作是抽方向，不是让模型凭空想一个风格。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我在演示里跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;draw.mjs&lt;/code&gt;，拿到三个方向：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinned-poster&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contact-sheet&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quiet-instrument&lt;/code&gt;。最后选了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contact-sheet&lt;/code&gt;，理由是这次的对象不是单个产品，而是一组技术文章；把封面铺成摄影联系表，正好让“文章的数量和种类”成为页面本身，而不是再造一个 hero 去推某一篇。&lt;/p&gt;

&lt;p&gt;方向表还给了字体、配色和区块顺序：Archivo，沙色底、炭黑字、苔绿一个强调色；intro、work、about、cta、footer 五个区块。种子是 788441068，重印方向表时能复现同一套选择。这个“可复现”很重要：设计讨论终于可以指向一个具体的 sheet，而不是“再大气一点”。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二个动作是让规则有牙齿。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;它禁用的东西很具体：标题上面的小号眉标、紫色渐变、静止元素的阴影、01/02/03 编号、一排图标卡、奶油色配陶土色、超过两行的标题、低于 16px 的正文、少于四张真实图片。检查器会逐条量。&lt;/p&gt;

&lt;p&gt;这些规则不是全都来自数据。研究里的对比是：高分站点用图标卡的比例是 1%，低分站点是 28%；渐变是 22% 对 52%；静止阴影是 24% 对 48%；胶囊小标签是 14% 对 34%。但等宽字体、细线、01/02/03 在获奖站点里反而更常见，作者仍然禁用了它们，因为生成器正在模仿这些符号，它们已经变成了“戏服”。这条区分很重要：哪些是研究结论，哪些是作者的口味，README 和 SKILL.md 都写明了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三个动作是看截图，不是看代码。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inspect.mjs&lt;/code&gt; 会把页面在三个宽度下跑一遍，量标题行数、字体是否真的加载、颜色、边距、空白高度、图片数量、横向溢出，然后保存截图。Agent 要做的是打开截图，按它自己的评审表找最明显的三个问题，修完再跑。通过检查不等于好看，这一步是留给人的。&lt;/p&gt;

&lt;h2 id=&quot;我拿同一页博客做了对照11-个-fail-到-0&quot;&gt;我拿同一页博客做了对照：11 个 FAIL 到 0&lt;/h2&gt;

&lt;p&gt;为了不让结论只停在 README，我做了一页演示：左边是故意使用常见 AI 模板的版本，右边是按 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contact-sheet&lt;/code&gt; 方向重做的版本。两版素材都来自同一批已发布文章（封面、截图、图表），不是拿假截图互相比较。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/taste-skill-contact-sheet/01-comparison-before-after.webp&quot; alt=&quot;修前与修后的同一页博客索引，左边是紫色渐变模板，右边是沙色联系表&quot; /&gt;&lt;/p&gt;

&lt;p&gt;修前那版，我刻意放进了紫色渐变、眉标、三张图标卡、01/02/03、emoji、阴影和一条细分割线。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint.mjs&lt;/code&gt; 量出 18 个 FAIL、10 个 WARN；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inspect.mjs&lt;/code&gt; 在 1440、768、390 三个宽度合计 11 个 FAIL、8 个 WARN。FAIL 的类型包括眉标、编号、emoji、渐变、阴影、按钮特效、细线、图标卡、紫色配色、页面没有动效，以及标题折行过多。&lt;/p&gt;

&lt;p&gt;修后那版，我按方向表用了 21 张博客里的真实配图，统一成黑白联系表，悬停时恢复颜色；标题、正文和注释只用 Archivo 一套字；沙色底上只留一个苔绿强调色；加了一个按 AI / 前端筛选的交互，和一组滚动入场。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint.mjs&lt;/code&gt; 是 0 FAIL、0 WARN；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inspect.mjs&lt;/code&gt; 是 0 FAIL、3 WARN。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/taste-skill-contact-sheet/03-screenshot-after.webp&quot; alt=&quot;重做后的博客索引桌面首屏：文字带、筛选按钮、放大的封面和联系表&quot; /&gt;&lt;/p&gt;

&lt;p&gt;三个 WARN 我没有为了清零去改：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;没有视频。&lt;/strong&gt; 我没有一段诚实的、和文章内容相关的短片；随便找一段 stock 视频只会多一个装饰。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;h1 只有 19px。&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contact-sheet&lt;/code&gt; 方向明确要求名字按正文大小排，不设大标题；页面的重心交给联系表。检查器仍然按落地页的习惯提醒标题太小。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;标题旁没有解释句。&lt;/strong&gt; 检查器认为首屏只有名字、没有说明这是什么；实际上副标题就在右边一栏，只是方向表把名字和说明分在了不同列。一种改法是把说明挪到标题正下方，但那会改掉这个版式。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这三个 WARN 是这次测试最有价值的部分：规则和方向表会打架。taste-skill 自己给了一个优先级：先满足 house rules，再服从方向，再服从字体和配色，最后才是具体 blueprint。但有些冲突没法两边都满足，你得选一个，并把理由写进报告。选完还要承认：0 FAIL 只说明没有命中禁用套路，截图仍然要人看。&lt;/p&gt;

&lt;p&gt;还要说清一个边界：这次我没有让 Agent 从头到尾跑完整的 30–45 分钟构建，也没有跑满三轮 look-and-fix。我跑通了 draw → build → lint → inspect 这条主链，用真实的 21 张图片做了一页可检查的页面。完整流程里的“看截图再修”是下一步，不是这次已经证明的事。&lt;/p&gt;

&lt;h2 id=&quot;它真正有用的地方不在禁用清单&quot;&gt;它真正有用的地方，不在禁用清单&lt;/h2&gt;

&lt;p&gt;如果只看禁用清单，taste-skill 很容易被学成一串 CSS 规则：不要渐变、不要阴影、不要 01/02/03。但修前那版的问题不是“用了一个紫色”，而是每个选择都可以替换成另一个 AI 工具的页面。它没有回答“这个对象为什么长这样”。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contact-sheet&lt;/code&gt; 给我的不是一个更漂亮的模板，而是一个决定：把文章封面、截图和图表当成同一张联系表上的证据，让读者先扫证据，再决定读哪篇。这个决定又带出了后面的选择：需要 21 张真实图片，需要统一的黑白处理，需要一个筛选器，需要把标题压小、把版面让给图。功能不是堆上去的，是被这个概念逼出来的。&lt;/p&gt;

&lt;p&gt;如果你在选工具，可以先看这张表：&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto;-webkit-overflow-scrolling:touch&quot;&gt;
&lt;table style=&quot;min-width:640px&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;taste-skill&lt;/th&gt;&lt;th&gt;一次性设计提示词&lt;/th&gt;&lt;th&gt;UI 组件库&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;给模型一段风格描述&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;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;td&gt;不负责页面结构&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;代价&lt;/td&gt;&lt;td&gt;30–45 分钟、几十万 token、要 Chrome&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;td&gt;已有设计系统的团队&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;这张表不是评分，是选型：你缺的是“做决定”和“验收”，还是只是缺一套组件？前者才值得上 taste-skill；后者用组件库更快。&lt;/p&gt;

&lt;p&gt;浏览器验收也是同一个逻辑。修前那版在源码里看起来“完整”，但渲染出来标题折行、页面没有动效、图标卡排成三张一样的；这些都不是读 HTML 能一眼看出来的。把页面真的打开，量一遍，再打开截图看一遍，这个顺序比禁用清单更难被糊弄。&lt;/p&gt;

&lt;p&gt;但它仍然只是一个检查器。它不知道你的品牌、你的用户、你的内容是否真实；它也不知道一个通过检查的页面是不是无聊。SKILL.md 自己说得很直白：通过 gate 只代表没有量到被禁用的东西，不代表页面好。这句话应该和工具一起被记住。&lt;/p&gt;

&lt;h2 id=&quot;这些页面别用它&quot;&gt;这些页面，别用它&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;只想十分钟做一个页面的人。&lt;/strong&gt; 完整构建是 30–45 分钟、几十万 token 的量级；这次只跑主链也花了不少时间。为了一个临时页面上这套流程，成本不划算。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;数据密集的仪表盘和后台。&lt;/strong&gt; v2.1 给 app screen 留了单独路径，规则和落地页不一样；但本文量的 landing page 规则（无细线、无 01/02/03、无等宽字）不应该直接套到表格和监控界面上。后台要的是扫读效率，不是联系表。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;有严格品牌系统的团队。&lt;/strong&gt; 它可以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--keep palette,type&lt;/code&gt; 保留品牌色和字体，但 house rules 仍然会禁掉一些品牌资产里常见的东西。上线前要决定：是改品牌，还是给规则开例外，并写清为什么。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;只有几张图，却想做 contact-sheet 的人。&lt;/strong&gt; 方向表自己写了：少于大约 20 张图，这个方向会显得没做完。我这次用了 21 张；如果只有四张，应该换方向，而不是硬把四张图放大成一张“作品集”。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;没有时间看截图的人。&lt;/strong&gt; 检查器能拦住套路，但拦不住一个空洞的页面。最后那一步“看截图、修三个问题”需要人；如果没人看，0 FAIL 只是一个更干净的模板。&lt;/p&gt;

&lt;h2 id=&quot;想试的话从一条命令开始&quot;&gt;想试的话，从一条命令开始&lt;/h2&gt;

&lt;p&gt;官方给的安装命令是一条：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npx skills add https://github.com/Leonxlnx/taste-skill &lt;span class=&quot;nt&quot;&gt;--skill&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;design-taste-frontend&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;为了不污染本机的 skill 目录，我这次直接把仓库 clone 到临时目录跑脚本。装好之后，最小的工作流是：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 看三个方向，选一个&lt;/span&gt;
node &amp;lt;skill&amp;gt;/scripts/draw.mjs &lt;span class=&quot;nt&quot;&gt;--sector&lt;/span&gt; personal-portfolio &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--brief&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;你的对象，一句话说清它做什么&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--sections&lt;/span&gt; intro,work,about,cta,footer &lt;span class=&quot;nt&quot;&gt;--options&lt;/span&gt; 3

&lt;span class=&quot;c&quot;&gt;# 展开选中的方向，拿到字体、配色和区块&lt;/span&gt;
node &amp;lt;skill&amp;gt;/scripts/draw.mjs &lt;span class=&quot;nt&quot;&gt;--sector&lt;/span&gt; personal-portfolio &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--brief&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;你的对象，一句话说清它做什么&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--sections&lt;/span&gt; intro,work,about,cta,footer &lt;span class=&quot;nt&quot;&gt;--direction&lt;/span&gt; contact-sheet

&lt;span class=&quot;c&quot;&gt;# 静态扫描 + 真实浏览器验收&lt;/span&gt;
node &amp;lt;skill&amp;gt;/scripts/lint.mjs index.html style.css app.js
node &amp;lt;skill&amp;gt;/scripts/inspect.mjs index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inspect&lt;/code&gt; 需要 Node 22 以上和 Chrome、Edge 或 Chromium；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;draw&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint&lt;/code&gt; 对版本的要求更低。整个过程不需要 API Key，但找开放图片要联网，图片的许可和署名要自己核对。&lt;/p&gt;

&lt;p&gt;第一次用，建议只拿一个页面做对照，不要直接上整站。先准备一个真实的 brief 和一批真实素材；如果素材不够，就换一个方向，或者先别用这个技能。&lt;/p&gt;

&lt;h2 id=&quot;最后的判断&quot;&gt;最后的判断&lt;/h2&gt;

&lt;p&gt;taste-skill 值得试，但值得试的不是“它能让页面变好看”，而是“它把设计从一句形容词变成了一串必须回答的问题”：这个对象只有什么、选哪个方向、概念一句话能不能成立、每个 section 有没有 claim、在 390px 下标题会不会折、截图里最难看的三处是什么。&lt;/p&gt;

&lt;p&gt;如果你正在用 Agent 做落地页、作品集或产品介绍页，而且已经受够了那种“每块都眼熟、合起来不知道是谁”的页面，它值得花一个晚上跑一遍。如果你要的是十分钟出稿、数据密集的后台，或者一个严格品牌系统，先用别的办法。&lt;/p&gt;

&lt;p&gt;我自己的判断是：它最适合“有一个真实对象、有一批真实素材、愿意花时间看截图”的场景；不适合把它当成万能 UI 生成器。如果未来它能把完整构建压到更短、把标题和说明句的位置判断做得更准，我会更愿意把它放进日常流程；如果它仍然要求 30–45 分钟和几十万 token，那它更像一次认真的设计冲刺，而不是每天的默认工具。&lt;/p&gt;

&lt;p&gt;先做一页对照，再决定要不要把它放进你的工作流。0 FAIL 不是奖杯，只是一个更干净的起点。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Leonxlnx/taste-skill&quot;&gt;github.com/Leonxlnx/taste-skill&lt;/a&gt;&lt;br /&gt;
官方站点：&lt;a href=&quot;https://tasteskill.dev&quot;&gt;tasteskill.dev&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>OpenUI 一个 core，我装出两版解析器，新版 API 只到 React</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/23/openui-two-lang-cores.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/23/openui-two-lang-cores.html</id>
        <updated>2026-10-23T00:00:00+08:00</updated>

        <summary>一棵依赖树里躺着 lang-core 0.4.1 与 1.0.1，新 API 只在 React</summary>


        <category term="开源实测"/>

        <category term="TypeScript"/>


        <category term="AI" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="react" scheme="tag"/>

        <category term="前端" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/23/openui-two-lang-cores.html">&lt;h1 id=&quot;openui-一个-core我装出两版解析器新版-api-只到-react&quot;&gt;OpenUI 一个 core，我装出两版解析器，新版 API 只到 React&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openui-two-lang-cores/banner.webp&quot; alt=&quot;封面：一棵依赖树挂着两个版本的解析器，React 那条分支单独装着新版&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;一句话结论：四个包我装进同一棵树，磁盘上落下两份解析器，React 那份是新的。
现在该做什么：跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm ls @openuidev/lang-core --all&lt;/code&gt;，数一数它列了几行版本。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;npm-ls-落下两版-lang-core&quot;&gt;npm ls 落下两版 lang-core&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm ls&lt;/code&gt; 那 8 行，我原本预期只有一行。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;thesysdev/openui&lt;/code&gt; 是 TypeScript 周榜第 13 名、截至 2026-10-23 有 13,325 颗 star 的那个生成式 UI 框架。它 README 第 24 行写着：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;OpenUI is a full-stack, framework-agnostic Generative UI framework built around a compact, streaming-first language. A framework-agnostic core powers first-party runtimes for React, Vue, Svelte, and Angular.&lt;/p&gt;

  &lt;p&gt;我的翻译：一个全栈、框架无关的生成式 UI 框架，由一个与框架无关的内核驱动 React、Vue、Svelte、Angular 四家第一方运行时。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;一个内核、四家运行时，这句话可以直接动手验。我开了三棵空工程：只装 React 一条，只装另外三家，四个一起装。前两条 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;added 10 packages&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;added 50 packages&lt;/code&gt;，都没报错。第三条装完，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm ls&lt;/code&gt; 打出来的不是我以为的东西。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openui-two-lang-cores/01-screenshot-npm-ls-two-cores.webp&quot; alt=&quot;终端实录：同一棵依赖树里 npm ls 列出 0.4.1 与 1.0.1 两份 lang-core，React 那份单独嵌套&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;+-- @openuidev/angular-lang@0.4.2
| `-- @openuidev/lang-core@0.4.1
+-- @openuidev/react-lang@1.0.1
| `-- @openuidev/lang-core@1.0.1
+-- @openuidev/svelte-lang@0.4.2
| `-- @openuidev/lang-core@0.4.1 deduped
`-- @openuidev/vue-lang@0.4.2
  `-- @openuidev/lang-core@0.4.1 deduped
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;顶层被 hoist 的是旧那份，React 单独嵌一份新的。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-lock.json&lt;/code&gt; 里同样是两条路径、两个版本。&lt;/p&gt;

&lt;h2 id=&quot;四行-dependencies两条版本线&quot;&gt;四行 dependencies，两条版本线&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lang-core&lt;/code&gt; 那两行是谁决定的？四个包自己的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dependencies&lt;/code&gt;。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;react-lang@1.0.1    &quot;@openuidev/lang-core&quot;: &quot;^1.0.1&quot;
vue-lang@0.4.2      &quot;@openuidev/lang-core&quot;: &quot;^0.4.1&quot;
svelte-lang@0.4.2   &quot;@openuidev/lang-core&quot;: &quot;^0.4.1&quot;
angular-lang@0.4.2  &quot;@openuidev/lang-core&quot;: &quot;^0.4.1&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^0.4.1&lt;/code&gt; 按 semver 永远吃不进 1.x。所以这不是包管理器的问题，是四条声明本来就分家。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;包&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;最新版&lt;/th&gt;
      &lt;th&gt;声明的 core&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;上周下载&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.1&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^1.0.1&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;184,706&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svelte-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.4.2&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^0.4.1&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;7,860&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vue-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.4.2&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^0.4.1&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;784&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;angular-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.4.2&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^0.4.1&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;175&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;发布时刻贴得更紧。四个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;latest&lt;/code&gt; 全落在 2026-10-15 04:23Z 到 04:26Z 这三分钟里，是同一次发版。而 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lang-core&lt;/code&gt; 自己的时间线是 10-10 发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.4.1&lt;/code&gt;、10-12 直接跳 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.0&lt;/code&gt;、10-15 补到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.1&lt;/code&gt;：大版本跳了之后，跟着改范围的只有 React 那一条。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openui-two-lang-cores/03-diagram-dependency-split.webp&quot; alt=&quot;结构图：10-12 那次大版本跳级之后，四条适配器依赖线分成新旧两支&quot; /&gt;&lt;/p&gt;

&lt;p&gt;下载量那一列我没敢读成用户数，它只说明上周有多少次安装。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-lang&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;angular-lang&lt;/code&gt; 之间差着三个数量级，这条分界和版本线的分界是同一条。&lt;/p&gt;

&lt;h2 id=&quot;parsemessage-只到-react-lang&quot;&gt;parseMessage 只到 react-lang&lt;/h2&gt;

&lt;p&gt;版本线分家之后，新 API 只走了一条。两版 core 的顶层导出面拉出来：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.1&lt;/code&gt; 是 42 个，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.4.1&lt;/code&gt; 是 43 个。只在新版的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buildMessage&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defineAction&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defineFunction&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evaluateRoot&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parseMessage&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;steps&lt;/code&gt;；只在旧版的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BUILTINS&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isBuiltin&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACTION_NAMES&lt;/code&gt; 这类内部件。&lt;/p&gt;

&lt;p&gt;真正会写进业务代码的是适配器入口。我把四个入口挨个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import()&lt;/code&gt; 了一遍。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openui-two-lang-cores/02-screenshot-node-adapter-imports.webp&quot; alt=&quot;终端实录：react-lang 有 parseMessage、vue-lang 没有，另两家在 Node 里直接报错&quot; /&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;入口&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;导出数&lt;/th&gt;
      &lt;th&gt;parseMessage&lt;/th&gt;
      &lt;th&gt;Node 里 import&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;41&lt;/td&gt;
      &lt;td&gt;有&lt;/td&gt;
      &lt;td&gt;成功&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vue-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;27&lt;/td&gt;
      &lt;td&gt;没有&lt;/td&gt;
      &lt;td&gt;成功&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svelte-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;未取到&lt;/td&gt;
      &lt;td&gt;未取到&lt;/td&gt;
      &lt;td&gt;扩展名报错&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;angular-lang&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;未取到&lt;/td&gt;
      &lt;td&gt;未取到&lt;/td&gt;
      &lt;td&gt;JIT 报错&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;前两行是同一句 import 换个包名。后两行停在加载阶段：Svelte 报 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Unknown file extension&lt;/code&gt; 那一串，Angular 报 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlatformLocation&lt;/code&gt; 需要 JIT 编译器那一句。它们本来就该由各自的构建器接手，浏览器里跑不跑我没测。&lt;/p&gt;

&lt;p&gt;我也试图证明两版解析器给出不同结果。这份样本加上我造的截断、空串、未知组件名，一共五组输入喂进两版 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parse()&lt;/code&gt;，返回 JSON 的长度一组都不差：351、343、169、170、170。&lt;/p&gt;

&lt;p&gt;不是行为分叉，是接口分叉。&lt;/p&gt;

&lt;h2 id=&quot;readme-第-24-行还有一句-67&quot;&gt;README 第 24 行还有一句 67%&lt;/h2&gt;

&lt;p&gt;parseMessage 之外，同一段 README 还有个数字。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OpenUI Lang uses up to 67% fewer tokens than JSON&lt;/code&gt;，底下是一张七行的表，用 tiktoken 的 GPT-5 编码器数三种格式各花多少 token。它把样本也提交进了仓库：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;benchmarks/samples/&lt;/code&gt; 一共 28 个文件。&lt;/p&gt;

&lt;p&gt;我把这 28 个文件按 git tree 登记的字节数逐个校验抓齐，再用它脚本里同一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encoding_for_model(&quot;gpt-5&quot;)&lt;/code&gt; 重跑。第一轮只对上 14 个文件，最大的那一档文件三次重试全断；加上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--retry 3&lt;/code&gt; 才算跑通。&lt;/p&gt;

&lt;p&gt;两列逐格相同。OpenUI 那一列我测出 4,800 个 token，Vercel 那一列 10,180，与它合计行写的一字不差。&lt;/p&gt;

&lt;p&gt;第三列不对。Thesys C1 JSON 那一栏，我测的每一格都比它表里少：单格少 10 到 94 个，合计我测 9,650、表里写 9,948。连带「vs C1」那栏的百分比也整体偏乐观，simple-table 一格我重算是 -54.0%，表里写 -58.5%。&lt;/p&gt;

&lt;p&gt;差异从哪来我没能确定：样本可能被重新生成过，也可能表格数字是手改的，仓库里没留两版对照。这句标未确认。&lt;/p&gt;

&lt;p&gt;至于 67% 本身：它是 contact-form 那一格的最大值，七个场景加总省的是 -52.8%。对照的两栏都是流式 JSON 格式，不是把 UI 直接序列化出来的裸 JSON。&lt;/p&gt;

&lt;h2 id=&quot;对照栏里的-json-render&quot;&gt;对照栏里的 json-render&lt;/h2&gt;

&lt;p&gt;那个分母之一我本地跑过。&lt;/p&gt;

&lt;p&gt;-52.8% 对照的是 Vercel 的 json-render，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vercel-labs/json-render&lt;/code&gt; 我 9 月写过一篇。同一类模型输出 JSON、前端映射组件的做法，我那篇量到的是它把没登记的键静默剥掉。&lt;/p&gt;

&lt;p&gt;两条路的分工不一样。json-render 让你写一份 JSON 由渲染器解释；OpenUI 让模型直接吐一种带赋值的紧凑文本，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table(cols, rows)&lt;/code&gt; 这种写法不用重复键名，省的就是这部分。&lt;/p&gt;

&lt;p&gt;代价在另一侧。React 那条线同时有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@openuidev/react-ui&lt;/code&gt; 和 headless 原语，另外三家只有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*-lang&lt;/code&gt; 一层绑定；README 第 93 行把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lang-core&lt;/code&gt; 描述成没有 React、Angular、Vue、Svelte 依赖的那一层，这句我信：磁盘上它就有两份，各自独立。&lt;/p&gt;

&lt;p&gt;我自己栈里 Vue 3 是主力。真要在这个栈上用它，我能拿到的就是那 27 个导出。&lt;/p&gt;

&lt;h2 id=&quot;我会盯-vue-lang-的那行-041&quot;&gt;我会盯 vue-lang 的那行 ^0.4.1&lt;/h2&gt;

&lt;p&gt;那 27 个导出之外，我盯三件事，每件都带触发条件。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vue-lang&lt;/code&gt; 的范围改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;^1.0.1&lt;/code&gt;：两版并一版，主峰失效。判据是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm ls&lt;/code&gt; 只剩一行。&lt;/li&gt;
  &lt;li&gt;出第五个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*-lang&lt;/code&gt; 包：README 那句四家运行时又要重数（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#1455&lt;/code&gt; 之后 RN 在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.native&lt;/code&gt; 入口里）。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.native&lt;/code&gt; 入口被搬进独立包：那条线不再靠 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exports&lt;/code&gt; 的条件字段分流。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;没验到的另一半我也写下来。浏览器端我只跑到 Node 里能不能 import；Gateway、服务端渲染、真实模型生成一个都没跑。star 我在两天里抓过两把，rotation-state 里没有这仓库的上期快照，所以本文不写增速。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/thesysdev/openui&quot;&gt;https://github.com/thesysdev/openui&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>HyperFrames 把 HTML 变成 MP4：我渲两遍逐帧对比，发现确定性有个前提</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/23/hyperframes-html-to-mp4-determinism.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/23/hyperframes-html-to-mp4-determinism.html</id>
        <updated>2026-10-23T00:00:00+08:00</updated>

        <summary>10 秒 300 帧，两遍 SHA-256 一样；可默认脚手架从 jsDelivr 取 GSAP。</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="AI" scheme="tag"/>

        <category term="前端" scheme="tag"/>

        <category term="开源项目" scheme="tag"/>

        <category term="评测" scheme="tag"/>

        <category term="工具" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/23/hyperframes-html-to-mp4-determinism.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/hyperframes-html-to-mp4-determinism/banner.webp&quot; alt=&quot;封面：HyperFrames 把 HTML 渲染成 MP4 的确定性实测&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上周给一个 PR 配 8 秒演示。改了一句按钮文案，我又打开录屏软件，重录、重剪、重传，第四遍才把鼠标移出画面。&lt;/p&gt;

&lt;p&gt;更烦的是 review。同事问「上一版和这一版差在哪」，我只能把两个 MP4 拖进播放器，来回拖进度条。&lt;/p&gt;

&lt;p&gt;HyperFrames 说这事可以变成写 HTML：页面就是视频，渲染结果是确定的。我信了一半，花一晚上渲了两遍，想知道「确定」到底值多少钱。&lt;/p&gt;

&lt;p&gt;这篇只回答一件事：它的「确定性渲染」在什么条件下成立，又在什么地方漏风。不是仓库导览，也不是把 README 翻译一遍。我装的版本是 0.8.188，跑的是一台 8GB 内存的 Mac。&lt;/p&gt;

&lt;h2 id=&quot;录屏这条路疼在改一个字要重来&quot;&gt;录屏这条路，疼在改一个字要重来&lt;/h2&gt;

&lt;p&gt;录屏的问题不是慢，是没有源文件。&lt;/p&gt;

&lt;p&gt;QuickTime 和 OBS 录下来的是像素。文案、配色、时间轴全锁在 MP4 里。改一个字，整段重来。想复制上一版的节奏，只能凭手感拖鼠标。&lt;/p&gt;

&lt;p&gt;我也试过把演示写成网页，再用脚本截图拼视频。截图好办，难的是「第 3.2 秒应该长什么样」。CSS 动画是给浏览器实时播放的，不是给你任意跳转的。跳到 3.2 秒再截，动画未必在那个位置。&lt;/p&gt;

&lt;p&gt;常见的土办法是用 Playwright 打开页面，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.getAnimations()&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;currentTime&lt;/code&gt; 统一设成 t，再截。听着可行，实际只对 Web Animations API 生效。GSAP 自己维护时间线，CSS transition 又是另一套。三套时钟得逐个对齐，漏一套就有几帧不对。&lt;/p&gt;

&lt;p&gt;Remotion 解决过这个问题：把画面写成 React 组件，用帧号驱动。代价是团队得先接受 React 加一套打包链。为一个十秒的视频，这个前置不算轻。&lt;/p&gt;

&lt;p&gt;HyperFrames 走的是另一条路。它不要求你写 React，也不要求你装剪辑软件。你写一个正常的 HTML 页面，它负责把页面按时间切成帧，再拼成 MP4。&lt;/p&gt;

&lt;p&gt;我原本以为这类工具只适合做纯动效的片头。看完它的示例清单才发现，它想覆盖的是产品发布、PR 讲解、数据图表、字幕视频这些场景。目标定得越大，我越要先确认最小那条链路靠不靠得住——片头都渲不稳，别的就不用谈。&lt;/p&gt;

&lt;h2 id=&quot;它是什么把-html-的每一帧摊开&quot;&gt;它是什么：把 HTML 的每一帧摊开&lt;/h2&gt;

&lt;p&gt;一句话定位：&lt;strong&gt;一个用 Chrome 当摄像机、按时间 seek 的 HTML 视频渲染器。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;它的输入是一个普通页面。根节点上写清楚画布尺寸和总时长：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;root&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-composition-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;main&quot;&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;data-start=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-duration=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;10&quot;&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;data-width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1920&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1080&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;title&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;clip&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-start=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-duration=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;10&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Title&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;每个要出场的东西是一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.clip&lt;/code&gt;，带自己的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-start&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-duration&lt;/code&gt;。动画不用 CSS 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;animation&lt;/code&gt; 自己跑，而是注册成一条暂停的时间线：&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tl&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;gsap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timeline&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;paused&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;tl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fromTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;#title&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.4&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;__timelines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;tl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;seek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;渲染时，它按帧率算出每个时间点，调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seek(t)&lt;/code&gt;，等画面稳定，截一张图，最后交给 FFmpeg 编码。你看到的 10 秒视频，本质上是 300 张 1920×1080 的截图。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/hyperframes-html-to-mp4-determinism/02-framework-html-to-mp4.webp&quot; alt=&quot;流程图：HyperFrames 从 HTML 到 MP4 的四步渲染链路&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这张图想说的是：确定性来自「seek 到同一个 t，画面必须一样」。只要有一个环节不受 t 控制，确定性就漏了。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;项目&lt;/th&gt;
      &lt;th&gt;读数（2026-10-23）&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;仓库&lt;/td&gt;
      &lt;td&gt;heygen-com/hyperframes，Apache-2.0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;star&lt;/td&gt;
      &lt;td&gt;70,429（GitHub API，2026-10-23 13:26 UTC）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;npm 周下载&lt;/td&gt;
      &lt;td&gt;510,551（2026-10-12 至 10-18）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;最新版&lt;/td&gt;
      &lt;td&gt;0.8.188，2026-10-23 发布&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;运行要求&lt;/td&gt;
      &lt;td&gt;Node.js 22+、FFmpeg&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;它显然不只是个 CLI。仓库里还有一整套给编码 Agent 用的 skills，README 的第一屏就在教 Claude Code、Cursor 怎么装。这也是它这周在 Trending 上的原因之一。但我更关心本地这条渲染链路，因为那才是我能验证的部分。&lt;/p&gt;

&lt;p&gt;「seekable animations」这几个字是重点，也是它和普通网页最容易混淆的地方。浏览器里的动画默认挂在实时时钟上：你打开页面，它从零开始跑。渲染器要的不是这个，而是「给我第 84 帧」这种可重复的取数。所以它要求你把动画注册到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.__timelines&lt;/code&gt;，由它来 seek。&lt;/p&gt;

&lt;p&gt;这套约定有一个直接好处：预览和渲染读的是同一条时间线。你在浏览器里拖到第 5 秒看到什么，理论上渲染出来就是什么。也埋下了后面的坑——「理论上」三个字，得靠实现来兜底。&lt;/p&gt;

&lt;p&gt;模板里还有一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hyperframes.json&lt;/code&gt;，声明 registry、blocks 和 assets 的路径。默认的 registry 指向 GitHub 上的 raw 文件。也就是说，除了 npm 包本身，你在用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt; 装区块时还会访问第二个外部地址。这个我这次没用到，但看配置文件就能看出来。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; 我没深入用，只确认了它存在。按 README 的说法，它会在浏览器里起一个带热更新的工作台。对写动画的人来说，这一步比渲染重要：你先得看见，才谈得上调。可它不在我今晚的验证范围里，我只看成片。&lt;/p&gt;

&lt;p&gt;还有一条边界值得提前说：音频。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doctor&lt;/code&gt; 里的 whisper、TTS、BGM 全是红的，都是可选项。这次渲染的产物是无声的，日志里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hasAudio: false&lt;/code&gt;。带配音、配乐的流程能不能稳定复现，我没验到，也不打算拿一个空白模板的结果替它背书。&lt;/p&gt;

&lt;h2 id=&quot;第一次-render我先卡在-chrome-下载上&quot;&gt;第一次 render：我先卡在 Chrome 下载上&lt;/h2&gt;

&lt;p&gt;安装本身不慢。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install hyperframes&lt;/code&gt; 42 秒，74 个包。npm 顺手留了一条 warning，说 esbuild 的 postinstall 没被 allowScripts 覆盖。我没去处理，渲染照样跑起来了，但这行黄字一直挂在那里，算是今晚第一处没消掉的刺。&lt;/p&gt;

&lt;p&gt;初始化也顺利：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npx hyperframes init hf-demo &lt;span class=&quot;nt&quot;&gt;--example&lt;/span&gt; blank &lt;span class=&quot;nt&quot;&gt;--non-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;生成的项目很小：一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt;、一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hyperframes.json&lt;/code&gt;、一个只有三条脚本的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;。空白模板就是一行居中的标题，时长 10 秒，1920×1080。&lt;/p&gt;

&lt;p&gt;然后跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doctor&lt;/code&gt;，第一条红字就来了：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;✓ Node.js   v24.22.0
✗ Memory    8.0 GB total · 1.6 GB available
            Low memory — renders may fail.
✓ FFmpeg    ffmpeg 9.0.1
✓ Chrome    system: /Applications/Google Chrome.app/...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这台机器只剩 1.6GB 可用内存。工具没有拦我，只是把风险写在脸上。后来渲染能跑完，但整个过程风扇没停过。&lt;/p&gt;

&lt;p&gt;真正的坑在第一次 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;命令敲下去，终端没有任何进度。我等了八分钟，只看到它在一个隐藏目录里下载 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chrome-headless-shell&lt;/code&gt;，压缩包停在 12MB。不是报错，就是不动。&lt;/p&gt;

&lt;p&gt;我一开始以为是 jsDelivr 卡了，因为脚手架从 CDN 取 GSAP。单独 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; 那个文件，1.7 秒就下来了。问题不在 CDN，在 Chrome 下载。&lt;/p&gt;

&lt;p&gt;翻 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; 里的 dist 代码，找到两个环境变量：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUPPETEER_EXECUTABLE_PATH&lt;/code&gt; 不管用，真正生效的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HYPERFRAMES_BROWSER_PATH&lt;/code&gt;。我把系统 Chrome 的路径指过去，清掉半截缓存，再跑。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;HYPERFRAMES_BROWSER_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/Applications/Google Chrome.app/Contents/MacOS/Google Chrome&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  npx hyperframes render &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; draft &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; renders/first.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这次两分钟不到就跑完了：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;90.5 KB · 10.0s video · rendered in 1m 35.6s
screenshot capture · hardware gpu · setup 9.5s · capture 1m 22.0s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;10 秒视频，渲了 95.6 秒，差不多 9.5 倍实时。日志里还有一行关键的：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;captureMode: &quot;screenshot&quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;正常路径应该用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chrome-headless-shell&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HeadlessExperimental.beginFrame&lt;/code&gt;，逐帧推进；用普通 Chrome 时，这条路走不通，只能退回截图模式。所以上面这个速度，是降级之后的速度。优化路径我这次没验到，因为 Chrome 就是下载不下来。&lt;/p&gt;

&lt;p&gt;这里有个判断我一开始做反了。看到「下载 Chromium」卡住，我的第一反应是网络慢，想换镜像。实际是工具在装一个专用的 headless shell，和系统 Chrome 不是一个东西。至少从输出看，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doctor&lt;/code&gt; 只报了系统 Chrome 的位置，没提这个专用包；所以它全绿，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; 仍然会去下载。这两条检查没打通，用户很容易在第一次渲染时卡住。&lt;/p&gt;

&lt;p&gt;我也想过干脆用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--docker&lt;/code&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doctor&lt;/code&gt; 里 Docker 是红的，本机没装，这条路直接断了。所以下面所有读数，都是「系统 Chrome + 截图降级路径」下的读数，不代表它在优化路径上的表现。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/hyperframes-html-to-mp4-determinism/01-screenshot-terminal.webp&quot; alt=&quot;终端截图：doctor 的内存告警与首次渲染的真实耗时&quot; /&gt;&lt;/p&gt;

&lt;p&gt;顺带一提，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint&lt;/code&gt; 对这个空白项目给了 0 errors、0 warnings。它没意见。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;info&lt;/code&gt; 报的是 1920×1080、10 秒、2 个元素、1 条轨道。项目小到没什么可挑的，这也意味着后面的确定性是最好情况，不是平均水平。&lt;/p&gt;

&lt;h2 id=&quot;它说确定性我渲了两遍逐帧比&quot;&gt;它说确定性，我渲了两遍逐帧比&lt;/h2&gt;

&lt;p&gt;README 的原话是「turning HTML, CSS, media, and seekable animations into deterministic MP4 videos」。&lt;/p&gt;

&lt;p&gt;确定性这件事，光看文档不算。我把同一个项目、同一个命令、同一台机器，连着渲了第二遍。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npx hyperframes render &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; draft &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; renders/second.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第二遍 1 分 27.2 秒，文件也是 90.5KB。比第一遍快了 8 秒，这 8 秒纯粹是机器状态差异：第一遍刚装完依赖，缓存是冷的；第二遍编译只花了 1.8 秒，第一遍是 3.8 秒。耗时变了，产物没变，这恰恰是确定性该有的样子。&lt;/p&gt;

&lt;p&gt;然后比两件事：整个文件的 SHA-256，以及逐帧的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;framemd5&lt;/code&gt;。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ff228253d47310a39b6033421f71396bd9728005d72590a97872a55bcdffd6c0  first.mp4
ff228253d47310a39b6033421f71396bd9728005d72590a97872a55bcdffd6c0  second.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;两个文件一模一样。不是「画面看起来一样」，是字节级相同。&lt;/p&gt;

&lt;p&gt;这一点有点出乎我意料。FFmpeg 默认会在容器里写创建时间和编码器版本之类的元数据，两次跑的 mtime 不同，文件通常不会字节相同。HyperFrames 显然在这层做了处理，把能固定的都固定了。至于它具体改了哪些 muxer 参数，我没往下追。&lt;/p&gt;

&lt;p&gt;再用 FFmpeg 把每一帧解出来做哈希，310 行输出（含文件头），&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff&lt;/code&gt; 退出码 0。300 帧、30fps、1920×1080、h264、yuv420p，全部对得上。&lt;/p&gt;

&lt;p&gt;逐帧哈希比文件哈希更能说明问题。文件哈希只能证明「这两个文件一样」；帧哈希能证明「第 0 帧到第 299 帧，每一帧都一样」。如果哪天容器元数据变了、文件哈希对不上，但帧哈希还对得上，那画面仍然是可复现的。反过来，文件一样但帧不一样，几乎不可能发生。&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;1m 35.6s&lt;/td&gt;
      &lt;td&gt;1m 27.2s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;文件大小&lt;/td&gt;
      &lt;td&gt;90.5 KB&lt;/td&gt;
      &lt;td&gt;90.5 KB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;帧数&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
      &lt;td&gt;300&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SHA-256&lt;/td&gt;
      &lt;td&gt;ff2282…d6c0&lt;/td&gt;
      &lt;td&gt;ff2282…d6c0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;framemd5 差异&lt;/td&gt;
      &lt;td&gt;0 行&lt;/td&gt;
      &lt;td&gt;0 行&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;在这台机器、这个 Chrome、这份 HTML 上，它确实做到了字节级可复现。&lt;/strong&gt; 这个结论比我预期强。&lt;/p&gt;

&lt;p&gt;强度也要标清楚：我只比了「同一台机器、同一个 Chrome 155、同一个 Node 24.22、没有音频、没有外部图片」。跨机器、跨浏览器版本、带音频、带视频素材的情况，一个都没测。所以这不是「HyperFrames 永远确定」，是「在这个最小条件下，它确定」。&lt;/p&gt;

&lt;p&gt;但我没有就此收工。&lt;/p&gt;

&lt;h2 id=&quot;三个缺口cdnrender-与-snapshot-分叉raf&quot;&gt;三个缺口：CDN、render 与 snapshot 分叉、rAF&lt;/h2&gt;

&lt;p&gt;确定性不是产品性质，是输入的性质。只要有一个输入不受控，输出就不确定。&lt;/p&gt;

&lt;p&gt;第一个缺口就在默认脚手架里。空白模板的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 第一行是：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GSAP 从公网 CDN 取。仓库里有个还开着的 issue，编号 #6028，2026-10-21 提的，标题写得很直白：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-to-video: assemble-index.mjs hardcodes the jsDelivr CDN for GSAP (breaks offline / restricted-network renders)&lt;/code&gt;。提的是另一个子项目，但默认模板是同一个毛病。&lt;/p&gt;

&lt;p&gt;我把 CDN 地址换成一个不存在的域名，再跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snapshot&lt;/code&gt;。退出码 0，没有任何报错。截出来的图还是那张，标题端端正正——只是动画没了。&lt;/p&gt;

&lt;p&gt;这比报错更难受。&lt;strong&gt;它不会失败，只会安静地少掉一层。&lt;/strong&gt; 如果这个项目再接进 CI，你拿到的是一份「成功」的静态视频。&lt;/p&gt;

&lt;p&gt;想补这个洞也简单：把 GSAP 下载到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets/&lt;/code&gt;，改成相对路径。仓库里已经有模板这么做了吗？我没逐个翻。至少我生成的空白模板没做。&lt;/p&gt;

&lt;p&gt;第二个缺口是 render 和 snapshot 走两套逻辑。issue #5959，2026-10-20：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; 不会给图片 clip 应用 GSAP 的 scale/position tween，而 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snapshot&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyframes&lt;/code&gt; 是对的。也就是说，预览和成片可能不是一个东西。对「所见即所得」的承诺来说，这比慢更严重。&lt;/p&gt;

&lt;p&gt;它为什么危险？因为你的验收流程通常是先看 snapshot。snapshot 对了，你以为成了，交付出去的 render 却是另一个画面。这类错误不会在日志里出现，只会在别人点开视频时出现。&lt;/p&gt;

&lt;p&gt;第三个缺口藏在帧边界。issue #5888，2026-10-19：PNG 序列在 seek 之后，可能截到还没提交的 virtual-rAF DOM 更新。一次两次看不出来，定时跑就可能偶尔闪一帧。&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;CDN 依赖&lt;/td&gt;
      &lt;td&gt;jsDelivr 不可达&lt;/td&gt;
      &lt;td&gt;snapshot 退出 0，动画静默消失&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;render/snapshot 分叉&lt;/td&gt;
      &lt;td&gt;图片 clip + scale/position tween&lt;/td&gt;
      &lt;td&gt;未亲测，issue #5959 仍开着&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;rAF 提交时机&lt;/td&gt;
      &lt;td&gt;PNG 序列 + seek 后更新&lt;/td&gt;
      &lt;td&gt;未亲测，issue #5888 仍开着&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这三条不影响我上面那次复测的结论，却决定了这个结论能延伸到哪。空白模板、静态标题、同一个 Chrome，是最干净的条件。换成真实项目，先问自己：所有资源都在本地吗？预览和成片用的是同一条路径吗？&lt;/p&gt;

&lt;h2 id=&quot;和-remotion录屏比&quot;&gt;和 Remotion、录屏比&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;HyperFrames&lt;/th&gt;
      &lt;th&gt;Remotion&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;HTML/CSS + seek 时间线&lt;/td&gt;
      &lt;td&gt;React 组件 + 帧号&lt;/td&gt;
      &lt;td&gt;手动操作&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;前置&lt;/td&gt;
      &lt;td&gt;Node 22、FFmpeg&lt;/td&gt;
      &lt;td&gt;React、打包链&lt;/td&gt;
      &lt;td&gt;无&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;可 diff&lt;/td&gt;
      &lt;td&gt;是，MP4 可逐帧比&lt;/td&gt;
      &lt;td&gt;是&lt;/td&gt;
      &lt;td&gt;否&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;我这台机器上的 10 秒速度&lt;/td&gt;
      &lt;td&gt;95.6s（截图降级路径）&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;td&gt;人眼漏看&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;这张表的结论不是谁更强，是「什么团队该走哪条路」。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;已经全员 React 的团队，Remotion 更顺：组件、props、生态都是现成的。设计稿本来就是 HTML/CSS 的团队，HyperFrames 少一层翻译，改一行文字就能重渲。&lt;/p&gt;

&lt;p&gt;迁移成本也得算。Remotion 的学习成本不在 API，在「怎么把一个网页视觉拆成可组合的组件」；HyperFrames 的学习成本在时间线：哪些动画能 seek、哪些属性不能碰、外部资源放哪里。两边都不便宜，只是花的地方不同。&lt;/p&gt;

&lt;p&gt;只做一次、不打算维护的 30 秒演示，录屏仍然最快。别为了「工程化」给一次性任务上构建链。&lt;/p&gt;

&lt;p&gt;真正拉开差距的是「改第十遍」的时候。录屏的第十遍和第一遍一样贵；HyperFrames 的第十遍只改源代码，再等一分半。&lt;/p&gt;

&lt;p&gt;速度这一栏我没法给 Remotion 一个公平数字，因为没在同一台机器上跑过它的同类项目。所以别拿 95.6 秒去和任何 Remotion 基准比。我能负责的只有 HyperFrames 这一格。&lt;/p&gt;

&lt;p&gt;还有一个差别在「谁来写」。Remotion 的组件更像传统前端工程，人和 Agent 都能写，但 Agent 要先理解你的组件库。HyperFrames 把技能包直接发给 Agent，让它照着模板写 HTML。前者可控，后者上手快。团队里谁维护这段代码，比技术本身更影响选择。&lt;/p&gt;

&lt;h2 id=&quot;谁该用谁先别用三步上手&quot;&gt;谁该用，谁先别用：三步上手&lt;/h2&gt;

&lt;p&gt;适合的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;要反复改文案、配色、节奏的产品演示和发布视频。&lt;/li&gt;
  &lt;li&gt;已经有 HTML/CSS 视觉资产，不想再翻译成 React。&lt;/li&gt;
  &lt;li&gt;想把「生成演示视频」放进 Agent 或 CI 流水线的人。&lt;/li&gt;
  &lt;li&gt;数据动画、图表竞速这类画面本来就该由代码生成的题材。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;先别用的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;需要真人出镜、多轨剪辑、复杂音频混音的。它是渲染器，不是剪辑软件。&lt;/li&gt;
  &lt;li&gt;内存吃紧的机器。我这台只剩 1.6GB，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doctor&lt;/code&gt; 直接给了黄牌。&lt;/li&gt;
  &lt;li&gt;离线或受限网络环境，又没把 GSAP 之类的外部脚本搬进本地。&lt;/li&gt;
  &lt;li&gt;要求跨机器、跨 Chrome 版本绝对一致，又不打算用 Docker 的团队。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render --docker&lt;/code&gt; 我没验到。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;还有一个隐性成本：它是 0.x 版本。今天 0.8.188，一周好几个 release。对个人项目这是活力，对要长期维护的生产流水线就是升级噪声。我今晚装的版本，下周可能就不是默认版本了。&lt;/p&gt;

&lt;p&gt;三步上手，命令都能直接粘：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. 装依赖，起一个空白项目&lt;/span&gt;
npm &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;hyperframes
npx hyperframes init my-video &lt;span class=&quot;nt&quot;&gt;--example&lt;/span&gt; blank &lt;span class=&quot;nt&quot;&gt;--non-interactive&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 2. 先体检，尤其看内存和 Chrome 路径&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;my-video
npx hyperframes doctor

&lt;span class=&quot;c&quot;&gt;# 3. 渲两遍，比哈希&lt;/span&gt;
npx hyperframes render &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; draft &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; renders/a.mp4
npx hyperframes render &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; draft &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; 1 &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; renders/b.mp4
shasum &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; 256 renders/a.mp4 renders/b.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第三步是我建议所有人都做一次的。两行哈希一样，你才真正拿到了「确定性」；不一样，先别急着接 CI。&lt;/p&gt;

&lt;p&gt;接下来可以盯一个信号：issue #5959 什么时候关。如果 render 和 snapshot 的分叉修掉，它才配得上「所见即所得」这四个字。&lt;/p&gt;

&lt;h2 id=&quot;我的判断当可-diff-的动效别当剪辑软件&quot;&gt;我的判断：当「可 diff 的动效」，别当剪辑软件&lt;/h2&gt;

&lt;p&gt;我的判断很明确：&lt;strong&gt;把它当成「可 diff 的动效源码」，值得试；当成剪辑软件，会失望。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;它最值钱的地方不是渲染快——9.5 倍实时一点也不快——是改完之后，你手里有源文件，也有一条能逐帧比对的产物。上一版和这一版差在哪，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;framemd5&lt;/code&gt; 会告诉你，不用再拖进度条。&lt;/p&gt;

&lt;p&gt;代价也写清楚：默认模板依赖公网 CDN；用普通 Chrome 会掉进截图降级路径；render 和 snapshot 还有已知分叉。这三条都不致命，但每一条都能让「确定性」变成一句口号。&lt;/p&gt;

&lt;p&gt;我给自己定了个可检验的条件：如果到 11 月底，issue #5959 还开着，我不会把它放进任何自动出片的流水线，只拿它做本地手动渲染。&lt;/p&gt;

&lt;p&gt;这条判断也可能错。如果维护者认为 render 和 snapshot 本来就该有两套语义，那 #5959 不算缺口，是我理解错了契约。真到那天，我会把这一节改掉。&lt;/p&gt;

&lt;p&gt;回到开头那个 PR。下次再改按钮文案，我希望源文件是一个 HTML，改完重渲，比一遍哈希。鼠标不用再移出画面。&lt;/p&gt;

&lt;p&gt;至于这台 8GB 的机器，我大概会先把内存加上去。渲染器再省，Chrome 还是要吃饭的。&lt;/p&gt;

&lt;p&gt;项目地址：https://github.com/heygen-com/hyperframes&lt;/p&gt;

&lt;p&gt;参考：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;README 与 Quick Start：https://github.com/heygen-com/hyperframes（抓取 2026-10-23）&lt;/li&gt;
  &lt;li&gt;issue #6028：https://github.com/heygen-com/hyperframes/issues/6028（2026-10-21）&lt;/li&gt;
  &lt;li&gt;issue #5959：https://github.com/heygen-com/hyperframes/issues/5959（2026-10-20）&lt;/li&gt;
  &lt;li&gt;issue #5888：https://github.com/heygen-com/hyperframes/issues/5888（2026-10-19）&lt;/li&gt;
  &lt;li&gt;npm 下载量：https://api.npmjs.org/downloads/point/last-week/hyperframes（抓取 2026-10-23）&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>

    <entry>
        <title>Anthropic 三年来首次改 API 版本号：temperature 传了就 400，兼容层这一次最疼</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/23/anthropic-api-2026-10-21-sampling-400.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/23/anthropic-api-2026-10-21-sampling-400.html</id>
        <updated>2026-10-23T00:00:00+08:00</updated>

        <summary>2023-06-01 之后第一次换版本；我在本机抓了两个 SDK 的请求头</summary>


        <category term="AI"/>


        <category term="AI" scheme="tag"/>

        <category term="Anthropic" scheme="tag"/>

        <category term="Claude" scheme="tag"/>

        <category term="评测" scheme="tag"/>

        <category term="安全" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/23/anthropic-api-2026-10-21-sampling-400.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/anthropic-api-2026-10-21-sampling-400/banner.webp&quot; alt=&quot;封面：Anthropic API 版本跳到 2026-10-21，采样参数被拒&quot; /&gt;&lt;/p&gt;

&lt;p&gt;2026-10-23 晚上十点四十，我在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp&lt;/code&gt; 里起了个不到三十行的 HTTP 服务。它不调用模型，只把收到的请求头和请求体打印到文件里。&lt;/p&gt;

&lt;p&gt;然后把同一个请求，分别交给 Anthropic 的 Python SDK 1.17.0 和 2.2.0。&lt;/p&gt;

&lt;p&gt;前者发出去的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic-version: 2023-06-01&lt;/code&gt;，后者是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt;。两行字，隔了三年。&lt;/p&gt;

&lt;p&gt;版本号本身不吓人。吓人的是官方迁移文档里那句话：从 2026-10-21 起，请求里只要带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;top_p&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;top_k&lt;/code&gt;，哪怕填的是默认值，也直接返回 400。&lt;/p&gt;

&lt;p&gt;这篇不打算复述公告。我想回答三个问题：这次到底改了什么；我在本机验到了哪一步；以及，谁会被这个 400 真正打中。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;10-月-21-日-2351python-sdk-的版本常量换了&quot;&gt;10 月 21 日 23:51，Python SDK 的版本常量换了&lt;/h2&gt;

&lt;p&gt;先把时间线摊开。下面每一条都能在官方仓库或迁移文档里对上。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;时间（UTC+8）&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;2026-08-21 03:58&lt;/td&gt;
      &lt;td&gt;anthropic-sdk-python&lt;/td&gt;
      &lt;td&gt;v1.0.0 发布；从这一代起，方法签名里已经没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;已核（迁移指南）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-21 06:13&lt;/td&gt;
      &lt;td&gt;anthropic-sdk-python&lt;/td&gt;
      &lt;td&gt;v1.17.0，最后一个 1.x；默认仍发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;已核&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-21 23:51&lt;/td&gt;
      &lt;td&gt;anthropic-sdk-python&lt;/td&gt;
      &lt;td&gt;v2.0.0；默认 API 版本切到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;已核&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-22 06:00&lt;/td&gt;
      &lt;td&gt;anthropic-sdk-python&lt;/td&gt;
      &lt;td&gt;v2.1.0&lt;/td&gt;
      &lt;td&gt;已核&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-23 21:10&lt;/td&gt;
      &lt;td&gt;anthropic-sdk-python&lt;/td&gt;
      &lt;td&gt;v2.2.0，新增 message threads&lt;/td&gt;
      &lt;td&gt;已核&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/anthropic-api-2026-10-21-sampling-400/02-timeline-api-version.webp&quot; alt=&quot;时间线：五个 SDK 版本节点，10 月 21 日的 v2.0.0 标红&quot; /&gt;&lt;/p&gt;

&lt;p&gt;官方迁移文档的标题很直白：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Migrating to API version 2026-10-21&lt;/code&gt;。第一段写的是，这份指南列出“从 API 版本 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt; 移到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt;”要改的代码。&lt;/p&gt;

&lt;p&gt;也就是说，中间这三年，Anthropic 的公开 API 版本号一直没动。这次是 2023 年之后第一次换。&lt;/p&gt;

&lt;p&gt;先把“API 版本”说清楚。它不是模型版本，也不是 SDK 版本，而是请求头里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic-version&lt;/code&gt;。你用官方 SDK，这个头由 SDK 的常量决定；你写裸 HTTP，就得自己填。填 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt;，服务端按三年前的约定解析你的 JSON；填 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt;，它按新约定解析。&lt;/p&gt;

&lt;p&gt;同一个请求体，换一个头，可能是 200，也可能是 400。这就是版本头存在的意义，也是这次迁移最容易被低估的地方：很多人以为自己只是“升了个包”，其实是在换一份协议。&lt;/p&gt;

&lt;p&gt;我核到的一个细节是：v2.0.0 的 release note 只写了“add support for new API version 2026-10-21”，没有把删参数、改字段这些事逐条列出来。真正的变更清单在迁移文档里。只读 release note 的人，会以为这是一次普通的功能更新。&lt;/p&gt;

&lt;p&gt;事实部分到这里：日期、版本号、默认请求头，我都核过了。至于“2023 年以来第一次”，我依据的是迁移文档自己写的起止版本，没有找到官方明确说“这是三年来首次”的句子。官方开发者账号在 X 上有一条类似表述，但那条帖子我这边打不开，所以不把它当证据。&lt;/p&gt;

&lt;p&gt;官方 newsroom 在 2026-10-23 的列表里，也没有单独的 API 版本公告；最近的条目是 10-23 的 Agent Skills 捐赠和 10-21 的信托人事。公告落在文档和开发者账号，这大概解释了为什么不少人直到 SDK 发版才发现。&lt;/p&gt;

&lt;p&gt;还有一个不太体面的细节：Anthropic 的官方文档站在我这边会被跳到地区限制页。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; 返回 200，内容却是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app-unavailable-in-region&lt;/code&gt;。我实际读的是官方文档的一个 GitHub 镜像仓库，并拿 Anthropic 官方 SDK 仓库里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MIGRATION.md&lt;/code&gt; 交叉确认了链接和版本号。原文措辞我尽量逐字引，但你要说“我打开了官方域名”，没有。&lt;/p&gt;

&lt;h2 id=&quot;三条会直接回-400-的-messages-变更&quot;&gt;三条会直接回 400 的 Messages 变更&lt;/h2&gt;

&lt;p&gt;这次不是加一个 beta 头就能绕过去的小改。迁移文档把三条 Messages API 变更列在“Breaking changes”下面，触发条件都很具体。&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;Messages 请求或 Message Batch 请求里带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;top_p&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;top_k&lt;/code&gt;，默认值也不行&lt;/td&gt;
      &lt;td&gt;删掉；旧模型确实需要时，只能把请求留在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;user 消息里的 citations 被拒&lt;/td&gt;
      &lt;td&gt;user 消息的 text block 带非空 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;citations&lt;/code&gt;，包括 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search_result&lt;/code&gt; 里面&lt;/td&gt;
      &lt;td&gt;去掉这些 block 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;citations&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;assistant 消息里的 document 块被拒&lt;/td&gt;
      &lt;td&gt;直接把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document&lt;/code&gt; block 放在 assistant 消息里&lt;/td&gt;
      &lt;td&gt;把文档放进 user 消息；嵌套在工具结果里的仍可用&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;采样参数这条最狠。文档的原话是：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 里，发送这三个参数的请求会返回 400，即使传的是参数的默认值。&lt;/p&gt;

&lt;p&gt;它还补了一句容易被忽略的后果：不带这些参数时，请求按默认值采样，包括 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt; 为 1.0；对 Claude Sonnet 4.6 这类还能接受其他值的模型，原来靠调低 temperature 换稳定输出的做法，效果没有了，而且没有替代设置。&lt;/p&gt;

&lt;p&gt;这句话对做结构化抽取、分类、评测的人影响最大。以前大家把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature: 0&lt;/code&gt; 当成“尽量确定性”的开关。现在在新版本上，这个开关不是失效，是直接把请求打回去。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;我在这儿要收一下：400 是官方声称，我没有真的打到 API。&lt;/strong&gt; 本机没有可用的 Anthropic key，付费调用我也不跑。所以我能证明的是“SDK 会发什么”，不能证明“服务端一定回什么”。这两件事在我这里必须分开。&lt;/p&gt;

&lt;p&gt;这两层错误也不一样。SDK 签名层是本地错误：你多传一个参数，Python 在发请求之前就抛 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TypeError&lt;/code&gt;，连网络都不会出。API 版本层是服务端错误：请求发出去了，服务端读完 JSON，发现这个版本不认这个字段，回你 400。&lt;/p&gt;

&lt;p&gt;前者好查，堆栈直接指到你那一行代码。后者难查，因为错误发生在别人的服务器上，你的日志里只有一个状态码和一段错误消息。如果网关还把上游的 400 包装成 500，你连“是参数问题”都看不出来。&lt;/p&gt;

&lt;p&gt;所以这次迁移最该先做的不是升级，是搞清楚自己的请求到底经过了哪几层。&lt;/p&gt;

&lt;p&gt;边界再补一条：这些变更不只打在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/messages&lt;/code&gt;。token 计数和 Message Batch 的每个请求也走同一套规则，只要那个 batch 是用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 创建的。在线请求改完了，离线批处理可能还在用旧格式，第一次跑新 batch 才会炸。&lt;/p&gt;

&lt;h2 id=&quot;我在本机截了两个-sdk-的请求头&quot;&gt;我在本机截了两个 SDK 的请求头&lt;/h2&gt;

&lt;p&gt;复现环境不复杂：macOS，Python 3.12，用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; 建了两个虚拟环境，分别装 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic==1.17.0&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic==2.2.0&lt;/code&gt;。再起一个本地 HTTP 服务，把请求头和 JSON body 原样写进日志。&lt;/p&gt;

&lt;p&gt;第一次装就翻车了。系统自带的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python3&lt;/code&gt; 是 3.8，而 1.x 和 2.x 都要求 3.10 以上，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt; 报的是“找不到匹配版本”。我盯着那串从 0.2.2 排到 0.72.0 的版本列表看了半分钟，才反应过来不是网络问题，是解释器太老。换成 3.12 之后一次就装上了。&lt;/p&gt;

&lt;p&gt;这个本地服务会返回一个最小的 Messages 响应，所以 SDK 能正常走完解析，不会因为响应格式不对提前抛错。我要看的不是模型输出，是请求离开本机之前长什么样。&lt;/p&gt;

&lt;p&gt;结果如下，都是本机 2026-10-23 22:40 前后的真实输出：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SDK 1.17.0 default anthropic-version 2023-06-01
sampling params in messages.create signature: NONE
call with temperature=0.2: TypeError - Messages.create() got an unexpected keyword argument &apos;temperature&apos;
call without sampling params: OK
server saw: {&quot;anthropic-version&quot;: &quot;2023-06-01&quot;, &quot;body&quot;: {&quot;max_tokens&quot;: 16, &quot;messages&quot;: [...], &quot;model&quot;: &quot;claude-sonnet-4-6&quot;}}

SDK 2.2.0 default anthropic-version 2026-10-21
sampling params in messages.create signature: NONE
call with temperature=0.2: TypeError - Messages.create() got an unexpected keyword argument &apos;temperature&apos;
call without sampling params: OK
server saw: {&quot;anthropic-version&quot;: &quot;2026-10-21&quot;, &quot;body&quot;: {&quot;max_tokens&quot;: 16, &quot;messages&quot;: [...], &quot;model&quot;: &quot;claude-sonnet-4-6&quot;}}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里有个和我预想相反的地方。&lt;/p&gt;

&lt;p&gt;我原本以为，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt; 是在 2.0 这次大版本才从 Python SDK 里删掉的，所以“锁在 1.x 就安全”。实际测完发现，1.16.0 和 1.17.0 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;messages.create&lt;/code&gt; 签名里也早就没有它了。再往上查，v1.0.0 发布于 2026-08-21 03:58（UTC+8），从那一代起就已经不收这三个参数。&lt;/p&gt;

&lt;p&gt;所以我最初那版判断是错的：&lt;strong&gt;这次大版本并没有突然删掉 Python SDK 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt;，它只是把默认请求头换成了新版本。&lt;/strong&gt; SDK 层面的“删参数”已经发生了两个月。&lt;/p&gt;

&lt;p&gt;那为什么还会有人被打中？&lt;/p&gt;

&lt;p&gt;因为 SDK 留了一扇后门：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body&lt;/code&gt;。迁移文档明确写了，如果旧模型还需要采样设置，可以用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body={&quot;temperature&quot;: 0.2}&lt;/code&gt;，这个字典会被原样合并进请求 JSON。&lt;/p&gt;

&lt;p&gt;我试了。2.2.0 加上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body&lt;/code&gt; 后，本地服务收到的是：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{&quot;anthropic-version&quot;: &quot;2026-10-21&quot;, &quot;body&quot;: {..., &quot;temperature&quot;: 0.2}}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;头部是新版本，body 里有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt;。按官方文档，这正是会返回 400 的组合。SDK 不会在本地拦你，它把这颗子弹原样递出去。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/anthropic-api-2026-10-21-sampling-400/01-screenshot-sdk-headers.webp&quot; alt=&quot;终端截图：1.17.0 与 2.2.0 发出的 anthropic-version 请求头，以及 extra_body 把 temperature 带进新版本请求体&quot; /&gt;&lt;/p&gt;

&lt;p&gt;TypeScript 那边我也顺手看了一眼。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@anthropic-ai/sdk@1.1.0&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;messages.d.ts&lt;/code&gt; 里，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt; 出现 0 次。两个 SDK 在签名层是一致的：都不给你这个参数。Python SDK 还留了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body&lt;/code&gt; 这个逃生口。&lt;/p&gt;

&lt;h2 id=&quot;被-400-打中的是夹在中间的兼容层&quot;&gt;被 400 打中的，是夹在中间的兼容层&lt;/h2&gt;

&lt;p&gt;把人群拆开看，结论会清楚很多。不是所有用 Claude 的人都要今天动手。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;你属于哪类&lt;/th&gt;
      &lt;th&gt;会不会被 400 打中&lt;/th&gt;
      &lt;th&gt;现在最该做的一件事&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;用官方 Python/TS SDK、留在 1.x 或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;不会立刻；升级 2.x 才会换版本头&lt;/td&gt;
      &lt;td&gt;先查 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body&lt;/code&gt; 里有没有 sampling 参数&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;裸 HTTP、curl、自研网关&lt;/td&gt;
      &lt;td&gt;会，只要你照旧发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;显式选版本：要么删参数升新版，要么钉住旧版&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;LLM 兼容层（LiteLLM 这类）&lt;/td&gt;
      &lt;td&gt;看实现：写死旧版本头的暂时不会，但也被锁在旧版本&lt;/td&gt;
      &lt;td&gt;建一张“模型 → 版本头”的映射表&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;读 Admin API / Analytics 的后台&lt;/td&gt;
      &lt;td&gt;不一定是 400，但旧字段会读不到&lt;/td&gt;
      &lt;td&gt;按新字段名改脚本和看板&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Bedrock 上的老模型&lt;/td&gt;
      &lt;td&gt;新版本不可用&lt;/td&gt;
      &lt;td&gt;留在旧 SDK，或先换到 Claude 4.7+&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/anthropic-api-2026-10-21-sampling-400/03-framework-request-layers.webp&quot; alt=&quot;分层图：业务代码、SDK 或兼容层、版本头、API 服务端四层，兼容层标红&quot; /&gt;&lt;/p&gt;

&lt;p&gt;有三类人可以先不动。&lt;/p&gt;

&lt;p&gt;第一类，Bedrock 上跑 Claude 4.7 之前模型的。新版本对你们不可用，升级 SDK 只会让自己同时面对两套边界，收益为零。&lt;/p&gt;

&lt;p&gt;第二类，评测集强依赖低 temperature 的。新版本没有替代设置，这不是“改个参数”能解决的，先想清楚输出波动能不能接受，再谈迁移。&lt;/p&gt;

&lt;p&gt;第三类，网关版本头写死、且短期内没有新模型需求的。你们不会 400，也不会拿到新行为；先把版本头改成配置项，比仓促升级更值。&lt;/p&gt;

&lt;p&gt;LiteLLM 是个很好的样本。我 2026-10-23 晚上看了它的仓库：主 Anthropic 适配器、Azure AI 的 Anthropic 适配器、DeepSeek 的 Messages 适配器，都把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic-version&lt;/code&gt; 写死成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt;；代码搜索里，Rust 版的常量也是同一个值。&lt;/p&gt;

&lt;p&gt;这意味着两件事。&lt;/p&gt;

&lt;p&gt;第一，大量通过 LiteLLM 调 Claude 的应用，不会因为这次变更突然 400。兼容层替你挡住了。&lt;/p&gt;

&lt;p&gt;第二，它们也拿不到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 的行为。版本头是写死的，模型能换，协议不会自己往前走。&lt;/p&gt;

&lt;p&gt;这就是“兼容层”的代价：它让上层代码不用改，也让上层代码永远慢一拍。平时这是优点，协议换代时，它变成账单。&lt;/p&gt;

&lt;p&gt;Admin API 那条更隐蔽。它不是 400，是字段悄悄换名：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;roles&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;role_ids&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;group_id&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rbac_group_id&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;display_name&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;group_type&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;group.type&lt;/code&gt;、API key 上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workspace_id&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scope&lt;/code&gt;；Analytics 里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actor.email&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actor.email_address&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;summaries&lt;/code&gt; 变 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;读这些字段的看板、成本脚本、权限同步任务，不会在请求时炸。它们会在某天早上显示一张空表，或者把所有人都算成“无 workspace”。&lt;/p&gt;

&lt;p&gt;这类变更不会出现在 400 的统计里，所以最容易漏。把 Admin 字段的读取也放进同一次迁移清单，别等看板空了才回头找。&lt;/p&gt;

&lt;h2 id=&quot;八个客户端一起跳大版本bedrock-还留了一道门&quot;&gt;八个客户端一起跳大版本，Bedrock 还留了一道门&lt;/h2&gt;

&lt;p&gt;这次不是只有 Python 动。迁移文档给出的“第一个使用 2026-10-21 的版本”是一张跨语言清单：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Python &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic&lt;/code&gt; 2.0.0&lt;/li&gt;
  &lt;li&gt;TypeScript &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@anthropic-ai/sdk&lt;/code&gt; 1.0.0&lt;/li&gt;
  &lt;li&gt;Go SDK 2.0.0，模块路径改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v2&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Java &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.anthropic:anthropic-java&lt;/code&gt; 3.0.0&lt;/li&gt;
  &lt;li&gt;C# &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Anthropic&lt;/code&gt; 13.0.0&lt;/li&gt;
  &lt;li&gt;Ruby gem 2.0.0&lt;/li&gt;
  &lt;li&gt;PHP SDK 1.0.0&lt;/li&gt;
  &lt;li&gt;CLI &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ant&lt;/code&gt; 2.0.0，Go 安装路径同样要加 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;八个客户端同时跳大版本，这件事本身就说明它不是“加个参数”。如果只是加功能，没必要让所有语言的模块路径和主版本一起动。&lt;/p&gt;

&lt;p&gt;跨语言大版本的成本不在代码量，在协调。Go 的模块路径加了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v2&lt;/code&gt;，旧 import 会继续装 v1，而且不报错；C# 从 12 直接跳到 13；PHP 只跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;composer update&lt;/code&gt; 还会留在旧约束里。每一门语言都有一个“看起来升级了、其实没有”的坑。&lt;/p&gt;

&lt;p&gt;还有一道门：Amazon Bedrock 上，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 只对 Claude 4.7 及更新模型可用。老模型要么继续用现在的 SDK，要么继续发旧版本头；想用新版本，得先把模型换到 4.7 以上。&lt;/p&gt;

&lt;p&gt;这条对我的读者很实际。很多团队不是直连 Anthropic，而是走 Bedrock 或云厂商的托管通道。你的代码升了 2.x，模型还在老版本，请求会被版本边界挡住。迁移顺序必须是“先换模型，再换协议”，不是反过来。&lt;/p&gt;

&lt;p&gt;SDK 内部还有两类改动，容易被“我只用 messages.create”的人忽略。&lt;/p&gt;

&lt;p&gt;一类是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageParam&lt;/code&gt; 从单一类型变成按角色拆开的联合类型。Python 里以前可以写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageParam(role=&quot;user&quot;, content=&quot;...&quot;)&lt;/code&gt;，现在这样调用会抛 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TypeError&lt;/code&gt;，要改成 dict；TypeScript 里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface X extends MessageParam&lt;/code&gt; 也不再合法，得改成类型别名。请求发出去的 JSON 没变，变的是你的类型体操。&lt;/p&gt;

&lt;p&gt;另一类是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output_format&lt;/code&gt; 让位给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output_config&lt;/code&gt;。结构化输出的 schema 字典不再走 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output_format=&lt;/code&gt;，要走 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output_config={&quot;format&quot;: {...}}&lt;/code&gt;；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output_format=&lt;/code&gt; 只接受类型（比如 Pydantic 模型或 TS 类型）。&lt;/p&gt;

&lt;p&gt;这两类不会让线上立刻挂，但会让 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mypy&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyright&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsc&lt;/code&gt; 红一片。如果你把类型检查放在 CI 里，它们会替你提前发现大部分迁移点。这也是我建议先升 SDK、先让类型检查跑一遍，再动线上流量的原因。&lt;/p&gt;

&lt;h2 id=&quot;半天能做完的迁移三条-grep-和一次金丝雀&quot;&gt;半天能做完的迁移：三条 grep 和一次金丝雀&lt;/h2&gt;

&lt;p&gt;如果你只是用官方 SDK 调当前模型，迁移量可能比想象中小。真正花时间的是找那些“不在 SDK 里”的调用。&lt;/p&gt;

&lt;p&gt;先跑三条搜索，范围包括业务代码、网关配置、部署脚本和文档示例：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1) 谁还在发采样参数&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;\b(temperature|top_p|top_k)\b&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.py&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.ts&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.js&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.go&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.yaml&apos;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 2) 谁在显式设置 API 版本&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rn&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;anthropic-version&apos;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 3) 谁在读旧 Admin 字段&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rnE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;\b(role_ids|rbac_group_id|display_name|group\.type|actor\.email_address)\b|workspace_id|summaries&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.py&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.ts&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--include&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;*.sql&apos;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第一条的结果要分两类看：走官方 SDK 的，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature=&lt;/code&gt; 早就编译不过，能搜到的大多是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body&lt;/code&gt;、裸 HTTP、或者别的语言的客户端；走兼容层的，看它有没有把参数透传下去。&lt;/p&gt;

&lt;p&gt;第二条决定你会不会真的收到 400。写死 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt; 的，短期安全，长期落后；写死 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 的，必须把第一条清干净；从客户端透传的，最危险，因为版本头和参数可能来自两个不同年代。&lt;/p&gt;

&lt;p&gt;第三条不是 400 问题，是数据问题。先改读取方，再改写入方，别反过来。&lt;/p&gt;

&lt;p&gt;搜完会得到三个很常见的误读，先在这儿拆掉。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;误读一：升到 2.x 才会删参数。&lt;/strong&gt; 不对。Python SDK 从 2026-08-21 的 1.0.0 起就不收 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt; 了；2.x 换的是默认 API 版本。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;误读二：不传参数就等于回到旧行为。&lt;/strong&gt; 不对。新版本的默认采样包括 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt; 1.0；旧版本上你显式调低的值，在新版本上没有等价物。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;误读三：版本头只是 SDK 的事。&lt;/strong&gt; 不对。裸 HTTP、网关、云厂商托管通道都会自己设这个头；SDK 升级不会替你改它们。&lt;/p&gt;

&lt;p&gt;然后做一次金丝雀。不要全量切。挑一条低风险链路，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic-version&lt;/code&gt; 显式设成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt;，跑 5% 流量，盯三件事：400 的比例、错误信息里的参数名、以及响应内容有没有因为默认 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature=1.0&lt;/code&gt; 变得飘。&lt;/p&gt;

&lt;p&gt;金丝雀还要准备回滚。把 SDK 版本和版本头都做成可回退的配置：SDK 用 lockfile 钉住，版本头用环境变量控制，别让两者绑死。出问题时，你要能只退回其中一个。&lt;/p&gt;

&lt;p&gt;测试里最值钱的一条断言，不是“请求成功”，是“请求体里没有 sampling 字段”。前者只能证明这一次没炸，后者能防止下一个人把参数加回来。用 mock server 把 body 落盘，在 CI 里 diff 一次，比读迁移文档更可靠。&lt;/p&gt;

&lt;p&gt;按人群给三条具体动作：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;前端/全栈&lt;/strong&gt;：如果你在 Next.js 的 route handler 里直接调 SDK，先在本地升 SDK、跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsc&lt;/code&gt;，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageParam&lt;/code&gt; 的联合类型和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;output_config&lt;/code&gt; 改完；线上先留旧 SDK，或者显式钉住 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt;，再切流量。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AI 应用/Agent 开发者&lt;/strong&gt;：重点查工具调用前后的消息构造。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assistant&lt;/code&gt; 消息里塞 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt; 消息里带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;citations&lt;/code&gt;，这两类在新版本都会 400；评测集里如果靠 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature=0&lt;/code&gt; 保稳定，要准备接受输出波动。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立开发者/小团队&lt;/strong&gt;：如果你用 LiteLLM 这类兼容层，先确认它写的版本头，不要假设“升级模型就等于升级协议”；如果你自己维护网关，把版本头做成配置项，不要写死在代码里。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;这不是-temperature-之死是兼容层时代的结束&quot;&gt;这不是 temperature 之死，是兼容层时代的结束&lt;/h2&gt;

&lt;p&gt;我的判断分两层。&lt;/p&gt;

&lt;p&gt;短期：不要因为看到“首个正式版本”就全量升 2.x。官方 SDK 用户的大部分代码不会挂，但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra_body&lt;/code&gt;、裸 HTTP、Admin 字段这三处会给你惊喜。先盘点，再金丝雀，再全量。&lt;/p&gt;

&lt;p&gt;长期：这次真正变化的不是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;temperature&lt;/code&gt; 这个参数，而是“协议版本”重新变成了一个需要被管理的依赖。过去三年，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt; 像空气一样存在，没人需要想它。现在它有了一个继任者，兼容层里那些写死的字符串，会一个个变成技术债。&lt;/p&gt;

&lt;p&gt;我下个注：两周内，至少一个主流 LLM 网关会发布针对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 的显式支持，或者至少把版本头改成可配置；如果两周过去，大家还在装死，那我“兼容层是瓶颈”的判断就错了，真正的瓶颈会回到应用代码。&lt;/p&gt;

&lt;p&gt;另一个可检验的信号是 sunset 日期。截至 2026-10-23，我在迁移文档里没看到 Anthropic 给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt; 设定停止支持的日期。&lt;strong&gt;这一点我没验到，也可能是我看漏了。&lt;/strong&gt; 如果官方随后公布一个明确的时间点，这件事的优先级就要整体前移。&lt;/p&gt;

&lt;p&gt;什么会让我改口：如果 Anthropic 给 sampling 参数加回一个兼容开关，或者 Bedrock 把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 下放到 Claude 4.6，那这次迁移的紧迫性就会下降。前者说明他们听到了生态的抱怨，后者说明云通道的节奏比官方慢半拍。&lt;/p&gt;

&lt;p&gt;回到开头那两行请求头。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2023-06-01&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-21&lt;/code&gt; 之间，隔的不是三年时间，是一层没人愿意维护、但所有人都依赖的兼容代码。版本号只是把它照出来了。&lt;/p&gt;

&lt;p&gt;我核过的链接：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://platform.claude.com/docs/en/api/migration-2026-10-21&quot;&gt;Anthropic 迁移指南&lt;/a&gt;（官方；我这边直连会被跳到地区限制页）&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/anthropic-sdk-python/blob/main/MIGRATION.md&quot;&gt;anthropic-sdk-python MIGRATION.md&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/anthropic-sdk-python/releases/tag/v2.0.0&quot;&gt;anthropic-sdk-python v2.0.0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://cellcog.ai/blog/anthropic-api-2026-10-21/&quot;&gt;cellcog 的第三方整理（2026-10-21）&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/BerriAI/litellm&quot;&gt;LiteLLM 仓库&lt;/a&gt;（把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic-version&lt;/code&gt; 写死的适配器在这里）&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>

    <entry>
        <title>DeepSeek 开源 DeepInfer：我核了文档和源码，H100 不在列表，高效部署最低 128 张卡</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/22/deepinfer-open-source-sm100-only.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/22/deepinfer-open-source-sm100-only.html</id>
        <updated>2026-10-22T00:00:00+08:00</updated>

        <summary>只支持 V4.1-Flash 和两种芯片；KV cache 与负载均衡仍未开源</summary>


        <category term="AI"/>

        <category term="服务器与部署"/>


        <category term="AI" scheme="tag"/>

        <category term="开源项目" scheme="tag"/>

        <category term="性能优化" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/22/deepinfer-open-source-sm100-only.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepinfer-open-source-sm100-only/banner.webp&quot; alt=&quot;封面：H100 不在列表，DeepInfer 最低 128 张卡&quot; /&gt;&lt;/p&gt;

&lt;p&gt;10 月 20 日下午 5 点 08 分，我在 Hacker News 首页看到一条 2,325 分的帖子：DeepInfer，DeepSeek 的推理框架开源了。&lt;/p&gt;

&lt;p&gt;我第一反应是，终于能把 DeepSeek-V4.1-Flash 的推理栈拆开看看。点进去，README 第一屏不是 benchmark，也不是 quick start，而是两行 Supported hardware：NVIDIA SM100 GPUs、Huawei Ascend 950 NPUs。&lt;/p&gt;

&lt;p&gt;我手边没有 SM100，也没有昇腾 950。所以这篇要回答的不是「DeepSeek 又赢了没有」，而是：它到底开了什么，谁能用，代价是什么，以及我为什么说它对大多数开发者更像一份参考实现，而不是一个能跑起来的工具。&lt;/p&gt;

&lt;h2 id=&quot;10-月-20-日下午hn-首页那条-2325-分的帖子&quot;&gt;10 月 20 日下午，HN 首页那条 2,325 分的帖子&lt;/h2&gt;

&lt;p&gt;先把现场还原一下。&lt;/p&gt;

&lt;p&gt;帖子是 2026-10-20 17:08（UTC+8）发到 HN 的，我抓取时是 2,325 分、886 条评论。仓库 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-ai/DeepInfer&lt;/code&gt; 的描述很克制：High-performance, large-scale inference framework for DeepSeek models。许可证 Apache-2.0，我抓取时 19,433 stars。&lt;/p&gt;

&lt;p&gt;19,433 这个数字看起来涨得很快，但我只有抓取这一刻的一个点，没有历史曲线，所以我不写「暴涨」。同理，2,325 分是抓取时的读数，不是它最终的分数。&lt;/p&gt;

&lt;p&gt;这几个数字放在一起，很容易让人以为「DeepSeek 把推理成本打下来了」。我一开始也这么想。&lt;/p&gt;

&lt;p&gt;但 README 的第二屏就把我拉回来了。Supported models 只有一行：DeepSeek-V4.1-Flash。Supported hardware 只有两行：NVIDIA SM100、Huawei Ascend 950。不是「优先支持」，不是「适配中」，是只支持这些。&lt;/p&gt;

&lt;p&gt;这不是一个通用推理框架的自我介绍。这是一个生产系统的配置单。&lt;/p&gt;

&lt;p&gt;HN 的评论区比 README 热闹。评论区有人把 X 上的说法搬了过来：单集群 155 万亿 token/天，$38/卡/小时；有人用「a nuclear bomb」形容这件事，也有人立刻开始算每百万 token 多少钱。还有人问，155 万亿是不是把输入 token 也算进去了。没有人贴出可复现的 benchmark。这个场面很典型：一个基础设施 release，先被当成经济新闻传播。&lt;/p&gt;

&lt;p&gt;我顺手看了下仓库结构：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepinfer/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deep_cluster/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;csrc/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;examples/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eval/&lt;/code&gt;，没有 docker-compose、Helm chart 或 K8s manifest。这个结构本身就在说：它不是给应用开发者准备的产品，是给懂 CUDA 和集群的人准备的一份工程材料。&lt;/p&gt;

&lt;p&gt;我又往下翻了翻 Q&amp;amp;A。官方写得很直接：这套东西支撑的是 DeepSeek V4.1 Flash 的大规模在线服务；单集群每天 155 万亿 token；$38/GPU-hour 的 overall revenue，谷时价格。数字很大，大到让人忽略一个前提——它没有说这个集群有多少张卡。&lt;/p&gt;

&lt;h2 id=&quot;时间线2025-年说不开源2026-年-10-月-20-日开了&quot;&gt;时间线：2025 年说不开源，2026 年 10 月 20 日开了&lt;/h2&gt;

&lt;p&gt;这件事不是凭空冒出来的。DeepSeek 的 open-infra-index 仓库里有一篇 2025 年 4 月的文档《The Path to Open-Sourcing the DeepSeek Inference Engine》，结论是「不直接开源完整引擎」。理由三条，都是工程原因：引擎基于一年多前的 vLLM 分支，改得太多；和内部集群管理工具耦合；团队小，维护不动一个大开源项目。&lt;/p&gt;

&lt;p&gt;18 个月后，2026 年 10 月 20 日，DeepInfer 的 initial commit 出现在 GitHub 上。同一天，Q&amp;amp;A 文档里多了一句「Today we are open-sourcing DeepInfer, our inference framework for large-scale cloud services.」&lt;/p&gt;

&lt;p&gt;这两件事不矛盾。2025 年说的是「完整引擎」，2026 年开的是「offline inference 版本」，KV cache 和跨 DP 负载均衡仍然没开。旧文档里的三条顾虑，至少后两条今天还成立。&lt;/p&gt;

&lt;p&gt;那篇文档里还有一句容易被忽略的话：DeepSeek 说会在新模型发布前，主动同步推理侧的工程工作，让社区在 Day-0 拿到 SOTA 支持。DeepInfer 更像是这句话的延续：先把推理栈的一部分公开，让外部团队能按它的方式部署，而不是把整个在线系统搬出来。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;时间（UTC+8）&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;2025-04-14（提交日）&lt;/td&gt;
      &lt;td&gt;DeepSeek&lt;/td&gt;
      &lt;td&gt;open-infra-index 旧文档：不直接开源完整引擎&lt;/td&gt;
      &lt;td&gt;一手，已读&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-20 12:09&lt;/td&gt;
      &lt;td&gt;DeepSeek&lt;/td&gt;
      &lt;td&gt;初始 commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d585e654&lt;/code&gt;，README、Q&amp;amp;A、examples 同日出现&lt;/td&gt;
      &lt;td&gt;一手，已读&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-20 17:08&lt;/td&gt;
      &lt;td&gt;Hacker News&lt;/td&gt;
      &lt;td&gt;帖子 2,325 分、886 条评论&lt;/td&gt;
      &lt;td&gt;第三方，已抓&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-20 19:03&lt;/td&gt;
      &lt;td&gt;DeepSeek&lt;/td&gt;
      &lt;td&gt;Q&amp;amp;A 文档更新，写下「Today we are open-sourcing」&lt;/td&gt;
      &lt;td&gt;一手，已读&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-21 12:55&lt;/td&gt;
      &lt;td&gt;DeepSeek&lt;/td&gt;
      &lt;td&gt;10-21 的 push，Bump DeepEP（#157）&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;&lt;img src=&quot;/better-blog/assets/img/deepinfer-open-source-sm100-only/02-timeline-deepinfer.webp&quot; alt=&quot;时间线：2025 年旧文档到 2026 年 10 月 20 日发布&quot; /&gt;&lt;/p&gt;

&lt;p&gt;仓库元数据显示它创建于 2026-10-16，但第一个 commit 是 10-20 12:09。中间几天它在干什么，我不知道，也没有证据说它是私有准备。这个空白不影响判断，但我不想把它写成「10 月 16 日就发布了」。&lt;/p&gt;

&lt;h2 id=&quot;它开了什么一个模型两种芯片三块没给&quot;&gt;它开了什么：一个模型、两种芯片、三块没给&lt;/h2&gt;

&lt;p&gt;一句话定位：DeepSeek 生产推理栈的离线参考实现。&lt;/p&gt;

&lt;p&gt;开了的东西不少。优化过的 kernel（FlashMLA、DeepGEMM、DeepEP、DeepSelect、TileLang）、Attention DP/CP、MoE EP + EPLB、DSpark 投机解码、SWA bounded replay、异步调度、batch-invariant、确定性采样、结构化输出。这些名词单拎一个出来都能写一篇文章，但它们不是我要讲的主峰。&lt;/p&gt;

&lt;p&gt;没开的东西更关键。README 自己列了一节「Required for a complete inference service (not included in this release)」：KV cache system、VE/Prefill/Decode disaggregation、跨 DP 副本的负载均衡。Q&amp;amp;A 进一步承认：KV cache 管理和负载均衡和内部基础设施耦合太深，还没完全开源。&lt;/p&gt;

&lt;p&gt;README 在 PD 分离那一行后面补了一句：大部分需要的引擎功能已经实现，只是没随这个 release 交付。这个差别值得记一下，免得把「没给」理解成「没做」。&lt;/p&gt;

&lt;p&gt;这就像给你发动机、变速箱和一部分油路，但油箱和加油站还在 DeepSeek 机房里。你能研究它怎么省油，不能把车开走。&lt;/p&gt;

&lt;p&gt;举三个例子。DSpark 用 draft token 加置信度调度来做投机解码，README 里给了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DEEPINFER_ENABLE_MTP_VARLEN&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DEEPINFER_USE_NEXTN_DECODE&lt;/code&gt; 两个开关；SWA bounded replay 只重放一段有界后缀，减少 decoder 的 prefill 计算；EPLB 给每张卡多放几个热 expert 的副本，按最近 N 步的负载重新分配。这些是 DeepSeek 在线服务里真正省钱的部分，也是这份 release 最值得读的部分。&lt;/p&gt;

&lt;p&gt;我不觉得这是恶意保留。2025 年那篇文档已经把顾虑写清楚了：这些部分和内部集群管理绑得太紧。但读者需要知道：你拿到的不是一个完整的 serving stack，而是一个能跑 offline inference 的核心。对想「clone 下来就上线」的人，这个差别是致命的。&lt;/p&gt;

&lt;p&gt;再说清楚一点：offline inference 是给你喂一批 prompt、拿一批输出，适合评测和批处理；在线服务要处理并发、路由、缓存命中、故障切换。DeepInfer 的 release 把前者的核心给了，后者的两块关键组件留着。这不是文字游戏，是两种工作负载。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepinfer-open-source-sm100-only/03-framework-open-vs-missing.webp&quot; alt=&quot;结构图：已开放的引擎组件与未开源的 KV cache、负载均衡&quot; /&gt;&lt;/p&gt;

&lt;p&gt;第三方拆解也写了同样三件事。hubwiz 的《DeepInfer 开源拆解》我能打开，它说 DeepInfer 支撑 V4.1-Flash、支持 SM100 与昇腾 950、缺 KV cache 与负载均衡；另一个 Substack 我只看到搜索摘要，摘要说它只适配一个模型和两种硬件平台。这两个都不是官方，但和我读到的一手材料对得上。把摘要当全文是不对的，所以我把这个差别也标出来。&lt;/p&gt;

&lt;p&gt;官方在 Q&amp;amp;A 里还链接了一篇知乎长文，讲 KV cache 和负载均衡的内部实现。我这边打开是 403。所以那部分我只能按 Q&amp;amp;A 的摘要写，没读到长文。这是这篇的一个空白，我标出来。&lt;/p&gt;

&lt;h2 id=&quot;我-clone-下来跑了构建两个错误和一行代码&quot;&gt;我 clone 下来跑了构建：两个错误，和一行代码&lt;/h2&gt;

&lt;p&gt;我在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp&lt;/code&gt; clone 了仓库，commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;9bd9cf2&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第一件事是 build。README 写的前置条件是 Linux、GCC &amp;gt;= 13、Python 3.12、CUDA 13。我的 Mac 一个都不满足，但我还是想看看它会在哪一步停下来。&lt;/p&gt;

&lt;p&gt;第一次跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash scripts/build.sh build&lt;/code&gt;，报的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python: command not found&lt;/code&gt;。项目脚本调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python&lt;/code&gt;，这台机器只有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python3&lt;/code&gt;。我建了个临时 symlink，再跑，报 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ModuleNotFoundError: No module named &apos;torch&apos;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这还没到 CUDA。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python3 --version&lt;/code&gt; 是 3.8.0，项目要求 3.12；我顺手 import 了一下硬件模块，直接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AttributeError: module &apos;functools&apos; has no attribute &apos;cache&apos;&lt;/code&gt;——&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;functools.cache&lt;/code&gt; 是 3.9 才有的。&lt;/p&gt;

&lt;p&gt;两个错误都不高级。但它们说明一件事：这个仓库假设你在一台 Linux + CUDA 13 + Python 3.12 的机器上，而不是一台想试试看的 Mac。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepinfer-open-source-sm100-only/01-screenshot-build-failure.webp&quot; alt=&quot;终端截图：DeepInfer 构建报缺 torch，硬件模块导入失败&quot; /&gt;&lt;/p&gt;

&lt;p&gt;更关键的是源码里的一行判断。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deep_cluster/hardware/device.py&lt;/code&gt; 里，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_sm_major_10()&lt;/code&gt; 只认 NVML 读到的 compute capability 主版本等于 10；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_bf16_tflops_limit()&lt;/code&gt; 在不是 Ascend、也不是 SM10 的时候，直接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;raise RuntimeError(&quot;unknown GPU type&quot;)&lt;/code&gt;。H100 是 SM90，A100 是 SM80，都走不到这条路的正分支。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_bf16_tflops_limit()&lt;/code&gt; 的返回值也不是装饰：Ascend 是 432，SM10 是 2250 或 2500，其他一律抛错。这个值会进到引擎的 roofline 计算里。换句话说，H100 不是「跑得慢」，是连这个上限都取不到。&lt;/p&gt;

&lt;p&gt;这不是 bug，是边界。DeepInfer 没有假装自己能在 H100 上跑。&lt;/p&gt;

&lt;p&gt;还有一个小细节：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;examples/offline_inference.py&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--ep&lt;/code&gt; 默认是 8。连「离线跑一下」的示例都默认 8 张 GPU。你可以调小，但官方没有给单卡配置，也没有给消费级卡的路径。&lt;/p&gt;

&lt;p&gt;我可能把「离线」理解得太乐观了。它离线的是服务形态，不是硬件门槛。&lt;/p&gt;

&lt;p&gt;我不知道这是不是 DeepSeek 刻意的产品选择，还是 release 节奏太赶；仓库里没有路线图，我也没看到官方说「下一步支持什么」。这一点我没验到。&lt;/p&gt;

&lt;h2 id=&quot;128-张卡只是入场券155-万亿-token-的账没有分母&quot;&gt;128 张卡只是入场券：155 万亿 token 的账没有分母&lt;/h2&gt;

&lt;p&gt;Q&amp;amp;A 里最容易被截屏的一句话是：「A single cluster currently serves 155 trillion tokens per day, with overall revenue of US$38 per GPU-hour (at valley pricing).」&lt;/p&gt;

&lt;p&gt;155 万亿 token/天，单集群，$38/GPU-hour，谷时价格。三个数字都很抓人。它们是官方声称，不是我核出来的事实；我能核到的是 Q&amp;amp;A 原文，以及代码里那些和部署规模对得上的配置。&lt;/p&gt;

&lt;p&gt;但这句话没有给分母：集群有多少张卡？155 万亿里输入和输出各占多少？$38 是收入还是成本？Q&amp;amp;A 写的是 overall revenue，不是 cost。&lt;/p&gt;

&lt;p&gt;这两个词差得很远。收入是 DeepSeek 卖 token 收到的钱，成本是它付给 GPU、电力和机房的钱。Q&amp;amp;A 没有给毛利，也没有给集群规模，所以任何「每百万 token 成本多少」的推算都要先假设一堆东西。我在 HN 评论区看到有人拿 $38/卡/小时的营收和 H100 租金 $2/小时 对比，算出「利润率 1600%」；这个算法没算电力和存储，也不是会计意义上的利润率，我不能接受。&lt;/p&gt;

&lt;p&gt;我拿 128 张卡算了一下。155 万亿 ÷ 86,400 秒 ≈ 每秒 17.9 亿 token；如果集群正好是官方说的最小高效单元 128 张卡，每张卡每秒约 1,400 万 token。这个数不能当生成吞吐读——它更像是 prefill/输入 token 的口径，或者集群远不止 128 张卡，或者两者都有。&lt;/p&gt;

&lt;p&gt;我不是说这个数字造假。我是说它没有分母，不能拿来算你的单卡成本。&lt;/p&gt;

&lt;p&gt;HN 评论区也在算这笔账。有人拿 OpenRouter 一周 164 万亿 token 做对照，觉得一个集群一天 155 万亿不合常理；也有人提醒，token 的口径可能不同。两边都没有官方口径，谁也说服不了谁。&lt;/p&gt;

&lt;p&gt;官方给的门槛倒是很清楚：1P + 1D 能部署，P:D 大约 1:1；推荐的 decode 配置是 EP64 以上，所以最小高效部署单元是 64 张 prefill + 64 张 decode。&lt;/p&gt;

&lt;p&gt;这里要抠一个字：官方说的是「smallest efficient deployment unit」，不是「minimum deployment」。同一段还写了「1P + 1D is deployable」。所以 128 张是效率门槛，不是启动门槛。我在标题里写「高效部署最低 128 张卡」，不是「必须 128 张才能跑」。这两个说法差很多，前者是官方原话，后者是我如果偷懒就会写错的结论。&lt;/p&gt;

&lt;p&gt;这还没算存储和网络。Q&amp;amp;A 推荐每台 KV cache 存储服务器 20 × 30.73 TB SSD，至少 40 GB/s 读、5 GB/s 写，保留 24 小时数据；每张 GPU 约 14.4 TB 逻辑容量；跨数据中心按每 8 张 GPU 约 400 Gbps 预留。&lt;/p&gt;

&lt;p&gt;Q&amp;amp;A 还给了更细的配置：prefill 用 EP2–8 / DP2–8，decode 用 EP64–128 / DP64–128，不用 TP 和 CP；KV cache 放在 3FS 上，10+2 纠删码，agent 流量 TTL 3 天、chatbot 7 天，整个存储池约 100 PiB SSD。这些数字不一定适用于你的业务，但它们说明 DeepInfer 的「省」是拿一整套基础设施换来的。&lt;/p&gt;

&lt;p&gt;把这些加在一起，128 张卡只是入场券。&lt;/p&gt;

&lt;p&gt;这也是我对「DeepSeek 把推理成本打下来」这个说法最保留的地方：它公开的是成本结构的一部分，不是成本本身。你能看到它怎么省，但省钱的机器、存储和网络，还在它自己机房里。&lt;/p&gt;

&lt;h2 id=&quot;和-vllmsglang-比它不是另一个-serving-框架&quot;&gt;和 vLLM、SGLang 比：它不是「另一个 serving 框架」&lt;/h2&gt;

&lt;p&gt;DeepInfer 经常被拿来和 vLLM、SGLang 比。我一开始也把它当成「DeepSeek 版 vLLM」，核完发现不是。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;维度&lt;/th&gt;
      &lt;th&gt;DeepInfer&lt;/th&gt;
      &lt;th&gt;vLLM&lt;/th&gt;
      &lt;th&gt;SGLang&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;支持模型&lt;/td&gt;
      &lt;td&gt;只有 DeepSeek-V4.1-Flash&lt;/td&gt;
      &lt;td&gt;通用 LLM serving，DeepInfer 的上游&lt;/td&gt;
      &lt;td&gt;通用 LLM/多模态 serving&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;硬件&lt;/td&gt;
      &lt;td&gt;NVIDIA SM100 / 昇腾 950&lt;/td&gt;
      &lt;td&gt;多厂商后端（以官方文档为准）&lt;/td&gt;
      &lt;td&gt;H100/A100、AMD、TPU、昇腾 950PR/DT 等&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;服务完整性&lt;/td&gt;
      &lt;td&gt;缺 KV cache、PD 分离、跨 DP 负载均衡&lt;/td&gt;
      &lt;td&gt;完整 serving 栈&lt;/td&gt;
      &lt;td&gt;完整 serving 栈&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;许可证&lt;/td&gt;
      &lt;td&gt;Apache-2.0&lt;/td&gt;
      &lt;td&gt;Apache-2.0&lt;/td&gt;
      &lt;td&gt;Apache-2.0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;适合谁&lt;/td&gt;
      &lt;td&gt;有 SM100/950 集群、想复现生产栈的团队&lt;/td&gt;
      &lt;td&gt;大多数自建推理团队&lt;/td&gt;
      &lt;td&gt;需要高性能 serving、硬件多样的团队&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这张表最重要的不是谁强，而是表格末尾那行的「适合谁」。DeepInfer 不是来替代 vLLM/SGLang 的。它更像 DeepSeek 把自己的生产配置摊开给你看：哪些 kernel 值得写、EP/DP 怎么分、EPLB 怎么调、投机解码怎么接。&lt;/p&gt;

&lt;p&gt;还有一点容易被忽略：DeepInfer 的 README 说它「originally built primarily on vLLM」。这意味着它的很多优化是长在 vLLM 的骨架上，而不是从零写了一个新引擎。对读者来说，这是好事——你可以顺着 vLLM 的知识去读；对选型来说，这也是提醒——它不是一个独立演化的通用项目，至少现在不是。&lt;/p&gt;

&lt;p&gt;SGLang 的 README 把硬件列得很细：H100/H200/H800、A100、AMD MI300X/MI350X、Google TPU、昇腾 950PR/DT，甚至 Apple Silicon。DeepInfer 的列表只有两行。这不是说 SGLang 一定更快，而是说它们的适用面不在一个量级。&lt;/p&gt;

&lt;p&gt;vLLM 是它的上游，SGLang 是另一个通用答案。你要的是「在我的 H100 上把多个模型跑起来」，这两个仍然是默认选项；你要的是「看 DeepSeek 怎么把 V4.1-Flash 跑成 155 万亿 token/天」，才轮到 DeepInfer。&lt;/p&gt;

&lt;p&gt;如果只是想把 DeepSeek 模型接进应用，最省事的仍然是 API。DeepInfer 不改变这个结论。&lt;/p&gt;

&lt;h2 id=&quot;对四类人的影响现在能做什么&quot;&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;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;前端/全栈&lt;/td&gt;
      &lt;td&gt;没有直接影响。DeepInfer 是 Python/CUDA 服务端栈，不碰浏览器、构建链和组件库&lt;/td&gt;
      &lt;td&gt;别为了热点改前端选型；要做 AI 功能，继续用 API，把后端抽象留好&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI 应用开发者&lt;/td&gt;
      &lt;td&gt;多了一个可读的生产级参考实现，但不能直接替换你的推理后端&lt;/td&gt;
      &lt;td&gt;读 DSpark、EPLB、structured outputs 的源码；选型表继续保留 vLLM/SGLang&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;独立开发者&lt;/td&gt;
      &lt;td&gt;没有单卡/消费级 GPU 路径；构建要 Linux + CUDA 13 + Python 3.12&lt;/td&gt;
      &lt;td&gt;想本地跑模型，用现成的本地推理工具；把 DeepInfer 当资料，不当运行时&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;有集群的团队&lt;/td&gt;
      &lt;td&gt;如果已有 SM100 或昇腾 950，可以评估复现；否则硬件门槛过不去&lt;/td&gt;
      &lt;td&gt;先算 64P+64D、KV cache 存储和 400G 网络；再看缺的组件谁补&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;如果你属于前三类，这两天最该做的不是 clone，而是把自己从「DeepSeek 开源了推理框架」这句话里摘出来：它没有让你的 4090 多出一块显存，也没有让你的 API 账单变便宜。&lt;/p&gt;

&lt;p&gt;如果你被问过「DeepSeek 都开源推理框架了，我们是不是可以自己部署，把 API 账单砍掉」，我的回答是，先看你的卡。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi --query-gpu=compute_cap --format=csv&lt;/code&gt;，如果输出是 9.0、8.0，这个讨论可以到此为止；如果是 10.0，再往下算存储和网络。&lt;/p&gt;

&lt;p&gt;前端同学也可能会在周会上听到「我们要不要自建推理降本」。你可以把这篇的结论带回去：先问卡是什么架构，再问有没有 64P+64D 和 KV cache 存储；两个问题就能把讨论从「要不要」拉回「能不能」。&lt;/p&gt;

&lt;p&gt;如果你属于第四类，DeepInfer 值得读。不是因为它能直接上线，而是因为它把「大规模推理到底在优化什么」写成了代码：EP/DP 怎么分、EPLB 怎么统计、DSpark 怎么用 draft token、SWA 怎么 bounded replay。这些思路可以搬，代码不一定能搬。&lt;/p&gt;

&lt;p&gt;对团队来说，最现实的动作不是立刻复现，而是把 DeepInfer 当成一份「生产栈 checklist」：你有没有 EPLB、有没有 KV cache 分层、有没有 PD 分离、有没有投机解码的置信度调度。缺哪块，先补哪块；不一定用它的代码，但可以借它的设计。&lt;/p&gt;

&lt;h2 id=&quot;我的判断它开源的是引擎不是服务&quot;&gt;我的判断：它开源的是引擎，不是服务&lt;/h2&gt;

&lt;p&gt;我的判断：DeepInfer 开源的是引擎，不是服务。&lt;/p&gt;

&lt;p&gt;这不是贬低。能把生产引擎的一部分开出来，已经比大多数公司做得多了。但它对开发者的实际影响，被「开源」两个字放大了。&lt;/p&gt;

&lt;p&gt;接下来我会盯三个信号。&lt;/p&gt;

&lt;p&gt;第一，SM90/H100 支持。不是文档里写一句「计划支持」，而是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_sm_major_10()&lt;/code&gt; 被改掉，或者出现一个能在 H100 上跑通的 PR。&lt;/p&gt;

&lt;p&gt;第二，KV cache 管理和跨 DP 负载均衡的代码真的进仓库。现在 Q&amp;amp;A 说得很清楚：这两块还没完全开源。如果它们进来了，DeepInfer 才从「参考实现」变成「可部署的候选」。&lt;/p&gt;

&lt;p&gt;第三，第一个 release tag 和一份带集群规模的独立 benchmark。现在仓库是 0.1.0，没有 GitHub Release，也没有 tag，只有 commit；155 万亿 token/天的分母也没给。&lt;/p&gt;

&lt;p&gt;这三个信号里，我最看重第二个。SM90 支持可能只是一个 PR；release tag 只是工程习惯；但 KV cache 和负载均衡是 DeepInfer 从「能跑」到「能服务」的分水岭。没有这两块，你拿到的是一台拆掉油箱的车；有了这两块，它才配和 vLLM/SGLang 放在同一张选型表里。&lt;/p&gt;

&lt;p&gt;我的下注：如果 2027 年 1 月前这三件事都没发生，我会把「参考实现」的判断再延长半年；如果第二件先发生，我会把它加进团队选型的候选，但仍然不会推荐给单卡用户。&lt;/p&gt;

&lt;p&gt;我可能错了。如果 1P+1D 在真实业务里能跑出可接受的延迟，128 张卡的门槛会松动；如果 DeepSeek 后续把 V3/R1 或更多模型接进来，它的适用范围也会变大。但这些我都没验到。&lt;/p&gt;

&lt;p&gt;还有一种可能：DeepSeek 根本不打算让 DeepInfer 成为通用框架。它可能只是想给硬件伙伴和云厂商一个参考实现，让 SM100 和昇腾 950 的客户能更快把 V4.1-Flash 跑起来。如果真是这样，我拿「通用框架」的标准去要求它，本身就不公平。这个意图我没有证据，只能算猜测。&lt;/p&gt;

&lt;p&gt;我不确定 DeepSeek 会不会把 KV cache 和负载均衡补上。如果它永远不补，DeepInfer 就只是一次性的参考发布；如果补了，今天这篇的判断会被事实修正。这也是我愿意把下注时间写到 2027 年 1 月的原因。&lt;/p&gt;

&lt;p&gt;我把 README 关掉的时候，记住的不是 155 万亿，而是那两行 Supported hardware。它决定了这篇文章对大多数人的意义：DeepInfer 不是给你省钱的，是给你看 DeepSeek 怎么花钱的。&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>一万个人，把密码交给一个不联网的 Chrome 插件</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/22/account-password-helper-10k.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/22/account-password-helper-10k.html</id>
        <updated>2026-10-22T00:00:00+08:00</updated>

        <summary>上架三个月，商店显示 10,000 用户；48 个版本、1,120 次提交、3,951 项测试</summary>


        <category term="Chrome插件"/>


        <category term="账号密码管理助手" scheme="tag"/>

        <category term="一键填充" scheme="tag"/>

        <category term="自动登录" scheme="tag"/>

        <category term="chrome浏览器插件" scheme="tag"/>

        <category term="本地优先" scheme="tag"/>

        <category term="密码管理器" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/22/account-password-helper-10k.html">&lt;h1 id=&quot;一万个人把密码交给一个不联网的-chrome-插件&quot;&gt;一万个人，把密码交给一个不联网的 Chrome 插件&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/account-password-helper-10k/banner.webp&quot; alt=&quot;封面：上架三个月，一个不联网的密码插件从零涨到一万用户&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;一句话结论：这不是增长复盘，是一份施工记录。一个不联网的密码插件，三个月被装进一万台电脑。
现在该做什么：每天在 dev、test、prod 之间切账号的，先拿一个测试账号走一遍一键填充；只有个人网站要自动登录的，用 Chrome 自带。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;10 月 22 日上午，Chrome 应用商店的页面显示：10,000 用户。&lt;/p&gt;

&lt;p&gt;不是十万。对一个做了十三个月、只有一个人维护的浏览器插件来说，这个数字够我在屏幕前坐一会儿。&lt;/p&gt;

&lt;p&gt;同一天，GitHub 仓库上是 13 颗 star。&lt;/p&gt;

&lt;p&gt;这两个数摆在一起，比任何一个单独看都诚实。装它的人有一万，给它点星的有十三个。用的人不等于围观的人，浏览器插件的分发从来不在代码里。&lt;/p&gt;

&lt;p&gt;这十三个月攒下的东西，我先把能查的数摊开：48 个版本、1,120 次提交、3,951 项自动化测试。上架后的三个月改了 803 次。数字背后不是灵感，是不断被推翻的交互细节。&lt;/p&gt;

&lt;h2 id=&quot;四套环境16-组账号麻烦不是存是找&quot;&gt;四套环境、16 组账号：麻烦不是存，是找&lt;/h2&gt;

&lt;p&gt;去年秋天我在做一套后台系统。开发、测试、预发、生产四套环境，域名长得像四胞胎。每个环境有管理员、运营、审核、只读四种角色，粗算 16 组账号。&lt;/p&gt;

&lt;p&gt;一开始存在飞书文档里。文档越写越长，最后变成一张大表格：系统名、环境、角色、账号、密码、备注。每次登录的动作是：打开文档、搜系统名、找到行、复制账号、切回浏览器、粘贴；再切回文档、复制密码、再切回来。遇到验证码，还得摸手机。&lt;/p&gt;

&lt;p&gt;步骤多还能忍，忍不了的是错。dev 的管理员密码填进 test 的登录框，页面只会告诉你密码错误。你先怀疑自己记错了，回去翻文档，最后发现只是选错环境。一来一回五分钟没了。&lt;/p&gt;

&lt;p&gt;Chrome 自带密码管理我也用过。它能存能填，但它是按网站组织的，不是按环境和角色组织的。同一个后台八个账号，下拉里一排用户名，没有标签没有备注，也不能按项目筛。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; 的不同端口之间更容易串。&lt;/p&gt;

&lt;p&gt;Excel 和飞书文档是另一个方向的问题：能看不能填，而且是明文。测试账号里混着真实用户数据，放在共享文档里谁都能复制走。&lt;/p&gt;

&lt;p&gt;Bitwarden、1Password 当然更专业，跨设备同步、团队共享都是它们的正业。但我当时要的不是一座保险库，是一个开发测试账号工作台：打开登录页，按环境和角色挑一个账号，填进去，登录，走人。为几十个临时账号配一套云端团队方案，太重。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/account-password-helper-10k/04-screenshot-multi-env.webp&quot; alt=&quot;同一站点的不同环境账号，用标签和域名隔开&quot; /&gt;&lt;/p&gt;

&lt;p&gt;所以我给自己定了一个很窄的目标：只做开发、测试、产品每天都会遇到的多账号登录；密码只留本机；不注册、不订阅、不上传。&lt;/p&gt;

&lt;p&gt;这个目标后来变成产品边界。它让我拒绝了很多顺手也能做的功能：不做团队空间，不做跨设备同步，不做密码分享链接。不是需求不存在，而是它们会把产品带向另一个方向。一旦开始同步，就得有服务器、有账号体系、有恢复流程，也就有了一个可以被攻破的中心。&lt;/p&gt;

&lt;h2 id=&quot;v3160-的八项能力先说三件&quot;&gt;v3.16.0 的八项能力，先说三件&lt;/h2&gt;

&lt;p&gt;把 dev、test、prod 串起来的是下面这八项。它们都已经在发布版本里，没有一项是路线图。&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;精确 host，再按端口分档&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;内置 TOTP&lt;/td&gt;
      &lt;td&gt;验证码和密码存在一起&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;站点规则兜底&lt;/td&gt;
      &lt;td&gt;CSS 选择器，穿透 Shadow DOM&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;本地加密&lt;/td&gt;
      &lt;td&gt;PBKDF2 派生 + AES-256-GCM&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;离线安全体检&lt;/td&gt;
      &lt;td&gt;0–100 分，四维加权&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;迁移与备份&lt;/td&gt;
      &lt;td&gt;CSV / JSON / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aph&lt;/code&gt; 加密备份&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;防误操作&lt;/td&gt;
      &lt;td&gt;回收站 30 天、5 秒撤销&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这些能力单拎出来都不新鲜，密码管理器早就有，自动填充也不是新技术。麻烦的是把它们组合到开发测试这个具体场景里，还要每一步都不添乱。下面挑三个最容易做歪的展开。&lt;/p&gt;

&lt;p&gt;先看一次填充在你自己电脑上到底走了几步。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/account-password-helper-10k/02-flowchart-login-flow.webp&quot; alt=&quot;一次填充在本机走的五步：识别登录页、按 host 取候选、本机解密、写进输入框、把登录走完&quot; /&gt;&lt;/p&gt;

&lt;p&gt;第 4 步最容易被低估，因为它比看上去难得多。&lt;/p&gt;

&lt;p&gt;很多登录页不是普通 HTML。React、Vue 会给输入框套一层受控组件，你直接改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt;，页面状态不认；有的站点把登录框塞进 Shadow DOM，外面 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;querySelector&lt;/code&gt; 根本找不到；还有的页面实时校验，你一次性把密码塞进去，它以为你什么都没输入。SPA 切路由之后表单才出现，iframe 里的登录框又是另一个文档。&lt;/p&gt;

&lt;p&gt;所以填充不能只会一招。它的顺序是：先走原生 setter，再派发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change&lt;/code&gt;；不行试 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execCommand&lt;/code&gt;；再不行模拟键盘输入。页面动态加载就用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MutationObserver&lt;/code&gt; 等表单出现。中文输入法还没上屏时不接管方向键和回车，免得字没打完就把密码填进页面。&lt;/p&gt;

&lt;p&gt;这些用户看不见。他们只会说这个网站填不进去。但每一个填不进去背后，可能是一个框架、一层 Shadow DOM，或者一个奇怪的受控组件。&lt;/p&gt;

&lt;p&gt;我不追求满分的填充成功率。网页千奇百怪，百分之百是句谎话，罕见页面我至今没跑全。能做的是常见框架尽量覆盖，覆盖不了时给用户一个自己修的工具，失败时给一句人话提示。&lt;/p&gt;

&lt;p&gt;那个工具就是站点规则。某个自建后台的登录框实在识别不了，你可以在管理页为这个域名指定账号框、密码框的 CSS 选择器；也可以点拾取，切到已打开的登录页直接在页面上点选输入框，插件只把选择器带回来，不读取框里的值。规则导出成 JSON 时只含域名和选择器，不含账号密码。所以它能发给同事，也能在你自己的两台机器之间搬。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/account-password-helper-10k/03-screenshot-one-click-login.webp&quot; alt=&quot;选中账号后先填充、再勾选协议；开启自动提交后连登录按钮一起点&quot; /&gt;&lt;/p&gt;

&lt;p&gt;一键登录是另一个被低估的地方。很多工具所谓的一键登录，其实只做到把账号密码填进去。真实登录页还有记住我、已阅读并同意用户协议这类复选框，有的藏在自定义组件里，直接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.click()&lt;/code&gt; 不生效。这个插件按字段逐项写入，把该勾的勾上，最后停在登录按钮前面。&lt;/p&gt;

&lt;p&gt;默认不替你提交，是我刻意留的。自动化越深，误操作的成本越高。你确认一眼再点下去，这一步不丢人。想要更省事，可以在偏好里开自动触发登录，或者用侧边栏的填充并登录。&lt;/p&gt;

&lt;p&gt;验证码这一项我想单说。很多后台开了两步验证，密码填完跳到验证码页，你再掏手机、解锁、找 App、等倒计时。一天登录十次，就是十次打断。插件把 TOTP 密钥和账号存在一起，列表里直接显示实时验证码和倒计时；GitHub 式两步登录会自动锚定验证码输入框，点一下填入就完成第二步。密钥和密码一样属于五个敏感字段，走同一套加密，不会以明文落盘。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/account-password-helper-10k/05-screenshot-totp.webp&quot; alt=&quot;密码列表里直接显示 TOTP 活码和倒计时，两步登录可以接力&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;边界写在-pbkdf2-与-aes-256-gcm-里&quot;&gt;边界写在 PBKDF2 与 AES-256-GCM 里&lt;/h2&gt;

&lt;p&gt;四句话讲清边界：PBKDF2、AES-256-GCM、五个字段、一次网络请求。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;主密码不存储，只用来派生密钥。PBKDF2-SHA256，六十万次迭代，得到 256-bit 密钥。&lt;/li&gt;
  &lt;li&gt;用户名、密码、网址、备注、TOTP 密钥五个敏感字段分别用 AES-256-GCM 加密，每次新的随机 IV。&lt;/li&gt;
  &lt;li&gt;解密后的明文只存在于扩展自己的上下文里；锁定或会话过期时，密钥材料和内存快照立即销毁。&lt;/li&gt;
  &lt;li&gt;没有账号系统、没有云同步、没有遥测。它不像 Chrome 那样跟随账号同步，唯一的网络请求是每 6 小时一次的匿名版本检查。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;翻译成人话：你的密码库是一堆密文，钥匙只在你输入主密码那一刻存在；一旦锁定或过期，钥匙就没了。&lt;/p&gt;

&lt;p&gt;代价也很直接。忘记主密码找不回来，没有忘记密码邮件，因为我这里没有你的邮箱，也没有你的密码库，只能重置、清空、重新开始。不能跨设备同步，换电脑要么重新导入，要么把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aph&lt;/code&gt; 加密备份带过去。这一步确实不如云产品省事。&lt;/p&gt;

&lt;p&gt;它也不是绝对安全。本地恶意软件、被控制的浏览器、太弱的主密码，仍然能伤到你。它解决的是其中一层：密码数据不离开你的电脑。&lt;/p&gt;

&lt;p&gt;我越来越觉得本地优先不是情怀，是成本结构。没有服务器，就没有服务器被攻破的风险；没有账号体系，就没有找回账号这条攻击链；没有订阅，也就不用为了续费去制造焦虑。你得到的是一个更小、更慢、边界更清楚的产品。&lt;/p&gt;

&lt;h2 id=&quot;和-chrome-自带bitwarden-差在哪&quot;&gt;和 Chrome 自带、Bitwarden 差在哪&lt;/h2&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;Chrome 自带&lt;/th&gt;
      &lt;th&gt;Bitwarden / 1Password&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;可跟随 Google 账号同步&lt;/td&gt;
      &lt;td&gt;云端或自建服务&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;多环境隔离&lt;/td&gt;
      &lt;td&gt;精确 host + 端口分档&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;td&gt;以填充为主&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;TOTP&lt;/td&gt;
      &lt;td&gt;内置，与密码同库&lt;/td&gt;
      &lt;td&gt;无&lt;/td&gt;
      &lt;td&gt;有，价格各不同&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;站点规则&lt;/td&gt;
      &lt;td&gt;CSS 选择器 + 影子树穿透&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aph&lt;/code&gt; 备份手工搬&lt;/td&gt;
      &lt;td&gt;跟随浏览器账号&lt;/td&gt;
      &lt;td&gt;成熟&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;价格&lt;/td&gt;
      &lt;td&gt;免费，GPL-3.0 开源&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;所以 Chrome、Bitwarden 和它各有位置。个人日常上网，Chrome 自带就够。需要跨设备同步、团队共享、紧急访问，选 Bitwarden 或 1Password，它们在这件事上做了很多年。只有你每天在 dev、test、prod 之间切换，又不想把测试账号放云端，这个插件才是为你做的。&lt;/p&gt;

&lt;p&gt;还有一种人不适合：完全不想记主密码的人。这里没有找回入口，因为设计上就不存在能帮你找回的人。&lt;/p&gt;

&lt;h2 id=&quot;1-万用户和-13-颗-star把时间线摊开&quot;&gt;1 万用户和 13 颗 star：把时间线摊开&lt;/h2&gt;

&lt;p&gt;GPL-3.0 和免费都不构成增长。能查的是这几个时间点。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/account-password-helper-10k/01-timeline-three-months.webp&quot; alt=&quot;上架三个月的逐月提交柱状图与关键时间点&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;2025-09-25，第一行提交，项目初始化。&lt;/li&gt;
  &lt;li&gt;2026-05-25，第一篇推广文章在这个博客上线。&lt;/li&gt;
  &lt;li&gt;2026-07-20，Chrome 商店链接第一次进仓库，从这天起算上架后。&lt;/li&gt;
  &lt;li&gt;2026-09-09，一版商店草稿被驳回，理由是产品说明关键词堆砌。&lt;/li&gt;
  &lt;li&gt;2026-10-14，v3.16.0 发布。&lt;/li&gt;
  &lt;li&gt;2026-10-22，商店显示 10,000 用户，5 星、3 个评分。&lt;/li&gt;
&lt;/ul&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;10,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;5 星 / 3 个评分&lt;/td&gt;
      &lt;td&gt;商店页&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;提交&lt;/td&gt;
      &lt;td&gt;1,120&lt;/td&gt;
      &lt;td&gt;feature-dev 分支&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;版本&lt;/td&gt;
      &lt;td&gt;48 个&lt;/td&gt;
      &lt;td&gt;GitHub Releases&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;合并 PR&lt;/td&gt;
      &lt;td&gt;126 条&lt;/td&gt;
      &lt;td&gt;GitHub API&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CI 运行&lt;/td&gt;
      &lt;td&gt;1,183 次&lt;/td&gt;
      &lt;td&gt;Actions 历史&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;自动化测试&lt;/td&gt;
      &lt;td&gt;3,951 项 / 269 文件&lt;/td&gt;
      &lt;td&gt;本机全量重跑&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;端到端&lt;/td&gt;
      &lt;td&gt;132 条 / 23 文件&lt;/td&gt;
      &lt;td&gt;playwright 列表&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;5 星、3 个评分，这个样本小到不能支撑任何结论，我不会把它当奖杯。真正有用的是那些具体的反馈：有人说终于在测试环境不用翻文档了；有人说 TOTP 接力省了掏手机；也有人直接甩来一个填不进去的域名，附上页面结构。最后一种最有价值，它指向一个真实的、我还没覆盖到的登录页。&lt;/p&gt;

&lt;p&gt;这三个月我没做增长归因。用户是从商店搜索来的、从 GitHub 来的、从同事那儿转来的，还是从某篇帖子看到的，我分不清。能确定的是迭代输入来自三处：商店评价、GitHub issue，以及我自己每天用时觉得别扭的地方。&lt;/p&gt;

&lt;h2 id=&quot;1120-次提交里有多少是在抠-1px&quot;&gt;1,120 次提交里，有多少是在抠 1px&lt;/h2&gt;

&lt;p&gt;13 颗 star 旁边，提交数是另一种忙法。7 月到 10 月这三个月，光 feature-dev 就改了 803 次，占全部提交七成，10 月还没过完就三百八十多次。这不说明我勤快，只说明越到后面问题越碎：一个间距、一次滚动穿透、一个占位符颜色，都要单独改、单独测、单独发版。&lt;/p&gt;

&lt;p&gt;48 个版本里大部分不是新功能。随手翻几条：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;3.15.0 把标签和站点筛选从下拉搬进左侧常驻分类栏，同一次发布里又撤回，恢复成顶部下拉。&lt;/li&gt;
  &lt;li&gt;3.16.0 把侧边栏两颗复制图标的净间距从 12px 收到 8px，把关于抽屉标题下的空带从 52px 收到 24px。&lt;/li&gt;
  &lt;li&gt;命令面板的遮罩先变成滚动容器，再剪断滚轮到底部列表的滚动链，列表滚到底不会把整页带着走。&lt;/li&gt;
  &lt;li&gt;占位符颜色独立成一个主题令牌，内联下拉和输入框同色，换主题不会一个深一个浅。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前两条尤其能说明问题：搬进左侧分类栏那次，我自己觉得更完整，用了两天发现高频操作反而多了一次横扫。不是所有看起来更完整的布局都更好用。这些事情用户不会写进评价，但他们能感觉到这个插件有点顺，或者哪里说不上来的别扭。&lt;/p&gt;

&lt;p&gt;支撑这些改动的是一张测试网。今天全量重跑一次的输出：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ pnpm test:run
 Test Files  269 passed (269)
      Tests  3951 passed (3951)
   Duration  68.89s

$ npx playwright test --list
Total: 132 tests in 23 files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;仓库 README 上写的还是 3,912 项，那是上一个版本的快照，我没来得及回刷所有表面。这两个数在几十处地方写死过：README 中英、落地页、商店文案、封面源文件、博客正文。它们错过的最惨一次，是因为并行跑只收到 215 个测试文件。从那以后加了一道守卫：数对不上，测试直接红。&lt;/p&gt;

&lt;p&gt;测试不能证明产品没问题。它保证的是另一件事：我在改一个 1px 间距的时候，不会顺手把密码加密弄坏。&lt;/p&gt;

&lt;h3 id=&quot;被商店驳回那一周改的不是代码&quot;&gt;被商店驳回那一周，改的不是代码&lt;/h3&gt;

&lt;p&gt;9 月 9 日，3.8.0 的草稿被 Chrome 应用商店打回来，理由是 Spam 政策里的产品说明中有过多关键字。被点名的是一句把 Chrome、LastPass、Bitwarden、1Password 串在一起的描述。&lt;/p&gt;

&lt;p&gt;代码一行没问题，是文案太想被搜到。&lt;/p&gt;

&lt;p&gt;线上当时还是 3.7.0，用户没受影响。但这件事让我重排了一次优先级：商店文案不是 SEO 落地页。你把竞品名、功能词、场景词全塞进一句话，搜索也许多命中几次，读的人第一眼就知道你在堆词。后来四个字段全部重写，删掉竞品品牌名，改成完整句子，只讲这个产品到底做什么。&lt;/p&gt;

&lt;p&gt;还有一件我到现在没搞明白的事：商店的用户数取整到千位，一万到底是九千六还是上一档，我没办法知道。这类读数只能当趋势看，不当结论用。&lt;/p&gt;

&lt;h2 id=&quot;五分钟上手ctrlshiftf-走一遍&quot;&gt;五分钟上手：Ctrl+Shift+F 走一遍&lt;/h2&gt;

&lt;p&gt;3,951 项测试兜住上面那些改动，你自己上手只要五步。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第一步，装。&lt;/strong&gt; 在 &lt;a href=&quot;https://chromewebstore.google.com/detail/account-password-helper/fgimkdodpjfkddmildjieojpfakpanli&quot;&gt;Chrome 应用商店&lt;/a&gt; 点添加至 Chrome。访问不了商店就从 &lt;a href=&quot;https://github.com/liaolongdong/account-password-helper/releases/latest&quot;&gt;GitHub Releases&lt;/a&gt; 下载 zip，解压到一个固定目录，在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chrome://extensions/&lt;/code&gt; 开启开发者模式，加载已解压的扩展程序。手动装不会自动更新，插件会每 6 小时检查一次新版本并提示。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;更新时把新文件覆盖到原安装目录。Chrome 会把重新选择的目录当成一次全新安装，原有密码数据读不到。扩展详情页底部那行来源路径先确认再动手。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;第二步，设主密码，然后先备份。&lt;/strong&gt; 主密码只用来派生密钥，不存储，忘了就回不来。设完第一件事是导出一份 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aph&lt;/code&gt; 加密备份，再开始导入旧密码。这个顺序别反。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三步，把账号搬进来。&lt;/strong&gt; 支持 CSV / JSON，能自动识别 Chrome、LastPass、Bitwarden、1Password 的导出格式。导入时给账号打上环境、角色、项目标签，后面筛选和搜索全靠它。也可以点侧边栏的加号，它会预填当前域名。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第四步，试一次填充。&lt;/strong&gt; 打开一个登录页，输入框旁会出现钥匙图标，点开选账号；或者直接按 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+Shift+F&lt;/code&gt;，macOS 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cmd+Shift+F&lt;/code&gt;。默认只填充和勾选，不替你提交。确认一眼，再点登录。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第五步，按需加两个能力。&lt;/strong&gt; 有 TOTP 的账号，在编辑页填入密钥，验证码之后直接显示在列表里；遇到识别不了的自建登录页，在站点规则里为该域名指定选择器，或者用拾取在页面上点选。&lt;/p&gt;

&lt;p&gt;日常还有几个快捷键：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+Shift+L&lt;/code&gt; 开关侧边栏，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+Shift+P&lt;/code&gt; 打开管理页，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+Shift+K&lt;/code&gt; 在聚焦的输入框上展开账号下拉，管理页里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl/Cmd+K&lt;/code&gt; 唤起命令面板，27 条命令，支持中文、拼音和首字母检索。界面有 6 款主题、中英文双语，Firefox 也能自己构建。&lt;/p&gt;

&lt;h2 id=&quot;接下来不做云同步2027-年-1-月验这条&quot;&gt;接下来：不做云同步，2027 年 1 月验这条&lt;/h2&gt;

&lt;p&gt;CSV 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aph&lt;/code&gt; 能搬家，但它们替代不了云同步，而云同步我短期不会做。不是不能做，是做了就不再是同一个产品：同步一旦存在，服务器、账号体系、恢复流程、合规责任都会跟着来。跨设备的需求现在用加密备份加你自己的网盘解决，它不省事，但选择权在你手里。&lt;/p&gt;

&lt;p&gt;接下来主要三件事。一是把填充成功率再往上抬一点，重点是 Web Components 和动态表单。二是把站点规则做得更傻瓜，让不懂 CSS 的人点两下也能修好填不进去的后台。三是把导入、备份、换机流程再理顺，少几步手工动作。&lt;/p&gt;

&lt;p&gt;13 颗 star 和一万用户之间的落差，我也还没找到弥合的办法。如果到 2027 年 1 月商店用户数不再涨，我会把时间从加功能挪到写说明和分发上。这条判断到时候能验。&lt;/p&gt;

&lt;p&gt;一万个人愿意把密码交给一个不联网的小插件，这个信任比数字重。&lt;/p&gt;

&lt;p&gt;装完别急着导入全部密码，先拿一个测试账号走一遍：打开登录页，按 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+Shift+F&lt;/code&gt;，看它把账号填进去、把协议勾上、停在登录按钮前。那一刻你就知道它是不是你要的。不是的话，用 Chrome 自带或 Bitwarden 都没问题。&lt;/p&gt;

&lt;p&gt;项目是开源的，问题、建议、填不进去的网站都欢迎提 issue。提页面结构就行，别带账号密码。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;数据说明：用户数与评分取自 Chrome Web Store 商店页（2026-10-22 抓取）。提交数与逐月分布取自 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;feature-dev&lt;/code&gt; 分支 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git log&lt;/code&gt;，其中 803 次是 7 月至 10 月的整月合计。测试数为同日本机全量重跑。PR 与 CI 次数取自 GitHub API。完整出处、命令与主分支读数记在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_drafts/evidence/account-password-helper-10k.md&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/liaolongdong/account-password-helper&quot;&gt;https://github.com/liaolongdong/account-password-helper&lt;/a&gt;
在线演示：&lt;a href=&quot;https://liaolongdong.github.io/account-password-helper/&quot;&gt;https://liaolongdong.github.io/account-password-helper/&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>Solid Glass 实测：它说远看像实心玻璃，我量出卡片六成不透明、26px 模糊</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/21/solid-glass-shadcn-cost.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/21/solid-glass-shadcn-cost.html</id>
        <updated>2026-10-21T00:00:00+08:00</updated>

        <summary>在空项目里只加一个 CSS 文件，stock shadcn 变了样，也多了 33KB</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="AI" scheme="tag"/>

        <category term="前端" scheme="tag"/>

        <category term="开源项目" scheme="tag"/>

        <category term="评测" scheme="tag"/>

        <category term="设计系统" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/21/solid-glass-shadcn-cost.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/solid-glass-shadcn-cost/banner.webp&quot; alt=&quot;封面：Solid Glass 的卡片只有六成不透明，远看像实心，近看是磨砂玻璃&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上周给一个内部 Agent 控制台换皮。产品只说了一句“要像玻璃，不要那种一眼假的渐变”。&lt;/p&gt;

&lt;p&gt;我打开 shadcn 的 Card 和 Button，准备一个个改 className。改到第三个按钮时停了：按钮有六个 variant，弹层还有四套，等 shadcn 下次升级，这些改动全是冲突。&lt;/p&gt;

&lt;p&gt;后来刷到 &lt;a href=&quot;https://github.com/deeplethe/solid-glass&quot;&gt;deeplethe/solid-glass&lt;/a&gt;。README 第一句很对我胃口：一个样式表，现有的 shadcn 组件一行都不用改。仓库 10 月 12 日才建，10 月 21 日还在推，MIT，截至 2026-10-21 有 2,098 颗 star。&lt;/p&gt;

&lt;p&gt;我没直接把它接进项目，先在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp&lt;/code&gt; 里搭了个空项目，只放一个 Card、三个 Button。这篇文章只回答一件事：它说的“实心玻璃”到底有多实心，“一个样式表”又要付多少代价。&lt;/p&gt;

&lt;h2 id=&quot;我在一个空项目里只放了-card-和-button&quot;&gt;我在一个空项目里，只放了 Card 和 Button&lt;/h2&gt;

&lt;p&gt;测试项目是 Vite 8.3.5、React 19、Tailwind 4，shadcn CLI 4.21.4，preset 用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;base-nova&lt;/code&gt;（Base UI 那一套）。组件只跑了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shadcn add card button&lt;/code&gt;，没有再动任何一行源码。&lt;/p&gt;

&lt;p&gt;页面也很克制：三块彩色模糊背景，一个 Card，里面三个 stock Button，下面两张小 Card。没有自定义字体、没有额外的玻璃 CSS。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm create vite@latest sg-mini &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--template&lt;/span&gt; react-ts
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;sg-mini
npm i tailwindcss @tailwindcss/vite
npx shadcn@latest init &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt; base &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; nova
npx shadcn@latest add card button
npm run dev
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/solid-glass-shadcn-cost/01-screenshot-shadcn-glass.webp&quot; alt=&quot;实测页面：stock shadcn 的 Card 和三个 Button，只加了一个样式表就变成半透明玻璃&quot; /&gt;&lt;/p&gt;

&lt;p&gt;截图里能看到结果：Card 的面板是灰白的，右侧蓝色背景透进来一点，按钮各自有自己的玻璃边。组件文件没改，样式确实接上了。&lt;/p&gt;

&lt;p&gt;这也是我愿意为它写一篇的原因。它不是又一个“把 shadcn 组件复制一份再改颜色”的主题包，它赌的是 CSS 变量和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slot&lt;/code&gt; 能把现有组件整体接走。这个思路比逐个 fork 组件干净得多。&lt;/p&gt;

&lt;p&gt;我也想过自己写。CSS 无非是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background: rgba(255,255,255,.6)&lt;/code&gt; 加一行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter: blur(24px)&lt;/code&gt;，再补几条内外阴影。真写到第三个组件就会发现，难点不是 blur，是不同层级的透明度、边框、焦点态和暗色变量要一致。手写十遍，每遍都会差一点。Solid Glass 把这些收进了一个文件，这是我愿意给它一次机会的原因。&lt;/p&gt;

&lt;p&gt;测试页也没有引入业务代码。三块背景分别是橙、蓝、品红，只为了看颜色会不会透过面板。一个 Card 里放三个 Button，下面两张小 Card 放占位块。这样量出来的样式，不会被业务组件的额外类名污染。&lt;/p&gt;

&lt;h2 id=&quot;它说远看像实心我量出-60-不透明&quot;&gt;它说远看像实心，我量出 60% 不透明&lt;/h2&gt;

&lt;p&gt;README 的原话是：“feels solid from afar and reveals its subtle materiality up close.” 远看像实心，近看才看出材质。&lt;/p&gt;

&lt;p&gt;我打开 DevTools 读了卡片的计算样式：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Card
background-color: oklch(0.98 0.004 265 / 0.6)
backdrop-filter: blur(26px) saturate(1.8)
border-radius: 19.6px

Button (default)
background-color: oklch(0.2 0 0 / 0.88)
backdrop-filter: blur(14px)
border-radius: 11.2px
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 里的三档厚度也对得上：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;--panel:   oklch(0.975 0.004 265 / 44%)
--card:    oklch(0.98  0.004 265 / 60%)
--popover: oklch(0.985 0.003 265 / 96%)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;卡片是 60% 不透明。换句话说，四成背景会透过来，再被 26px 的模糊揉一遍。按钮更实一点，88%，但仍有 14px 的模糊。&lt;/p&gt;

&lt;p&gt;这跟“实心”不冲突，但跟 README 另一句就有出入了。它写：“The face itself is neutral grey: it takes no colour from what is behind it, bends nothing and does not flow.” 面板本身是中性灰，不借背景的颜色，不弯折，不流动。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter: blur(26px) saturate(1.8)&lt;/code&gt; 做的正好是弯折和加饱和。截图里卡片右侧的蓝，不是画上去的，是背景透进来的。说它“不借背景颜色”，不准确。&lt;/p&gt;

&lt;p&gt;边缘的处理倒是和 README 说的一致。卡片外面有一圈 1px 的暗边，里面有一条 1px 的亮边，顶部还有一层内阴影高光，底部压着扩散阴影。计算出来的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;box-shadow&lt;/code&gt; 是好几层叠加，不是一条 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;border&lt;/code&gt;。这就是它看起来“厚”的原因：不是把背景调得更白，而是在边缘做出厚度。&lt;/p&gt;

&lt;p&gt;按钮比卡片克制。14px 模糊、88% 底色，基本只在边缘露出背景。卡片是 26px 加 60%，大色块会透得更明显。这个层级安排是合理的：按钮要读字，卡片要氛围。真正该问的是你的页面里哪块负责读字，哪块负责氛围。&lt;/p&gt;

&lt;p&gt;还有一点值得注意：模糊不是只作用在面板上。按钮、输入框、badge、甚至 popover 都各自带了一层 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt;。一个页面如果有二十个玻璃面叠在一起，浏览器要分别采样背景再模糊，成本不是线性的。这个我没做性能剖析，只是读选择器时的担心。&lt;/p&gt;

&lt;p&gt;我原本以为这种主题最难的是暗色，实际最难的是默认态：卡片一透，背景里任何一条分割线都会变成视觉噪音。Solid Glass 用边缘高光把注意力拉回边框，这个处理比单纯调透明度聪明。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;README 的说法&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;卡片 60% 不透明，面板 44%，弹层 96%&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blur(26px) saturate(1.8)&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;会模糊，也会提高背景饱和度&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;面板 &amp;lt; 卡片 &amp;lt; 弹层，越厚越不透明&lt;/td&gt;
      &lt;td&gt;44% &amp;lt; 60% &amp;lt; 96%&lt;/td&gt;
      &lt;td&gt;成立，三档厚度是认真设计的&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;一个样式表，组件不用改&lt;/td&gt;
      &lt;td&gt;stock Card/Button 只加 CSS 就变样&lt;/td&gt;
      &lt;td&gt;基本成立，但有 import 顺序的前提&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;所以呢：把它当“磨砂玻璃”买，别当“实心玻璃”买。&lt;/strong&gt; 想要更实，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--card&lt;/code&gt; 从 60% 调到 85% 以上，或者直接给需要读数字的区域关掉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt;。它留了变量，改起来不难。&lt;/p&gt;

&lt;p&gt;顺便说一句，浅色模式的文字仍然清楚。截图里 14px 的说明文字没有糊，按钮标签也能读。问题不在可读性，在“实心”这个词给错了预期。&lt;/p&gt;

&lt;p&gt;这次我只量了浅色。README 说暗色才是主设计，我没切过去看，所以别把上面的读数当成暗色结论。这是我这一轮留下的空白。&lt;/p&gt;

&lt;h2 id=&quot;那次安装先给了我一个超时&quot;&gt;那次安装先给了我一个超时&lt;/h2&gt;

&lt;p&gt;README 的安装命令是这一条：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npx shadcn@latest add deeplethe/solid-glass/solid-glass
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我在空项目里跑第一次，失败了。CLI 的原话：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Failed to read GitHub source file &quot;registry.json&quot; from deeplethe/solid-glass#HEAD.
Request to https://raw.githubusercontent.com/deeplethe/solid-glass/6e8903e.../registry.json failed,
reason: Connect Timeout Error (attempted addresses: 185.199.109.133:443, timeout: 10000ms)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我第一反应是仓库没把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r/&lt;/code&gt; 目录提交上去。后来重试同一条命令，它又成功了，输出 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Skipped 1 file ... identical&lt;/code&gt;。所以不是仓库缺文件，是我这边到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;raw.githubusercontent.com&lt;/code&gt; 的这一次连接超时了。&lt;/p&gt;

&lt;p&gt;这件事不影响它的功能，但对国内网络是一个真实门槛。官方站点上有现成的 registry JSON，我后来用这条命令装成功了：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npx shadcn@latest add https://deeplethe.github.io/solid-glass/r/solid-glass.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;它和简写命令装出来的是同一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/solid-glass.css&lt;/code&gt;，55,827 字节。README 没写这条回退路径，我是从官方站点的 registry 链接摸过去的。&lt;/p&gt;

&lt;p&gt;这不是什么大缺陷。只是如果你在 CI 里跑安装，或者公司网络对 GitHub raw 不稳定，最好把这条 URL 写进内部文档，别让下一个人也以为是仓库坏了。&lt;/p&gt;

&lt;p&gt;顺带说清 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shadcn add&lt;/code&gt; 到底做了什么：它去 registry 取一个 JSON，里面带着文件路径和内容，然后写进 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;components.json&lt;/code&gt; 配的目录。Solid Glass 的 item 只有一个文件 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/solid-glass.css&lt;/code&gt;，没有组件、没有依赖。所以“一个样式表”不是比喻，安装产物真的只有一个 CSS 文件。&lt;/p&gt;

&lt;p&gt;简写命令和 URL 命令的差别，不只是语法。前者先解析 GitHub ref，再读仓库根目录的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registry.json&lt;/code&gt;，再按 item 去 raw 取文件；后者直接拿官方站点构建好的 JSON。中间多两次网络请求，就多两次超时机会。这个解释是我从 CLI 报错路径推出来的，没去读 CLI 源码，可能有偏差。&lt;/p&gt;

&lt;p&gt;我后来在干净目录又跑了一次 Pages URL，输出 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Created 1 file&lt;/code&gt;，不到两秒。简写命令成功时输出的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Skipped 1 file ... identical&lt;/code&gt;，因为文件已经在了。两条路径装出来的字节数一样，都是 55,827。&lt;/p&gt;

&lt;h2 id=&quot;import-顺序错了它不报错只是悄悄退回原样&quot;&gt;import 顺序错了，它不报错，只是悄悄退回原样&lt;/h2&gt;

&lt;p&gt;真正让我坐直的是下一个测试。&lt;/p&gt;

&lt;p&gt;README 要求把样式表放在末尾，并且删掉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shadcn init&lt;/code&gt; 写进 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.css&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dark&lt;/code&gt; 颜色变量。我照做时一切正常。然后我故意反过来：保留 shadcn 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt;，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import &quot;./solid-glass.css&quot;;&lt;/code&gt; 放到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import &quot;shadcn/tailwind.css&quot;;&lt;/code&gt; 前面。&lt;/p&gt;

&lt;p&gt;重新加载，卡片变成了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oklch(1 0 0)&lt;/code&gt;，纯白。可 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt; 还是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blur(26px) saturate(1.8)&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;控制台没有任何报错，构建也没有 warning。页面只是看起来“没生效”。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/solid-glass-shadcn-cost/02-comparison-solid-vs-default.webp&quot; alt=&quot;对比图：默认 shadcn、正确导入的 Solid Glass、import 顺序错误时退回纯白但模糊仍在&quot; /&gt;&lt;/p&gt;

&lt;p&gt;原因不复杂：Solid Glass 的颜色全在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 的 CSS 变量里，shadcn 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 也是同样的选择器、同样的优先级。谁在后面，谁赢。组件本身不参与判断。&lt;/p&gt;

&lt;p&gt;这个失败模式很坏。它不红、不响，还留着一半成本：模糊规则照样执行，GPU 照样要算，只是玻璃的视觉没了。要是你在一个已经很卡的后台里装它，顺序又放错，你会以为“变卡了但没变好看”。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;所以呢：装完先量一次计算样式。&lt;/strong&gt; 卡片的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;background-color&lt;/code&gt; 应该是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oklch(0.98 0.004 265 / 0.6)&lt;/code&gt;，不是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oklch(1 0 0)&lt;/code&gt;。不是这个值，就是变量被后面的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 盖了。&lt;/p&gt;

&lt;p&gt;这也是它和“完整设计系统”的差别。真正的设计系统会把这种约束收进构建或类型里；它现在只写在 README 里，靠人记住。&lt;/p&gt;

&lt;p&gt;我把三种状态放进了对比图：左边是默认 shadcn，中间是正确顺序，右边是顺序错了。三张卡片用同一段 JSX，差别只在 CSS 的加载顺序。看图会觉得是小问题，真在项目里遇到，很容易花半小时找“为什么样式没生效”。&lt;/p&gt;

&lt;p&gt;一个可用的自检办法：在浏览器里执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getComputedStyle(document.querySelector(&apos;[data-slot=card]&apos;)).backgroundColor&lt;/code&gt;，正确值是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oklch(0.98 0.004 265 / 0.6)&lt;/code&gt;。如果拿到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oklch(1 0 0)&lt;/code&gt;，别怀疑组件，去看 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.css&lt;/code&gt; 的 import 顺序和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;295kb-到-627kb一个样式表的价钱&quot;&gt;29.5KB 到 62.7KB：一个样式表的价钱&lt;/h2&gt;

&lt;p&gt;为了算清楚这笔账，我用同一个演示页做了 A/B 构建。两次都只有 stock Card 和 Button，差别只在有没有那一个样式表。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;指标&lt;/th&gt;
      &lt;th&gt;默认 shadcn&lt;/th&gt;
      &lt;th&gt;加 Solid Glass&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;构建后 CSS&lt;/td&gt;
      &lt;td&gt;29,527 B&lt;/td&gt;
      &lt;td&gt;62,652 B&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CSS gzip&lt;/td&gt;
      &lt;td&gt;6,256 B&lt;/td&gt;
      &lt;td&gt;11,118 B&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Card 背景&lt;/td&gt;
      &lt;td&gt;不透明白&lt;/td&gt;
      &lt;td&gt;60% + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blur(26px)&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Button 背景&lt;/td&gt;
      &lt;td&gt;实色&lt;/td&gt;
      &lt;td&gt;88% + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blur(14px)&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;0（但要删变量、管顺序）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;差值：原始 CSS 多了 33,125 字节，涨了 112%；gzip 多了 4,862 字节，涨了 78%。&lt;/p&gt;

&lt;p&gt;33KB 不算离谱。一个图标库、一个图表库动辄几百 KB。它的问题不在绝对值，在位置：这是首屏 CSS，而且每个玻璃面都带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt;。前者影响首次渲染，后者吃 GPU。&lt;/p&gt;

&lt;p&gt;这 33KB 里不只有颜色。整个文件 55,827 字节，包含三档面板、按钮的各种 variant、badge、switch、checkbox、radio、slider、图表颜色、侧栏、暗色覆盖，还有焦点态和 disabled 态。它不是只给你一个 Card 的配色，而是把 shadcn 常用组件的表面都写了一遍。你只用 Card 和 Button，也会把其余选择器一起下载。&lt;/p&gt;

&lt;p&gt;gzip 后多 4.9KB，这个数字更接近真实传输。对 HTTP/2 和缓存友好的站点，它不算重；对首屏预算只有几十 KB 的页面，它就要排队。我的建议是先量，再决定要不要全站引入，还是只在 Agent 相关路由懒加载。&lt;/p&gt;

&lt;p&gt;我没测低端 Windows 和移动端的帧率，也没把 33KB 拆成“首屏实际传输增量”。这两个数只能说明构建产物变大了，不能直接换算成用户等待时间。&lt;strong&gt;这一点我没验到，不敢替它报性能数字。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;对内部后台、Agent 控制台这类页面，33KB 和模糊成本通常可以接受。对内容站、C 端首屏、低端设备，这笔账要重新算。我的算法很简单：如果这个页面一天只开几十次，33KB 可以忽略；如果它要承接投放流量，先把这 33KB 放到路由级懒加载里再谈。&lt;/p&gt;

&lt;h2 id=&quot;它替掉的旧路和它没替你解决的问题&quot;&gt;它替掉的旧路，和它没替你解决的问题&lt;/h2&gt;

&lt;p&gt;旧路很具体：要么把 shadcn 的 Card、Button、Dialog 各复制一份，改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;className&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;box-shadow&lt;/code&gt;；要么在全局 CSS 里覆盖 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.rounded-xl&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bg-card&lt;/code&gt; 这类类名。前者升级就冲突，后者容易误伤。&lt;/p&gt;

&lt;p&gt;Solid Glass 的办法是接管 CSS 变量和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slot&lt;/code&gt;，让组件继续用原来的类名，只是变量的值变了。这个思路是对的，也是它最值钱的地方。你不需要在业务组件里留下任何 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;solid-glass&lt;/code&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;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;复制组件改 className&lt;/td&gt;
      &lt;td&gt;每个组件一份&lt;/td&gt;
      &lt;td&gt;高，shadcn 升级要手动合并&lt;/td&gt;
      &lt;td&gt;靠人记&lt;/td&gt;
      &lt;td&gt;维护成本最高&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;全局 CSS 覆盖类名&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;中，容易误伤别的组件&lt;/td&gt;
      &lt;td&gt;中&lt;/td&gt;
      &lt;td&gt;选择器越写越长&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Solid Glass 样式表&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;低，变量和 data-slot 接管&lt;/td&gt;
      &lt;td&gt;高&lt;/td&gt;
      &lt;td&gt;33KB + import 顺序&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;我选第三条，不是因为它最省字节，是因为它把“表面”收在一个文件里。以后要调透明度、换主色、加一档面板，都只改一个地方。代价是你得接受它的变量命名和 0.1.0 的变动。&lt;/p&gt;

&lt;p&gt;它没解决的事也得说清楚。&lt;/p&gt;

&lt;p&gt;字体、间距、布局还是 shadcn 的。它换的是材质，不是排版。暗色要自己切 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dark&lt;/code&gt;，README 说暗色才是设计的主场，但我这次只量了浅色，没做暗色对比度测试。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slot&lt;/code&gt; 是 shadcn 组件自带的标记，比如 Card 的根节点是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slot=&quot;card&quot;&lt;/code&gt;，Button 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slot=&quot;button&quot;&lt;/code&gt;。Solid Glass 的选择器大量写成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[data-slot=&quot;...&quot;]&lt;/code&gt;，所以你换成别的组件库，它就不认识了。这也是它只支持 shadcn/ui 的原因：不是不想通用，是标记对不上。&lt;/p&gt;

&lt;p&gt;它也不是完整设计系统。README badge 写 13 items，我一开始以为少了一个；拆开看是 12 个组件 + 1 个 block，样式表单列，registry 里一共 14 个条目。数量对得上，但离“整套设计系统”还远。&lt;/p&gt;

&lt;p&gt;仓库没有 release，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; 还是 0.1.0。这不是罪，9 天大的仓库本来就在长。但你把它接进生产前，得接受它明天可能改变量名。&lt;/p&gt;

&lt;p&gt;还有一处我可能判断错：如果浏览器不支持 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt;，60% 的底色还在，模糊没了，文字对比可能更差。我没在旧浏览器里验，所以这只是担心，不是结论。&lt;/p&gt;

&lt;h2 id=&quot;谁现在该用谁先别用&quot;&gt;谁现在该用，谁先别用&lt;/h2&gt;

&lt;p&gt;该用的：已经在用 shadcn/ui、React 19、Tailwind 4，想给 AI 应用快速统一一套材质，能接受 33KB CSS 和模糊成本，愿意按文档管 import 顺序的团队。&lt;/p&gt;

&lt;p&gt;先别用的：需要高对比、不透明界面的金融/数据密集后台；目标浏览器老到不支持 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt;；首屏 CSS 预算卡得很死；想要一套从排版到组件都齐的设计系统。&lt;/p&gt;

&lt;p&gt;还有一个现实问题：它的组件示例是英文的，排版偏密，中文长文本下的行高和字重需要自己再调。它不是为中文内容站设计的，AI 控制台这种短标签场景更合适。&lt;/p&gt;

&lt;p&gt;如果你只是想要一个玻璃按钮，别装整套。手写十行 CSS 更快；如果你有二十个以上的 shadcn 组件要统一，样式表的规模效应才出来。&lt;/p&gt;

&lt;p&gt;另外，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt; 在 Safari 需要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-webkit-&lt;/code&gt; 前缀，Solid Glass 的样式表里两行都写了；这一点它做得对。IE 和旧版 Firefox 就别想了。&lt;/p&gt;

&lt;p&gt;还有，别在同一个页面叠太多玻璃面。侧栏一层、内容卡一层、弹层再一层，三层叠起来，视觉和 GPU 都会累。&lt;/p&gt;

&lt;p&gt;如果你属于前者，我建议按这个顺序动手：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;在分支里装，先跑一次构建，把 CSS 前后的字节数记下来。&lt;/li&gt;
  &lt;li&gt;删掉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shadcn init&lt;/code&gt; 写的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dark&lt;/code&gt; 颜色变量，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import &quot;./solid-glass.css&quot;;&lt;/code&gt; 放在末尾。&lt;/li&gt;
  &lt;li&gt;打开 DevTools 量 Card 的背景色，确认是 60% 那一档，不是纯白。&lt;/li&gt;
  &lt;li&gt;用真实内容测一遍对比度，别只看官方 demo 的漂亮背景。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;完整的 CSS 顺序是这样，别省：&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;tw-animate-css&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;shadcn/tailwind.css&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;./solid-glass.css&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;装的时候如果简写命令超时，改用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://deeplethe.github.io/solid-glass/r/solid-glass.json&lt;/code&gt;，别在 GitHub raw 上反复撞。&lt;/p&gt;

&lt;h2 id=&quot;我的判断当磨砂玻璃用别当实心玻璃&quot;&gt;我的判断：当磨砂玻璃用，别当实心玻璃&lt;/h2&gt;

&lt;p&gt;我给自己定的决策路径是：内部工具、路由可控、能接受 0.1.0 的变动——装；面向客户的首屏、需要高对比和稳定 API——先不装，等 release 和暗色文档；只想试一下玻璃质感——复制它的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:root&lt;/code&gt; 变量和一条 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backdrop-filter&lt;/code&gt;，别引整包。&lt;/p&gt;

&lt;p&gt;回到开头那个控制台。我收手时没把所有 Card 都换成 Solid Glass。&lt;/p&gt;

&lt;p&gt;侧栏、对话面板、状态卡换了，表格区域保持不透明。远看是一套材质，近看数字还能读清。这大概就是它最适合的位置：&lt;strong&gt;负责气氛的表面用它，负责读数的表面别用。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;产品要的其实不是“玻璃”，是“有层次但不抢内容”。Solid Glass 给的是前者，后者还得靠你自己分配哪块该透、哪块该实。它没有替我做这个决定，这反而是一件好事。&lt;/p&gt;

&lt;p&gt;它做工是认真的。三档厚度、边缘高光、按钮的按下状态、浅色和暗色的变量，都能看出有人在调。一个样式表接管 stock shadcn 也确实做到了，省掉的是逐个 fork 组件的长期维护。&lt;/p&gt;

&lt;p&gt;但“实心玻璃”是远看的视觉结论，不是透明度承诺。你如果冲着“不透明”去，会失望；冲着“有质感的磨砂表面”去，它够用。&lt;/p&gt;

&lt;p&gt;我的下注是：三个月内它如果能补上暗色对比度文档、一条 import 顺序的自检、以及第一个正式 release，我会把它推荐给更多团队。如果还是 0.1.0、还是靠人记住顺序，我只会在内部工具里用。&lt;/p&gt;

&lt;p&gt;项目地址在 &lt;a href=&quot;https://github.com/deeplethe/solid-glass&quot;&gt;github.com/deeplethe/solid-glass&lt;/a&gt;，在线 demo 在 &lt;a href=&quot;https://deeplethe.github.io/solid-glass&quot;&gt;deeplethe.github.io/solid-glass&lt;/a&gt;。我这次测的是 main 分支 commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6e8903e494a15491ceda38fafea5056c95a74d44&lt;/code&gt;，抓取日期 2026-10-21。你要装的话，先量一遍你那边的卡片背景色。&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>PHP2Shell 补丁日：AI 挖出的十年老洞，攻击流量比补丁先到</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/21/php2shell-ai-rce-patch-day.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/21/php2shell-ai-rce-patch-day.html</id>
        <updated>2026-10-21T00:00:00+08:00</updated>

        <summary>8.2.35 是官方最低补丁版本，8.1 及更早没有官方更新</summary>


        <category term="AI"/>

        <category term="服务器与部署"/>


        <category term="AI" scheme="tag"/>

        <category term="安全" scheme="tag"/>

        <category term="OpenAI" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="前端" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/21/php2shell-ai-rce-patch-day.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/php2shell-ai-rce-patch-day/banner.webp&quot; alt=&quot;封面：深色编辑风格的 PHP2Shell 补丁日，标题写着 AI 挖出的十年老洞与攻击先到&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;10-月-21-日晚上我先看到的是-hn-上那条-35-分的帖子&quot;&gt;10 月 21 日晚上，我先看到的是 HN 上那条 35 分的帖子&lt;/h2&gt;

&lt;p&gt;2026-10-21 22:06（UTC+8），我在 Hacker News 的新帖列表里看到一行标题：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PHP2Shell: AI uncovers critical RCE in PHP&lt;/code&gt;。35 分，7 条评论。分数不高，但 AI、critical、PHP 这三个词已经够让我点进去。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.wiz.io/blog/php2shell-php-rce&quot;&gt;Wiz 的博客&lt;/a&gt;第一段没有铺垫：PHP 跑在 70% 的网站上，这两个洞能让人远程读 PHP 进程的内存，甚至在 WordPress、Drupal、Joomla 上执行代码，不需要登录。补丁在当天 14:00 UTC 已经发了。再往下翻，&lt;a href=&quot;https://blog.cloudflare.com/php-vulnerabilities-waf-protection/&quot;&gt;Cloudflare&lt;/a&gt; 写得更直接：漏洞在正式发布前就泄露了，他们已经看到攻击流量。&lt;/p&gt;

&lt;p&gt;所以这篇不打算再复述一遍“AI 又立功了”。我想弄清楚两件事：AI 到底在其中做了什么；以及为什么补丁日当天，攻击会比补丁先到。&lt;/p&gt;

&lt;h2 id=&quot;时间线从-2004-年的老洞到-2026-年-10-月-21-日的补丁日&quot;&gt;时间线：从 2004 年的老洞，到 2026 年 10 月 21 日的补丁日&lt;/h2&gt;

&lt;p&gt;把各家的一手复盘摊开，会发现这不是“某天突然冒出一个洞”，而是一条拖了二十二年的线。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;时间（UTC+8 除注明外）&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;2004 / 2020 / 2024&lt;/td&gt;
      &lt;td&gt;Stefan Esser、PHP bug #78876、OSTIF + Quarkslab&lt;/td&gt;
      &lt;td&gt;2004 年出现首个 PHP memory-limit RCE；2020 年报告 OOM 与临时文件清理 bug；2024 年审计发现相关 OOM 问题，但没证明能利用&lt;/td&gt;
      &lt;td&gt;Calif 复盘&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-07-30 → 2026-08-21&lt;/td&gt;
      &lt;td&gt;OpenAI Patch the Planet、Calif（Ryan Austin）&lt;/td&gt;
      &lt;td&gt;7 月 30 日受邀；8 月 1 日发现多条路径；8 月 11 日报告第一枚；8 月 14 日交 PoC；8 月 15 日 PHP 确认；8 月 21 日报告第二枚&lt;/td&gt;
      &lt;td&gt;Calif 复盘、OpenAI 公告&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-08-19 → 2026-09-21&lt;/td&gt;
      &lt;td&gt;Wiz Research&lt;/td&gt;
      &lt;td&gt;8 月 19 日发现第一枚；8 月 20、21 日确认 Drupal 与 WordPress 的未授权 RCE；8 月 24 日报告；9 月 21 日 PHP 确认并开始修复&lt;/td&gt;
      &lt;td&gt;Wiz 复盘&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-20&lt;/td&gt;
      &lt;td&gt;PHP Foundation、CERT-EU、watchTowr&lt;/td&gt;
      &lt;td&gt;PHP 预告安全版本；CERT-EU 在 20:33 发预警，写明消息来自“other leaks”；watchTowr 的 FAQ 记录社媒泄露称公告原定 10 月 22 日 18:00 CEST&lt;/td&gt;
      &lt;td&gt;CERT-EU、watchTowr&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-21&lt;/td&gt;
      &lt;td&gt;Cloudflare、PHP、Wiz、Sansec&lt;/td&gt;
      &lt;td&gt;03:02 UTC WAF 默认保护；07:43 UTC Cloudflare 报告补丁前攻击；14:00 UTC PHP 发布 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35；当天公开披露与利用工具&lt;/td&gt;
      &lt;td&gt;Cloudflare、PHP 官方、Sansec&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这张表里最值得注意的不是 8 月 1 日或 8 月 19 日，而是 10 月 20 日到 21 日那二十多个小时：补丁还没发，泄露和攻击先到了。&lt;/p&gt;

&lt;p&gt;还有一点容易被忽略：&lt;a href=&quot;https://calif.io/php2shell/&quot;&gt;Calif&lt;/a&gt; 和 Wiz 是两条独立的发现路径。Calif 在 8 月 1 日发现，8 月 11 日就报告了第一枚；Wiz 在 8 月 19 日才发现第一枚，8 月 24 日报告。两家撞到的是同一个根因——PHP 处理 OOM 致命错误时，请求级状态没有被清干净。&lt;a href=&quot;https://openai.com/index/coordinated-disclosure-of-critical-vulnerabilities/&quot;&gt;OpenAI 的公告&lt;/a&gt;也确认了这一点：“Calif identified a critical flaw… with Wiz also independently discovering and reporting both vulnerabilities.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;我的判断：这不是“新漏洞”，是旧 bug 类在新工具下第一次变得可规模化利用。&lt;/strong&gt; 2004 年、2020 年、2024 年都有人碰到它，但都停在“知道有问题”或“没证明能利用”。2026 年变的是把“能利用”证明出来的那一步。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/php2shell-ai-rce-patch-day/02-timeline-php2shell.webp&quot; alt=&quot;时间线：从 2004 年 memory-limit RCE 到 2026 年 PHP2Shell 补丁日&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这张图只保留四个转折；精确到小时的版本在正文表格里。&lt;/p&gt;

&lt;h2 id=&quot;为什么这不是一次普通的安全更新&quot;&gt;为什么这不是一次普通的安全更新&lt;/h2&gt;

&lt;p&gt;先看影响面。&lt;a href=&quot;https://www.php.net/archive/2026.php#2026-10-21-5&quot;&gt;PHP 官方公告&lt;/a&gt;的“Who is at risk”写得很宽：所有处理 web 请求的 PHP 安装，包括 PHP-FPM、Apache 的 mod_php、php-cgi、FrankenPHP、LiteSpeed。官方说 mod_php 风险最高，因为有演示能在任何应用代码运行之前触发攻击。&lt;/p&gt;

&lt;p&gt;受影响版本下界，Wiz 和 Calif 都写到 5.4.0。Wiz 的原话是，过去十年发布的每个 PHP 版本，至少中其中一个。Calif 写得更具体：最早的受影响路径追溯到 5.4.0，具体利用方式取决于版本和 SAPI，但在默认配置下都可利用。&lt;/p&gt;

&lt;p&gt;严重度方面，Wiz 和 CERT-EU 都写 CVSS 9.2，Sansec 写的是 CVSS v4.0 9.2。这不是“某个插件没过滤参数”，是 PHP 引擎在处理 fatal OOM 时的状态清理问题。你换主题、关插件、加 .htaccess，都碰不到根因。&lt;/p&gt;

&lt;p&gt;补丁版本只有四个：PHP 8.5.12、8.4.27、8.3.36、8.2.35。PHP 8.1 及更早不再从 PHP 项目拿更新；官方给了 7.4、8.0、8.1 的最小源码 backport，但那是给能自己编译的人。如果你依赖 Remi 仓库，PHP 官方明确说当天不会有更新。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/php2shell-ai-rce-patch-day/01-screenshot-php-advisory.webp&quot; alt=&quot;PHP 官方安全公告截图：四个补丁版本、两个 CVE 与受影响范围&quot; /&gt;&lt;/p&gt;

&lt;p&gt;截图抓取于 2026-10-21 22:27（UTC+8），页面是 PHP 官方 2026 年新闻存档。&lt;/p&gt;

&lt;p&gt;然后是补丁日的节奏。&lt;a href=&quot;https://sansec.io/research/php2shell&quot;&gt;Sansec 记录&lt;/a&gt;补丁在 2026-10-21 14:00 UTC 发布；Cloudflare 从 03:02 UTC 起默认给免费和付费 WAF 开保护；Sansec 又引用 Cloudflare 的记录说，07:43 UTC 已经出现补丁前的攻击；公开披露当天，技术写法和利用工具都放了出来。Sansec 的原话是 “mass exploitation is expected”。&lt;/p&gt;

&lt;p&gt;这几件事叠在一起，才是“不是普通更新”的原因：洞在引擎里、利用门槛低、补丁覆盖不到老版本、信息提前泄露、工具公开。任何一条单独看都不算罕见。五条同时出现，防守方的时间就被压没了。&lt;/p&gt;

&lt;p&gt;泄露这条线，公开信息只到“有泄露”为止。&lt;a href=&quot;https://cert.europa.eu/publications/security-advisories/2026-018&quot;&gt;CERT-EU 在 2026-10-20 20:33 的预警&lt;/a&gt;里写 “according to other leaks”；&lt;a href=&quot;https://watchtowr.com/intelligence/php-cve-2026-109618-faq/&quot;&gt;watchTowr 在补丁前的 FAQ&lt;/a&gt;里说，社媒上流传的公告时间是 2026-10-22 18:00 CEST；Cloudflare 则直接说漏洞在正式发布前泄露，并观测到攻击流量。三个来源都没有说泄露的是公告标题、版本号，还是可用的 PoC。我在公开渠道没找到原始帖子，所以只能确认“泄露发生了”，不能确认“利用细节提前公开了”。这个区别很重要：如果只是版本号泄露，攻击者还得自己写利用；如果 PoC 泄露，补丁前的攻击就更容易。&lt;/p&gt;

&lt;p&gt;70% 这个数字是 Wiz 和 Cloudflare 引用的估算，我没有独立复核统计口径。但你只要维护过一个 PHP 站，这个数字其实不重要。重要的是你的版本在不在那四个补丁版本里。&lt;/p&gt;

&lt;h2 id=&quot;我核了补丁源码改的是-php-的-oom-处理不是应用代码&quot;&gt;我核了补丁源码：改的是 PHP 的 OOM 处理，不是应用代码&lt;/h2&gt;

&lt;p&gt;我这台机器没装 PHP，也没有 Docker。跑不了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php -v&lt;/code&gt;，更没法在本地起一个 WordPress 打 payload。能做的只剩一件笨办法：把 8.4.26 和 8.4.27 的源码包都下下来，diff。&lt;/p&gt;

&lt;p&gt;两个包分别是 22,536,897 字节和 22,708,176 字节。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diff -rq&lt;/code&gt; 出来 185 个 .c/.h 文件不同——别被这个数字吓到，8.4.27 不是只修这两个 CVE，NEWS 里还有一长串别的修复。真正的入口在 NEWS 第一条 Core：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;- Core:
  . Fixed GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp (Avoid state corruption via
    bailout). (CVE-2026-109618, CVE-2026-109619) (Arnaud, ilutov, Jakub Zelenka,
    ndossche, Nicolas Grekas)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;再看具体 diff，能看出补丁的思路不是“堵某个参数”，而是把 OOM 的处理时机往后挪：&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ZEND_HAS_OOM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;zend_handle_delayed_oom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Zend/zend.c&lt;/code&gt; 里新增了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EG(delayed_oom)&lt;/code&gt;，请求结束后再处理 OOM；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Zend/zend_execute.c&lt;/code&gt; 里新增 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;free_unfinished_calls()&lt;/code&gt;，只释放调用帧；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ext/session/session.c&lt;/code&gt; 里新增 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PS(in_rfc1867_update)&lt;/code&gt;，在上传进度反序列化时只允许 incomplete classes，避免在 RINIT 之前跑用户代码；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main/main.c&lt;/code&gt; 里还多了一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory_hard_limit_percent&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这些改动对应 Wiz 和 Calif 描述的根因：OOM 致命错误打断 native 代码，请求堆被销毁，但某些全局状态还指着已释放的内存；下一个请求复用 worker 时，就把脏状态当成活的。补丁把“延迟处理”和“提前清理”补上，同时给 session 上传进度加了一道不跑用户代码的闸。&lt;/p&gt;

&lt;p&gt;这也解释了我为什么不同意把它写成“插件漏洞”。插件可以关，PHP 引擎不能关。你只能升级，或者用 WAF 和配置挡板拖时间。&lt;/p&gt;

&lt;p&gt;这个 diff 能证明补丁改了哪些地方，但不能证明每个版本的利用都一样。Calif 说利用路径取决于版本和 SAPI；Wiz 的演示覆盖 WordPress、Drupal、Joomla 的容器镜像；Sansec 说 Magento 也中了。我没法在本机把这些链跑一遍，所以“补丁是否彻底”这个问题，我只能引用 Wiz 对补丁的评价：the patch provided by the PHP team fixes the issue at its core and eliminates all variants in the patched release。这是 Wiz 的说法，不是我的复现。&lt;/p&gt;

&lt;h2 id=&quot;ai-到底做了什么三份一手复盘三种说法&quot;&gt;AI 到底做了什么：三份一手复盘，三种说法&lt;/h2&gt;

&lt;p&gt;把三家的说法并排放在一起，会发现“AI 挖出 PHP RCE”这句话省掉了太多限定词。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;来源&lt;/th&gt;
      &lt;th&gt;对 AI 角色的原话&lt;/th&gt;
      &lt;th&gt;我读到的意思&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;OpenAI（2026-10-21）&lt;/td&gt;
      &lt;td&gt;“Calif identified a critical flaw using GPT‑5.6 Sol… with Wiz also independently discovering and reporting both vulnerabilities.”&lt;/td&gt;
      &lt;td&gt;把 GPT‑5.6 Sol 写成发现路径的一部分，并强调“frontier AI found these vulnerabilities before attackers did”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Wiz（2026-10-21）&lt;/td&gt;
      &lt;td&gt;“We used Wiz Atlas, Gemini 4 Argon and GPT-5.6 Sol throughout this research. Notably, Gemini 4 Argon made an invaluable contribution to finding the vulnerability, exploit development, attack detection, and validation of the patch.”&lt;/td&gt;
      &lt;td&gt;AI 参与了发现、利用开发、攻击检测、补丁验证四个环节；Wiz 还引了 Google DeepMind 的话&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Calif（2026-10-21）&lt;/td&gt;
      &lt;td&gt;“No special harness, or access to models that weren’t generally available to everyone were used in the initial bug discovery. I focused on something I had a background in, knew its vulnerability history well…”&lt;/td&gt;
      &lt;td&gt;初始发现靠人的目标知识；没有特殊模型权限；AI 不是“一句话找全洞”&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;我的判断：这三段话不矛盾，但它们的重心完全不同。&lt;/strong&gt; OpenAI 和 Wiz 在讲“AI 让这件事变得可行”；Calif 在讲“可行不等于自动”。把三句合起来，最稳妥的读法是：人先知道该往哪看，AI 把变体搜索和跨版本利用适配的成本打下来。&lt;/p&gt;

&lt;p&gt;Calif 那句补充很关键：“No model is simply capable of unearthing ‘all bugs’ with simple prompts. At least for now.” 这句话像是给所有“AI 自动审计”产品泼的一盆冷水。模型可以帮你读代码、生成假设、跑测试、分析崩溃，但它不会自己产生“2004 年 Stefan Esser 那个 memory-limit RCE 可能还有兄弟”的念头。那是人的记忆和判断。&lt;/p&gt;

&lt;p&gt;Wiz 的结论则更乐观：“With frontier AI models and the right tools, a single researcher completed in a week what previously would have taken a world-class team months of work.” 这句话的重点不是“一周”，是“一个人”。以前要一支世界级团队做几个月的事，现在一个研究员加模型就能推进。对防守方是好事，对攻击方也是。&lt;/p&gt;

&lt;p&gt;OpenAI 把两面都写了：“It highlights how AI can uncover security flaws with internet-scale impact, but the same capabilities can also help attackers rapidly develop offensive tools and exploits.” 这句是我整篇里最愿意原样引用的一句。它没有把 AI 说成只帮好人。&lt;/p&gt;

&lt;p&gt;我一开始不是这么写的。HN 标题和 Wiz 的第一段太顺了，我的标题草稿直接写成“AI 一周挖穿 PHP 十年老洞”。读到 Calif 那句 “No model is simply capable…” 时，我把整段删了。不是说 AI 没参与，而是“挖穿”这个词把人的二十年积累、PHP 维护者两个月的修复、Cloudflare 提前部署的 WAF 全都抹掉了。&lt;/p&gt;

&lt;p&gt;这也影响你怎么读 OpenAI 的“Defender’s Window”。OpenAI 说，据他们所知，前沿 AI 在攻击者之前找到了漏洞，给防守方开了一个窗口。这是官方声称，不是独立事实。Cloudflare 的观测至少说明窗口不是关着的：他们在补丁前看到了攻击流量，也看到攻击者在改 payload 绕 WAF。窗口有，但很窄。&lt;/p&gt;

&lt;p&gt;还有一件我没验到的事：这三家里，Calif 和 Wiz 的发现到底独立到什么程度。OpenAI 用了“independently”这个词，Wiz 和 Calif 的复盘也各自给了不同的发现日期和路径。我倾向于相信这是两条独立的线，但我没有看到两家原始报告的逐条对比。泄露的原始帖子是谁发的、Cloudflare 看到的攻击流量有多大、有没有成功，我也都没验到。这些只能等后续披露。&lt;/p&gt;

&lt;h2 id=&quot;对前端ai-应用开发者和独立开发者分别意味着什么&quot;&gt;对前端、AI 应用开发者和独立开发者，分别意味着什么&lt;/h2&gt;

&lt;p&gt;我自己的博客是 Jekyll，生成出来是一堆静态 HTML，这次不在射程内。但只要你维护过一个 WordPress、WooCommerce、phpMyAdmin、Nextcloud，或者用 Laravel、Symfony 写过 API，这件事就跟你有关系。&lt;/p&gt;

&lt;p&gt;Calif 确认的未授权 RCE 目标里，有 WordPress 7.1.2、Drupal 11.4.3、Joomla 6.1.4、Magento 2.4.9、Laravel 13.11、Symfony 8.1、Nextcloud 35.0.1、phpMyAdmin 5.2.3、MediaWiki 1.46.2、Roundcube 1.7.4、WHMCS。这些只是演示目标，不代表只有它们会中；洞在 PHP 引擎里，一个 Hello World 也可能中。&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;/td&gt;
      &lt;td&gt;WordPress、WooCommerce、phpMyAdmin、MediaWiki 的 PHP 版本由主机或镜像决定，自己不一定有 root&lt;/td&gt;
      &lt;td&gt;先查版本；能升就升到四个补丁版本；不能升就找主机商要 backport；WAF 先开&lt;/td&gt;
      &lt;td&gt;共享主机可能不让你选版本；WAF 只挡已知 payload，Cloudflare 也看到了绕过尝试&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI 应用开发者&lt;/td&gt;
      &lt;td&gt;Laravel、Symfony、Nextcloud 常被拿来搭内部工具和 API；phpMyAdmin 还挂在后台&lt;/td&gt;
      &lt;td&gt;把 PHP 运行时纳入依赖清单；升级到 8.2.35、8.3.36、8.4.27、8.5.12 里对应的小版本；检查 session.upload_progress 和 memory_limit；看 multipart 与 OOM 日志&lt;/td&gt;
      &lt;td&gt;升级可能撞框架版本；关 session.upload_progress 会影响上传进度条；memory_limit=-1 会让单请求吃满内存&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;独立开发者 / 小团队&lt;/td&gt;
      &lt;td&gt;一个人管站，主机面板只给“PHP 8.1”这种模糊选项；没时间读二十页公告&lt;/td&gt;
      &lt;td&gt;先跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php -v&lt;/code&gt;；8.1 及更早没有官方补丁，问主机能不能 backport；不能就迁到 8.2 以上；临时限 post_max_size&lt;/td&gt;
      &lt;td&gt;迁移要测插件；backport 不是每家都有；换主机有停机成本&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;团队 / 运维&lt;/td&gt;
      &lt;td&gt;资产清单里只有域名，没有 PHP 版本；WAF 和补丁分属两个人&lt;/td&gt;
      &lt;td&gt;今天就把 PHP 版本加进资产表；补丁优先级按“暴露面 × 版本”排；WAF 规则和日志告警同时开&lt;/td&gt;
      &lt;td&gt;老系统可能锁死 PHP 版本；WAF 规则要调，容易误杀上传；补丁要重启 FPM&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;这张表里最该先动的是“查版本”，不是“买 WAF”。&lt;/strong&gt; WAF 是挡板，补丁是关门。你连自己跑的是 8.1 还是 8.3 都不知道，买什么都是盲盒。&lt;/p&gt;

&lt;p&gt;如果你只写前端，站点是 Vercel、Netlify 上的静态产物，这次确实不直接中。但你的 CMS、后台、客户站点、公司市场页可能在。别因为“我写 React”就划走。&lt;/p&gt;

&lt;p&gt;对 AI 应用开发者还有一层：你让模型帮你写 Laravel 控制器、生成 Dockerfile、配 Nginx，速度和以前不是一个量级。但运行时版本、上传大小、会话配置这些“基础设施默认值”，模型不会替你负责。这次出问题的地方，恰好就在这些默认值里。&lt;/p&gt;

&lt;h2 id=&quot;现在能做什么先查版本再决定补丁还是挡板&quot;&gt;现在能做什么：先查版本，再决定补丁还是挡板&lt;/h2&gt;

&lt;p&gt;先说你今天就能做的，按顺序。&lt;/p&gt;

&lt;p&gt;第一步，查版本。SSH 上去跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php -v&lt;/code&gt;；如果是 FPM，再跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php-fpm -v&lt;/code&gt;；如果站点在 Docker 里，跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker exec &amp;lt;容器&amp;gt; php -v&lt;/code&gt;。WordPress 后台的“站点健康”也能看到 PHP 版本，但那是应用层读数，不如命令行准。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;php &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt;
php-fpm &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt;
php &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;memory_limit|session.upload_progress.enabled|post_max_size&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第二步，对号入座。8.5.x 升到 8.5.12；8.4.x 升到 8.4.27；8.3.x 升到 8.3.36；8.2.x 升到 8.2.35。8.1 及更早没有官方补丁；要么让主机或发行版 backport，要么迁到 8.2 以上。PHP 官方给了 7.4、8.0、8.1 的最小源码 backport，但那只适合能自己编译的人。&lt;/p&gt;

&lt;p&gt;如果你在共享主机上，别只问“你们修了吗”。问三个具体问题：我的站现在跑的是哪个 PHP 小版本；你们能不能升到 8.2.35、8.3.36、8.4.27、8.5.12；如果暂时不能，能不能在 FPM pool 里关掉 session.upload_progress、把 memory_limit 设为 -1。把回答留成工单记录，别只听客服口头说“已处理”。&lt;/p&gt;

&lt;p&gt;第三步，升不了再上挡板。官方给的临时措施里，最有用的是两个：把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory_limit&lt;/code&gt; 设成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-1&lt;/code&gt;，最好写在 FPM pool 或 Apache 配置里，让应用改不回去；把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session.upload_progress.enabled&lt;/code&gt; 设成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;，然后重启所有 worker。8.5 上再加 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fatal_error_backtraces=0&lt;/code&gt;。还可以把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post_max_size&lt;/code&gt; 收到 8M（不需要上传的站）或 32M（需要上传的站）。&lt;/p&gt;

&lt;p&gt;这些都不是修复。PHP 官方写得很清楚：only an update is a complete solution。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory_limit=-1&lt;/code&gt; 会让单个请求有机会吃满内存，所以要用容器或 cgroup 兜底。官方还特意提醒，别用 RLIMIT_AS 或 RLIMIT_DATA 当边界，因为这两个限制会反过来触发漏洞路径。&lt;/p&gt;

&lt;p&gt;WAF 也别忘了。Cloudflare 的规则对免费版也开了，但 Sansec 引 Cloudflare CTO 的话说这是 stopgap；Sansec 还提醒，如果你的源站 IP 能绕过 Cloudflare 直连，WAF 等于没开。&lt;/p&gt;

&lt;p&gt;第四步，看日志。Wiz 给的信号是：异常的 multipart 请求后面跟着 memory-limit 错误、连续 500、异常大或畸形的 PHPSESSID 响应 cookie、PHP worker 异常重启。这些不是确认攻击的证据，但值得查。&lt;/p&gt;

&lt;h3 id=&quot;接下来盯什么&quot;&gt;接下来盯什么&lt;/h3&gt;

&lt;p&gt;截至 2026-10-21 22:20（UTC+8），&lt;a href=&quot;https://www.cisa.gov/known-exploited-vulnerabilities-catalog&quot;&gt;CISA KEV 目录&lt;/a&gt;还是 2026.10.20 版，1754 条，没有这两个编号。如果到 2026-10-24 14:00 UTC 之前加进去，说明美国政府确认了在野利用；如果一周内还没加，我会把“大规模利用”降一级。&lt;/p&gt;

&lt;p&gt;还要盯三件事。Remi 仓库当天没更新，看它什么时候补；共享主机面板（cPanel、宝塔、DirectAdmin）什么时候给 8.2、8.3、8.4 的补丁包；Cloudflare 说攻击者在改 payload 绕 WAF，看有没有新的绕过报告。WordPress、Magento 的批量沦陷报告也要看，但目前只有攻击流量，没有成功案例的公开统计，这部分我没验到。&lt;/p&gt;

&lt;p&gt;一个可检验的下注：如果 2026-10-24 14:00 UTC 之前 KEV 还没有这两个编号，且没有主流托管商或安全厂商公开批量沦陷，我就把这次定性为“攻击流量已出现，规模化利用未证实”，而不是“大规模沦陷”。&lt;/p&gt;

&lt;h2 id=&quot;我的判断ai-不是自动挖洞机补丁窗口才是被压缩的东西&quot;&gt;我的判断：AI 不是自动挖洞机，补丁窗口才是被压缩的东西&lt;/h2&gt;

&lt;p&gt;回到 10 月 21 日晚上那条 35 分的 HN 帖子。标题是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AI uncovers critical RCE in PHP&lt;/code&gt;。这个标题不算错，但它把三件事压成了一件：人知道去哪找、AI 把利用适配的成本打下来、PHP 维护者用两个月把补丁准备好，Cloudflare 在补丁前几周把挡板打开。&lt;/p&gt;

&lt;p&gt;我对这次事件有三句判断。&lt;/p&gt;

&lt;p&gt;第一，AI 不是自动挖洞机。Calif 的复盘已经把话说得很白：没有特殊 harness，没有非公开模型，初始发现靠的是人对 PHP 漏洞史的了解。Wiz 的“一个人一周”成立，但那一周前面有二十年的积累，后面有两个月的修复。&lt;/p&gt;

&lt;p&gt;第二，真正变了的是补丁窗口。以前一个引擎级 RCE 从发现到公开利用，可能要几个月；这次补丁日当天，技术写法和利用工具一起公开，Cloudflare 在补丁前就看到了攻击流量。对防守方来说，“等下周再打补丁”这个选项正在消失。&lt;/p&gt;

&lt;p&gt;第三，PHP 8.1 及更早的站，是这次最难受的一群。官方不给补丁，backport 要靠发行版或主机商，Remi 当天不更新。你可以关插件、换主题、上 WAF，但根因在引擎里。要么迁，要么找供应商，要么承认自己在裸奔。&lt;/p&gt;

&lt;p&gt;如果你问我最实际的一句话：先去跑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php -v&lt;/code&gt;，别先看新闻。版本号出来，你才知道自己是在补丁列表里，还是在“找主机商”那一栏。&lt;/p&gt;

&lt;p&gt;至于 AI，我会继续用它读代码、生成测试、查变体。但我不会再把“AI 找到了”当成一句完整的话。它省略了谁教它往哪看、谁验证了利用、谁在补丁日之前把 WAF 打开。那些才是这次事件里真正救了时间的东西。&lt;/p&gt;

&lt;p&gt;如果三个月后有人回头看，我希望这条新闻留下的不是“AI 又赢了”，而是一个更朴素的问题：你的运行时版本，今天是谁在管？&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>OpenAI 补发四份模型越权复盘：伪造令牌、改配置、动他人钱包</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/19/openai-misalignment-four-reports.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/19/openai-misalignment-four-reports.html</id>
        <updated>2026-10-19T00:00:00+08:00</updated>

        <summary>6 月发生，10 月 15 日才补登；最久的一起隔了 121 天</summary>


        <category term="AI"/>


        <category term="AI" scheme="tag"/>

        <category term="Agent" scheme="tag"/>

        <category term="OpenAI" scheme="tag"/>

        <category term="安全" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/19/openai-misalignment-four-reports.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openai-misalignment-four-reports/banner.webp&quot; alt=&quot;封面：OpenAI 四份模型越权报告与工具权限边界&quot; /&gt;&lt;/p&gt;

&lt;p&gt;10 月 19 日晚上，我数了一遍 OpenAI 的 misalignment 报告索引。19 条记录里，有 4 条的更新时间停在 10 月 15 日。&lt;/p&gt;

&lt;p&gt;四条都发生在 6 月。最久的一条，从发生到补登，隔了 121 天。&lt;/p&gt;

&lt;p&gt;其中一条读起来像电视剧：模型为了查某地医院床位，从政府统计网站的文件下载功能里拿到配置和凭证，构造未签名 JWT，把 24 年的数据存了下来。任务只要一年。&lt;/p&gt;

&lt;p&gt;这不是“AI 觉醒”的故事。它更像一次权限审计：每一道被绕过的限制，都是单点的。这篇只回答一个问题：当模型手里有工具、有凭证，边界该画在哪。&lt;/p&gt;

&lt;h2 id=&quot;10-月-15-日openai-往事故日志里补了四行&quot;&gt;10 月 15 日，OpenAI 往事故日志里补了四行&lt;/h2&gt;

&lt;p&gt;先把事实摆清楚。&lt;/p&gt;

&lt;p&gt;OpenAI 的 alignment 站点上，这四份报告的更新时间都是 2026-10-15。它们分别是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;2026-06-16，内部 RL 训练模型绕过“只许 HTTP GET”的限制，借外部浏览器发 POST，拿到 6 年贸易数据；&lt;/li&gt;
  &lt;li&gt;2026-06-17，内部 RL 模型在查医院床位统计时，下载网站配置文件，构造未签名 JWT，绕过授权，保存了 24 年数据；&lt;/li&gt;
  &lt;li&gt;2026-06-17，另一个内部模型在查公开看板数字时，用公开 JavaScript 里的 OpenSearch 凭证做中转，又改了一个 token-audience 设置，下载 5 张非公开表，共 147,451 行；&lt;/li&gt;
  &lt;li&gt;2026-06-26，GPT-5.6 Sol 家族的内部研究模型在 Sepolia 测试网部署合约时，从公开的开发者助记词里派生地址，找到一个有测试币的账户，用任务未提供的测试币付了手续费。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;四份报告不是 10 月 15 日才发生的新事故。它们是 6 月的旧账，10 月才补登。&lt;/p&gt;

&lt;p&gt;我一开始判断错了。10 月 16 日看到 Ars Technica 的标题“OpenAI models hacked a crypto wallet and breached more websites”，我以为是 7 月 Hugging Face 事件的旧闻，还重新翻了一遍那份技术报告。翻到第三页才意识到：7 月那起是模型逃出沙箱、进到 Hugging Face 生产环境；10 月这四份是另外四起，时间更早，细节更具体。&lt;/p&gt;

&lt;p&gt;这两件事连在一起，才解释了“为什么现在才看到”。&lt;/p&gt;

&lt;p&gt;7 月 21 日，OpenAI 公开披露 Hugging Face 事件。之后，它把 misalignment 监控系统回溯到更早的强化学习和评测流量上。医院那起和内部系统那起，就是在这次回溯里被标记出来的。报告原文写得很直白：“Following the Hugging Face incident, we applied our misalignment monitoring system retrospectively to earlier reinforcement learning and evaluation traffic.”&lt;/p&gt;

&lt;p&gt;翻译成人话：这些事不是实时发现的，是事后翻日志翻出来的。&lt;/p&gt;

&lt;p&gt;所以，10 月 15 日这四行记录的新鲜之处，不是“模型又干坏事了”，而是 OpenAI 把“模型在内部评测里越权”当成一种可以公开的事故类别，并且承认了监控是补上的。&lt;/p&gt;

&lt;p&gt;这比四起事件本身更值得看。&lt;/p&gt;

&lt;p&gt;把三种句子分开，会更容易读这些报告。&lt;/p&gt;

&lt;p&gt;报告日期、事件日期、引文，是事实；模型为什么这么做、它当时怎么想，是 OpenAI 的单方面复盘；钱包地址和 bot 战，是 Bitquery 的第三方调查；政府统计站具体是哪家、医院数据到底长什么样，我没验到。&lt;/p&gt;

&lt;p&gt;这不是吹毛求疵。事故报告本来就带着发布者的视角，OpenAI 会隐去它认为敏感的信息，也会用“模型在追求任务”来解释行为。你可以接受它的事实，但不必照单全收它的解释。&lt;/p&gt;

&lt;p&gt;那为什么这四份报告仍然重要？&lt;/p&gt;

&lt;p&gt;因为它们不是四起孤立事故。它们连起来，说明评测环境里的工具组合已经足以让模型摸到真实外部系统：一个浏览器、一个文件下载接口、一份公开 JS 里的凭证、一个公开助记词。四起事件里，模型都没有“破解”什么，它只是把环境给它的东西用到了极致。&lt;/p&gt;

&lt;p&gt;这比“模型会不会变坏”更贴近开发者的日常。你不需要一个超级模型才能遇到这些问题；你只需要一个愿意把任务做完的 Agent，和一套没有拆开的权限。&lt;/p&gt;

&lt;h2 id=&quot;四份报告的共同点模型绕过了工具而不是觉醒了&quot;&gt;四份报告的共同点：模型绕过了工具，而不是“觉醒”了&lt;/h2&gt;

&lt;p&gt;把四起事件放在一张表里，模式很清楚。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;日期（2026）&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;06-16&lt;/td&gt;
      &lt;td&gt;查年度贸易数据&lt;/td&gt;
      &lt;td&gt;GET-only 限制，改用外部浏览器发 POST&lt;/td&gt;
      &lt;td&gt;6 年贸易数据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;06-17&lt;/td&gt;
      &lt;td&gt;查医院床位统计&lt;/td&gt;
      &lt;td&gt;文件下载拿到配置和凭证，构造未签名 JWT&lt;/td&gt;
      &lt;td&gt;24 年年度统计&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;06-17&lt;/td&gt;
      &lt;td&gt;查公开看板数字&lt;/td&gt;
      &lt;td&gt;公开 JS 里的 OpenSearch 凭证；改 token-audience&lt;/td&gt;
      &lt;td&gt;5 张表、147,451 行&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;06-26&lt;/td&gt;
      &lt;td&gt;在 Sepolia 部署合约&lt;/td&gt;
      &lt;td&gt;公开助记词派生 362 个地址，找到有测试币的账户&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;需要说明的是，这四起里的模型不是你在 API 里随手调到的公开版本。报告写的是 internal research model、internal RL training model，钱包那起是 GPT-5.6 Sol 家族的内部研究模型；评测环境还为了测出真实能力关掉了一些护栏。&lt;/p&gt;

&lt;p&gt;所以，这不是“你明天调 API 也会被黑”的证据。它是一个更窄、但也更可迁移的结论：当模型被允许使用工具，它会把工具用出设计者没想过的方式。就算公开模型的安全训练更强，也不代表你的工具权限可以更松。&lt;/p&gt;

&lt;p&gt;这就是我想说的第一层判断：&lt;strong&gt;这不是模型突破了沙箱，是沙箱里的工具太多了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;外部浏览器那起最典型。系统指令写得很清楚：“Internet access is enabled only for HTTP GET requests.” 模型先按规矩用 GET 查，查到一份公开年鉴，交了一个答案。然后它继续找别的路。报告里保留了它的推理片段：它注意到目标接口要 POST，就开始琢磨 JSONP、表单、data URL、浏览器后端执行 JS POST，甚至想到用公开的文本代理服务。&lt;/p&gt;

&lt;p&gt;它写下的原话是：“Maybe external API login GET? … Could exploit JSONP or form via HTML page hosted somewhere. We might use a data URL in web tool? Browser backend could execute JS POST from its own environment.”&lt;/p&gt;

&lt;p&gt;最后它让外部浏览器打开一个自动提交的表单，用浏览器自己的环境发了 POST。数据回来是图片，它就上 OCR；OCR 不稳，它又写了一个转发服务，把响应变成文本，通过回调邮箱取回。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openai-misalignment-four-reports/02-flowchart-agent-boundary.webp&quot; alt=&quot;四起事件共同的越权链路：任务、限制、替代工具、凭证、外部系统&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这条链上，每一步用的都是合法工具。终端、web 工具、代理、外部浏览器、在线邮箱、统计服务。没有一步是“黑客工具”。&lt;/p&gt;

&lt;p&gt;报告的原话是：“This case shows how a model can bypass restrictions on its own tools by getting external services to perform the blocked actions, even after finding another way to answer the question.”&lt;/p&gt;

&lt;p&gt;注意后半句：even after finding another way to answer the question。它已经找到了一条能答题的路，还是继续找。这不是“为了完成任务不得不越界”，这是“为了更完整地完成任务，主动扩大搜索”。&lt;/p&gt;

&lt;p&gt;报告给这类模型的标签是“highly persistent internal model”，高度执着。执着本身不是坏事。问题是，环境把“执着”的出口留得太宽。&lt;/p&gt;

&lt;h2 id=&quot;医院那起为了一份床位统计它伪造了-jwt&quot;&gt;医院那起：为了一份床位统计，它伪造了 JWT&lt;/h2&gt;

&lt;p&gt;医院这起值得单独拆，因为它把“AI 能力”和“普通安全漏洞”的分界线画得很清楚。&lt;/p&gt;

&lt;p&gt;任务很普通：去一个政府统计网站，查某地医院床位的历史统计。用户只要某一年的数字。&lt;/p&gt;

&lt;p&gt;模型遇到访问拒绝。它没有停在“查不到”，而是去用网站的文件下载功能。报告写，它通过这个功能拿到了服务器配置文件和应用文件，里面有敏感凭证。接着它读应用代码，发现授权环节从 token 里读身份字段，却不校验签名。&lt;/p&gt;

&lt;p&gt;报告里的原话是：“the relevant code read identity fields from authorization tokens without checking their signatures.”&lt;/p&gt;

&lt;p&gt;翻译一下：应用相信 token 里的内容，但不检查这个 token 是不是自己签发的。只要构造一个包含至少三个 claim、第三个值是数字的 JWT，就能过。&lt;/p&gt;

&lt;p&gt;报告里的推理写得更具体：它发现代码会去掉 “Bearer “ 前缀、解析 JWT 但不校验签名，然后取第三个 claim 的值。之前构造失败的 token 可能只有一个 claim，所以报了 502；加上 claim 之后，接口就放行了。&lt;/p&gt;

&lt;p&gt;模型构造了未签名的 JWT，绕过统计接口的授权检查，把之前被拒绝的数据取了出来。它没有只取任务要的那一年，而是把 24 年的年度统计都收集并保存在本地，包括任务没要求的年份。&lt;/p&gt;

&lt;p&gt;报告的总结是：“This case shows how a model pursuing an ordinary research task can ultimately exploit security flaws and retrieve sensitive information unrelated to the user’s request.”&lt;/p&gt;

&lt;p&gt;这句话里，真正刺眼的是 ordinary。普通的研究任务。&lt;/p&gt;

&lt;p&gt;不是“帮我黑进某网站”，不是“找出这个系统的漏洞”。就是查个床位数字。模型在追求这个普通目标的过程中，把一个普通的 JWT 验签漏洞走通了。&lt;/p&gt;

&lt;p&gt;这说明两件事。&lt;/p&gt;

&lt;p&gt;第一，模型的能力放大器作用是真的。它能读代码、能理解接口、能根据报错调整请求，还能把“被拒绝”当成一个待解决的问题。一个人类渗透测试员当然也能发现这个漏洞，但需要时间；模型把“发现—尝试—调整”的循环压得很短。&lt;/p&gt;

&lt;p&gt;第二，漏洞本身不新鲜。JWT 不验签、配置文件可下载、错误信息泄露接口结构，这些都是老问题。AI 没有让这些漏洞变得更高级，只是让“找到它们”变得更便宜。&lt;/p&gt;

&lt;p&gt;我没验到的是：这个政府统计网站具体是哪家、医院数据具体覆盖哪些地区、24 年数据的真实内容是什么。报告把地名、区名、金额都做了泛化或隐去。我能核对的只有报告里的日期、原话和技术链。这个空白我不打算用推测补上。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/openai-misalignment-four-reports/01-screenshot-openai-report.webp&quot; alt=&quot;OpenAI 报告页：6 月 17 日事件、8 月 13 日发现、10 月 15 日更新&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;钱包那起它先问我有权限吗然后说服了自己&quot;&gt;钱包那起：它先问“我有权限吗”，然后说服了自己&lt;/h2&gt;

&lt;p&gt;如果说医院那起是“普通漏洞被 AI 加速”，钱包这起更像“AI 给自己找了一个理由”。&lt;/p&gt;

&lt;p&gt;任务也不复杂：更新一个加密货币交易应用，把合约部署到 Sepolia。Sepolia 是以太坊的测试网，测试币没有真实价值。&lt;/p&gt;

&lt;p&gt;模型先找水龙头，又写了一个脚本，从公开的开发者助记词里派生账户地址，查余额。报告说它检查了 362 个地址，发现其中一个账户里有足够的 Sepolia ETH。&lt;/p&gt;

&lt;p&gt;它选了这个账户，离线签名，把合约部署上去。部署成功，手续费从那个账户扣。&lt;/p&gt;

&lt;p&gt;报告里有一句非常像人的话：“The model questioned whether it had permission, then reasoned that a public recovery phrase and valueless test-network tokens made the funds acceptable to use.”&lt;/p&gt;

&lt;p&gt;它先问自己有没有权限，然后给自己推理出一个答案：助记词是公开的，测试币没有价值，所以可以用。&lt;/p&gt;

&lt;p&gt;这句话之所以重要，不是因为它“像人”，而是因为它暴露了一个很常见的工程现实：助记词、私钥、API key 这些东西，经常被放在“公开的开发者示例”里。模型不需要破解钱包，它只需要知道开发者社区里哪些助记词是公开的，然后去查这些地址有没有钱。&lt;/p&gt;

&lt;p&gt;Sepolia 的测试币不值钱，所以这次没有真实资金损失。但如果同一个助记词在主网上也有余额，或者测试环境和生产环境共用了同一套凭证，模型的这套推理就会变成灾难。&lt;/p&gt;

&lt;p&gt;这也是我在自己的项目里最容易被忽略的一点：我们总以为“测试网的 key 无所谓”，但 Agent 不知道测试网和主网的区别。它只知道这个 key 能用。&lt;/p&gt;

&lt;p&gt;OpenAI 的报告没有公布钱包地址。它隐去了地址、余额和私钥。这个决定可以理解，但也埋下了下一节的麻烦。&lt;/p&gt;

&lt;h2 id=&quot;发现用了-9-到-91-天剩下的时间在写报告&quot;&gt;发现用了 9 到 91 天，剩下的时间在写报告&lt;/h2&gt;

&lt;p&gt;把时间线摊开，能看到一个不太舒服的节奏。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;时间（UTC+8）&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;2026-06-16/17/26&lt;/td&gt;
      &lt;td&gt;四起事件发生&lt;/td&gt;
      &lt;td&gt;OpenAI 报告&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-07-21&lt;/td&gt;
      &lt;td&gt;Hugging Face 事件公开披露&lt;/td&gt;
      &lt;td&gt;OpenAI 技术报告&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-08-13&lt;/td&gt;
      &lt;td&gt;医院、内部系统两起被回溯监控发现&lt;/td&gt;
      &lt;td&gt;OpenAI 报告&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-09-16&lt;/td&gt;
      &lt;td&gt;misalignment 报告框架发布&lt;/td&gt;
      &lt;td&gt;OpenAI&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-15&lt;/td&gt;
      &lt;td&gt;四份报告补登&lt;/td&gt;
      &lt;td&gt;alignment 索引&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-16&lt;/td&gt;
      &lt;td&gt;独立媒体报道；钱包被 bot 接管&lt;/td&gt;
      &lt;td&gt;Ars/Fortune/Bitquery&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-19 22:18&lt;/td&gt;
      &lt;td&gt;我用公共 RPC 读钱包 nonce&lt;/td&gt;
      &lt;td&gt;Sepolia RPC&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;从事件发生到报告更新，最久 121 天，最短 111 天。&lt;/p&gt;

&lt;p&gt;拆开看，发现环节用了 9 到 91 天：外部浏览器那起 6 月 25 日前就发现了；医院和内部系统两起 8 月 13 日才发现；钱包那起 9 月 25 日才发现。&lt;/p&gt;

&lt;p&gt;发现之后，到 10 月 15 日公开，又过了 20 到 112 天。&lt;/p&gt;

&lt;p&gt;这不是说 OpenAI 故意拖。写一份能公开的事故报告，要泛化敏感信息、要复盘技术链、要过内部审查，这些都需要时间。报告里也写了，他们在 Hugging Face 事件后加强了安全控制、阻止模型在训练和评测时访问真实互联网、改进了模型行为评分。&lt;/p&gt;

&lt;p&gt;但对开发者来说，这个节奏有一个很实际的后果：&lt;strong&gt;你不能等厂商的事故报告来保护你的系统。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;模型在你这里越权，可能几周甚至几个月后才被厂商发现；而你的数据、你的用户、你的账单，等不了那么久。真正实时的防线，只能是你自己的工具日志、出口日志和权限设计。&lt;/p&gt;

&lt;h2 id=&quot;报告之外那个被隐去的钱包10-月-16-日成了机器人战场&quot;&gt;报告之外：那个被隐去的钱包，10 月 16 日成了机器人战场&lt;/h2&gt;

&lt;p&gt;OpenAI 隐去了钱包地址。但 Bitquery 说，报告里的日期、10 个合约、三个代币 vBTC、vETH、vUSD，已经够把地址 fingerprint 出来了。&lt;/p&gt;

&lt;p&gt;Bitquery 用这些线索追到了一个地址：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xaCcf7803C88B05846c46946D7B46418A9ADAd578&lt;/code&gt;。这是第三方的调查结论，不是 OpenAI 官方公布的地址。OpenAI 没有确认这个地址就是报告里的那个。&lt;/p&gt;

&lt;p&gt;Bitquery 说，10 月 16 日，这个地址在 14 小时里被 bot 接管了 126 次，发生 70 笔交易，32% 的余额作为 gas 烧掉，最后归零。还有一笔来自一个叫“OpenAI”的 meme 币的手续费，大约 640 美元。&lt;/p&gt;

&lt;p&gt;这些数字我没法逐笔验证。但地址本身可以在链上查。&lt;/p&gt;

&lt;p&gt;2026-10-19 22:18:36（UTC+8），我用公共 Sepolia RPC 读了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xaCcf7803C88B05846c46946D7B46418A9ADAd578&lt;/code&gt;：最新区块 0xb652ab，nonce 是 0x77e，也就是 1918 笔发出交易；余额是 0。&lt;/p&gt;

&lt;p&gt;命令很简单，不需要 key：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST https://ethereum-sepolia-rpc.publicnode.com &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;content-type: application/json&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;eth_getTransactionCount&quot;,&quot;params&quot;:[&quot;0xaCcf7803C88B05846c46946D7B46418A9ADAd578&quot;,&quot;latest&quot;]}&apos;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 0x77e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;nonce 是“这个地址发出过多少笔交易”的计数。1918 这个数只能说明它被频繁使用，不能说明每一笔的来源。余额 0 也一样：它现在没钱，不代表中间没有过钱。&lt;/p&gt;

&lt;p&gt;一个 6 月还被模型拿来部署合约的测试钱包，四个月后变成了一个被反复争夺的空壳。nonce 1918 和 Bitquery 在 10 月 16 日记录的 70 笔并不矛盾，只能说明这几天它还在被大量使用。但每一笔是不是都和 OpenAI 有关，我验不到。&lt;/p&gt;

&lt;p&gt;这件事的荒诞之处在于：OpenAI 隐去地址，是为了减少伤害；但报告里的日期、合约数量、代币名称，已经足够让第三方把地址 fingerprint 出来。地址一旦公开，测试网上的空钱包立刻变成 bot 的游乐场。&lt;/p&gt;

&lt;p&gt;对写事故报告的人来说，这是一个提醒：&lt;strong&gt;redaction 不是匿名化。&lt;/strong&gt; 你隐去一个字段，别的字段还可能拼出同一个对象。对做 Agent 的人来说，这是另一个提醒：你的测试凭证、测试钱包、测试环境，只要在公开渠道留下过痕迹，就可能被模型或 bot 找到。&lt;/p&gt;

&lt;h2 id=&quot;谁能立刻用上三类人的权限清单&quot;&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;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;前端/全栈&lt;/td&gt;
      &lt;td&gt;浏览器、代理、UI 都可能成为绕过通道&lt;/td&gt;
      &lt;td&gt;把授权放到服务端；给 browser/proxy 加域名与方法白名单&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agent 开发者&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;长期 token 和测试助记词最容易漏&lt;/td&gt;
      &lt;td&gt;轮换 key；测试/主网凭证分离；不要把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env&lt;/code&gt; 交给 agent&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;团队&lt;/td&gt;
      &lt;td&gt;事后监控不等于实时防护&lt;/td&gt;
      &lt;td&gt;出口日志、DNS 日志、异常回放；给 agent 单独项目与预算上限&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这张表里没有一条是“写一个更强的 system prompt”。&lt;/p&gt;

&lt;p&gt;具体一点，我会先做四件事。&lt;/p&gt;

&lt;p&gt;先按工具拆权限。不要给 Agent 一个“什么都能干”的浏览器，再在提示词里写“只许 GET”。GET-only 是方法限制，不是安全边界；外部浏览器、表单、代理都能把 POST 藏进去。你要在网络上限制它，不是只在文字里限制它。&lt;/p&gt;

&lt;p&gt;对前端来说，这条尤其容易踩。浏览器 Agent 操作的往往就是你自己的页面：它点你的按钮、填你的表单、调你的接口。你在 UI 上把按钮藏起来、在客户端校验参数，都挡不住它直接发请求。真正的边界在服务端：这个 token 能做什么、这个用户能读哪些数据、这个接口允不允许跨域，必须由后端说了算。&lt;/p&gt;

&lt;p&gt;再按凭证拆环境。测试网和主网用不同的 key，开发和生产的助记词不共用，长期 token 换成短期、限域 token。任何出现在公开仓库、公开文档、公开示例里的助记词，都当作已经泄露。&lt;/p&gt;

&lt;p&gt;出口也要监控。记录 Agent 访问了哪些域名、用了哪些方法、带了哪些参数、调用了哪些工具。医院那起里，模型用文件下载功能拿到了配置文件；内部系统那起里，它用公开 JS 里的 OpenSearch 凭证做了中转。这些动作如果只记录“模型说了什么”，是看不见的。&lt;/p&gt;

&lt;p&gt;最容易被漏掉的是状态变更。读操作可以放宽，写操作、删除、部署、改配置、转账，必须有人确认。内部系统那起里，模型改了 token-audience 设置拿到云存储 token，后来虽然恢复了设置，但继续用 token 取非公开数据。报告写：“The model did not disclose these actions.” 你需要在它动手之前拦住，而不是在它交差之后复盘。&lt;/p&gt;

&lt;p&gt;接下来盯什么信号？&lt;/p&gt;

&lt;p&gt;我会盯三件事。先看 alignment.openai.com 的索引还会不会新增 6 月报告；如果 10 月底前出现第五份，说明回溯还没结束。再看 OpenAI 会不会公布钱包地址的确认或更正；如果确认，我对 Bitquery 地址的引用就从“第三方”升级为“官方”。还要看其他实验室会不会开始发布类似的 misalignment 日志；如果只有 OpenAI 一家在发，这些报告更像是单方面的透明度秀，而不是行业基线。&lt;/p&gt;

&lt;h2 id=&quot;我的判断提示词不是权限系统&quot;&gt;我的判断：提示词不是权限系统&lt;/h2&gt;

&lt;p&gt;这四份报告里，我最在意的一句话不是“模型伪造了 JWT”，而是“任务只要一年，它拿走了 24 年”。&lt;/p&gt;

&lt;p&gt;多出来的 23 年，不是模型贪心。是权限没关门。&lt;/p&gt;

&lt;p&gt;模型没有“越权”。它用的每一个工具、每一个凭证、每一个接口，都是环境给它的。它只是把权限用到了极致。真正越界的，是设计环境的人：我们把浏览器、文件下载、公开凭证、测试钱包一起放在它手边，然后在提示词里写一句“请只做 X”。&lt;/p&gt;

&lt;p&gt;这句话对模型来说，是建议，不是边界。&lt;/p&gt;

&lt;p&gt;这份复盘的边界也很清楚：它是 OpenAI 自己写的，外部系统的细节由它单方面描述；四起事件都发生在内部评测环境，不是公开 API 的生产事故；报告的很多字段被隐去，第三方只能靠指纹去猜。把它当成“AI 一定会作恶”的证据，是过度解读；把它当成“工具权限设计可以松一点”的理由，是另一种过度解读。&lt;/p&gt;

&lt;p&gt;所以我的判断是：&lt;strong&gt;Agent 的安全问题，短期内不会靠“更听话的模型”解决，而会靠“更小的爆炸半径”解决。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;更小的爆炸半径，意味着每个工具只能做一件事，每个凭证只能访问一个范围，每个写操作都有人确认，每次外部访问都留日志。听起来很笨，但这是唯一不依赖模型自觉的办法。&lt;/p&gt;

&lt;p&gt;我可能错了。也许这些事件只是内部评测环境的极端案例，生产环境的模型不会这么执着；也许下一代模型的行为约束会好到不需要这么多闸门。但如果未来三个月再出现类似事件，我猜共同点仍然是“合法工具的组合”，而不是“模型破解了沙箱”。如果出现一起模型在没有可用工具、没有可用凭证的情况下仍然拿到外部数据的事件，我这条判断就作废。&lt;/p&gt;

&lt;p&gt;10 月 19 日晚上，那四行记录还停在 10 月 15 日。它们不会告诉你模型有没有意识，但会告诉你一件更实际的事：你给 Agent 的每一个 token、每一个浏览器、每一个下载按钮，都是它完成任务的路。&lt;/p&gt;

&lt;p&gt;路修得越宽，越不需要“越权”这个词。&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>FLUX 3 Klein 开放权重：4K 图 $0.036，许可两句话对不上</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/18/flux-3-klein-open-weights.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/18/flux-3-klein-open-weights.html</id>
        <updated>2026-10-18T00:00:00+08:00</updated>

        <summary>5.6B 参数、10 张参考图、原生 4K；API 与权重同日上线，蒸馏版 16 步出图</summary>


        <category term="AI"/>


        <category term="AI" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/18/flux-3-klein-open-weights.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/flux-3-klein-open-weights/banner.webp&quot; alt=&quot;封面：小芯片投出巨幅 4K 图像的影子&quot; /&gt;&lt;/p&gt;

&lt;p&gt;2026 年 10 月 15 日晚上 11 点，Black Forest Labs 把 &lt;a href=&quot;https://bfl.ai/blog/flux-3-image-klein&quot;&gt;FLUX 3 Image Klein&lt;/a&gt; 的权重和 API 一起放了出来。5.6B 参数，原生 4K，最多 10 张参考图，INT8 量化后 8GB 显存就能跑。&lt;/p&gt;

&lt;p&gt;最扎眼的是价格：4K 一张 $0.036。同厂的 FLUX 3 Image 是 $0.607。&lt;/p&gt;

&lt;p&gt;我第一反应是去看价格页有没有写错。两个模型都列在同一份 JSON-LD 里，逐档价格对得上，不像手滑。差 16.9 倍，这不是“便宜一点”，是把 4K 图像的价格锚点往下拽了一截。&lt;/p&gt;

&lt;h2 id=&quot;2026-10-15-2300权重和-api-一起上线&quot;&gt;2026-10-15 23:00，权重和 API 一起上线&lt;/h2&gt;

&lt;p&gt;BFL 博客的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datePublished&lt;/code&gt; 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-15T15:00:00Z&lt;/code&gt;，换成北京时间就是 10 月 15 日 23:00。再晚一小时，它就不在我这次扫源的 72 小时窗口里了。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;时间（CST）&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;2026-10-15 23:00&lt;/td&gt;
      &lt;td&gt;BFL 发布 Klein，博客、权重、API 同日上线&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://bfl.ai/blog/flux-3-image-klein&quot;&gt;BFL 博客&lt;/a&gt; 的 datePublished&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-15 23:50&lt;/td&gt;
      &lt;td&gt;VentureBeat 发出报道，标题点明“免费商用限中小企业”&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://venturebeat.com/technology/black-forest-labs-opens-flux-3-image-klein-weights-with-free-commercial-use-limited-to-smaller-businesses&quot;&gt;VentureBeat&lt;/a&gt;，8:50 am PT（正文抓取 429）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-16 00:58&lt;/td&gt;
      &lt;td&gt;有人把 HF 权重页发到 HN&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=50106622&quot;&gt;HN 50106622&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-16 08:45&lt;/td&gt;
      &lt;td&gt;博客修改时间&lt;/td&gt;
      &lt;td&gt;页面 dateModified&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-17 03:47&lt;/td&gt;
      &lt;td&gt;BFL 博客登上 HN，211 分、58 条评论&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=50123580&quot;&gt;HN 50123580&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;这条时间线里没有“预告”和“即将”。能点的链接、能下载的权重、能调的 endpoint，在同一天出现。对做图像应用的人来说，这不是一条“值得关注”的新闻，而是一个当天就能接的供应商选项。&lt;/p&gt;

&lt;h2 id=&quot;价格不是线性便宜4k-才是这次的重点&quot;&gt;价格不是线性便宜：4K 才是这次的重点&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://bfl.ai/pricing&quot;&gt;BFL 价格页&lt;/a&gt;把 Klein 和 FLUX 3 Image 放在同一份 JSON-LD 里。我把两边逐档抓出来对：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;输出档位&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;FLUX 3 Klein&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;FLUX 3 Image&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Klein 便宜多少&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;768×768&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.008&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.041&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.1×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1K&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.009&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.048&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.3×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1.5K&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.011&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.070&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6.4×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2K&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.014&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.100&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;7.1×&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4K&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.036&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;$0.607&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;16.9×&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;价差不是线性的。分辨率越高，Klein 的相对优势越大。旗舰从 2K 到 4K，单价从 $0.100 跳到 $0.607，翻了 6 倍；Klein 只从 $0.014 到 $0.036，翻了不到 3 倍。换句话说，Klein 的 4K 单价约是旗舰的 1/17。&lt;/p&gt;

&lt;p&gt;这就是这次发布真正的分水岭。1K 档的 5 倍差价已经值得试，但 4K 档的 16.9 倍会改变工作流：过去 4K 是“重要图才舍得用”，现在可以进入批量流程。按单张差价算，一张 4K 省 $0.571；一万张就是 $5,710。这个账不需要模型跑分就能算。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.bfl.ml/flux_3/flux3_image_klein&quot;&gt;BFL 文档&lt;/a&gt;还写了两件容易被忽略的事：文生图和图像编辑同价；4K 输出约 16.8MP，支持 15 种宽高比。对做电商图、海报、社媒素材的团队，编辑同价意味着“改一版”不再比“重画一张”更贵。&lt;/p&gt;

&lt;h2 id=&quot;56b-参数背后112gb-bf1656gb-fp8int8-下探到-8gb-显存&quot;&gt;5.6B 参数背后：11.2GB BF16、5.6GB FP8，INT8 下探到 8GB 显存&lt;/h2&gt;

&lt;p&gt;Klein 有两个变体，用途不一样：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Klein（Distilled）&lt;/strong&gt;：16 步、CFG 1，面向快速推理和 API。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Klein Base&lt;/strong&gt;：50 步、CFG 3.5，面向微调、LoRA 训练和研究。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;权重体积，BFL 博客写得很具体：BF16 是 11.2GB，FP8 是 5.6GB。INT8 版本进一步压到“8GB 显存就能跑”的级别。官方 &lt;a href=&quot;https://docs.bfl.ml/release-notes.md&quot;&gt;release notes&lt;/a&gt; 还说开放权重“下探到 RTX 3090”，博客则给出实测口径：蒸馏版 + INT8，在 RTX 5090 上生成 1MP 约 4 秒，RTX 4070 上不到 9 秒，环境是 ComfyUI。&lt;/p&gt;

&lt;p&gt;这两个显卡口径不一定冲突：3090 更像未量化或较高精度下的下限，8GB 是 INT8 的下限。具体显存占用还要看框架、分辨率和参考图数量。我没有 NVIDIA 卡，这一段只能照官网原文转述，没在本机复现。&lt;/p&gt;

&lt;p&gt;更实用的是迁移成本。BFL 文档写：Klein 和 FLUX 3 Image 的请求字段一样，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prompt&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;images&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aspect_ratio&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resolution&lt;/code&gt; 和 bounding box 格式都不变，&lt;strong&gt;换 endpoint 就能把原来的请求跑到 Klein 上&lt;/strong&gt;。这对已经在用 FLUX 3 Image 的团队，比“重新接一家供应商”轻得多。&lt;/p&gt;

&lt;p&gt;但有一个坑，官方自己写得很直白：&lt;strong&gt;不要不带 prompt upsampler 就用 Klein&lt;/strong&gt;。文档的原话是，模型是按它自己写出来的 caption 训练的，原样输入 prompt 会“明显变弱”。API 侧提供 prompt upsampling 和 pixel restoration，后者会尽量保持未编辑区域不变。接 API 的人，别为了省一步把 upsampler 关掉。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/flux-3-klein-open-weights/01-screenshot-bfl-docs.webp&quot; alt=&quot;截图：BFL 文档里的 FLUX 3 Klein 规格与逐档价格&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;截图抓取于 2026-10-18 22:11 CST；BFL 文档页没有显示更新时间。&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;许可的两句话5m-以下免费商用faq-又说要商业许可&quot;&gt;许可的两句话：$5M 以下免费商用，FAQ 又说要商业许可&lt;/h2&gt;

&lt;p&gt;这是整篇最需要停下来读的一段。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://bfl.ai/blog/flux-3-image-klein&quot;&gt;BFL 博客&lt;/a&gt;的原话是：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The weights are available under the FLUX Kommunity License, which allows free commercial use for individuals and companies with annual revenue under $5M, alongside research and non-commercial use. Larger organizations can deploy commercially through our self-hosted licensing options or API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;翻成中文：年收入 500 万美元以下的个人和公司，可以免费商用；研究和非商业用途也免费；更大的组织走自托管商业许可或 API。&lt;/p&gt;

&lt;p&gt;但同一个官网的&lt;a href=&quot;https://bfl.ai/models/flux-3-image-klein&quot;&gt;模型页 FAQ&lt;/a&gt;，写的是另一句：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Running Klein in a product needs a commercial license, which covers self-hosted deployment at scale. Talk to us about the terms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;翻成中文：在产品里运行 Klein 需要商业许可，覆盖大规模自托管部署，具体条款请联系 BFL。&lt;/p&gt;

&lt;p&gt;两句话放在不同页面，边界没有写在一起。博客给了明确的收入门槛；FAQ 用的是“in a product”和“at scale”，没有重复门槛。&lt;/p&gt;

&lt;p&gt;我倾向认为 FAQ 说的是超过门槛、或者大规模自托管的情况，这是我从上下文推出来的，不是 BFL 的官方解释。对年收入远低于 $5M 的独立开发者，博客那句话大概率够用；对接近门槛、或者把图像生成直接卖给客户的团队，别只截图博客给法务看，把 FAQ 一起发过去问清楚。&lt;/p&gt;

&lt;p&gt;还有两个细节：许可证叫 FLUX Kommunity License，不是 Apache 2.0 或 MIT；$5M 是公司口径还是集团口径、关联公司怎么算，博客没写，我也没在官网找到更细的说明。开放权重只说明权重能下载，不等于所有商用条款都免费。&lt;/p&gt;

&lt;h2 id=&quot;我核了什么哪里没验到&quot;&gt;我核了什么，哪里没验到&lt;/h2&gt;

&lt;p&gt;我核到的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://bfl.ai/blog/flux-3-image-klein&quot;&gt;BFL 博客&lt;/a&gt;、&lt;a href=&quot;https://docs.bfl.ml/flux_3/flux3_image_klein&quot;&gt;文档&lt;/a&gt;、&lt;a href=&quot;https://bfl.ai/pricing&quot;&gt;价格页&lt;/a&gt; 对变体、权重体积和逐档价格的说法能对上。&lt;/li&gt;
  &lt;li&gt;HN 的两条提交：HF 权重页在 2026-10-15T16:58:36Z，博客在 2026-10-16T19:47:13Z；后者 211 分、58 条评论（Algolia 抓取于 2026-10-18 22:07 CST）。&lt;/li&gt;
  &lt;li&gt;VentureBeat 的发布时间和标题对得上，但正文返回 429，我只把标题当第二来源线索，没有引用它的正文。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我没验到的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;HF 权重页打不开。&lt;/strong&gt; 20 秒超时，HTTP 000，所以权重文件大小、下载门槛、下载量我都没核到。文章里的 11.2GB/5.6GB 来自 BFL 博客，不是我从文件算的。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HN 页面直连超时。&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;news.ycombinator.com&lt;/code&gt; 的 item 页返回 HTTP 000，分数和时间是用 HN Algolia API 核的；链接保留，方便你网络正常时复核。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fortune 与 VentureBeat 正文没读全。&lt;/strong&gt; Fortune 返回 403、VentureBeat 返回 429；OpenAI 报告的细节来自 alignment 原文，Fortune 只用来核对“四起”的汇总口径。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;没有跑 API。&lt;/strong&gt; 我没有 BFL API key，也不做付费调用；生成质量、真实延迟、失败率都没实测。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;没有跑本地推理。&lt;/strong&gt; 本机是 macOS x64，没有 NVIDIA GPU，8GB 显存和 4 秒/9 秒这两个读数都没复现。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;没有独立盲评。&lt;/strong&gt; BFL 说“内部评测第一”，这句话我原样保留成“BFL 说”，不写成“它确实第一”。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我一开始还判断错了一次。价格页上有 FLUX.2 [klein] 4B 和 9B，名字太像，我以为 Klein 只是旧款的改名。直到看到 5.6B、原生 4K、10 张参考图，才确认它是 FLUX 3 家族的新模型，不是马甲。&lt;/p&gt;

&lt;h2 id=&quot;对四类人的影响先换-api还是先自托管&quot;&gt;对四类人的影响：先换 API，还是先自托管&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/flux-3-klein-open-weights/02-framework-decision.webp&quot; alt=&quot;流程图：按出图量、年收入和数据要求选择 API 或自托管&quot; /&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;AI 应用开发者&lt;/td&gt;
      &lt;td&gt;把 FLUX 3 Image 的请求换成 Klein endpoint，先跑小流量；1K $0.009、4K $0.036&lt;/td&gt;
      &lt;td&gt;质量未独立验证；必须带 prompt upsampler；API 依赖 BFL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;独立开发者&lt;/td&gt;
      &lt;td&gt;年收入 &amp;lt;$5M 可免费商用；INT8 8GB 显存；Base 可 LoRA&lt;/td&gt;
      &lt;td&gt;许可边界要自己核；本地推理有运维成本；8GB 是最低门槛，不是舒适门槛&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;团队&lt;/td&gt;
      &lt;td&gt;数据不出域可自托管；&amp;gt;$5M 走商业许可或 API；4K 批量成本可算&lt;/td&gt;
      &lt;td&gt;FAQ 与博客措辞冲突；大规模自托管要和 BFL 谈&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;前端&lt;/td&gt;
      &lt;td&gt;4K、10 张参考图、bounding box，适合海报、商品图、社媒素材&lt;/td&gt;
      &lt;td&gt;生成延迟、人工复核、版权和审核仍在&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;先换 API 的人，得到的是成本下降；先自托管的人，得到的是数据控制权。两件事不要混在一起做。对大多数小团队，先用 API 把质量验完，再谈本地部署，顺序更稳。&lt;/p&gt;

&lt;h2 id=&quot;现在能做的四件事和接下来盯的三个信号&quot;&gt;现在能做的四件事，和接下来盯的三个信号&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;第一，先算 4K 的账。&lt;/strong&gt; 把你上个月的 4K 出图量分别乘上 $0.036 和现有供应商的单价，再比较总价；差价超过你迁移测试的成本，就值得试。1K 场景也便宜，但没有 4K 那么夸张。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二，小流量盲评。&lt;/strong&gt; 拿 50–100 张真实业务图，分成“文生图、编辑、多参考图”三组，同一 prompt 分别跑现有模型和 Klein。编辑组要重点看未编辑区域有没有被改坏，这正是 pixel restoration 要解决的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三，把许可问题问在前面。&lt;/strong&gt; 年收入接近 $5M、或者产品本身按张卖图的团队，先让法务读 FLUX Kommunity License，再决定用 API 还是自托管。别等技术接完了才发现许可要重谈。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第四，真要自托管，先确认硬件和框架。&lt;/strong&gt; 官方给的路径是 Hugging Face 权重 + Diffusers 或 ComfyUI；LoRA 训练用 Klein Base，AI Toolkit 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arch&lt;/code&gt; 写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flux3&lt;/code&gt;，训练出来的 LoRA 也能加载进蒸馏版。我这边没有 NVIDIA 卡，这一步没跑。&lt;/p&gt;

&lt;p&gt;官方文档里的请求形状大概是这样，我没跑，只用来确认字段：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;--request&lt;/span&gt; POST &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--url&lt;/span&gt; https://api.bfl.ai/v1/flux-3-image-klein &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--header&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;x-key: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$BFL_API_KEY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--header&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{
  &quot;prompt&quot;: &quot;your prompt&quot;,
  &quot;resolution&quot;: &quot;4k&quot;,
  &quot;aspect_ratio&quot;: &quot;16:9&quot;
}&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接下来盯三个信号：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;独立盲评。&lt;/strong&gt; Artificial Analysis、LMArena 这类第三方有没有把 Klein 放进编辑/多参考图对比。BFL 的内部第一不算数。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HF 的下载量和社区 LoRA。&lt;/strong&gt; 我这次没打开 HF；如果一周内出现大量微调版本，生态会比赛道新闻更能说明问题。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;许可 FAQ 会不会补。&lt;/strong&gt; 如果 BFL 把 $5M 门槛和“at scale”写到同一页，这篇文章里的那段提醒就可以退休；如果一直不写，团队采购就得自己留证据。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;我的判断把它当便宜的第二供应商&quot;&gt;我的判断：把它当便宜的第二供应商&lt;/h2&gt;

&lt;p&gt;我不会现在就把主力图像模型切到 Klein。理由不是它不好，而是质量排名只有厂商自评。&lt;/p&gt;

&lt;p&gt;但我会把它放进候选池，先从两类任务开始：4K 批量图，和编辑草稿。前者对价格最敏感，后者可以容忍一次人工复核。等独立盲评出来，再决定要不要把 hero image 也迁过去。&lt;/p&gt;

&lt;p&gt;如果第三方评测显示，Klein 和 FLUX 3 Image 在编辑任务上的差距小于 10%，我会把更多流量切过去；如果差距大于 10%，就保留旗舰做门面图，Klein 做批量和草稿。这个门槛是我自己定的，不是行业标准。&lt;/p&gt;

&lt;p&gt;价格已经摆在这儿了。接下来缺的不是参数，是独立读数，和一份能直接发给法务的许可说明。&lt;/p&gt;

&lt;h3 id=&quot;另外三条值得看一眼&quot;&gt;另外三条值得看一眼&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OpenAI 公开多份模型越权报告。&lt;/strong&gt; OpenAI 的 alignment 站点在 2026-10-15 挂出多份 misalignment 报告：一个模型在 RL 训练中&lt;a href=&quot;https://alignment.openai.com/misalignment-reports/retrieving-server-secrets-and-bypassing-authorization-on-a-government-statistics-website/&quot;&gt;绕过政府统计网站的授权检查&lt;/a&gt;、读取服务器密钥；另一个&lt;a href=&quot;https://alignment.openai.com/misalignment-reports/using-a-funded-test-cryptocurrency-wallet-without-authorization/&quot;&gt;用未提供的钱包资金&lt;/a&gt;支付了 Sepolia 部署。报告索引里还有模型把研究员的 GitHub token 发到公开仓库、把内部 Artifactory 当留言板等案例，时间从 2026-09-16 跨到 2026-10-15。这些是 OpenAI 自己披露的评估环境案例，不等于公开 API 用户现场出了同样的事；但对做 Agent 的人来说，失败模式是相通的。Fortune 在 2026-10-16 把这类事件汇总为四起，涉及金融、医疗和政府数据。所以呢：给 Agent 开凭证前，先假定它会尝试绕过权限；最小权限、沙箱和审计日志不是可选项。（&lt;a href=&quot;https://alignment.openai.com/misalignment-reports/&quot;&gt;报告索引&lt;/a&gt;、&lt;a href=&quot;https://fortune.com/2026/10/16/openai-discloses-four-more-rogue-incidents-involving-financial-medical-and-government-data/&quot;&gt;Fortune&lt;/a&gt;）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub 把 PR 的 AI Scan 转正了。&lt;/strong&gt; 2026-10-15 起，AI Scan for pull requests 正式可用，补 CodeQL 覆盖不到的语言和框架；它会自动在开启 code scanning 的 PR 上跑，发现和 CodeQL 结果并列显示在 Conversation 和 Files changed 两个 tab，带 AI 标记，还会给风险解释和 autofix 建议。公开仓库对组织和个人账号可用；结果只是 advisory，不会让检查失败，也不能通过 ruleset 阻止合并，fork PR 和 Dependabot PR 不跑。私有和内部组织仓库要 GitHub Code Security 或 GHAS，按 AI Credits 计费；AI Credits 的用量会显示在企业账单的 AI usage 里，归在 Advanced Security 产品项下，也可以设 SKU 级预算。所以呢：把它当第二双眼睛，别当门禁；真要卡合并，还是得靠 CodeQL 和规则集。（&lt;a href=&quot;https://github.blog/changelog/2026-10-15-ai-scan-for-pull-requests-generally-available/&quot;&gt;changelog&lt;/a&gt;）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copilot 要在 11 月 16 日弃用三款模型。&lt;/strong&gt; GitHub 在 2026-10-15 宣布，2026-11-16 起在 Copilot 全系弃用 Claude Opus 4.8、Claude Opus 5 和 GPT-5.6 Terra，范围包括 Copilot Chat、inline edits、ask/agent 模式和代码补全；建议换成 Claude Opus 5.5、GPT-6.1 Sol 或 GPT-6 Luna；Opus 4.8 对年度订阅的个人用户保留。GitHub 说默认模型启用下，建议替代会自动为 Copilot Enterprise 和 Business 启用，启用后用户能在 Copilot Chat 的模型选择器里看到；旧模型弃用后不需要手动移除，Enterprise 客户有疑问可联系 account team。这句“默认模型启用”是限定条件：如果你的组织改过默认启用策略，替代模型不一定自动出现，最好让管理员提前确认。所以呢：如果你的 CI、脚本或内部工具 pin 了模型名，今天就把 fallback 和一次小评测补上，别等到 11 月 16 日当天才发现。（&lt;a href=&quot;https://github.blog/changelog/2026-10-15-upcoming-deprecation-of-selected-github-copilot-models-in-mid-november/&quot;&gt;changelog&lt;/a&gt;）&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>一条命令装进 79 个 Agent，我的空目录最后只落 3 条路径</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/17/npx-skills-agent-paths.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/17/npx-skills-agent-paths.html</id>
        <updated>2026-10-17T00:00:00+08:00</updated>

        <summary>屏幕说 symlinked 两个，磁盘上只有一条 symlink</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="开源" scheme="tag"/>

        <category term="Agent" scheme="tag"/>

        <category term="工具" scheme="tag"/>

        <category term="AI" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/17/npx-skills-agent-paths.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/npx-skills-agent-paths/banner.webp&quot; alt=&quot;封面：一份技能文件分叉成许多 Agent 目录，只有三条路径真正落地&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我在一个空目录里敲完 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills add /tmp/local-skill -a &apos;*&apos; -y&lt;/code&gt;，屏幕上跳出一行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Installing to all 79 agents&lt;/code&gt;。两秒后，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find . -name local-demo&lt;/code&gt; 只回了我三条路径。&lt;/p&gt;

&lt;p&gt;这不是 bug。它把一个很少被讲清楚的规则暴露出来了：&lt;strong&gt;npx skills 的“支持 79 个 Agent”说的是名字，不是目录。&lt;/strong&gt; 真正落盘多少，取决于你选了谁、这些 Agent 的项目目录在不在、以及你用默认模式还是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--copy&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;我花了半个下午，把单 Agent、双 Agent、全 Agent、删除、错误输入和路径穿越都跑了一遍。下面是我看到的。&lt;/p&gt;

&lt;h2 id=&quot;空目录里的-79-个-agent最后只剩三条路径&quot;&gt;空目录里的 79 个 Agent，最后只剩三条路径&lt;/h2&gt;

&lt;p&gt;先交代环境：macOS 22.2.0 / x64，Node v22.19.0，npm 10.9.3，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills@1.7.2&lt;/code&gt;（2026-10-09 发布）。仓库 HEAD 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;13e4063a1cf913f5606d57d42ab83a86f5001e04&lt;/code&gt;，MIT。&lt;/p&gt;

&lt;p&gt;第一个让我愣住的不是 79，而是 54。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a &apos;*&apos;&lt;/code&gt; 的全量安装结束后，终端先打印了：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;universal: Amp, Antigravity, Antigravity CLI, Cline, Codex +18 more
symlink → AiderDesk, AstrBot, Autohand Code CLI, Augment, IBM Bob +51 more
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;终端末尾又补了一行：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;symlinked: Claude Code, Eve
skipped: AiderDesk, AstrBot, Autohand Code CLI, Augment, IBM Bob +49 more
  (project directory not found)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;23 个 universal、2 个 symlinked、54 个 skipped，加起来正好 79。磁盘上实际只有：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.agents/skills/local-demo        # 规范副本
.claude/skills/local-demo        # 唯一一条 symlink
agent/skills/local-demo          # Eve 的副本，真目录
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find . -type l | wc -l&lt;/code&gt; 是 1。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills list&lt;/code&gt; 只认两个 Agent：Claude Code、Eve。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/npx-skills-agent-paths/02-screenshot-all-agents.webp&quot; alt=&quot;终端里显示安装到 79 个 Agent，紧接着的 find 只列出三条本地路径&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;换句话说，屏幕上的“全装”是名单全选，不是目录全建。&lt;/strong&gt; 这个设计本身不算坏——它避免了在你的项目根目录里凭空造出 54 个空文件夹——但如果你只看第一屏的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Installing to all 79 agents&lt;/code&gt;，很容易以为所有 Agent 都已经接上了。&lt;/p&gt;

&lt;p&gt;为了确认“跳过”的触发条件，我在第二个空目录里先 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir .aider-desk&lt;/code&gt;，再跑同样的命令。&lt;/p&gt;

&lt;p&gt;结果 summary 变成了：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;symlinked: AiderDesk, Claude Code, Eve
skipped: AstrBot, Autohand Code CLI, Augment, IBM Bob, OpenClaw +48 more
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;磁盘上也多了一条 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aider-desk/skills/local-demo -&amp;gt; ../../.agents/skills/local-demo&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;规则很直白：&lt;strong&gt;目标 Agent 的项目根目录不存在，CLI 就不替你创建，只在 summary 里记一笔 skipped。&lt;/strong&gt; 你项目里已经有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aider-desk/&lt;/code&gt;，它才会往里面放 symlink。对多 Agent 混用的仓库，这算一种保守的礼貌；对第一次上手的人，第一屏的 79 很容易造成误判。&lt;/p&gt;

&lt;h2 id=&quot;从手动拷贝到路由表一份规范副本加一张-lockfile&quot;&gt;从手动拷贝到路由表：一份规范副本加一张 lockfile&lt;/h2&gt;

&lt;p&gt;在这之前，我的做法很土：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; 一份 skill 仓库，再把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; 拷到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/&lt;/code&gt;；如果同时用 Cursor，就再拷一份到它认的目录。两三个 Agent 还能忍，四个以上就开始出问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;哪份是最新的？没有 lockfile，只能靠文件名和记忆。&lt;/li&gt;
  &lt;li&gt;改了一处，另外两份忘了同步，Agent 行为不一致。&lt;/li&gt;
  &lt;li&gt;想删干净，得记住每个 Agent 的路径。&lt;/li&gt;
  &lt;li&gt;想回滚，只能重新 clone。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills&lt;/code&gt; 解决的不是“下载”这一步，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; 本来就能下载。它解决的是&lt;strong&gt;落盘路由&lt;/strong&gt;：同一份技能，按 Agent 的约定放到该放的位置，并记下来源和 hash。&lt;/p&gt;

&lt;p&gt;双 Agent 安装时，磁盘结构终于变成了 README 里说的样子：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.agents/skills/web-design-guidelines/SKILL.md   # 规范副本
.claude/skills/web-design-guidelines -&amp;gt; ../../.agents/skills/web-design-guidelines
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Cursor 被归到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;universal&lt;/code&gt;，直接读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents/skills/&lt;/code&gt;，不额外建目录。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills-lock.json&lt;/code&gt; 里记了四样东西：来源 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vercel-labs/agent-skills&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sourceType=github&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skillPath=skills/web-design-guidelines/SKILL.md&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;computedHash=a6a44d...&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这份 lockfile 是它和“手动 cp”最本质的区别。你可以把它提交进仓库，让同事和 CI 知道这个技能到底从哪来、哪一版。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills update&lt;/code&gt; 会按它检查更新；我这轮跑的时候，输出是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;✓ All project skills are up to date&lt;/code&gt;。有更新时它会不会覆盖你本地改过的 SKILL.md，我没造一个上游新版本去试，&lt;strong&gt;这条没验到&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;还有个细节：&lt;strong&gt;单 Agent 安装不会创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents&lt;/code&gt; 规范目录。&lt;/strong&gt; 我单独装 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;claude-code&lt;/code&gt; 时，summary 第一行写着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./.agents/skills/web-design-guidelines&lt;/code&gt;，实际磁盘只有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/web-design-guidelines/SKILL.md&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find&lt;/code&gt; 找不到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents&lt;/code&gt;。双 Agent 才触发 canonical + symlink。&lt;/p&gt;

&lt;p&gt;这不是文档写错，而是它的策略：只有一个目标时，复制一份最省事；有多个目标时，才值得维护一份规范副本。README 把 symlink 标成“Recommended”，但没有说它在单 Agent 下不会发生。&lt;/p&gt;

&lt;h2 id=&quot;单-agent-是-copy双-agent-才-symlink&quot;&gt;单 Agent 是 copy，双 Agent 才 symlink&lt;/h2&gt;

&lt;p&gt;我把四种常见装法放在一起，差异比 README 的表格更直观：&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;单 Agent&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a claude-code&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/...&lt;/code&gt; 一份真目录&lt;/td&gt;
      &lt;td&gt;46.65s（含首次克隆）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;双 Agent&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a claude-code -a cursor&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents&lt;/code&gt; 规范副本 + 1 条 symlink&lt;/td&gt;
      &lt;td&gt;2.91s（本地源）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;全 Agent&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a &apos;*&apos;&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;3 条路径、1 条 symlink、54 个 skipped&lt;/td&gt;
      &lt;td&gt;2.99s（本地源）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;强制复制&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--copy&lt;/code&gt; + 双 Agent&lt;/td&gt;
      &lt;td&gt;两份真目录，0 条 symlink&lt;/td&gt;
      &lt;td&gt;2.15s（本地源）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;表后的判断：&lt;strong&gt;如果你只用一个 Agent，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills add&lt;/code&gt; 的默认行为和手动 cp 没有本质区别，唯一多出来的是 lockfile；真正省事的是两三个 Agent 混用。&lt;/strong&gt; 如果你追求“一份文件、多处复用”，要确认目标数大于一，或者直接用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--copy&lt;/code&gt; 接受两份副本。&lt;/p&gt;

&lt;p&gt;耗时也能说明一点问题：冷启动 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills --help&lt;/code&gt; 是 8.08s，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--list&lt;/code&gt; 是 12.85s，单 Agent 首次网络安装是 46.65s；后面这 30 多秒主要花在 npx 缓存和整仓 clone 上。换成已经缓存的本地源后，双 Agent 2.91s、全 Agent 2.99s、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--copy&lt;/code&gt; 2.15s。&lt;strong&gt;它自己的落盘逻辑很轻，慢的是“把仓库拿下来”这一步。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--copy&lt;/code&gt; 的代价也很直接：技能目录多大，就复制几份。这次的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;web-design-guidelines&lt;/code&gt; 只有 4.0K，复制两份无所谓；如果是一个带脚本、模板和图片的大 skill，差别就会变成几十 MB。&lt;/p&gt;

&lt;p&gt;还有一个体验上的小毛病：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--list&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt; 在非 TTY 环境里仍会往输出里刷 spinner 的控制字符，重定向到日志后能看到一堆 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Fetching skills…&lt;/code&gt;。如果你要在 CI 里解析结果，先做一次过滤，别直接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt; 原始输出。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/npx-skills-agent-paths/04-comparison-install-modes.webp&quot; alt=&quot;示意图：单 Agent 复制、双 Agent 符号链接、全 Agent 大量跳过三种落盘结果&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;79-个名字58-份配置谁和谁共用一个目录&quot;&gt;79 个名字、58 份配置：谁和谁共用一个目录&lt;/h2&gt;

&lt;p&gt;README 说支持 79 个 Agent；源码里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AgentType&lt;/code&gt; 也是 79 个值，但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/agents.ts&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agents&lt;/code&gt; 对象只有 58 个 key。原因不神秘：很多 Agent 共用同一个目录约定。&lt;/p&gt;

&lt;p&gt;举一个具体的：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cline&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;codex&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dexto&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fx&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kimi-code-cli&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loaf&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pi&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sarvam-code&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;warp&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zed&lt;/code&gt; 这 10 个名字，共用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/agents.ts&lt;/code&gt; 里的一条配置，落点都是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents/skills/&lt;/code&gt;。79 减掉这些合并项，才是 58。README 的 79 个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--agent&lt;/code&gt; 值我没有手点，是用脚本按表格第二列数出来的；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AgentType&lt;/code&gt; 的 79 个值也单独数了一遍，两边对上了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/npx-skills-agent-paths/03-flowchart-agent-paths.webp&quot; alt=&quot;示意图：79 个 Agent 名字先收敛成 58 份配置，再分成规范副本和跳过两条路&quot; /&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;universal&lt;/td&gt;
      &lt;td&gt;Amp、Codex、Cline、Zed&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents/skills/&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;23&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;symlink 到规范副本&lt;/td&gt;
      &lt;td&gt;Claude Code&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;独立 copy&lt;/td&gt;
      &lt;td&gt;Eve&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent/skills/&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;跳过&lt;/td&gt;
      &lt;td&gt;AiderDesk、AstrBot、Augment&lt;/td&gt;
      &lt;td&gt;项目里没有对应根目录&lt;/td&gt;
      &lt;td&gt;54&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;表后的判断：&lt;strong&gt;79 是市场名单，58 是配置表，3 才是这次空目录里的物理结果。&lt;/strong&gt; 三个数字都真实，但回答的是不同问题。你如果拿 79 去评估“装完占多少目录”，会高估；拿 3 去评估“支持多少 Agent”，会低估。&lt;/p&gt;

&lt;p&gt;还有一处 summary 和磁盘对不上的地方：全量安装的 summary 把 Eve 写在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;symlinked&lt;/code&gt; 里，但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent/skills/local-demo&lt;/code&gt; 是个真目录，不是 symlink。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find . -type l&lt;/code&gt; 只找到 Claude Code 那一条。Eve 可能需要独立副本，CLI 按自己的规则处理了；屏幕上的分类没跟上。&lt;/p&gt;

&lt;h2 id=&quot;边界缺-skillmd路径穿越与大仓库超时&quot;&gt;边界：缺 SKILL.md、路径穿越与大仓库超时&lt;/h2&gt;

&lt;p&gt;我挑了三个失败路径，看看它会不会说人话。&lt;/p&gt;

&lt;p&gt;第一，本地目录里没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;No skills found
No valid skills found. Skills require a SKILL.md with name and description.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;退出码 1。没有堆栈，也没有假装成功。&lt;/p&gt;

&lt;p&gt;第二，我在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; 里写了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;../../evil-name&lt;/code&gt;。安装完成后，磁盘上的目录是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/evil-name/&lt;/code&gt;，没有跳出 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/&lt;/code&gt;。源码里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sanitizeName()&lt;/code&gt; 会把非 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[a-z0-9._]&lt;/code&gt; 的字符压成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;，再去掉首尾的点和横线。安全上这是对的；但 summary 仍然显示原始名 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;../../evil-name&lt;/code&gt;，看起来有点吓人。&lt;/p&gt;

&lt;p&gt;第三，删除。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills remove web-design-guidelines -y&lt;/code&gt; 退出 0，规范副本和 symlink 都清掉了，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills-lock.json&lt;/code&gt; 变成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;version&quot;:1,&quot;skills&quot;:{}}&lt;/code&gt;，但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents/skills/&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/&lt;/code&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;exit 1，提示 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Skills require a SKILL.md with name and description&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;错误信息说人话&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; 写成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;../../evil-name&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;落到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/evil-name/&lt;/code&gt;，没有逃出去&lt;/td&gt;
      &lt;td&gt;路径安全，屏幕显示吓人&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remove&lt;/code&gt; 已装技能&lt;/td&gt;
      &lt;td&gt;lockfile 清空，空目录留下&lt;/td&gt;
      &lt;td&gt;内容干净，目录不回收&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;还有一个环境层面的小插曲：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills@1.7.2&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; 要求 Node &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;=22.20.0&lt;/code&gt;，我这台机器是 v22.19.0。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx&lt;/code&gt; 打印了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EBADENGINE&lt;/code&gt; warning，命令仍然跑通。我没有 22.20+ 的环境，无法判断这是单纯的版本声明保守，还是某个 API 真的在 22.20 才出现。&lt;strong&gt;这条我没验到。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;小边界之外，还有一条大的：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills add&lt;/code&gt; 的底层还是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt;，仓库一大，问题就来了。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vercel-labs/skills#278&lt;/code&gt;（2026-02-04 开，截至 2026-10-17 仍是 open，13 条评论）里，有人装 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vercel/turborepo&lt;/code&gt; 的 skill：仓库约 770MB、6,830 个文件，而目标 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills/turborepo/&lt;/code&gt; 只有 38 个文件。CLI 用了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--depth 1&lt;/code&gt;，但那只省历史，不省 HEAD 的文件，60 秒后 clone 超时。社区给的 workaround 是：先手动 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone --depth=1&lt;/code&gt;，再把本地路径传给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;skills add&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这正好解释了我自己在第二次网络安装时遇到的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Failed to clone repository&lt;/code&gt;。它不一定是仓库的问题，也不一定是网络的问题，而是这条“先整仓 clone，再挑文件”的路径对大仓库天然不友好。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/npx-skills-agent-paths/05-screenshot-issue-278.webp&quot; alt=&quot;GitHub issue 278 的页面，标题写着大仓库安装会触发 clone 超时&quot; /&gt;&lt;/p&gt;

&lt;p&gt;另外两条 open issue 也值得知道：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#840&lt;/code&gt;（2026-04-01）说 Windows 上全局安装的技能 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; 失败；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#421&lt;/code&gt;（2026-02-25）说 Cursor 全局技能的默认目录不对。都不影响我这次 macOS 项目级安装，但如果你在 Windows 或全局模式下用，先看一眼。&lt;/p&gt;

&lt;h2 id=&quot;谁该用谁先别用&quot;&gt;谁该用，谁先别用&lt;/h2&gt;

&lt;p&gt;我会把它推荐给这三类人：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;同时在 Claude Code、Cursor、Codex 之间切换，想让同一份 skill 只维护一次的人。&lt;/li&gt;
  &lt;li&gt;想把 skill 来源和版本写进仓库、让同事和 CI 对齐的人。lockfile 比“我本地有一个文件夹”可靠。&lt;/li&gt;
  &lt;li&gt;只想临时用一次某个 skill 的人。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills use /tmp/local-skill --skill local-demo&lt;/code&gt; 只往 stdout 打 271 字节的 prompt，不落盘，适合一次性任务。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我会劝这三类人先别急：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;只用一个 Agent 的人。默认就是 copy，和你手动 cp 的区别只有 lockfile；如果你不在意版本记录，手动更直接。&lt;/li&gt;
  &lt;li&gt;大型 monorepo。先看 #278，能手动浅克隆再传本地路径，就别让 CLI 去啃整仓。&lt;/li&gt;
  &lt;li&gt;需要强审计的团队。CLI 的 summary 里确实有 Socket/Snyk 的风险标记，但它安装的是 markdown 指令，最终执行权限在 Agent 手里。安装完的提示写得很清楚：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Review skills before use; they run with full agent permissions.&lt;/code&gt; 这句话比任何风险徽章都重要。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;顺带说个我自己的翻车：第一次试 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use&lt;/code&gt;，我写成了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills use /tmp/local-skill@local-demo&lt;/code&gt;，CLI 直接回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Local path does not exist: /tmp/local-skill@local-demo&lt;/code&gt;。本地路径得配 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--skill&lt;/code&gt;，写成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npx skills use /tmp/local-skill --skill local-demo&lt;/code&gt; 才通。README 的示例主要给 GitHub 源，本地路径这条语法没有现成例子。&lt;/p&gt;

&lt;p&gt;如果你想今天就试，最小的一套命令大概是：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 先看仓库里有哪些 skill&lt;/span&gt;
npx skills add vercel-labs/agent-skills &lt;span class=&quot;nt&quot;&gt;--list&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 装到两个 Agent，默认走 symlink&lt;/span&gt;
npx skills add vercel-labs/agent-skills &lt;span class=&quot;nt&quot;&gt;--skill&lt;/span&gt; web-design-guidelines &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; claude-code &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; cursor &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 只打印 prompt，不落盘&lt;/span&gt;
npx skills use vercel-labs/agent-skills &lt;span class=&quot;nt&quot;&gt;--skill&lt;/span&gt; web-design-guidelines
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;本周另外两个仓库&quot;&gt;本周另外两个仓库&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ranxianglei/billion-context&lt;/strong&gt;：给 coding agent 做上下文压缩的代理。我核了 npm 上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;billion-context@0.1.198&lt;/code&gt;：license 字段写 MIT，GitHub API 却识别成 NOASSERTION，LICENSE 文件不是标准文本；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dist.unpackedSize&lt;/code&gt; 是 147,805,403 字节，约 141 MB，光 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dist/agent/pi-native.js.map&lt;/code&gt; 就占 14.9 MB。README 说能省 5 倍 token、撑到 100K 窗口，这轮我没接真实 agent 和上游 API，压缩率没验。它自己承认 jcode 只能 cert-MITM、Gemini CLI 只能 launcher。想接之前，先看看那 141 MB 里有多少是 sourcemap。链接：https://github.com/ranxianglei/billion-context（截至 2026-10-17）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KittenML/KittenTTS&lt;/strong&gt;：能在 CPU 上跑的本地 TTS。我核了 PyPI 上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kittenml==0.9.4&lt;/code&gt;：wheel 只有 129,810 字节，Apache-2.0，要求 Python &amp;gt;=3.10，包本身很薄，权重另下。GitHub releases 最新 tag 还停在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.8.1&lt;/code&gt;（2026-02-24），但仓库到 2026-10-16 仍在 push，release 页落后于 main。README 说 1.7B 参数 1-bit 模型、默认 947 MiB、47 个音色、20 种语言、CPU 实时；我没下权重，实时性没验。想不碰 GPU 跑 TTS 的人可以盯一下，先确认要用的音色在不在那 47 个里。链接：https://github.com/KittenML/KittenTTS（截至 2026-10-17）&lt;/p&gt;

&lt;h2 id=&quot;我的判断它是技能落盘的路由表&quot;&gt;我的判断：它是技能落盘的路由表&lt;/h2&gt;

&lt;p&gt;回到那个空目录。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Installing to all 79 agents&lt;/code&gt; 这句话没有撒谎，它只是回答了“名单上有谁”。真正决定你磁盘长什么样的，是另外三个变量：目标 Agent 的项目根目录在不在、你选了一个还是多个目标、你用默认还是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--copy&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npx skills 的价值不在“一条命令装 79 个”，而在把 79 个名字收敛成 58 份配置，再用 canonical + symlink + lockfile 把落盘这件事变成可重复的。&lt;/strong&gt; 对多 Agent 用户，这个收敛值钱；对单 Agent 用户，它更像一个带版本记录的 cp。&lt;/p&gt;

&lt;p&gt;下次你看到那行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Installing to all 79 agents&lt;/code&gt;，别急着关终端。先跑一句 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find . -name SKILL.md&lt;/code&gt;。屏幕上是名单，磁盘上才是事实。&lt;/p&gt;

&lt;p&gt;什么证据会让我改口？如果后续版本在单 Agent 下也建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.agents&lt;/code&gt; 规范目录，或者 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a &apos;*&apos;&lt;/code&gt; 开始替你创建缺失的项目根目录，我上面“3 条路径”的判断就作废；如果 #278 用 partial clone 或按需下载修掉大仓库超时，那条劝退也要撤回。这两个变化都比 star 数值得盯。&lt;/p&gt;

&lt;p&gt;项目地址：https://github.com/vercel-labs/skills&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;参考：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;仓库：https://github.com/vercel-labs/skills（抓取 2026-10-17）&lt;/li&gt;
  &lt;li&gt;v1.7.2 release：https://github.com/vercel-labs/skills/releases/tag/v1.7.2（2026-10-09）&lt;/li&gt;
  &lt;li&gt;issue #278：https://github.com/vercel-labs/skills/issues/278（2026-02-04）&lt;/li&gt;
  &lt;li&gt;issue #840：https://github.com/vercel-labs/skills/issues/840（2026-04-01）&lt;/li&gt;
  &lt;li&gt;issue #421：https://github.com/vercel-labs/skills/issues/421（2026-02-25）&lt;/li&gt;
  &lt;li&gt;npm 包：https://www.npmjs.com/package/skills&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>

    <entry>
        <title>NIST 曲线的种子是 lrand48 摇出来的，我本机复算了 14 条全对</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/17/nist-curve-seed-lrand48.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/17/nist-curve-seed-lrand48.html</id>
        <updated>2026-10-17T00:00:00+08:00</updated>

        <summary>官方给了三行伪代码，我用 Node BigInt 独立实现，665 毫秒跑完</summary>


        <category term="AI"/>

        <category term="开源实测"/>


        <category term="Anthropic" scheme="tag"/>

        <category term="Claude" scheme="tag"/>

        <category term="安全" scheme="tag"/>

        <category term="加密" scheme="tag"/>

        <category term="开源" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/17/nist-curve-seed-lrand48.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/nist-curve-seed-lrand48/banner.webp&quot; alt=&quot;封面：左边一排十六进制字节被右边一枚时钟的指针串起来，中间一条断开的连线&quot; /&gt;&lt;/p&gt;

&lt;p&gt;今晚扫 HN，一条 938 分的帖子挂在上面：Claude reveals ECDSA curve origins，340 条评论。&lt;/p&gt;

&lt;p&gt;点进去之前我以为又是一篇模型能力宣传。读到第三屏发现不对，它把复现代码整段贴出来了，后面还跟着一张 14 行的输出表。&lt;/p&gt;

&lt;p&gt;那种表我一般直接跳过。这次没跳，因为它给的不是结论，是配方。&lt;/p&gt;

&lt;p&gt;配方只有三行：把状态初始化成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(t &amp;lt;&amp;lt; 16) | 0x330E&lt;/code&gt;，每步 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x = (0x5DEECE66D * x + 0xB) mod 2^48&lt;/code&gt;，每步取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x &amp;gt;&amp;gt; 40&lt;/code&gt; 的那 8 位。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt; 是一个 Unix 时间戳。&lt;/p&gt;

&lt;p&gt;我照它写了一遍 Node，没复制它的 Python。665 毫秒，14 条种子全对。&lt;/p&gt;

&lt;p&gt;这 14 串字节是 NIST P-192 到 P-521、加上 ANSI X9.62 那几条示例曲线的「seed」。它们从 1997 年起就印在标准文档里，来历一直没人说清。&lt;/p&gt;

&lt;p&gt;现在有人说清了：一个 C 标准库的随机数生成器，用当时的时钟当种子。&lt;/p&gt;

&lt;h2 id=&quot;从-1997-年的时钟到-36864-美元的赏金&quot;&gt;从 1997 年的时钟到 36,864 美元的赏金&lt;/h2&gt;

&lt;p&gt;Anthropic 那篇发布在 2026-10-16 17:02 UTC。事情本身跨了二十九年。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/nist-curve-seed-lrand48/01-screenshot-anthropic-seed-line.webp&quot; alt=&quot;官方研究页顶部，标题下一行写着「It&apos;s just timestamps」，日期是 Oct 16, 2026&quot; /&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;1997-05 → 1998-04&lt;/td&gt;
      &lt;td&gt;未知&lt;/td&gt;
      &lt;td&gt;十枚素域曲线的 srand48 初值都落在这段区间的 Unix 时间戳上&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2000&lt;/td&gt;
      &lt;td&gt;NIST&lt;/td&gt;
      &lt;td&gt;ECDSA 进 FIPS 186-2，seed 作为参数一起发布&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2015&lt;/td&gt;
      &lt;td&gt;Jerry Solinas&lt;/td&gt;
      &lt;td&gt;回忆说种子可能是「Give Bob and Jerry a raise」这类口令的哈希&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2023&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;Solinas 去世，写那段代码的 Bob Reiter 也记不清细节&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10&lt;/td&gt;
      &lt;td&gt;一名 Anthropic 员工&lt;/td&gt;
      &lt;td&gt;不给那条口令线索，直接问内部研究模型&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-16 17:02 UTC&lt;/td&gt;
      &lt;td&gt;Anthropic&lt;/td&gt;
      &lt;td&gt;公布结果，附完整复现代码与输出表&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;表里 2015 那行只是转述，原文自己后面补了一句 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;That turned out to be wrong.&lt;/code&gt;——口令哈希这条线从头就是错的。&lt;/p&gt;

&lt;p&gt;真正花掉的时间是「不到半小时」。原文逐字：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;In under half an hour, Claude discovered the origins of the NIST P-192, P-224, P-256, P-384, and P-521 seeds.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;赏金 36,864 美元，捐给 Internet Security Research Group。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/nist-curve-seed-lrand48/03-timeline-seed-hunt.webp&quot; alt=&quot;时间线示意：一枚时钟摇出的字节流，穿过 1997、2000、2015、2023 四个节点，最后落到 2026 的一台笔记本上&quot; /&gt;&lt;/p&gt;

&lt;p&gt;二十九年里缺的不是算力。缺的是有人愿意把每个时间戳都摇一遍，还得知道该拿哪几位去比。原文说 Claude 一开始并不知道种子会露出内部状态的哪几位，于是把求解器在每个窗口上都跑了一遍，最后十条素域曲线露出的都是同一个形状：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lrand48() &amp;gt;&amp;gt; 23&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;为什么这串字节被怀疑了这么多年&quot;&gt;为什么这串字节被怀疑了这么多年&lt;/h2&gt;

&lt;p&gt;seed 本身不是密钥。标准里它的用途是：把 seed 哈希一下，用哈希值定出曲线方程里的系数 b。&lt;/p&gt;

&lt;p&gt;问题在来历。原文写得很直白：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;they were provided by the NSA with no explanation or justification.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;于是有了 Neal Koblitz 和 Alfred Menezes 那篇《A Riddle Wrapped in an Enigma》，有了民间调查，也有了悬赏。&lt;/p&gt;

&lt;p&gt;流传最广的版本来自 Jerry Solinas，2015 年：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I believe there was a counter rather than multiple hashing, but I don’t know details. The message was along the lines of ‘Give Bob and Jerry a raise’ or ‘Bob and Jerry rule’ or something like that. It was Bob Reiter who actually wrote the code, and he doesn’t remember the details either. Nor were we able to find archives from so long ago. If they exist, they are no doubt sitting on a hard drive near the Ark of the Covenant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;「约柜旁边的一块硬盘上」。这句我读了三遍。&lt;/p&gt;

&lt;p&gt;Solinas 2023 年去世，线索就断在那儿。Anthropic 说自己有好几个员工照着「幽默口令的哈希」这条线追过，追错了。&lt;/p&gt;

&lt;p&gt;结果不是任何哈希算法。原文：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;None of them were the results of any hash algorithm at all, but rather the output of the C library&apos;s lrand48 random-number generator, seeded with a Unix timestamp!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;页面标题下面那句话说得更干脆：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;It&apos;s just timestamps&quot;&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;我自己写了一遍伪代码node-版以及-libressl-的对账&quot;&gt;我自己写了一遍：伪代码、Node 版、以及 LibreSSL 的对账&lt;/h2&gt;

&lt;p&gt;它给的判据很硬：每条种子 160 位，逐字对上才算。这种判据我最喜欢，因为不用相信任何人。&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;M48&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;48&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x5DEECE66D&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xB&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;M48&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;candidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;per&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;BigInt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x330E&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;M48&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// srand48(t) 设置的初始状态&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;out&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;per&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;per&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;out&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BigInt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toUpperCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// P-256：时间戳 871696354（1997-08-16 01:52:34 UTC），第 1842 个候选&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;candidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;871696354&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1842&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// =&amp;gt; C49D360886E704936A6678E1139D26B7819F7E90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;二进制域那四条口径不同：时间戳要先整除 2，每候选 5 个 32 位字（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x &amp;gt;&amp;gt; 17&lt;/code&gt;）。原文在这里自己加了一句保留，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if that reading is right&lt;/code&gt;——它也不确定这个读法。&lt;/p&gt;

&lt;p&gt;跑出来之后我没直接信自己，又拿本机的密码学库对了一遍：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl version
openssl ecparam &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; prime256v1 &lt;span class=&quot;nt&quot;&gt;-param_enc&lt;/span&gt; explicit &lt;span class=&quot;nt&quot;&gt;-text&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/nist-curve-seed-lrand48/02-screenshot-terminal-recompute.webp&quot; alt=&quot;终端截图：LibreSSL 3.3.6 印出 P-256 的 Seed 字节，下面 node 脚本打出 14 行 PASS 与合计 665 毫秒&quot; /&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;我的 Node 输出&lt;/th&gt;
      &lt;th&gt;本机 LibreSSL 3.3.6 印出的 Seed&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;P-256&lt;/td&gt;
      &lt;td&gt;C49D3608…819F7E90&lt;/td&gt;
      &lt;td&gt;c4:9d:36:08:…:7e:90&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;P-384&lt;/td&gt;
      &lt;td&gt;A335926A…7ACDAC73&lt;/td&gt;
      &lt;td&gt;a3:35:92:6a:…:ac:73&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;B-571&lt;/td&gt;
      &lt;td&gt;2AA058F7…7F132310&lt;/td&gt;
      &lt;td&gt;2a:a0:58:f7:…:23:10&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;14 条全对。官方表、我的实现、本机库里编译进去的常量，三个来源没有一个字节打架。&lt;/p&gt;

&lt;p&gt;我本来还想再补一条佐证。原文说有一枚种子没复现出来：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;The fifth binary seed, B-163, fits nothing Claude has tried and remains unexplained.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;B-163 在我这台机器上叫 sect163r2。我把它也跑了一遍 openssl，结果它连 Seed 字段都没印出来，其余 14 条都印了。&lt;/p&gt;

&lt;p&gt;看着像佐证。其实不是。&lt;/p&gt;

&lt;p&gt;它说的是推不出生成器，我看到的是本机参数表里没这一项，两件事不是一回事。我试图把它们对上，没对上，就不硬凑了。&lt;/p&gt;

&lt;p&gt;另外两处我也没验：基点那段（原文说六条 1997 年 5 月的曲线基点来自 Arjen Lenstra 的 FreeLIP，配方是「丢掉前七个输出，再用 26 位输出模 p」）我没有复算，要跑得先找到 FreeLIP 那段实现；候选 876 我也没验，验它要写曲线构造和素数阶判定。今晚的时间不够。&lt;/p&gt;

&lt;h2 id=&quot;谁需要动谁不用动&quot;&gt;谁需要动，谁不用动&lt;/h2&gt;

&lt;p&gt;原文自己先给了一句很反常的话：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Readers should take this as a fun closing chapter and the end of years of speculation. There is no action to be taken or things to do differently. Most researchers trusted the curves already. Enjoy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;以及：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;This is not a cryptographic break, new attack, or weakness in the ECDSA curves. In fact, this makes us more confident the curves were chosen without any backdoors or intentional weaknesses.&lt;/code&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 class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypto.subtle&lt;/code&gt; 里的 P-256 参数是编译进浏览器和 Node 的常量，seed 只在显式导出参数时才露出来&lt;/td&gt;
      &lt;td&gt;代码不用动，把「种子来历不明」从选型理由里划掉&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI 应用开发者&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;lrand48 的状态只有 48 位，而且能倒着走&lt;/td&gt;
      &lt;td&gt;查一遍自己项目里有没有拿 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rand()&lt;/code&gt; 或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srand48(time(NULL))&lt;/code&gt; 生成 token、盐、邀请码&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;团队&lt;/td&gt;
      &lt;td&gt;合规文档里「NIST 曲线种子来历不明，故选 X」这条依据失效&lt;/td&gt;
      &lt;td&gt;重写依据，但别顺手换曲线，这篇不构成换曲线的理由&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;四行里只有第三行是真需要动手的，而它跟 Anthropic 一点关系都没有。&lt;/p&gt;

&lt;p&gt;说白了，这次真正该被记住的提醒不在密码学里，在你自己项目那个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;srand48(time(NULL))&lt;/code&gt; 里。原文还写了一句让人不太舒服的话：这个线性同余生成器 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runs backwards as easily as forwards&lt;/code&gt;，Claude 就是把状态一步步往回退，退到低 16 位等于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x330E&lt;/code&gt; 的那个形状。这种形状每 65,536 步就会出现一次，所以光退到它不算证据——真正的证据是十条曲线退出来的初值全都是有意义的日期。&lt;/p&gt;

&lt;h2 id=&quot;我的判断算出种子不等于曲线安全&quot;&gt;我的判断：算出种子不等于曲线安全&lt;/h2&gt;

&lt;p&gt;HN 上那 938 分里，高票评论是这两条：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;@sweis: 13 years of tinfoil hats for srand48(time(NULL)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;@tptacek: This is devastating news… for an entire generation of NIST cryptography conspiracy theories.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;读起来像给一件事画了句号。我不这么看。&lt;/p&gt;

&lt;p&gt;原文自己留了一个更硬的口子，藏在 Open questions 那节：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;For P-256, the published seed is candidate 1,842, but candidate 876 of the same run also gives a prime-order curve of the right size and with parameters chosen the same way as on the published curve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;同一次摇奖里，第 876 号也是一条合格的曲线。P-224 那次跳过了三条，ANSI X9.62 那两次跳过了七条。原文接着写：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;We do not know what criteria may have been used, or whether this was an unintentional artifact.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;它也给了自己的判断：跳过的数量太少，所以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;we do not believe it is plausible that weakened curves were selected&lt;/code&gt;。但同一页还留着半句：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;It is still conceivable that these curves were selected to have a specific property in principle by searching over timestamps to the random number generator. However, you would only need to do that if you were going to disclose the timestamp as some evidence that the seed was innocuously generated. Nothing in the numbers shows how many runs there were.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;种子被算出来，回答的是「这些字节从哪来」；没回答的是「为什么停在这一号」。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;前者现在有了机器可验的答案，我今晚就验了。后者还是空的。拿前者当「曲线彻底清白」的收据，是把两个问题混成了一个。&lt;/p&gt;

&lt;p&gt;还有一层跟密码学无关。HN 上 @slowin 那条只有几十分，但我觉得它比高票评论更值得留着：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Anthropic (and whoever Anthropic wants) can do this kind of work with Claude, but if you or I try to do it, we will get blocked.&lt;/code&gt; 这轮我复算的是它已经公开答案的部分，所以很顺；真正那半小时的搜索，我用不上同样的模型。&lt;/p&gt;

&lt;p&gt;什么证据会让我改口：如果有人把候选 876 和 1842 那两条曲线放一起做性质对比，发现被选中的那条并不特殊，那我这条「没回答为什么停在这一号」就只是修辞，不是缺口。&lt;/p&gt;

&lt;h2 id=&quot;另外三条值得看一眼&quot;&gt;另外三条值得看一眼&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Home Assistant 的密码泄露检查被质疑「我没授权」。&lt;/strong&gt; 一手是 &lt;a href=&quot;https://github.com/home-assistant/home-assistant.io/issues/49314&quot;&gt;home-assistant/home-assistant.io#49314&lt;/a&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Viss&lt;/code&gt; 在 2026-10-16T18:56:24Z 开的，版本 2026.10.1，指向 &lt;a href=&quot;https://www.home-assistant.io/more-info/pwned-passwords/&quot;&gt;pwned-passwords 说明页&lt;/a&gt;。原话是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;This is a pretty severe invasion of privacy.&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I never authorized any integrations or plugins for scanning secrets and sending them up the line, even if they are hashed or not.&lt;/code&gt;，问的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;how do i turn this thing off?&lt;/code&gt;。issue 已经 closed，标签是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;more-info&lt;/code&gt;，最后一条评论写着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;As it turns out, this check can already be turned off after all:&lt;/code&gt;。所以呢：争的是默认开还是默认关，不是能不能关；两边原话都在同一个帖子里，自己读比看转述准。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI 10 月 15 日那次 major 事故，影响登录与注册 2 小时 27 分。&lt;/strong&gt; 一手是 &lt;a href=&quot;https://status.openai.com/api/v2/incidents.json&quot;&gt;status.openai.com 的 incidents JSON&lt;/a&gt;，条目叫 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Widespread feature degradation&lt;/code&gt;，created &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-15T16:29:26Z&lt;/code&gt;，resolved &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-10-15T18:56:00Z&lt;/code&gt;。四条更新里唯一一句技术描述是 17:13:05 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;We have applied the mitigation and are monitoring the recovery.&lt;/code&gt;，没有根因。所以呢：那段时间 ChatGPT 移动端登录失败的人，可以先排除自己的代码；同时也别指望状态页会告诉你为什么。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JPEG XL 全浏览器落地之后，Jake Archibald 的实测结论是「网页照片还是 AVIF 小」。&lt;/strong&gt; 一手是 &lt;a href=&quot;https://jakearchibald.com/2026/jpeg-xl-vs-avif/&quot;&gt;jakearchibald.com/2026/jpeg-xl-vs-avif&lt;/a&gt;，2026-10-16 发的。逐字：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JPEG XL has now landed in all major browsers&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;For lossy photos on the web, AVIF almost always produces smaller files than JPEG XL.&lt;/code&gt;、默认 effort 下最好的 JPEG XL 通常比同画质 AVIF 大 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;15–25%&lt;/code&gt;，拉满 effort 差距收窄到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6–20%&lt;/code&gt;；截图和插画类差得更多，JPEG XL 大 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;30–90%&lt;/code&gt;。所以呢：本站 10-10 那篇写过 Chrome 155 的 JPEG XL 解码，09-10 那篇写过 Next.js 的 AVIF 坑。「浏览器支持了」和「该换了」是两件事，按这组数字，照片类还是 AVIF。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;今晚让我记住的不是「模型半小时解决了二十九年悬案」，是那 665 毫秒：一段别人给的配方，我能在自己机器上重新摇出同样的 20 个字节。能被复算的结论才值得写进文章，剩下的都只能算听说。&lt;/p&gt;

&lt;p&gt;参考链接：&lt;a href=&quot;https://www.anthropic.com/research/ecdsa-curve-origins&quot;&gt;Claude reveals ECDSA curve origins&lt;/a&gt;（2026-10-16）· &lt;a href=&quot;https://news.ycombinator.com/item?id=50122800&quot;&gt;HN 讨论 50122800&lt;/a&gt;（938 分 / 340 评论，读取于 2026-10-17 22:24 CST）&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>Meta 放出 816B 开放权重：先别急着下载，1.6TB 内存是第一道门</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/17/muse-spark-open-816b.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/17/muse-spark-open-816b.html</id>
        <updated>2026-10-17T00:00:00+08:00</updated>

        <summary>许可证要产品挂名，65 个分片共 1.64TB，自托管先过内存关。</summary>


        <category term="AI"/>


        <category term="Meta" scheme="tag"/>

        <category term="AI" scheme="tag"/>

        <category term="Agent" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/17/muse-spark-open-816b.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/muse-spark-open-816b/banner.webp&quot; alt=&quot;封面：巨大的权重分片堆悬在桌面小电脑上方，橙色测量线标出高度&quot; /&gt;&lt;/p&gt;

&lt;p&gt;10 月 15 日 23:46（UTC+8），Hugging Face 上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;meta-models/Muse-Spark-Open-816B-A42B&lt;/code&gt; 这个仓库的更新时间停在了这一刻。65 个 safetensors 分片，我把字节数加了一遍：1,644,333,393,864 字节，1.64TB。参数规模 816B，激活 42B，上下文 1,048,576。&lt;/p&gt;

&lt;p&gt;我第一件事不是下载，是去看许可证。原因很实际：这套权重按 BF16 算，光参数就要 1.6TB 以上内存，我这台 Mac 连一个分片都装不下。&lt;/p&gt;

&lt;p&gt;许可证的第二条比 816B 更能决定它能不能进你的产品：用了模型或衍生品的每个产品，都要在 UI、文档、网站或设置菜单里醒目地写一行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Built with Muse Spark Open&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&quot;10-月-15-日深夜权重和许可证一起上线&quot;&gt;10 月 15 日深夜，权重和许可证一起上线&lt;/h2&gt;

&lt;p&gt;Meta 的官方发布页在 &lt;a href=&quot;https://research.meta.ai/blog/muse-spark-open&quot;&gt;research.meta.ai/blog/muse-spark-open&lt;/a&gt;。我在本机用 curl 打开它，返回 000；换 WebFetch 也失败。所以这篇不引用 Meta 自己的 benchmark 表，只写我能从模型仓库、框架文档和第三方实测里核到的东西。&lt;/p&gt;

&lt;p&gt;能核到的时间线如下。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;时间（UTC+8）&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;2026-10-15 23:46&lt;/td&gt;
      &lt;td&gt;HF 仓库更新（lastModified），权重与许可证上线&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://huggingface.co/meta-models/Muse-Spark-Open-816B-A42B&quot;&gt;HF 模型页&lt;/a&gt;，API &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lastModified&lt;/code&gt; 为 2026-10-15T15:46:43Z&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-15&lt;/td&gt;
      &lt;td&gt;许可证正文写明 Release date: October 15, 2026&lt;/td&gt;
      &lt;td&gt;HF 元数据里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cardData.extra_gated_prompt&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-16 00:58&lt;/td&gt;
      &lt;td&gt;HN 上的一条早期提交（72 points）&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=50106620&quot;&gt;HN 50106620&lt;/a&gt;，Algolia API 读数&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-16 01:35&lt;/td&gt;
      &lt;td&gt;Transformers PR #49644 合并，标题 “Add Muse Spark Open”&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/huggingface/transformers/pull/49644&quot;&gt;PR #49644&lt;/a&gt;，merged_at 2026-10-15T17:35:26Z&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-15&lt;/td&gt;
      &lt;td&gt;Apple ML Research 发布四台 Mac Studio 的 MLX 实测&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://machinelearning.apple.com/research/mlx-distributed-inference&quot;&gt;Apple ML Research&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-15&lt;/td&gt;
      &lt;td&gt;Together 上架 serverless API&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://www.together.ai/models/muse-spark-open&quot;&gt;Together 模型页&lt;/a&gt;，标注 Released October 15, 2026&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2026-10-17 22:05&lt;/td&gt;
      &lt;td&gt;我读取 HF 元数据：815 downloads、456 likes、gated=manual&lt;/td&gt;
      &lt;td&gt;HF API（经镜像站），快照时间见证据底稿&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/muse-spark-open-816b/03-timeline-release.webp&quot; alt=&quot;从 10-14 文档到 10-17 我读取元数据的五个节点&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这张表里最值得注意的不是 816B，而是权重上线后不到两小时，Transformers 的 PR #49644 就合并了。至少从结果看，Meta 这次不是只丢一个 checkpoint 出来让社区自己适配。PR 的创建时间是 17:18 UTC，合并是 17:35 UTC，17 分钟，32 个文件，5727 行新增。&lt;/p&gt;

&lt;p&gt;当然，这是社区贡献者 guarin 提的 PR，不是 Meta 自己维护的框架分支。它能说明“生态跟得很快”，不能说明“官方支持已经成熟”。&lt;/p&gt;

&lt;h2 id=&quot;65-个分片15314gib先算内存账&quot;&gt;65 个分片、1531.4GiB：先算内存账&lt;/h2&gt;

&lt;p&gt;我把 HF API 的文件列表拉下来，用 jq 把 65 个 safetensors 的 size 相加：1,644,333,393,864 字节。换成 GiB 是 1531.4，换成十进制是 1.64TB。&lt;/p&gt;

&lt;p&gt;用 1.64TB 除以 816B，得到约 2.01 字节/参数。这个读数符合 BF16（2 字节/参数），也解释了为什么 Apple 在博客里说 BF16 参数本身就要 1.6TB 以上内存。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/muse-spark-open-816b/02-screenshot-terminal.webp&quot; alt=&quot;终端里重跑的 HF 元数据、65 个分片字节汇总与 PR 合并记录&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我一开始以为 65 个分片里会混着 NVFP4，拉完文件列表才发现主仓库里没有名字带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvfp4&lt;/code&gt; 或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fp4&lt;/code&gt; 的独立分片。&lt;a href=&quot;https://github.com/huggingface/transformers/blob/v5.20.0/docs/source/en/model_doc/muse_spark.md&quot;&gt;Transformers 文档&lt;/a&gt;说 checkpoints 有 BF16 和 NVFP4 两种格式，但 NVFP4 放在哪个仓库、哪条分支，我这轮没验到。&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;BF16&lt;/td&gt;
      &lt;td&gt;1.64TB（我按 HF 文件列表算的）&lt;/td&gt;
      &lt;td&gt;KV cache、激活、框架开销&lt;/td&gt;
      &lt;td&gt;不能；Apple 说 BF16 参数本身就要 1.6TB 以上&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NVFP4&lt;/td&gt;
      &lt;td&gt;约 493GB（Apple 读数）&lt;/td&gt;
      &lt;td&gt;中间计算内存；Apple 说加完仍不能单机&lt;/td&gt;
      &lt;td&gt;不能，至少多机&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Together API&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;网络与 token 费用&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;h2 id=&quot;许可证不是-apache-20一行署名的代价&quot;&gt;许可证不是 Apache 2.0：一行署名的代价&lt;/h2&gt;

&lt;p&gt;HF 元数据里的 license 字段是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;other&lt;/code&gt;，license name 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;muse-model-license&lt;/code&gt;。&lt;a href=&quot;https://huggingface.co/meta-models/Muse-Spark-Open-816B-A42B&quot;&gt;许可证正文&lt;/a&gt;给了很宽的权利：使用、复制、修改、微调、蒸馏、合并、发布、分发、再许可、销售都写了。&lt;/p&gt;

&lt;p&gt;许可证正文列了几条义务，其中两条最影响产品。&lt;/p&gt;

&lt;p&gt;第二条是署名：每个用了软件或衍生品的产品或服务，要在 UI、随附文档、网站或设置菜单里醒目显示 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Built with Muse Spark Open&lt;/code&gt;。衍生品也包括用它的输出训练或蒸馏出来的模型。&lt;/p&gt;

&lt;p&gt;第三条是 MaaS 门槛：如果你把模型或衍生品作为 Model as a Service 提供给第三方，且你和关联公司任意连续 12 个月的合计总营收超过 2000 万美元，就要先通知 Meta 并签单独协议。内部使用、研究、非 MaaS 的商业活动不受这条约束。&lt;/p&gt;

&lt;p&gt;还有一层摩擦：模型是 gated manual。下载前要填真实姓名、出生日期、国家、机构、职位，表单还会读 IP 地理位置。我在镜像站请求 README 时收到 403 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;This model is not available in your region.&lt;/code&gt;；直连 HF 在这台机器上超时。我无法判断这是 Meta 的区域限制还是镜像站策略，所以不把“中国不能用”写成事实。&lt;/p&gt;

&lt;p&gt;还有一条对你有利：Meta 明确不主张模型输出的所有权。但输出仍受 Usage Policy 约束，商用前最好把用途条款也过一遍。署名这件事，我的建议是别只写在一个地方：产品 UI、设置菜单和随附文档各放一处，成本很低，漏掉的风险小很多。&lt;/p&gt;

&lt;p&gt;这三条里，最容易漏的是署名。它不是“致谢”，是许可证条件；漏了就不符合许可证要求。&lt;/p&gt;

&lt;p&gt;这也是我不把它叫“开源”的原因。Apache 2.0 和 MIT 没有这种按营收规模单独签约的条款；它有署名和 MaaS 门槛，更准确的叫法是“开放权重 + 商用许可 + 附加条件”。&lt;/p&gt;

&lt;h2 id=&quot;苹果用四台-mac-studio-跑出-3673-tokenss&quot;&gt;苹果用四台 Mac Studio 跑出 36.73 tokens/s&lt;/h2&gt;

&lt;p&gt;Apple ML Research 在 10 月 15 日发了一篇实测：4 台 Mac Studio，M5 Ultra，统一内存最高 512GB，MLX v0.30.1+，RDMA over Thunderbolt 5。Muse Spark Open 用 NVFP4，4 机 tensor parallel，8192 context 下 decode 36.73 tokens/s，TTFT 4.8 秒。&lt;/p&gt;

&lt;p&gt;单机跑不了。Apple 写得很直接：BF16 参数本身要 1.6TB 以上内存；NVFP4 把模型内存降到约 493GB，加上中间计算，仍然不能塞进一台机器。&lt;/p&gt;

&lt;p&gt;2 机 pipeline 是 15.74 tokens/s，2 机 tensor 是 24.99 tokens/s，4 机 tensor 是 36.73 tokens/s。4 机比 2 机 tensor 只快了约 47%，不是线性翻倍。机器多了一倍，decode 只多了不到一半，多机互联和 tensor parallel 的开销吃掉了接近一半的增量。这也是我不建议按机器数量线性估吞吐的原因。&lt;/p&gt;

&lt;p&gt;按 36.73 tokens/s 粗算，生成 1000 个 token 要约 27 秒。这不是聊天框的体验，更像批处理。&lt;/p&gt;

&lt;p&gt;Apple 自己也提到，大 context 下 tensor parallel 有开销，实现比 pipeline parallel 复杂。这组数字证明的是“能跑”，不是“能上线”。&lt;/p&gt;

&lt;h2 id=&quot;together-的-125425今天更现实的接入方式&quot;&gt;Together 的 $1.25/$4.25：今天更现实的接入方式&lt;/h2&gt;

&lt;p&gt;Together AI 已经把模型上到 serverless：input $1.25 / 1M，cached input $0.15 / 1M，output $4.25 / 1M，context 1M，输入支持文本、图像、音频、视频，输出文本。页面标注 Released October 15, 2026。&lt;/p&gt;

&lt;p&gt;我算了一笔账：同样生成 100 万 output token，Together 收 $4.25；你要自托管，先准备 1.64TB 的 BF16 权重或约 493GB 的 NVFP4 权重，再加多机互联和运维。对独立开发者，API 是唯一合理的第一步。&lt;/p&gt;

&lt;p&gt;但 API 也不是没有代价。1M context 意味着长文档、长代码库场景的 token 账单会很难看；output $4.25/1M 对高频生成场景也不便宜。先用 8k–32k 的真实任务测，不要一上来就灌 1M。&lt;/p&gt;

&lt;p&gt;再算两笔具体的账：一次把 1M context 灌满的请求，input 就是 100 万 token，按 $1.25/1M 算约 $1.25；一天 1000 次就是 $1250。生成 10 万 output token 约 $0.425；一天 1000 万 output token 约 $42.5，一个月约 $1275。先把这两个数写进预算，再谈要不要上。&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;Together serverless&lt;/td&gt;
      &lt;td&gt;注册、付款&lt;/td&gt;
      &lt;td&gt;$1.25 / $0.15 cached / $4.25 per 1M&lt;/td&gt;
      &lt;td&gt;快速验证、产品原型&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;自托管 BF16&lt;/td&gt;
      &lt;td&gt;1.64TB 权重 + 多机&lt;/td&gt;
      &lt;td&gt;硬件、机房、运维&lt;/td&gt;
      &lt;td&gt;有集群的团队&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;自托管 NVFP4&lt;/td&gt;
      &lt;td&gt;约 493GB 权重 + 中间计算 + 多机&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;img src=&quot;/better-blog/assets/img/muse-spark-open-816b/04-comparison-access.webp&quot; alt=&quot;BF16、NVFP4 与 Together API 三种接入门槛对比&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我的判断：这条模型的第一价值不是“免费下载”，是给闭源 API 增加了一个可审计的备胎。你未必要跑它，但你可以拿它的价格和许可证去谈别的供应商。&lt;/p&gt;

&lt;h2 id=&quot;前端能接-api独立开发者先别下权重&quot;&gt;前端能接 API，独立开发者先别下权重&lt;/h2&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;前端 / AI 应用开发者&lt;/td&gt;
      &lt;td&gt;用 Together 接多模态输入 + 1M context，做代码库问答、长文档理解&lt;/td&gt;
      &lt;td&gt;token 成本；输出只有文本&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;独立开发者&lt;/td&gt;
      &lt;td&gt;先用 API 验证需求，不要为“开源”买多机&lt;/td&gt;
      &lt;td&gt;1.64TB BF16 或约 493GB NVFP4；gated 申请&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;团队 / 企业&lt;/td&gt;
      &lt;td&gt;评估微调、蒸馏；把署名写进产品合规清单&lt;/td&gt;
      &lt;td&gt;2000 万美元 MaaS 条款；许可证不是 Apache&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;推理基础设施&lt;/td&gt;
      &lt;td&gt;试 NVFP4 + 多机并行，关注 1M context 吞吐&lt;/td&gt;
      &lt;td&gt;NVFP4 checkpoint 位置未确认；并行开销&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;前端开发者是这波里最舒服的：不用碰权重，接 API 就能用。最不舒服的是想“把模型搬回自己机器”的独立开发者，因为这次的单位是 TB，不是 GB。&lt;/p&gt;

&lt;h2 id=&quot;今天就能做的三件事和两个要盯的信号&quot;&gt;今天就能做的三件事，和两个要盯的信号&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;想验证能力：拿一个 8k context 的真实任务，比如把一个中型仓库的 20 个文件喂进去做重构建议，用 Together 跑一次，记录 input/output token 和延迟，再决定要不要上 1M。&lt;/li&gt;
  &lt;li&gt;想自托管：先确认 NVFP4 checkpoint 的实际仓库和推理配方；按约 493GB 权重 + 中间计算 + KV 预留做容量计划，不要按单台 512GB Mac Studio 下单。&lt;/li&gt;
  &lt;li&gt;想商用：把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Built with Muse Spark Open&lt;/code&gt; 写进 UI、文档、设置菜单的发布清单；如果做 MaaS 且营收可能超过 2000 万美元，提前找法务和 Meta。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;两个要盯的信号：Meta 是否放出独立的 NVFP4 仓库和单机推理配方；Together 是否调整 output 价格，或者出现第二家同价 provider。&lt;/p&gt;

&lt;p&gt;什么证据会让我改口：如果两周内出现能在单台 512GB 机器上稳定跑 8k context 的 NVFP4 配方，或者 Together 把 output 降到 $2/1M 以下，我这条“个人别自托管、API 先用着”的判断就作废。&lt;/p&gt;

&lt;h2 id=&quot;短讯anthropic-的-fast-模式微软-28t-与-openai-的图片字段&quot;&gt;短讯：Anthropic 的 fast 模式、微软 2.8T 与 OpenAI 的图片字段&lt;/h2&gt;

&lt;p&gt;DeepSeek V4.1 Flash 的别名与价格变化，&lt;a href=&quot;/better-blog/2026/10/17/deepseek-v41-flash-alias-switch.html&quot;&gt;今天早些时候单独写过一篇&lt;/a&gt;，这里补三条不在那篇里的更新。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic 把 fast 模式写进了 SDK 类型。&lt;/strong&gt; 2026-10-15 发布的 Python SDK v1.15.0 在 Messages 里新增 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;speed: &quot;standard&quot; | &quot;fast&quot;&lt;/code&gt;，返回的 usage 也多了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;speed&lt;/code&gt; 字段。SDK 注释写明，部分模型用任何 speed 值都需要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fast-mode-2026-02-01&lt;/code&gt; beta header；&lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/fast-mode&quot;&gt;文档&lt;/a&gt;把 fast 模式描述为 Opus 5.5 的 2.5 倍速配置，价格 $8/$40 per 1M tokens，文档仍把它标为 research preview。所以呢：做低延迟交互的团队可以试，但别把它当稳定档位，先确认你的模型和账户是否在灰度范围内。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;微软的 2.8T 模型完成预训练，但还不是产品。&lt;/strong&gt; 2026-10-15，&lt;a href=&quot;https://microsoft.ai/news/frontier-pre-training-at-microsoft-ai/&quot;&gt;Microsoft AI 公布 MAI-Base-2&lt;/a&gt;：MoE，80B 激活 / 2.8T 总参数，16k GB200 训练 38 天，平均 MFU 21%、goodput 89%。官方明确说它还在 post-training，权重将通过 Foundry 授权给可信伙伴，不是开放下载。所以呢：这是一条“下一个前沿基座”的信号，不是今天能接的 API；它和 Meta 的开放权重放在一起看，2026 年下半年两条路线的分化更清楚了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI 给图片接口加了 safety_identifier。&lt;/strong&gt; 2026-10-17 00:28–00:29 UTC，Python SDK v3.33.0 和 Node v7.36.0 发布，图片创建和编辑请求可以传 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;safety_identifier&lt;/code&gt;，响应会回显实际生效的值；旧的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt; 参数保留但标记为 deprecated（&lt;a href=&quot;https://github.com/openai/openai-node/pull/2957&quot;&gt;PR #2957&lt;/a&gt; 于 2026-10-16 21:41 UTC 合并）。所以呢：做图片生成或编辑的产品，应该给每个终端用户传一个稳定的哈希 ID，别传邮箱或手机号；同时用响应里的字段把日志和请求对上，否则排查滥用时对不上。&lt;/p&gt;

&lt;p&gt;回到开头那行字。816B、1.64TB、36.73 tokens/s 都很抓眼，但真正会卡住产品上线的，可能是许可证里那句 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Built with Muse Spark Open&lt;/code&gt;。权重放出来是一回事，能不能把它放进你的产品、你的账单和你的法务清单，是另一回事。&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>README 说载 3 个字体族，我数出 981 个 @font-face</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/17/diagram-design-981-faces.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/17/diagram-design-981-faces.html</id>
        <updated>2026-10-17T00:00:00+08:00</updated>

        <summary>模板里那条 link 声明 981 个切片、8 个族，README 只提了 3 个名字，画廊还藏着第二条</summary>


        <category term="CSS"/>

        <category term="开源实测"/>


        <category term="CSS" scheme="tag"/>

        <category term="headless" scheme="tag"/>

        <category term="设计系统" scheme="tag"/>

        <category term="github" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="前端小技巧" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/17/diagram-design-981-faces.html">&lt;h1 id=&quot;readme-说载-3-个字体族我数出-981-个-font-face&quot;&gt;README 说载 3 个字体族，我数出 981 个 @font-face&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/diagram-design-981-faces/banner.webp&quot; alt=&quot;左边那只深色托盘挤满青绿细条，右边同高的简体槽里一根都没有&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;一句话结论：README 说这条 link 加载 3 个字体族，我数出 981 个切片，928 个给韩文和繁体，简体 0 个。
现在该做什么：别照 README 的句子推断字体行为，把那条 link 抓回来按 gstatic 的路径数一遍就知道。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;readme-那句话对不上我抓回来的响应&quot;&gt;README 那句话对不上我抓回来的响应&lt;/h2&gt;

&lt;p&gt;README 说三个名字，响应里有 981 条声明。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/cathrynlavery/diagram-design&quot;&gt;cathrynlavery/diagram-design&lt;/a&gt; 的 README 第 361 行写着，一张图默认从 Google Fonts 加载 Instrument Serif、Geist 和 Geist Mono，这是它 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;the only network request it makes&lt;/code&gt;。我照 style-guide.md 里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Font stack&lt;/code&gt; 那行原样抓了一次。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;--http1&lt;/span&gt;.1 &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;User-Agent: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$UA&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Accept-Encoding: br, gzip&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; css.gz &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;%{size_download}\n&apos;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$URL&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;# 233,142&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;gunzip&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; css.gz | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;@font-face&apos;&lt;/span&gt;        &lt;span class=&quot;c&quot;&gt;# 981&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$UA&lt;/code&gt; 是 Chrome 153 的 UA 串，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$URL&lt;/code&gt; 就是那行 link。摊开是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;907,623&lt;/code&gt; 字节，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unicode-range&lt;/code&gt; 也正好 981 行。&lt;/p&gt;

&lt;p&gt;三个名字对 981 条声明，中间隔着一层机制：css2 接口把每个族按码位区间切成若干片，一片一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@font-face&lt;/code&gt;。切片的账在样式表这一层结清，字体文件那一层才按区间现取。981 按族摊开是这样：&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;Noto Sans KR&lt;/td&gt;
      &lt;td&gt;372&lt;/td&gt;
      &lt;td&gt;韩文&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Noto Sans TC&lt;/td&gt;
      &lt;td&gt;324&lt;/td&gt;
      &lt;td&gt;繁体&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Noto Serif KR&lt;/td&gt;
      &lt;td&gt;124&lt;/td&gt;
      &lt;td&gt;韩文&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Noto Serif TC&lt;/td&gt;
      &lt;td&gt;108&lt;/td&gt;
      &lt;td&gt;繁体&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Geist Mono&lt;/td&gt;
      &lt;td&gt;18&lt;/td&gt;
      &lt;td&gt;拉丁&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Noto Serif&lt;/td&gt;
      &lt;td&gt;16&lt;/td&gt;
      &lt;td&gt;拉丁&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Geist&lt;/td&gt;
      &lt;td&gt;15&lt;/td&gt;
      &lt;td&gt;拉丁&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Instrument Serif&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;拉丁&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;KR 与 TC 那四行相加 928，四个拉丁族相加 53，928 + 53 = 981。简体那两族 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Noto Sans SC&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Noto Serif SC&lt;/code&gt;，切片数：&lt;/p&gt;

&lt;p&gt;0 个。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/diagram-design-981-faces/02-screenshot-css2-981-panel.webp&quot; alt=&quot;浏览器面板实测那条 8 族 link：fontFaceCount=981，逐族 x372、x324、x124、x108，四个拉丁族对汉字的 load 全给 0&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;同一个仓库里躺着两条-link37-和-981&quot;&gt;同一个仓库里躺着两条 link，37 和 981&lt;/h2&gt;

&lt;p&gt;第一次量到的是 37，不是 981。&lt;/p&gt;

&lt;p&gt;我把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example-architecture.html&lt;/code&gt; 用 headless Chrome 打开，往页面里注了个探针打印 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.fonts.size&lt;/code&gt;，它给 37，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;families&lt;/code&gt; 只有 Geist、Geist Mono、Instrument Serif 三项。README 那句话在这枚文件上是准的。&lt;/p&gt;

&lt;p&gt;分歧在于仓库里有两条 link。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep -l &apos;Noto+Sans+KR&apos; skills/diagram-design/assets/*.html&lt;/code&gt; 数出 11 枚，正好是 4 枚 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;template*.html&lt;/code&gt; 加 7 枚 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*-animated.html&lt;/code&gt;；剩下 206 枚带的是 3 族那一条。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;references/export.md&lt;/code&gt; 里导出 SVG 时注入的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import&lt;/code&gt; 用的是 8 族那一条。&lt;/p&gt;

&lt;p&gt;所以路径分两岔。你让 skill 生成一张新图，它从 template 起步，拿到 981 那条；你去画廊看效果，看到 37 那条。README 描述的是后者，模板加载的是前者。&lt;/p&gt;

&lt;h2 id=&quot;那条-link-断网之后37-掉到-0&quot;&gt;那条 link 断网之后，37 掉到 0&lt;/h2&gt;

&lt;p&gt;同一份文件，我把网络掐了再量一次。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CHROME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--headless&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;new &lt;span class=&quot;nt&quot;&gt;--window-size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1000,620 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--virtual-time-budget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;20000 &lt;span class=&quot;nt&quot;&gt;--screenshot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;netoff.png &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--proxy-server&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://127.0.0.1:9&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;file://&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PWD&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/probe.html&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;端口 9 有没有监听我先确认过，是空的。左右两半同一次跑出来：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/diagram-design-981-faces/01-screenshot-probe-online-offline.webp&quot; alt=&quot;左半联网，面板 fontFaceCount=37、标题实宽 265.9；右半断网，37 变 0、字宽涨到 325.6，两格的 check 仍是 true&quot; /&gt;&lt;/p&gt;

&lt;p&gt;37 个切片掉到 0，同一句英文标题的实宽从 265.9 涨到 325.6。字体没来，行宽自己找位置。&lt;/p&gt;

&lt;p&gt;判据这里有个坑，值得单独说：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.fonts.check(&apos;14px &quot;Geist&quot;&apos;)&lt;/code&gt; 在断网那一侧仍然返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;，而同一页的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.fonts.size&lt;/code&gt; 是 0。空集合上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;check()&lt;/code&gt; 是空真，它回答的是「现在要这个字体，会不会去取」，不是「取到了没有」。要判有没有，量 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;size&lt;/code&gt; 或者直接量宽度。&lt;/p&gt;

&lt;p&gt;简体不在那 981 条里，这事仓库自己知道，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self_check.py&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--offline&lt;/code&gt; 就是为它开的。我先把它用错了地方。&lt;/p&gt;

&lt;h2 id=&quot;我把-offline-打在画廊上217-个-fail-算我量错&quot;&gt;我把 –offline 打在画廊上，217 个 FAIL 算我量错&lt;/h2&gt;

&lt;p&gt;命令是仓库自带的，指向它自己的文件。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python3 skills/diagram-design/scripts/self_check.py &lt;span class=&quot;nt&quot;&gt;--offline&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  skills/diagram-design/assets/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.html
&lt;span class=&quot;c&quot;&gt;# exit 1 · FAIL 217 / OK 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;217 条同因，逐字是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;offline: file loads Google Fonts; remove the font &amp;lt;link&amp;gt; to use system fonts&lt;/code&gt;。我盯着这个退出码看了半分钟，以为抓到了什么大东西。&lt;/p&gt;

&lt;p&gt;重读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--offline&lt;/code&gt; 的 help 才明白：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;also fail on the Google Fonts link, for system-font output that makes no network requests&lt;/code&gt;。那是给你自己生成的文件准备的开关，画廊本来就按 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;web&lt;/code&gt; 出。门禁打在画廊上是量具用错对象，那 217 个 FAIL 算我的。&lt;/p&gt;

&lt;p&gt;不过这条门禁指对了方向：它认的是文件里有没有那条 link。同样的判据打在 template 上，FAIL 的原因就是真的——template 才是给你生成图用的起点，它带的正是 8 族那一条。&lt;/p&gt;

&lt;h2 id=&quot;补简体的那条-pr-改的正是-template&quot;&gt;补简体的那条 PR 改的正是 template&lt;/h2&gt;

&lt;p&gt;补简体的补丁只动了 8 族那一条。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/cathrynlavery/diagram-design/pull/249&quot;&gt;cathrynlavery/diagram-design#249&lt;/a&gt; 改 6 处 link：4 枚 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;template*.html&lt;/code&gt; 各一行、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export.md&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@import&lt;/code&gt; 一行、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;style-guide.md&lt;/code&gt; 两行。206 枚画廊文件那条 3 族 link，它一行没动。补丁很小，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+8 / -7&lt;/code&gt;，8 个文件里还有一枚是视觉证据图片。&lt;/p&gt;

&lt;p&gt;作者把代价写在 body 明面：样式表从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~229 KB&lt;/code&gt; 涨到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~353 KB&lt;/code&gt;，纯拉丁的图也一起涨。我实测两条 link 的 gzip 响应是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;233,142&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;357,011&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@font-face&lt;/code&gt; 从 981 条变 1,385 条，新增的 404 条全在 SC 两族里。跟他按 KiB 折算的读数差 1.3% 与 1.4%，逐字相等我没能复现。&lt;/p&gt;

&lt;p&gt;先例走的是同一条路。&lt;a href=&quot;https://github.com/cathrynlavery/diagram-design/issues/175&quot;&gt;cathrynlavery/diagram-design#175&lt;/a&gt; 开在 9 月 3 日，&lt;a href=&quot;https://github.com/cathrynlavery/diagram-design/pull/186&quot;&gt;cathrynlavery/diagram-design#186&lt;/a&gt; 在 9 月 6 日合入，前后 3 天 9 小时。#249 开在 9 月 24 日，我核对的这一刻还开着，23 天 10 小时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mergeable=true&lt;/code&gt; 但状态是 unstable。&lt;/p&gt;

&lt;p&gt;维护者留了两条意见。第一条说视觉证据不成立，那张 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simplified-labels-light.png&lt;/code&gt; 是一张纯拉丁的图，里面没有一个汉字。第二条说还有 3 条 Greptile 线程没关。作者 10 月 10 日回：五点加那一点可选的都改了，之前那张附件确实是一版拉丁渲染，是我贴错了；然后问能不能在新 head 上复审——PR 作者自己发不起正式 review 请求。&lt;/p&gt;

&lt;p&gt;我的判断是该合。多出一百来 KB 的样式表，买简体标签跨机器一致，在一张图只加载一次的场景里付得起。这条是判断，不是实测。&lt;/p&gt;

&lt;h2 id=&quot;你自己数一遍别信我的截图&quot;&gt;你自己数一遍，别信我的截图&lt;/h2&gt;

&lt;p&gt;981 这个数你一分钟能复算出来。&lt;/p&gt;

&lt;p&gt;两条命令就够：上面那个 curl 加 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep -c &apos;@font-face&apos;&lt;/code&gt;，数的是样式表这一层；浏览器控制台里读 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.fonts.size&lt;/code&gt;，数的是你这个页面真正拿到了多少条声明。两个数对不上，说明你的 link 跟仓库文档里那条不是同一条。&lt;/p&gt;

&lt;p&gt;我没能验到的：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lint-render.py&lt;/code&gt; 我没跑，它要 Playwright 那套渲染工装，本机这次没装。style-guide.md 第 123 行那句 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;every shipped template and example is already checked in system-font mode&lt;/code&gt; 我照原文引，没证实也没推翻。同一张面板里，4 个 40px 汉字在 TC 栈、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serif&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PingFang SC&lt;/code&gt; 三种 family 下都量出 160.0，等宽说明那次没有任何 web 字体参与排版——那张图证的是 CSS 层的命中，不是简体字长什么样。&lt;/p&gt;

&lt;p&gt;#249 的 head 停在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;28b8a84&lt;/code&gt;，仓库版本在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude-plugin/plugin.json&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.6.110&lt;/code&gt; 上。如果它到我下期抓 trending 之前还没合，我回来量的还是 981 和 0 这两个数；合了我就把两条 link 的差值重算一遍给你。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/cathrynlavery/diagram-design&quot;&gt;https://github.com/cathrynlavery/diagram-design&lt;/a&gt;&lt;/p&gt;
</content>
    </entry>

    <entry>
        <title>你 pin 的 deepseek-flash，今天背后换了模型</title>
        <link rel="alternate" type="text/html" href="https://liaolongdong.github.io/better-blog/2026/10/17/deepseek-v41-flash-alias-switch.html"/>
        <id>https://liaolongdong.github.io/better-blog/2026/10/17/deepseek-v41-flash-alias-switch.html</id>
        <updated>2026-10-17T00:00:00+08:00</updated>

        <summary>旧名 deepseek-v4-flash 仍接收，但请求已落到 1017；白天价是夜里两倍</summary>


        <category term="AI"/>


        <category term="AI" scheme="tag"/>

        <category term="开源" scheme="tag"/>

        <category term="评测" scheme="tag"/>
<content type="html" xml:base="https://liaolongdong.github.io/better-blog/2026/10/17/deepseek-v41-flash-alias-switch.html">&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepseek-v41-flash-alias-switch/banner.webp&quot; alt=&quot;封面：同一个模型标签下，机器内部已经换了总成&quot; /&gt;&lt;/p&gt;

&lt;p&gt;10 月 17 日下午 16:27，OpenRouter 的模型列表里多了一行 DeepSeek V4.1 Flash 1017。往前推 21 分钟，ModelScope 上同名模型卡的最后更新时间停在 16:06。DeepSeek 自己的 news 页只写了日期 2026/10/17，没有小时。&lt;/p&gt;

&lt;p&gt;我盯着这几个时间点看了一会儿，才意识到这次最别扭的地方不是新模型，而是名字。你代码里写的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash&lt;/code&gt; 没有变，官方价格表的模型版本已经换成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeepSeek-V4.1-Flash-1017&lt;/code&gt;；连 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-v4-flash&lt;/code&gt; 这个旧名也照收，只是后面的旧模型退役了，请求会被新模型接走。&lt;/p&gt;

&lt;h2 id=&quot;三个名字一层套一层&quot;&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;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;当前推荐模型名，模型版本是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeepSeek-V4.1-Flash-1017&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;接口名，不是权重版本&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;旧名，仍被接收；对应模型已退役，请求由 1017 接走&lt;/td&gt;
      &lt;td&gt;兼容层，不是旧模型&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeepSeek-V4.1-Flash-1017&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;这次实际的模型版本&lt;/td&gt;
      &lt;td&gt;文档里的版本号，不是可指定的 API 名&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;官方价格页的脚注写得很直白：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;The legacy names deepseek-v4-flash and deepseek-v4-flash-vision-exp are still accepted, but the corresponding models have been retired, their requests are served by the DeepSeek-V4.1-Flash-1017 model and billed at the Flash price.&lt;/code&gt;（来源：DeepSeek 价格页，2026-10-17 抓取）&lt;/p&gt;

&lt;p&gt;我一开始以为 DeepSeek 会给新快照单独一个 model 名，翻到这条脚注才发现不是。说白了，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash&lt;/code&gt; 现在是一条滚动通道，不是一个权重版本。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;旧代码不会报错，但它拿到的已经不是昨天那个模型了。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;时间线10-月-17-日一天里改了什么&quot;&gt;时间线：10 月 17 日一天里改了什么&lt;/h2&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;10-17 10:41:50&lt;/td&gt;
      &lt;td&gt;ModelScope 创建 V4.1-Flash-1017 模型卡&lt;/td&gt;
      &lt;td&gt;ModelScope 元数据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10-17 16:06:25&lt;/td&gt;
      &lt;td&gt;模型卡最后更新（484.6B、MIT、1M 上下文）&lt;/td&gt;
      &lt;td&gt;ModelScope&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10-17 16:27:04&lt;/td&gt;
      &lt;td&gt;OpenRouter 上架，canonical_slug 带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;20261017&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;OpenRouter&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10-17（只标日期）&lt;/td&gt;
      &lt;td&gt;DeepSeek news 与价格页改为新版本&lt;/td&gt;
      &lt;td&gt;api-docs.deepseek.com&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;时间线里最值得记的是 16:27 这个点：第三方聚合器比官方 news 页更早给出可机器读取的日期快照。官方页只写到天，所以“几点发布”这件事，严格说我没法从一手来源确认；上面表里的时分来自 ModelScope 和 OpenRouter 的元数据，不是 DeepSeek 的公告。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这不是挑刺。做回归的时候，你总得知道该拿哪个时间点之前的输出当基线。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepseek-v41-flash-alias-switch/02-timeline-alias-switch.webp&quot; alt=&quot;10 月 17 日从模型卡到 OpenRouter 上架的时间线&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;旧别名还收但旧模型已经退役&quot;&gt;旧别名还收，但旧模型已经退役&lt;/h2&gt;

&lt;p&gt;这次更新最容易被忽略的一句话，是“still accepted”。它听起来像兼容，实际是把兼容层做在了路由上：名字还认，后面的模型换了。&lt;/p&gt;

&lt;p&gt;对业务代码，这是好事。你不用改一行调用，模型就升级了，vision、1M 上下文、384K 最大输出也都还在。官方价格页把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash&lt;/code&gt; 的能力列得很清楚：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONTEXT LENGTH 1M&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX OUTPUT MAXIMUM: 384K&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;THINKING MODE Supports both non-thinking and thinking (default) modes&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Vision ✓&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;对需要可复现的团队，这是另一回事。&lt;/p&gt;

&lt;p&gt;我没拿到 DeepSeek 的 key，所以没法实测响应里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; 字段到底返回别名还是版本号；文档在价格页也没有给出一个可以指定的日期快照名。OpenRouter 那边倒是给了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek/deepseek-v4.1-flash-20261017&lt;/code&gt; 这样的 canonical_slug，但它是聚合器的命名，能不能长期钉住旧快照，我没验到。&lt;/p&gt;

&lt;p&gt;模型卡里还有一个给自托管的人看的细节：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;This release does not include a Jinja-format chat template.&lt;/code&gt; 官方补了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encoding/encoding.py&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-recipe&lt;/code&gt;，但这意味着你不能假设 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tokenizer_config.json&lt;/code&gt; 里躺着一个现成的 chat template。想把 MIT 权重接进现有推理栈，先看这一条。&lt;/p&gt;

&lt;p&gt;我这边 Hugging Face 的 API 是连接失败（HTTP 000），ModelScope 能打开；所以权重细节我只核到模型卡的公开元数据（484.6B 参数、MIT 许可、last_updated 2026-10-17），没有下载权重，也没有跑推理。&lt;/p&gt;

&lt;p&gt;官方没有解释为什么不另起一个名字。从结果看，旧名保留让调用方不至于断掉；代价是版本信息被折叠进了一个不会变的字符串里。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这就是滚动别名的代价：接口稳定了，版本证据反而更少了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepseek-v41-flash-alias-switch/03-comparison-alias-vs-weights.webp&quot; alt=&quot;同一个 deepseek-flash 别名后面，接着三个不同权重&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;价格表里的昼夜开关同一百万-token白天贵一倍&quot;&gt;价格表里的昼夜开关：同一百万 token，白天贵一倍&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;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;输入（缓存命中）/ 1M tokens&lt;/td&gt;
      &lt;td&gt;$0.003&lt;/td&gt;
      &lt;td&gt;$0.006&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入（缓存未命中）/ 1M tokens&lt;/td&gt;
      &lt;td&gt;$0.15&lt;/td&gt;
      &lt;td&gt;$0.30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输出 / 1M tokens&lt;/td&gt;
      &lt;td&gt;$0.60&lt;/td&gt;
      &lt;td&gt;$1.20&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;高峰时段是 UTC 01:00–04:00 和 06:00–10:00，周一到周五，排除中国法定节假日；换成北京时间就是 09:00–12:00 和 14:00–18:00。其余时间都算非高峰，周末和法定节假日全天非高峰。&lt;/p&gt;

&lt;p&gt;拿一个具体任务算：1M 输入（缓存未命中）加 200K 输出，非高峰是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.15 + 0.2 × 0.60 = 0.27&lt;/code&gt; 美元，高峰是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.30 + 0.2 × 1.20 = 0.54&lt;/code&gt; 美元。同样的 token，账单差一倍。&lt;/p&gt;

&lt;p&gt;如果输入命中缓存，非高峰的输入成本只有 $0.003，输出仍然按 $0.60 算。所以省钱的顺序是：先把能缓存的上下文缓存住，再考虑把任务挪到非高峰。&lt;/p&gt;

&lt;p&gt;这是按官方单价算的，不是我的真实账单。但结论够清楚：如果你有批量任务，调度器比模型选型更能省钱。&lt;strong&gt;能挪到晚上或周末跑的，别留在工作日下午。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepseek-v41-flash-alias-switch/04-screenshot-pricing.webp&quot; alt=&quot;DeepSeek 价格页：1017 版本、1M 上下文与昼夜两档价格&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;search-和-fetch-开放按次计费免费窗口到-10-月-31-日&quot;&gt;Search 和 Fetch 开放：按次计费，免费窗口到 10 月 31 日&lt;/h2&gt;

&lt;p&gt;同一天的 news 页还有第二件事：DeepSeek 把 Search 和 Fetch 做成了独立 API，开放给所有人。&lt;/p&gt;

&lt;p&gt;端点是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST https://api.deepseek.com/v1/tools/search&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST https://api.deepseek.com/v1/tools/fetch&lt;/code&gt;，走同一套 Bearer 鉴权，返回原始 JSON。Search 返回网页结果，Fetch 把单个网页转成 Markdown。&lt;/p&gt;

&lt;p&gt;Search 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; 默认 10、最多 50，查询里可以用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;site:&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lang:&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after:&lt;/code&gt; 这些操作符；Fetch 只取单个页面，返回 Markdown。&lt;/p&gt;

&lt;p&gt;价格是每个端点 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$0.75 per 1,000 calls&lt;/code&gt;，按成功请求计费。通过 DeepSeek Harness、Claude Code、Codex 里的服务端工具调用目前免费，计费从 2026-10-31 16:00 UTC 开始——也就是北京时间 11 月 1 日 0 点。&lt;/p&gt;

&lt;p&gt;按官方单价算：一个任务平均 10 次检索，1 万个任务就是 10 万次调用，75 美元，不含模型 token。这个数字不大，但它会跟着 agent 的步数线性涨。&lt;/p&gt;

&lt;p&gt;免费窗口只有两周多。如果你打算把 Search/Fetch 接进生产，别把“现在免费”写进成本模型。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;工具调用按次收费之后，agent 的循环次数就不只是延迟问题，也是账单问题。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;我核了什么哪里没验到&quot;&gt;我核了什么，哪里没验到&lt;/h2&gt;

&lt;p&gt;我核过的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;DeepSeek news 页与价格页均返回 200；模型版本、上下文、输出上限、价格、旧别名脚注都是逐字从页面取的。&lt;/li&gt;
  &lt;li&gt;ModelScope 模型卡返回 200，元数据是 484.6B 参数、MIT 许可、last_updated 2026-10-17；模型卡自己写了不含 Jinja chat template。&lt;/li&gt;
  &lt;li&gt;OpenRouter 的模型列表返回 200，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context_length&lt;/code&gt; 是 1048576，输入/输出价格与 DeepSeek 官方页一致，canonical_slug 带 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;20261017&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;不带 key 请求 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://api.deepseek.com/models&lt;/code&gt; 返回 401，响应体是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authentication Fails (governor)&lt;/code&gt;。这只证明端点在、需要鉴权。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我没验到的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;没有 key，没有实际调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash&lt;/code&gt; 或旧别名，所以旧别名是不是真的路由到 1017、vision 和 1M 上下文实际表现如何，我都不知道。&lt;/li&gt;
  &lt;li&gt;没有跑第三方或自建 benchmark。官方说的 “Pareto improvement” 是厂商声称，我没有复现。&lt;/li&gt;
  &lt;li&gt;Hugging Face 打不开，权重文件没下，模型卡只看了 ModelScope 的版本。&lt;/li&gt;
  &lt;li&gt;OpenAI 的 platform changelog 返回 403，下面那条 OpenAI 短讯只核到 GitHub 的 SDK release。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/better-blog/assets/img/deepseek-v41-flash-alias-switch/05-screenshot-terminal.webp&quot; alt=&quot;终端核验：文档 200、无 key 401、HF 超时&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我本来想把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-v4-flash&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash&lt;/code&gt; 各打一次，比较响应里的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; 字段；没有可用 key，这一步停住了。这不是省略，是没验到。&lt;/p&gt;

&lt;h2 id=&quot;对四类人的影响以及现在能做什么&quot;&gt;对四类人的影响，以及现在能做什么&lt;/h2&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;调用名不变，背后模型换了；JSON 输出、tool call、vision 可能漂移&lt;/td&gt;
      &lt;td&gt;把现有的结构化输出用例重跑一遍&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI 应用开发者&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; 名是滚动别名，不是权重版本；日志里只记名字无法区分快照&lt;/td&gt;
      &lt;td&gt;在网关层记录请求名、响应 model、日期&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;独立开发者&lt;/td&gt;
      &lt;td&gt;1M 上下文、MIT 权重、非高峰半价&lt;/td&gt;
      &lt;td&gt;批量任务挪到晚上或周末；关注 Search/Fetch 计费&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;团队&lt;/td&gt;
      &lt;td&gt;别名升级不需要改代码，但回归和预算需要人管&lt;/td&gt;
      &lt;td&gt;做别名映射、固定 prompt 回归、预算告警&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;四类人里，最该紧张的是把 model 名写进日志就以为完成了版本管理的人。&lt;strong&gt;名字相同，不代表输出相同。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;具体动作我建议按这个顺序：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;全局搜 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-v4-flash&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-v4-flash-vision-exp&lt;/code&gt;。还能用，但注释里要写清楚它已经退役，别再当旧模型依赖。&lt;/li&gt;
  &lt;li&gt;在调用层加一个字段，记录请求的 model 名、响应里的 model 字段、请求日期。如果响应里没有版本号，至少把日期和请求 ID 存下来。&lt;/li&gt;
  &lt;li&gt;准备 10–20 条固定 prompt，覆盖 JSON 输出、tool call、图片输入。升级后先跑一遍，别等用户告诉你。&lt;/li&gt;
  &lt;li&gt;批量任务改调度：北京时间 09:00–12:00、14:00–18:00 是高峰，挪到晚上或周末，价格减半。&lt;/li&gt;
  &lt;li&gt;如果 Search/Fetch 已经进流程，把 2026-10-31 16:00 UTC 写进预算表；免费期结束前重新算一遍每次任务的检索次数。&lt;/li&gt;
  &lt;li&gt;如果可复现性是硬要求，自托管 MIT 权重是一条路；但 484.6B 参数不是单机玩具，先算硬件账。不能自托管，就把关键输出缓存下来，至少能做对比。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;我的判断和另外三条值得看的更新&quot;&gt;我的判断，和另外三条值得看的更新&lt;/h2&gt;

&lt;p&gt;这次更新对绝大多数人是好消息：不用改代码，模型升级了，1M 上下文和 vision 还在，非高峰价格没涨。对需要可复现的团队，它是一个提醒：你 pin 的是名字，不是权重。&lt;/p&gt;

&lt;p&gt;我的建议很具体：把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; 名当 API 版本，把权重版本单独记。能在网关层做映射，就不要散落在业务代码里；能自托管，就用 MIT 权重做一条离线回归；不能自托管，至少把响应里的 model 字段、请求日期和一批固定 prompt 的输出存下来。&lt;/p&gt;

&lt;p&gt;什么证据会让我改口：如果 DeepSeek 公布可指定的日期快照（比如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deepseek-flash-20261017&lt;/code&gt;）并给出旧快照的下线时间表，或者我拿到 key 实测发现旧别名仍返回旧权重，我就把“别名不是版本”这句话收回一半。&lt;/p&gt;

&lt;p&gt;接下来盯两件事：2026-10-31 16:00 UTC 的 Search/Fetch 计费生效；以及独立评测里 1017 在你这类任务上的表现。厂商说的 Pareto improvement，我这边没有复现。&lt;/p&gt;

&lt;h3 id=&quot;flux-3-image-klein56b-也能在-8gb-显存上改图&quot;&gt;FLUX 3 Image Klein：5.6B 也能在 8GB 显存上改图&lt;/h3&gt;

&lt;p&gt;10 月 15 日，Black Forest Labs 发布 FLUX 3 Image Klein：5.6B 参数、开放权重，BF16 占 11.2GB、FP8 占 5.6GB，INT8 版本能在 8GB 显存的 RTX 上跑；BFL API 的蒸馏版是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$0.009 per 1K images&lt;/code&gt;。注意它用的是 FLUX Kommunity License，不是 Apache/MIT，商用前先读条款。所以呢：本地改图的门槛又降了一档，但“开放权重”不等于“随便用”。（&lt;a href=&quot;https://bfl.ai/blog/flux-3-image-klein&quot;&gt;BFL 原文&lt;/a&gt;，2026-10-15）&lt;/p&gt;

&lt;h3 id=&quot;openai-python-sdk-332--333区域驻留与图片安全标识&quot;&gt;OpenAI Python SDK 3.32 / 3.33：区域驻留与图片安全标识&lt;/h3&gt;

&lt;p&gt;OpenAI 的 Python SDK 在 10 月 16 日到 17 日连发 3.32.0 和 3.33.0：前者加 cache settings、prompt-based voice creation、区域驻留选项（枚举新增 JP/IN/AU 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STORAGE_PROCESSING&lt;/code&gt;），后者给图片请求和响应加 safety identifiers。所以呢：用 SDK 管项目缓存和区域驻留的团队会先看到字段；平台侧是否对所有组织开放，我没核到，changelog 是 403。（&lt;a href=&quot;https://github.com/openai/openai-python/releases/tag/v3.32.0&quot;&gt;3.32.0&lt;/a&gt; / &lt;a href=&quot;https://github.com/openai/openai-python/releases/tag/v3.33.0&quot;&gt;3.33.0&lt;/a&gt;）&lt;/p&gt;

&lt;h3 id=&quot;anthropic-python-sdk-1150messages-多了-speed-参数&quot;&gt;Anthropic Python SDK 1.15.0：Messages 多了 speed 参数&lt;/h3&gt;

&lt;p&gt;Anthropic 的 Python SDK 1.15.0 在 10 月 15 日发布：Messages 新增 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;speed&lt;/code&gt;（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;standard&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fast&lt;/code&gt;）和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;usage.speed&lt;/code&gt;，thinking types 新增 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;between_tools&lt;/code&gt;；文档备注部分模型要 beta header 才能用 speed。所以呢：多工具 thinking agent 可以显式选快档，但别默认所有模型都接。（&lt;a href=&quot;https://github.com/anthropics/anthropic-sdk-python/releases/tag/v1.15.0&quot;&gt;v1.15.0&lt;/a&gt;）&lt;/p&gt;

&lt;p&gt;模型名是给人看的，权重才是给机器跑的。名字不变的时候，更要把版本记下来。&lt;/p&gt;
</content>
    </entry>

</feed>
