<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>KazooTTT 声控烤箱</title><description>KazooTTT 的个人写作空间，记录技术、生活，以及那些还没有完全想明白的事情。</description><link>https://blog.kazoottt.top/</link><item><title>Quick PR Tool v1.0.17 - 新增 Back to Source Branch 功能</title><link>https://blog.kazoottt.top/posts/107/</link><guid isPermaLink="false">issue-107</guid><description>## 新功能:Back to Source Branch

从 merge 分支快速切换回源分支,解决冲突后一键返回。

🔗 **GitHub 仓库**: https://github.com/KazooTTT/quick-pr

### 安装与更新

```bash
npm install -g @kazoottt/quick-pr
# 或更新到最新版本
npm update -g @kazoottt/quick-pr
```

### 使用方式

**命令行直接使用:**
```bash
qkpr back
```

**主菜单选项:**
```bash
qkpr
# 选择 &quot;5. 🔙 Back to Source Branch&quot;
```

### 场景演示

#### 场景 1:单一匹配(最常见)

```bash
$ git branch
* merge/feat-M-12345-auth-to-main
  main
  feat/M-12345-auth

$ qkpr back
🔙  Back to Source Branch
  Merge branch: merge/feat-M-12345-auth-to-main
  Detected source: feat/M-12345-auth
  Detected target: main

🔄  Switching to source branch: feat/M-12345-auth
✅  Switched back to: feat/M-12345-auth
```

#### 场景 2:多个分支匹配

自动检测并提供选择菜单:

```bash
$ qkpr back
⚠️  Found 2 possible branch combinations:

? Which source branch do you want to switch to?
❯ feat/my-feature → main
  feat-my-feature → main
```

#### 场景 3:解析失败时

提供手动选择界面:

```bash
$ qkpr back
❌  Could not parse merge branch name: merge/invalid-name

? Select source branch (type to search):
❯ feat/login-page
  fix/bug-456
  chore/update-deps
```

#### 场景 4:本地没有源分支

支持从远程检出:

```bash
$ qkpr back
⚠️  Source branch &apos;feat/new-api&apos; is not available locally.

? What would you like to do?
❯ Checkout from remote (origin/feat/new-api)
  Select a different branch
  Cancel
```

### 版本信息

- **当前版本**: 1.0.17
- **更新日期**: 2026-09-21

### 项目地址

🌟 **欢迎 Star**: https://github.com/KazooTTT/quick-pr

🐛 **问题反馈**: https://github.com/KazooTTT/quick-pr/issues

---

*Quick PR Tool - Your All-in-One Git Workflow Assistant*
</description><pubDate>Tue, 22 Sep 2026 02:15:07 GMT</pubDate><content:encoded>&lt;h2&gt;新功能:Back to Source Branch&lt;/h2&gt;
&lt;p&gt;从 merge 分支快速切换回源分支,解决冲突后一键返回。&lt;/p&gt;
&lt;p&gt;🔗 &lt;strong&gt;GitHub 仓库&lt;/strong&gt;: &lt;a href=&quot;https://github.com/KazooTTT/quick-pr&quot;&gt;https://github.com/KazooTTT/quick-pr&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;安装与更新&lt;/h3&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -g&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; @kazoottt/quick-pr&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 或更新到最新版本&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; update&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -g&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; @kazoottt/quick-pr&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用方式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;命令行直接使用:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;qkpr&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; back&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主菜单选项:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;qkpr&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 选择 &quot;5. 🔙 Back to Source Branch&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;场景演示&lt;/h3&gt;
&lt;h4&gt;场景 1:单一匹配(最常见)&lt;/h4&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; git&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; merge/feat-M-12345-auth-to-main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  feat/M-12345-auth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; qkpr&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;🔙&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;  Back&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; Source&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; Branch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  Merge&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; merge/feat-M-12345-auth-to-main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  Detected&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; source:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; feat/M-12345-auth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  Detected&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; target:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;🔄&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;  Switching&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; source&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; feat/M-12345-auth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;✅&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;  Switched&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; back&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; to:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; feat/M-12345-auth&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;场景 2:多个分支匹配&lt;/h4&gt;
&lt;p&gt;自动检测并提供选择菜单:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; qkpr&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;⚠️&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;  Found&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; possible&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; combinations:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; Which source branch &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;do&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; you&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; want&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; switch&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; to?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;❯&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; feat/my-feature&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; →&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  feat-my-feature&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; →&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;场景 3:解析失败时&lt;/h4&gt;
&lt;p&gt;提供手动选择界面:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; qkpr&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;❌&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;  Could&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; parse&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; merge&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; name:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; merge/invalid-name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; Select source branch (&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; search&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;❯&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; feat/login-page&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  fix/bug-456&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  chore/update-deps&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;场景 4:本地没有源分支&lt;/h4&gt;
&lt;p&gt;支持从远程检出:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; qkpr&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;⚠️&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;  Source&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;feat/new-api&apos;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; is&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; available&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; locally.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; What would you like to do&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;❯&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; Checkout&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; remote&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (origin/feat/new-api)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  Select&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; a&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; different&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; branch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  Cancel&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;版本信息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当前版本&lt;/strong&gt;: 1.0.17&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新日期&lt;/strong&gt;: 2026-09-21&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;项目地址&lt;/h3&gt;
&lt;p&gt;🌟 &lt;strong&gt;欢迎 Star&lt;/strong&gt;: &lt;a href=&quot;https://github.com/KazooTTT/quick-pr&quot;&gt;https://github.com/KazooTTT/quick-pr&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;🐛 &lt;strong&gt;问题反馈&lt;/strong&gt;: &lt;a href=&quot;https://github.com/KazooTTT/quick-pr/issues&quot;&gt;https://github.com/KazooTTT/quick-pr/issues&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Quick PR Tool - Your All-in-One Git Workflow Assistant&lt;/em&gt;&lt;/p&gt;</content:encoded><updated>2026-09-21T09:39:03Z</updated><category>总结</category><category>工具</category><category>AI 写作为主</category></item><item><title>Tana 全局快捷键自定义失效：schema 键名与实际读取键名不一致</title><link>https://blog.kazoottt.top/posts/102/</link><guid isPermaLink="false">issue-102</guid><description>## 背景

Tana 桌面端（Electron 应用）有一个全局语音捕获功能，默认快捷键 `Cmd+Shift+E`，通过 Electron 的 `globalShortcut` 注册——**全局生效，Tana 在后台也会被触发**。这个组合键容易和滴答清单的主窗口唤起（`Cmd+Control+E`）误触，所以想把它禁用掉。

思路是：把快捷键改成一个物理上不存在的键（如 `F24`），等效于禁用，又比塞非法字符串干净（注册能成功，不产生错误日志）。

## 踩过的坑

直接改配置文件 `~/Library/Application Support/Tana Outliner/keyBindings.json`：

```json
{
	&quot;toggleVoiceCapture&quot;: &quot;CommandOrControl+Shift+F24&quot;
}
```

改完重启，`Cmd+Shift+E` **依然生效**。看起来像是「设置了 custom 不会禁用 default，两者并存」——但这是错觉。

## 真正的原因：键名不一致

读 Tana 的 `main.js`（版本 1.523.0）取证，发现两处键名对不上：

| 位置                          | 键名                                              |
| --------------------------- | ----------------------------------------------- |
| store schema 声明（`KRe`）      | `toggleVoiceCapture`（遗留名，全代码只出现一次，**没有任何地方读它**） |
| 命令列表（`dre`）里的 command       | `voiceCapture`                                  |
| 实际读取：`store.get(e.command)` | **`voiceCapture`**                              |

快捷键管理器的注册逻辑本身是**替换**，不是并存：

```js
bindAll() {
  this.globalShortcut.unregisterAll();            // 先全部注销
  for (let r of this.getKeyBindings()) {
    let n = r.customShortcut &amp;&amp; typeof r.customShortcut == &quot;string&quot;
            ? r.customShortcut : r.defaultShortcut;  // 有 custom 就用 custom
    this.globalShortcut.register(n, ...);          // 只注册一个
  }
}
```

但 `getKeyBindings()` 读的是 `store.get(&quot;voiceCapture&quot;)`，而我们写的是 `toggleVoiceCapture` → 读到 `undefined` → falsy → **静默回退到默认值 `Cmd+Shift+E`**。自定义配置被无声地忽略了。

本质上是 Tana 自己的一个 bug：schema 里的遗留键名和实际读取的 command 名不一致。

## 正确的写法

```json
{
	&quot;voiceCapture&quot;: &quot;CommandOrControl+Shift+F24&quot;,
	&quot;quickCapture&quot;: &quot;CommandOrControl+Shift+Space&quot;
}
```

Tana 的 GUI 入口（**Options → Customize Keybindings**）内部也是用 command 名写入的，所以这和 GUI 设置完全等效。

## 另外两个操作要点

1. **改配置前必须先退出 Tana**。Tana 用 electron-store 管理配置，App 退出时会把内存中的值写回文件；在它运行时改 JSON 会被静默覆盖。流程：备份 → 优雅退出 → 改配置 → 重启 → 读回验证。
2. **不要试图用空字符串「禁用」**。取值逻辑是 `customShortcut || defaultShortcut`，空串是 falsy，会静默回退默认值，你以为禁了其实没禁。要用合法但按不到的键（F24 在任何 Mac 键盘上都不存在，Electron accelerator 支持 F1–F24）。

## 替代方案：不一定要禁用全局捕获

Tana 还有个**非全局**的语音入口：`Cmd+Shift+L`（Quick chat with dictation），只在 Tana 前台生效。如果需要的只是「在 Tana 里语音输入」，禁用全局捕获后改用它就够了。代价是失去 Tana 语音捕获最值钱的能力——在别的 App 里（比如浏览器看资料时）不用切窗口直接说话记进 Tana。

## 一键禁用脚本

以下脚本可直接分享使用：自动退出 Tana → 备份 → 写入正确的键名（保留其他配置、清理无效的遗留键名）。已在 macOS 上验证。

```bash
#!/bin/bash
# 禁用 Tana 全局语音捕获快捷键（Cmd+Shift+E → 永不触发的 F24）
# 原理：Tana 读取 keyBindings.json 里的 &quot;voiceCapture&quot; 键作为自定义快捷键，
#       自定义存在时完全替换默认值。F24 在 Mac 键盘上不存在，等效禁用。
# 注意：schema 里的 &quot;toggleVoiceCapture&quot; 是无人读取的遗留键名，写了无效。
set -euo pipefail

KB=&quot;$HOME/Library/Application Support/Tana Outliner/keyBindings.json&quot;

# 1. 退出 Tana（electron-store 会在退出时把内存值写回文件，运行中修改会被覆盖）
if pgrep -f &quot;Tana Outliner&quot; &gt; /dev/null; then
  echo &quot;正在退出 Tana...&quot;
  osascript -e &apos;quit app &quot;Tana Outliner&quot;&apos; || true
  for _ in $(seq 1 10); do
    pgrep -f &quot;Tana Outliner&quot; &gt; /dev/null || break
    sleep 1
  done
  if pgrep -f &quot;Tana Outliner&quot; &gt; /dev/null; then
    echo &quot;错误：Tana 未能退出，请手动退出后重试&quot; &gt;&amp;2
    exit 1
  fi
fi

# 2. 备份现有配置
if [ -f &quot;$KB&quot; ]; then
  cp &quot;$KB&quot; &quot;$KB.bak.$(date +%Y%m%d-%H%M%S)&quot;
  echo &quot;已备份原配置&quot;
fi

# 3. 合并写入（保留其他键，清理无效的遗留键名）
python3 - &quot;$KB&quot; &lt;&lt; &apos;EOF&apos;
import json, os, sys
path = sys.argv[1]
data = {}
if os.path.exists(path):
    with open(path) as f:
        data = json.load(f)
removed = data.pop(&quot;toggleVoiceCapture&quot;, None)
data[&quot;voiceCapture&quot;] = &quot;CommandOrControl+Shift+F24&quot;
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, &quot;w&quot;) as f:
    json.dump(data, f, indent=&quot;\t&quot;, ensure_ascii=False)
    f.write(&quot;\n&quot;)
if removed:
    print(f&quot;已清理无效键 toggleVoiceCapture（原值: {removed}）&quot;)
print(&quot;已设置 voiceCapture = CommandOrControl+Shift+F24&quot;)
EOF

echo &quot;完成。启动 Tana 后，Cmd+Shift+E 将不再触发全局语音捕获。&quot;
echo &quot;回滚：Tana 菜单 → Options → Customize Keybindings → Voice capture 行点 × 清除自定义。&quot;
```

如果只想粘贴一行（需先手动退出 Tana）：

```bash
python3 -c &quot;import json,os; p=os.path.expanduser(&apos;~/Library/Application Support/Tana Outliner/keyBindings.json&apos;); d=json.load(open(p)) if os.path.exists(p) else {}; d.pop(&apos;toggleVoiceCapture&apos;,None); d[&apos;voiceCapture&apos;]=&apos;CommandOrControl+Shift+F24&apos;; os.makedirs(os.path.dirname(p),exist_ok=True); json.dump(d,open(p,&apos;w&apos;),indent=&apos;\t&apos;,ensure_ascii=False); print(&apos;已禁用，重启 Tana 生效&apos;)&quot;
```

## 经验总结

**逆向修改 Electron 应用的配置文件前，先确认代码实际 `get()` 的键名，而不是看 schema 声明。** schema 可能留有历史遗留键名，写错了不会报错，只会静默失效——排查时容易误判成「配置机制和预期不同」（这次就误判成了 custom/default 并存）。
</description><pubDate>Fri, 04 Sep 2026 07:14:10 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;Tana 桌面端（Electron 应用）有一个全局语音捕获功能，默认快捷键 &lt;code&gt;Cmd+Shift+E&lt;/code&gt;，通过 Electron 的 &lt;code&gt;globalShortcut&lt;/code&gt; 注册——&lt;strong&gt;全局生效，Tana 在后台也会被触发&lt;/strong&gt;。这个组合键容易和滴答清单的主窗口唤起（&lt;code&gt;Cmd+Control+E&lt;/code&gt;）误触，所以想把它禁用掉。&lt;/p&gt;
&lt;p&gt;思路是：把快捷键改成一个物理上不存在的键（如 &lt;code&gt;F24&lt;/code&gt;），等效于禁用，又比塞非法字符串干净（注册能成功，不产生错误日志）。&lt;/p&gt;
&lt;h2&gt;踩过的坑&lt;/h2&gt;
&lt;p&gt;直接改配置文件 &lt;code&gt;~/Library/Application Support/Tana Outliner/keyBindings.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;	&quot;toggleVoiceCapture&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;CommandOrControl+Shift+F24&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改完重启，&lt;code&gt;Cmd+Shift+E&lt;/code&gt; &lt;strong&gt;依然生效&lt;/strong&gt;。看起来像是「设置了 custom 不会禁用 default，两者并存」——但这是错觉。&lt;/p&gt;
&lt;h2&gt;真正的原因：键名不一致&lt;/h2&gt;
&lt;p&gt;读 Tana 的 &lt;code&gt;main.js&lt;/code&gt;（版本 1.523.0）取证，发现两处键名对不上：&lt;/p&gt;





















&lt;div class=&quot;table-scroll&quot;&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;store schema 声明（&lt;code&gt;KRe&lt;/code&gt;）&lt;/td&gt;&lt;td&gt;&lt;code&gt;toggleVoiceCapture&lt;/code&gt;（遗留名，全代码只出现一次，&lt;strong&gt;没有任何地方读它&lt;/strong&gt;）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;命令列表（&lt;code&gt;dre&lt;/code&gt;）里的 command&lt;/td&gt;&lt;td&gt;&lt;code&gt;voiceCapture&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;实际读取：&lt;code&gt;store.get(e.command)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&lt;code&gt;voiceCapture&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;快捷键管理器的注册逻辑本身是&lt;strong&gt;替换&lt;/strong&gt;，不是并存：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;bindAll&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.globalShortcut.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;unregisterAll&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;();            &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// 先全部注销&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; r &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;of&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;getKeyBindings&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;()) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    let&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; n &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; r.customShortcut &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; typeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; r.customShortcut &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;string&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;            ?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; r.customShortcut &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; r.defaultShortcut;  &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// 有 custom 就用 custom&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.globalShortcut.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;register&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(n, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);          &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// 只注册一个&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但 &lt;code&gt;getKeyBindings()&lt;/code&gt; 读的是 &lt;code&gt;store.get(&quot;voiceCapture&quot;)&lt;/code&gt;，而我们写的是 &lt;code&gt;toggleVoiceCapture&lt;/code&gt; → 读到 &lt;code&gt;undefined&lt;/code&gt; → falsy → &lt;strong&gt;静默回退到默认值 &lt;code&gt;Cmd+Shift+E&lt;/code&gt;&lt;/strong&gt;。自定义配置被无声地忽略了。&lt;/p&gt;
&lt;p&gt;本质上是 Tana 自己的一个 bug：schema 里的遗留键名和实际读取的 command 名不一致。&lt;/p&gt;
&lt;h2&gt;正确的写法&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;	&quot;voiceCapture&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;CommandOrControl+Shift+F24&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;	&quot;quickCapture&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;CommandOrControl+Shift+Space&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tana 的 GUI 入口（&lt;strong&gt;Options → Customize Keybindings&lt;/strong&gt;）内部也是用 command 名写入的，所以这和 GUI 设置完全等效。&lt;/p&gt;
&lt;h2&gt;另外两个操作要点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;改配置前必须先退出 Tana&lt;/strong&gt;。Tana 用 electron-store 管理配置，App 退出时会把内存中的值写回文件；在它运行时改 JSON 会被静默覆盖。流程：备份 → 优雅退出 → 改配置 → 重启 → 读回验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要试图用空字符串「禁用」&lt;/strong&gt;。取值逻辑是 &lt;code&gt;customShortcut || defaultShortcut&lt;/code&gt;，空串是 falsy，会静默回退默认值，你以为禁了其实没禁。要用合法但按不到的键（F24 在任何 Mac 键盘上都不存在，Electron accelerator 支持 F1–F24）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;替代方案：不一定要禁用全局捕获&lt;/h2&gt;
&lt;p&gt;Tana 还有个&lt;strong&gt;非全局&lt;/strong&gt;的语音入口：&lt;code&gt;Cmd+Shift+L&lt;/code&gt;（Quick chat with dictation），只在 Tana 前台生效。如果需要的只是「在 Tana 里语音输入」，禁用全局捕获后改用它就够了。代价是失去 Tana 语音捕获最值钱的能力——在别的 App 里（比如浏览器看资料时）不用切窗口直接说话记进 Tana。&lt;/p&gt;
&lt;h2&gt;一键禁用脚本&lt;/h2&gt;
&lt;p&gt;以下脚本可直接分享使用：自动退出 Tana → 备份 → 写入正确的键名（保留其他配置、清理无效的遗留键名）。已在 macOS 上验证。&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 禁用 Tana 全局语音捕获快捷键（Cmd+Shift+E → 永不触发的 F24）&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 原理：Tana 读取 keyBindings.json 里的 &quot;voiceCapture&quot; 键作为自定义快捷键，&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;#       自定义存在时完全替换默认值。F24 在 Mac 键盘上不存在，等效禁用。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 注意：schema 里的 &quot;toggleVoiceCapture&quot; 是无人读取的遗留键名，写了无效。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -euo&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; pipefail&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;KB&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;$HOME&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;/Library/Application Support/Tana Outliner/keyBindings.json&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 1. 退出 Tana（electron-store 会在退出时把内存值写回文件，运行中修改会被覆盖）&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; pgrep&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;Tana Outliner&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; /dev/null&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  echo&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;正在退出 Tana...&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  osascript&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -e&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;quit app &quot;Tana Outliner&quot;&apos;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; _ &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;seq&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;); &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;    pgrep&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;Tana Outliner&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; /dev/null&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;    sleep&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  done&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; pgrep&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;Tana Outliner&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; /dev/null&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    echo&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;错误：Tana 未能退出，请手动退出后重试&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &gt;&amp;#x26;2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    exit&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 2. 备份现有配置&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; [ &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-f&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;$KB&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ]; &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  cp&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;$KB&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;$KB&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;.bak.$(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; +%Y%m%d-%H%M%S)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  echo&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;已备份原配置&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 3. 合并写入（保留其他键，清理无效的遗留键名）&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;$KB&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; &apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;import json, os, sys&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;path = sys.argv[1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;data = {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;if os.path.exists(path):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    with open(path) as f:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;        data = json.load(f)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;removed = data.pop(&quot;toggleVoiceCapture&quot;, None)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;data[&quot;voiceCapture&quot;] = &quot;CommandOrControl+Shift+F24&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;os.makedirs(os.path.dirname(path), exist_ok=True)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;with open(path, &quot;w&quot;) as f:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    json.dump(data, f, indent=&quot;\t&quot;, ensure_ascii=False)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    f.write(&quot;\n&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;if removed:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    print(f&quot;已清理无效键 toggleVoiceCapture（原值: {removed}）&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;print(&quot;已设置 voiceCapture = CommandOrControl+Shift+F24&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;完成。启动 Tana 后，Cmd+Shift+E 将不再触发全局语音捕获。&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;回滚：Tana 菜单 → Options → Customize Keybindings → Voice capture 行点 × 清除自定义。&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只想粘贴一行（需先手动退出 Tana）：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;python3&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;import json,os; p=os.path.expanduser(&apos;~/Library/Application Support/Tana Outliner/keyBindings.json&apos;); d=json.load(open(p)) if os.path.exists(p) else {}; d.pop(&apos;toggleVoiceCapture&apos;,None); d[&apos;voiceCapture&apos;]=&apos;CommandOrControl+Shift+F24&apos;; os.makedirs(os.path.dirname(p),exist_ok=True); json.dump(d,open(p,&apos;w&apos;),indent=&apos;\t&apos;,ensure_ascii=False); print(&apos;已禁用，重启 Tana 生效&apos;)&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;经验总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;逆向修改 Electron 应用的配置文件前，先确认代码实际 &lt;code&gt;get()&lt;/code&gt; 的键名，而不是看 schema 声明。&lt;/strong&gt; schema 可能留有历史遗留键名，写错了不会报错，只会静默失效——排查时容易误判成「配置机制和预期不同」（这次就误判成了 custom/default 并存）。&lt;/p&gt;</content:encoded><updated>2026-09-04T07:14:08Z</updated><category>笔记</category><category>效率</category><category>mac</category><category>AI 写作为主</category></item><item><title>我给自己写的几个 Obsidian 插件</title><link>https://blog.kazoottt.top/posts/100/</link><guid isPermaLink="false">issue-100</guid><description>用 Obsidian 这几年，总有些小地方不顺手。社区插件找不到合适的，就自己上手写。攒到现在已经有 5 个了，其中 4 个最近整理并开源了出来。这篇文章做个汇总，主要也是怕自己忘了每个插件是干嘛的。

## KazooTTT Vault Timeline

&gt; 把整个 vault 的笔记变成一个类似 flomo / 微博的时间线。

- 仓库：[KazooTTT/obsidian-timeline](https://github.com/KazooTTT/obsidian-timeline)
- 插件 ID：`kazoottt-obsidian-timeline`

初衷是想要一个 flomo 那样的&quot;流水&quot;视角：所有笔记按创建或更新时间倒序排开，顶部就是一个编辑器，随手写一条就存成一条笔记（自动按时间命名，统一放进 `Timeline/` 文件夹）。编辑器直接复用了 Obsidian 原生的 Live Preview 内核，所以 `[[链接]]`、`#标签` 补全、粘贴图片这些体验和普通笔记一致。

还支持关键词和日期范围搜索、Excalidraw 笔记直接渲染画布预览、长文折叠、滚动加载。

![Vault Timeline 主视图](https://raw.githubusercontent.com/KazooTTT/obsidian-timeline/main/docs/screenshots/timeline-main-view.png)

一个教训：这个插件最初的 ID 是 `obsidian-timeline`，和社区市场里一个同名插件撞了 ID，结果某天被对方整个覆盖安装，时间线&quot;神秘消失&quot;。所以自己的插件 ID 一定要起得独一无二，最好带上个人前缀。

## Periodic Calendar Page

&gt; 主编辑区全屏的周期笔记日历，补齐官方 Calendar 插件不支持月报/季报/年报的短板。

- 仓库：[KazooTTT/obsidian-calendar-page](https://github.com/KazooTTT/obsidian-calendar-page)
- 插件 ID：`periodic-calendar-page`

官方 Calendar 插件只支持日记和周报，还只能待在侧边栏。这个插件把日历搬进主编辑区，完整支持 Periodic Notes 的日记、周报、月报、季报、年报五种周期，带日/年/多年三层缩放视图，以及一个按年统计写作量的&quot;记录统计&quot;页（条数、字数、记录天数的柱状图）。

![日历日视图](https://raw.githubusercontent.com/KazooTTT/obsidian-calendar-page/main/docs/screenshots/calendar-day-view.png)

![记录统计](https://raw.githubusercontent.com/KazooTTT/obsidian-calendar-page/main/docs/screenshots/stats-view.png)

## Obsidian Toolkit

&gt; 个人命令合集插件，以后零散的小命令都往这里塞。

- 仓库：[KazooTTT/obsidian-toolkit](https://github.com/KazooTTT/obsidian-toolkit)
- 插件 ID：`obsidian-toolkit`

目前只有一组功能：一键把当前笔记的标题大纲复制到剪贴板，支持两种格式——纯 Markdown 标题行，或者 `tree` 命令风格的层级树（根节点是文件名，跳级的标题会用 `(无标题)` 占位，方便发现层级问题）。写文章目录、贴 Issue、发给 AI 看结构都很好用。

定位是&quot;工具箱&quot;：以后再有小的自用命令需求，就继续加进这个插件，而不是每个都开新坑。

## Thino Linter Guard

&gt; 防止 Linter 在保存时污染 Thino 的虚拟编辑器。

- 仓库：[KazooTTT/thino-linter-guard](https://github.com/KazooTTT/thino-linter-guard)
- 插件 ID：`thino-linter-guard`

这是个小而具体的修复：在 Thino（Memos）视图里按 `Cmd+S` 时，Obsidian Linter 会误以为当前文件是 vault 根目录的 `index.md`，然后往里面塞 frontmatter。这个插件在 Thino 视图激活时让 Linter 跳过检查，保存时临时禁用再恢复。如果你也遇到 `index.md` 顶部莫名多出 YAML 的情况，就是它干的。

## 已停用：KazooTTT Blog Publisher

还有一个 `kazoottt-blog-publisher`，是把当前笔记一键发布到旧版博客（自建 API）的插件。博客迁移到现在的 GitHub Issues 方案后它就退役了，仓库保持私有，仅作纪念。现在从 Obsidian 发文章的流程是：写好笔记后用 `gh issue create` 发布为带 `blog:publish` 标签的 Issue——这篇文章就是这样发出来的。

## 小结

几个共同的经验：

- 插件 ID 加个人前缀，避免和社区插件撞车
- 源码、文档、BRAT release 分开放：README 写清楚&quot;这个插件是干嘛的&quot;，不然半年后自己都想不起来
- 小插件不可怕，解决一个具体问题就值得存在


</description><pubDate>Mon, 31 Aug 2026 06:00:21 GMT</pubDate><content:encoded>&lt;p&gt;用 Obsidian 这几年，总有些小地方不顺手。社区插件找不到合适的，就自己上手写。攒到现在已经有 5 个了，其中 4 个最近整理并开源了出来。这篇文章做个汇总，主要也是怕自己忘了每个插件是干嘛的。&lt;/p&gt;
&lt;h2&gt;KazooTTT Vault Timeline&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;把整个 vault 的笔记变成一个类似 flomo / 微博的时间线。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;仓库：&lt;a href=&quot;https://github.com/KazooTTT/obsidian-timeline&quot;&gt;KazooTTT/obsidian-timeline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;插件 ID：&lt;code&gt;kazoottt-obsidian-timeline&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;初衷是想要一个 flomo 那样的&quot;流水&quot;视角：所有笔记按创建或更新时间倒序排开，顶部就是一个编辑器，随手写一条就存成一条笔记（自动按时间命名，统一放进 &lt;code&gt;Timeline/&lt;/code&gt; 文件夹）。编辑器直接复用了 Obsidian 原生的 Live Preview 内核，所以 &lt;code&gt;[[链接]]&lt;/code&gt;、&lt;code&gt;#标签&lt;/code&gt; 补全、粘贴图片这些体验和普通笔记一致。&lt;/p&gt;
&lt;p&gt;还支持关键词和日期范围搜索、Excalidraw 笔记直接渲染画布预览、长文折叠、滚动加载。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/KazooTTT/obsidian-timeline/main/docs/screenshots/timeline-main-view.png&quot; alt=&quot;Vault Timeline 主视图&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个教训：这个插件最初的 ID 是 &lt;code&gt;obsidian-timeline&lt;/code&gt;，和社区市场里一个同名插件撞了 ID，结果某天被对方整个覆盖安装，时间线&quot;神秘消失&quot;。所以自己的插件 ID 一定要起得独一无二，最好带上个人前缀。&lt;/p&gt;
&lt;h2&gt;Periodic Calendar Page&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;主编辑区全屏的周期笔记日历，补齐官方 Calendar 插件不支持月报/季报/年报的短板。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;仓库：&lt;a href=&quot;https://github.com/KazooTTT/obsidian-calendar-page&quot;&gt;KazooTTT/obsidian-calendar-page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;插件 ID：&lt;code&gt;periodic-calendar-page&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方 Calendar 插件只支持日记和周报，还只能待在侧边栏。这个插件把日历搬进主编辑区，完整支持 Periodic Notes 的日记、周报、月报、季报、年报五种周期，带日/年/多年三层缩放视图，以及一个按年统计写作量的&quot;记录统计&quot;页（条数、字数、记录天数的柱状图）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/KazooTTT/obsidian-calendar-page/main/docs/screenshots/calendar-day-view.png&quot; alt=&quot;日历日视图&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/KazooTTT/obsidian-calendar-page/main/docs/screenshots/stats-view.png&quot; alt=&quot;记录统计&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Obsidian Toolkit&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;个人命令合集插件，以后零散的小命令都往这里塞。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;仓库：&lt;a href=&quot;https://github.com/KazooTTT/obsidian-toolkit&quot;&gt;KazooTTT/obsidian-toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;插件 ID：&lt;code&gt;obsidian-toolkit&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前只有一组功能：一键把当前笔记的标题大纲复制到剪贴板，支持两种格式——纯 Markdown 标题行，或者 &lt;code&gt;tree&lt;/code&gt; 命令风格的层级树（根节点是文件名，跳级的标题会用 &lt;code&gt;(无标题)&lt;/code&gt; 占位，方便发现层级问题）。写文章目录、贴 Issue、发给 AI 看结构都很好用。&lt;/p&gt;
&lt;p&gt;定位是&quot;工具箱&quot;：以后再有小的自用命令需求，就继续加进这个插件，而不是每个都开新坑。&lt;/p&gt;
&lt;h2&gt;Thino Linter Guard&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;防止 Linter 在保存时污染 Thino 的虚拟编辑器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;仓库：&lt;a href=&quot;https://github.com/KazooTTT/thino-linter-guard&quot;&gt;KazooTTT/thino-linter-guard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;插件 ID：&lt;code&gt;thino-linter-guard&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是个小而具体的修复：在 Thino（Memos）视图里按 &lt;code&gt;Cmd+S&lt;/code&gt; 时，Obsidian Linter 会误以为当前文件是 vault 根目录的 &lt;code&gt;index.md&lt;/code&gt;，然后往里面塞 frontmatter。这个插件在 Thino 视图激活时让 Linter 跳过检查，保存时临时禁用再恢复。如果你也遇到 &lt;code&gt;index.md&lt;/code&gt; 顶部莫名多出 YAML 的情况，就是它干的。&lt;/p&gt;
&lt;h2&gt;已停用：KazooTTT Blog Publisher&lt;/h2&gt;
&lt;p&gt;还有一个 &lt;code&gt;kazoottt-blog-publisher&lt;/code&gt;，是把当前笔记一键发布到旧版博客（自建 API）的插件。博客迁移到现在的 GitHub Issues 方案后它就退役了，仓库保持私有，仅作纪念。现在从 Obsidian 发文章的流程是：写好笔记后用 &lt;code&gt;gh issue create&lt;/code&gt; 发布为带 &lt;code&gt;blog:publish&lt;/code&gt; 标签的 Issue——这篇文章就是这样发出来的。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;几个共同的经验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插件 ID 加个人前缀，避免和社区插件撞车&lt;/li&gt;
&lt;li&gt;源码、文档、BRAT release 分开放：README 写清楚&quot;这个插件是干嘛的&quot;，不然半年后自己都想不起来&lt;/li&gt;
&lt;li&gt;小插件不可怕，解决一个具体问题就值得存在&lt;/li&gt;
&lt;/ul&gt;</content:encoded><updated>2026-08-31T06:09:07Z</updated><category>开源</category><category>笔记</category><category>obsidian</category><category>AI 写作为主</category></item><item><title>Rocket Loader：让 JavaScript 晚点运行，页面就一定更快吗？</title><link>https://blog.kazoottt.top/posts/86/</link><guid isPermaLink="false">issue-86</guid><description>最近博客里出现了一个很奇怪的问题：Mermaid 流程图没有渲染，页面直接显示了图表源码。

一开始我怀疑过 Mermaid 语法、前端构建产物和浏览器缓存。但检查线上 DOM 后发现，页面里能够找到 `.mermaid-diagram` 容器，却没有生成任何 SVG，也没有进入渲染失败的回退状态。这意味着问题甚至还没有走到 Mermaid 的解析阶段——负责初始化 Mermaid 的 JavaScript 根本没有正常执行。

继续检查浏览器实际收到的 HTML，才发现 Cloudflare Rocket Loader 改写了 Astro 生成的模块脚本。关闭 Rocket Loader 后，两张流程图立即恢复，DOM 中重新出现了两个 SVG。

这次故障让我重新认识了 Rocket Loader：它不是一个单纯的“JavaScript 加速开关”，而是一套会改变脚本加载时序的优化机制。

## Rocket Loader 是做什么的？

Rocket Loader 是 Cloudflare 提供的一项前端性能优化功能。

它要解决的问题是：浏览器解析 HTML 时，普通 JavaScript 脚本可能会阻塞后续内容的解析和渲染。如果页面引用了很多脚本，或者某个第三方脚本响应很慢，用户可能需要盯着空白页面等待。

Rocket Loader 的策略是让内容先出现，再执行 JavaScript。

按照 Cloudflare 的说明，它会延迟页面中的 JavaScript，让文字、图片和字体等内容优先渲染，同时异步处理内联脚本与外部脚本，并尽量维持原来的执行顺序。

因此，它优化的主要是“用户什么时候能看到内容”，而不是 JavaScript 本身的运行速度。

可以把这种取舍简单理解为：

```text
原来的页面加载

解析 HTML
  → 遇到 JavaScript
  → 下载并执行
  → 继续解析 HTML
  → 显示页面

启用 Rocket Loader 后

解析 HTML
  → 暂时推迟 JavaScript
  → 优先显示页面内容
  → 恢复并执行 JavaScript
```

用户可能更早看到正文，但依赖 JavaScript 的按钮、图表和其他交互要稍后才能使用。

## 它是如何实现的？

Rocket Loader 同时依赖 Cloudflare 边缘节点和浏览器端运行时代码。

请求经过 Cloudflare 时，Cloudflare 会在边缘修改返回给浏览器的 HTML。它暂时把原有脚本处理成浏览器不会立即执行的形式，让浏览器先继续解析页面。随后，Cloudflare 注入的 Rocket Loader 运行时再恢复这些脚本，并模拟普通脚本、`async` 和 `defer` 等不同执行方式。

整个过程可以概括为：

```text
源站返回 HTML
  ↓
Cloudflare 在边缘改写 script
  ↓
浏览器优先解析和绘制内容
  ↓
Rocket Loader 恢复脚本
  ↓
JavaScript 开始执行
```

这里最重要的一点是：Rocket Loader 会接管原本由浏览器负责的脚本加载时序。

这也是它既能改善首屏速度，又可能造成兼容问题的原因。

## 它和 `async`、`defer` 有什么区别？

`async` 和 `defer` 是浏览器原生支持的脚本属性。

- `async` 会并行下载脚本，下载完成后尽快执行，多个脚本之间不保证执行顺序。
- `defer` 会并行下载脚本，等 HTML 解析完成后按照文档顺序执行。
- Rocket Loader 会在 Cloudflare 边缘改写页面，然后通过自己的运行时延迟并调度多个脚本。

如果能够修改应用代码，通常应该先使用浏览器原生机制、ES modules、代码拆分和按需加载。它们的行为更明确，也更容易由构建工具和测试覆盖。

Rocket Loader 更像是站点之外的一层统一优化：不改源代码，也能尝试推迟大量脚本。但多了一层调度，就多了一层需要验证的行为。

## 哪些场景适合使用？

Rocket Loader 比较适合内容优先、交互较轻的传统网站，例如：

- 正文阅读是页面的主要目的；
- 页面包含多个同步或第三方脚本；
- 慢速脚本明显阻塞了首屏内容；
- 暂时无法调整源站模板或构建流程；
- 允许正文先显示、交互稍后可用；
- 有性能监控和功能回归测试。

例如，一个资讯站的正文被广告、统计和推荐脚本挡住时，Rocket Loader 可能让读者更快看到文章。

不过，“可能更快”必须通过数据证明。开启前后至少应该比较 FCP、LCP、TBT 或 INP，同时确认按钮、表单、埋点和第三方组件仍然正常。

## 哪些场景不适合？

如果 JavaScript 本身就是产品的核心，而不只是页面增强，使用 Rocket Loader 就需要格外谨慎。

风险较高的场景包括：

- React、Vue、Astro 等由构建工具管理模块加载和 hydration 的应用；
- 后台管理系统、编辑器和实时协作应用；
- 登录、支付、下单等关键流程；
- 依赖严格执行顺序或生命周期事件的脚本；
- 使用严格 CSP、SRI 或复杂第三方依赖的页面；
- 已经使用 ES modules、`defer`、代码拆分和按需加载的站点；
- 缺少端到端测试，无法及时发现静默失效。

这类页面即使 HTML 和 CSS 正常显示，也可能只有局部功能失效。相比直接白屏，这种“看起来没问题”的故障反而更难发现。

## 为什么它影响了 Astro 和 Mermaid？

Astro 会把组件脚本构建成 `type=&quot;module&quot;`，并负责 npm 依赖打包、TypeScript 转换、去重和代码拆分。

Mermaid 的初始化链路大致是：

```text
浏览器执行 Astro 生成的模块入口
  → 入口动态加载 Mermaid
  → Mermaid 读取页面中的图表源码
  → 生成 SVG
```

这次故障中，HTML 和 CSS 都正常返回，Mermaid 的源码容器也存在，但模块入口没有成功完成初始化。于是后面的动态加载和 SVG 生成都没有发生。

最终表现就是：读者看到的不是流程图，而是一整段 `graph LR` 源码。

关闭 Rocket Loader 后，Astro 模块恢复由浏览器原生机制加载，两张 Mermaid 图都成功生成了 SVG。

## 出现问题时如何排查？

第一步不是立即改代码，而是做开关对照。

1. 找到一个能够稳定复现问题的 URL。
2. 记录当前页面的 DOM、Console 和 Network 状态。
3. 临时关闭 Rocket Loader。
4. 使用无痕窗口或清理缓存后重新访问。
5. 比较入口脚本、依赖请求和最终 DOM。

还可以直接检查线上 HTML：

```bash
curl -fsS https://example.com/ \
  | grep -E &apos;rocket-loader|data-cf-settings|&lt;script&apos;
```

浏览器里则应检查：

- JavaScript 入口有没有发出请求；
- 入口的动态依赖有没有继续加载；
- Console 是否出现 CSP、MIME 或模块加载错误；
- 目标组件有没有完成初始化；
- 最终结果是否真的存在，而不是只看 HTTP 200。

对于 Mermaid，最直接的验收条件就是：

```js
document.querySelectorAll(&quot;.mermaid-diagram svg&quot;).length &gt; 0
```

构建成功只能证明产物生成了，不能证明经过 CDN 改写后仍能在真实浏览器中运行。

## 如果只想排除部分脚本怎么办？

Cloudflare 支持使用 `data-cfasync=&quot;false&quot;` 排除单个脚本：

```html
&lt;script data-cfasync=&quot;false&quot; src=&quot;/javascript.js&quot;&gt;&lt;/script&gt;
```

但它有几个限制：

- 属性必须直接存在于初始 HTML 中；
- `data-cfasync` 必须写在 `src` 前面；
- 如果脚本依赖其他脚本，依赖链也需要一并排除；
- 其他脚本仍然会被 Rocket Loader 接管。

对于 Astro 还多一层问题：Astro 默认只处理没有额外属性的组件脚本。直接添加 `data-cfasync` 可能让脚本退出 Astro 的打包和 TypeScript 处理管线。

因此，现代框架项目出现全局兼容问题时，直接关闭 Rocket Loader，或者使用 Cloudflare Configuration Rule 按主机名和路径关闭，通常比逐个排除构建产物更可靠。

## 是否应该开启 Rocket Loader？

可以用下面这组问题判断：

```text
首屏是否真的被传统同步脚本阻塞？
  ├─ 否 → 通常不需要 Rocket Loader
  └─ 是
      ↓
页面是否以内容阅读为主？
  ├─ 否 → 优先在应用中优化脚本
  └─ 是
      ↓
是否允许交互稍后可用？
  ├─ 否 → 不适合
  └─ 是
      ↓
是否有性能指标和功能回归测试？
  ├─ 否 → 先建立验证基线
  └─ 是 → 小范围开启并比较结果
```

Rocket Loader 不是一个“开启就会更快”的按钮。它实际上是在用脚本执行时序的变化，交换更早的内容绘制。

如果站点主要依赖 HTML 和 CSS，JavaScript 只是锦上添花，这笔交换可能值得。如果页面由模块脚本驱动，或者 JavaScript 承担核心功能，那么框架、浏览器原生加载机制和应用自身的性能优化通常更可控。

## 最后

这次 Mermaid 故障提醒了我：线上页面并不只由仓库里的代码决定。

代码经过构建、部署、CDN 缓存和边缘优化后，浏览器真正收到的内容可能已经发生变化。排查前端问题时，不能只看源代码和构建结果，还要观察最终 HTML、网络请求和真实 DOM。

性能优化也不应该以“某个开关是否开启”为目标。真正的目标应该是：

&gt; 用户更早看到有用内容，同时页面仍然完整可用。

如果速度提升的代价是流程图、按钮或关键交互静默失效，那么它就不是优化。

## 参考资料

- [Cloudflare：Rocket Loader](https://developers.cloudflare.com/speed/optimization/content/rocket-loader/)
- [Cloudflare：启用或关闭 Rocket Loader](https://developers.cloudflare.com/speed/optimization/content/rocket-loader/enable/)
- [Cloudflare：让 Rocket Loader 忽略指定脚本](https://developers.cloudflare.com/speed/optimization/content/rocket-loader/ignore-javascripts/)
- [Cloudflare：Configuration Rules 中的 Rocket Loader 设置](https://developers.cloudflare.com/rules/configuration-rules/settings/#rocket-loader)
- [Cloudflare：Rocket Loader 重构实现说明](https://blog.cloudflare.com/too-old-to-rocket-load-too-young-to-die/)
- [Astro：客户端脚本处理](https://docs.astro.build/zh-cn/guides/client-side-scripts/)
</description><pubDate>Fri, 31 Jul 2026 06:33:09 GMT</pubDate><content:encoded>&lt;p&gt;最近博客里出现了一个很奇怪的问题：Mermaid 流程图没有渲染，页面直接显示了图表源码。&lt;/p&gt;
&lt;p&gt;一开始我怀疑过 Mermaid 语法、前端构建产物和浏览器缓存。但检查线上 DOM 后发现，页面里能够找到 &lt;code&gt;.mermaid-diagram&lt;/code&gt; 容器，却没有生成任何 SVG，也没有进入渲染失败的回退状态。这意味着问题甚至还没有走到 Mermaid 的解析阶段——负责初始化 Mermaid 的 JavaScript 根本没有正常执行。&lt;/p&gt;
&lt;p&gt;继续检查浏览器实际收到的 HTML，才发现 Cloudflare Rocket Loader 改写了 Astro 生成的模块脚本。关闭 Rocket Loader 后，两张流程图立即恢复，DOM 中重新出现了两个 SVG。&lt;/p&gt;
&lt;p&gt;这次故障让我重新认识了 Rocket Loader：它不是一个单纯的“JavaScript 加速开关”，而是一套会改变脚本加载时序的优化机制。&lt;/p&gt;
&lt;h2&gt;Rocket Loader 是做什么的？&lt;/h2&gt;
&lt;p&gt;Rocket Loader 是 Cloudflare 提供的一项前端性能优化功能。&lt;/p&gt;
&lt;p&gt;它要解决的问题是：浏览器解析 HTML 时，普通 JavaScript 脚本可能会阻塞后续内容的解析和渲染。如果页面引用了很多脚本，或者某个第三方脚本响应很慢，用户可能需要盯着空白页面等待。&lt;/p&gt;
&lt;p&gt;Rocket Loader 的策略是让内容先出现，再执行 JavaScript。&lt;/p&gt;
&lt;p&gt;按照 Cloudflare 的说明，它会延迟页面中的 JavaScript，让文字、图片和字体等内容优先渲染，同时异步处理内联脚本与外部脚本，并尽量维持原来的执行顺序。&lt;/p&gt;
&lt;p&gt;因此，它优化的主要是“用户什么时候能看到内容”，而不是 JavaScript 本身的运行速度。&lt;/p&gt;
&lt;p&gt;可以把这种取舍简单理解为：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;原来的页面加载&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;解析 HTML&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 遇到 JavaScript&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 下载并执行&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 继续解析 HTML&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 显示页面&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;启用 Rocket Loader 后&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;解析 HTML&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 暂时推迟 JavaScript&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 优先显示页面内容&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 恢复并执行 JavaScript&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户可能更早看到正文，但依赖 JavaScript 的按钮、图表和其他交互要稍后才能使用。&lt;/p&gt;
&lt;h2&gt;它是如何实现的？&lt;/h2&gt;
&lt;p&gt;Rocket Loader 同时依赖 Cloudflare 边缘节点和浏览器端运行时代码。&lt;/p&gt;
&lt;p&gt;请求经过 Cloudflare 时，Cloudflare 会在边缘修改返回给浏览器的 HTML。它暂时把原有脚本处理成浏览器不会立即执行的形式，让浏览器先继续解析页面。随后，Cloudflare 注入的 Rocket Loader 运行时再恢复这些脚本，并模拟普通脚本、&lt;code&gt;async&lt;/code&gt; 和 &lt;code&gt;defer&lt;/code&gt; 等不同执行方式。&lt;/p&gt;
&lt;p&gt;整个过程可以概括为：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;源站返回 HTML&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Cloudflare 在边缘改写 script&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;浏览器优先解析和绘制内容&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Rocket Loader 恢复脚本&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;JavaScript 开始执行&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最重要的一点是：Rocket Loader 会接管原本由浏览器负责的脚本加载时序。&lt;/p&gt;
&lt;p&gt;这也是它既能改善首屏速度，又可能造成兼容问题的原因。&lt;/p&gt;
&lt;h2&gt;它和 &lt;code&gt;async&lt;/code&gt;、&lt;code&gt;defer&lt;/code&gt; 有什么区别？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 和 &lt;code&gt;defer&lt;/code&gt; 是浏览器原生支持的脚本属性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async&lt;/code&gt; 会并行下载脚本，下载完成后尽快执行，多个脚本之间不保证执行顺序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defer&lt;/code&gt; 会并行下载脚本，等 HTML 解析完成后按照文档顺序执行。&lt;/li&gt;
&lt;li&gt;Rocket Loader 会在 Cloudflare 边缘改写页面，然后通过自己的运行时延迟并调度多个脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果能够修改应用代码，通常应该先使用浏览器原生机制、ES modules、代码拆分和按需加载。它们的行为更明确，也更容易由构建工具和测试覆盖。&lt;/p&gt;
&lt;p&gt;Rocket Loader 更像是站点之外的一层统一优化：不改源代码，也能尝试推迟大量脚本。但多了一层调度，就多了一层需要验证的行为。&lt;/p&gt;
&lt;h2&gt;哪些场景适合使用？&lt;/h2&gt;
&lt;p&gt;Rocket Loader 比较适合内容优先、交互较轻的传统网站，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正文阅读是页面的主要目的；&lt;/li&gt;
&lt;li&gt;页面包含多个同步或第三方脚本；&lt;/li&gt;
&lt;li&gt;慢速脚本明显阻塞了首屏内容；&lt;/li&gt;
&lt;li&gt;暂时无法调整源站模板或构建流程；&lt;/li&gt;
&lt;li&gt;允许正文先显示、交互稍后可用；&lt;/li&gt;
&lt;li&gt;有性能监控和功能回归测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，一个资讯站的正文被广告、统计和推荐脚本挡住时，Rocket Loader 可能让读者更快看到文章。&lt;/p&gt;
&lt;p&gt;不过，“可能更快”必须通过数据证明。开启前后至少应该比较 FCP、LCP、TBT 或 INP，同时确认按钮、表单、埋点和第三方组件仍然正常。&lt;/p&gt;
&lt;h2&gt;哪些场景不适合？&lt;/h2&gt;
&lt;p&gt;如果 JavaScript 本身就是产品的核心，而不只是页面增强，使用 Rocket Loader 就需要格外谨慎。&lt;/p&gt;
&lt;p&gt;风险较高的场景包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React、Vue、Astro 等由构建工具管理模块加载和 hydration 的应用；&lt;/li&gt;
&lt;li&gt;后台管理系统、编辑器和实时协作应用；&lt;/li&gt;
&lt;li&gt;登录、支付、下单等关键流程；&lt;/li&gt;
&lt;li&gt;依赖严格执行顺序或生命周期事件的脚本；&lt;/li&gt;
&lt;li&gt;使用严格 CSP、SRI 或复杂第三方依赖的页面；&lt;/li&gt;
&lt;li&gt;已经使用 ES modules、&lt;code&gt;defer&lt;/code&gt;、代码拆分和按需加载的站点；&lt;/li&gt;
&lt;li&gt;缺少端到端测试，无法及时发现静默失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这类页面即使 HTML 和 CSS 正常显示，也可能只有局部功能失效。相比直接白屏，这种“看起来没问题”的故障反而更难发现。&lt;/p&gt;
&lt;h2&gt;为什么它影响了 Astro 和 Mermaid？&lt;/h2&gt;
&lt;p&gt;Astro 会把组件脚本构建成 &lt;code&gt;type=&quot;module&quot;&lt;/code&gt;，并负责 npm 依赖打包、TypeScript 转换、去重和代码拆分。&lt;/p&gt;
&lt;p&gt;Mermaid 的初始化链路大致是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;浏览器执行 Astro 生成的模块入口&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 入口动态加载 Mermaid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → Mermaid 读取页面中的图表源码&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 生成 SVG&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次故障中，HTML 和 CSS 都正常返回，Mermaid 的源码容器也存在，但模块入口没有成功完成初始化。于是后面的动态加载和 SVG 生成都没有发生。&lt;/p&gt;
&lt;p&gt;最终表现就是：读者看到的不是流程图，而是一整段 &lt;code&gt;graph LR&lt;/code&gt; 源码。&lt;/p&gt;
&lt;p&gt;关闭 Rocket Loader 后，Astro 模块恢复由浏览器原生机制加载，两张 Mermaid 图都成功生成了 SVG。&lt;/p&gt;
&lt;h2&gt;出现问题时如何排查？&lt;/h2&gt;
&lt;p&gt;第一步不是立即改代码，而是做开关对照。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到一个能够稳定复现问题的 URL。&lt;/li&gt;
&lt;li&gt;记录当前页面的 DOM、Console 和 Network 状态。&lt;/li&gt;
&lt;li&gt;临时关闭 Rocket Loader。&lt;/li&gt;
&lt;li&gt;使用无痕窗口或清理缓存后重新访问。&lt;/li&gt;
&lt;li&gt;比较入口脚本、依赖请求和最终 DOM。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;还可以直接检查线上 HTML：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -fsS&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; https://example.com/&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  |&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; grep&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -E&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;rocket-loader|data-cf-settings|&amp;#x3C;script&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器里则应检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript 入口有没有发出请求；&lt;/li&gt;
&lt;li&gt;入口的动态依赖有没有继续加载；&lt;/li&gt;
&lt;li&gt;Console 是否出现 CSP、MIME 或模块加载错误；&lt;/li&gt;
&lt;li&gt;目标组件有没有完成初始化；&lt;/li&gt;
&lt;li&gt;最终结果是否真的存在，而不是只看 HTTP 200。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 Mermaid，最直接的验收条件就是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;document.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;.mermaid-diagram svg&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建成功只能证明产物生成了，不能证明经过 CDN 改写后仍能在真实浏览器中运行。&lt;/p&gt;
&lt;h2&gt;如果只想排除部分脚本怎么办？&lt;/h2&gt;
&lt;p&gt;Cloudflare 支持使用 &lt;code&gt;data-cfasync=&quot;false&quot;&lt;/code&gt; 排除单个脚本：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;HTML&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; data-cfasync&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;false&quot;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;/javascript.js&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但它有几个限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性必须直接存在于初始 HTML 中；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-cfasync&lt;/code&gt; 必须写在 &lt;code&gt;src&lt;/code&gt; 前面；&lt;/li&gt;
&lt;li&gt;如果脚本依赖其他脚本，依赖链也需要一并排除；&lt;/li&gt;
&lt;li&gt;其他脚本仍然会被 Rocket Loader 接管。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 Astro 还多一层问题：Astro 默认只处理没有额外属性的组件脚本。直接添加 &lt;code&gt;data-cfasync&lt;/code&gt; 可能让脚本退出 Astro 的打包和 TypeScript 处理管线。&lt;/p&gt;
&lt;p&gt;因此，现代框架项目出现全局兼容问题时，直接关闭 Rocket Loader，或者使用 Cloudflare Configuration Rule 按主机名和路径关闭，通常比逐个排除构建产物更可靠。&lt;/p&gt;
&lt;h2&gt;是否应该开启 Rocket Loader？&lt;/h2&gt;
&lt;p&gt;可以用下面这组问题判断：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;首屏是否真的被传统同步脚本阻塞？&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├─ 否 → 通常不需要 Rocket Loader&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └─ 是&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;页面是否以内容阅读为主？&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├─ 否 → 优先在应用中优化脚本&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └─ 是&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;是否允许交互稍后可用？&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├─ 否 → 不适合&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └─ 是&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;是否有性能指标和功能回归测试？&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├─ 否 → 先建立验证基线&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └─ 是 → 小范围开启并比较结果&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rocket Loader 不是一个“开启就会更快”的按钮。它实际上是在用脚本执行时序的变化，交换更早的内容绘制。&lt;/p&gt;
&lt;p&gt;如果站点主要依赖 HTML 和 CSS，JavaScript 只是锦上添花，这笔交换可能值得。如果页面由模块脚本驱动，或者 JavaScript 承担核心功能，那么框架、浏览器原生加载机制和应用自身的性能优化通常更可控。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;这次 Mermaid 故障提醒了我：线上页面并不只由仓库里的代码决定。&lt;/p&gt;
&lt;p&gt;代码经过构建、部署、CDN 缓存和边缘优化后，浏览器真正收到的内容可能已经发生变化。排查前端问题时，不能只看源代码和构建结果，还要观察最终 HTML、网络请求和真实 DOM。&lt;/p&gt;
&lt;p&gt;性能优化也不应该以“某个开关是否开启”为目标。真正的目标应该是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用户更早看到有用内容，同时页面仍然完整可用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果速度提升的代价是流程图、按钮或关键交互静默失效，那么它就不是优化。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/rocket-loader/&quot;&gt;Cloudflare：Rocket Loader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/rocket-loader/enable/&quot;&gt;Cloudflare：启用或关闭 Rocket Loader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/speed/optimization/content/rocket-loader/ignore-javascripts/&quot;&gt;Cloudflare：让 Rocket Loader 忽略指定脚本&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/rules/configuration-rules/settings/#rocket-loader&quot;&gt;Cloudflare：Configuration Rules 中的 Rocket Loader 设置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/too-old-to-rocket-load-too-young-to-die/&quot;&gt;Cloudflare：Rocket Loader 重构实现说明&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/zh-cn/guides/client-side-scripts/&quot;&gt;Astro：客户端脚本处理&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><updated>2026-07-31T06:33:07Z</updated><category>javascript</category><category>笔记</category><category>前端</category><category>博客开发</category><category>cloudflare</category><category>astro</category><category>AI 写作为主</category></item><item><title>异步请求的顺序，应该由数据依赖决定</title><link>https://blog.kazoottt.top/posts/85/</link><guid isPermaLink="false">issue-85</guid><description>假设一个页面布局编辑器需要加载两类数据：

1. 服务端保存的页面详情，例如当前使用双栏布局。
2. 不同布局对应的预设配置，例如各区域的默认宽度和间距。

新建页面没有历史数据，可以选择一种基础布局完成初始化。为了方便讨论，假设默认使用单栏布局。

编辑页面刚挂载时，服务端详情还没有返回。此时表单为空，于是页面按照新建逻辑，请求了单栏布局的默认配置。

随后，服务端详情返回，表单被回填为双栏布局。到这里，页面状态仍然完全正确。

但先前发出的单栏配置请求仍在等待响应。它随后返回，又把单栏配置写入表单，覆盖了刚刚回填的双栏配置。

两次请求都成功返回了，数据也没有错，为什么页面最后显示的结果却不稳定？

## 请求顺序不等于生效顺序

理解这个问题，需要区分三种顺序：

- **发起顺序**：哪个请求先发送。
- **返回顺序**：哪个请求的响应先返回。
- **生效顺序**：响应返回后，哪个结果先后写入页面状态。

```mermaid
sequenceDiagram
    autonumber
    participant Page as 编辑页面
    participant Form as 表单状态
    participant Detail as 详情接口
    participant Config as 布局配置接口

    Page-&gt;&gt;Detail: 请求服务端详情
    Page-&gt;&gt;Form: 读取当前布局
    Form--&gt;&gt;Page: 当前为空
    Page-&gt;&gt;Config: 请求单栏默认配置

    Detail--&gt;&gt;Page: 返回已保存的双栏布局
    Page-&gt;&gt;Form: 写入双栏配置
    Note over Form: 当前状态正确

    Config--&gt;&gt;Page: 返回单栏默认配置
    Page-&gt;&gt;Form: 写入单栏配置
    Note over Form: 双栏配置被覆盖
```

这张图中，详情接口先写入双栏配置，布局配置接口随后又写入单栏配置。表单不会判断哪份数据更适合当前页面，只会保留最后一次写入的值。

因此，如果代码在每次响应返回后都直接更新表单，最终结果就取决于哪个响应更晚写入。

## 一次异步交错是怎样发生的

这里的两个请求访问的是不同接口。详情接口读取已保存的页面，布局配置接口读取单栏布局的默认参数。

它们互不冲突，却都会修改同一个表单：

- 详情接口返回后，把表单设置为已保存的双栏布局。
- 布局配置接口返回后，把默认的单栏参数写入表单。

问题就发生在这里。布局配置请求发出时，表单虽然为空，但这并不代表页面没有已保存的布局。

此时详情接口仍在请求中，前端只是暂时不知道布局是什么。只有确认这是新建页面，或者服务端确实没有保存布局，加载默认值的条件才成立。

代码却把“暂时还没拿到布局”当成了“布局为空”，提前请求了单栏默认配置。等到这个请求返回时，详情接口已经把双栏配置写入表单，于是默认值又覆盖了服务端数据。

所以，排查竞态不能只看两个请求谁快谁慢，还要看它们之间是否存在依赖。

## 请求返回时，发起条件可能已经变化

布局配置接口返回的单栏参数并没有错。问题在于，页面已经不再需要这份参数。

页面请求单栏配置时，代码依据的是这个判断：

&gt; 表单中的布局字段为空，所以请用单栏配置初始化。

这个判断混淆了两种状态：布局确实为空，以及布局尚未加载完成。等到详情接口返回双栏布局，这个区别才显现出来。

此时单栏配置本身仍然正确，但它从一开始就不适用于这个已有数据的编辑页面。

这也是为什么只查看接口日志很难发现问题。日志会显示两个请求都成功，参数和响应也都正常。真正出错的环节，是响应返回后对表单的写入。

请求成功只说明拿到了结果。这个结果能否用于当前页面，取决于发起它所需的条件是否已经确定。

## 有依赖的数据，应当按依赖顺序加载

页面详情和布局配置并不是两个互不相关的初始化任务。

布局配置请求需要一个确定的布局，而这个布局来自页面详情。只有详情返回后，程序才能知道应该加载双栏配置，还是在确认没有已保存布局时使用单栏默认配置。

它们的依赖关系是：

```text
页面详情
  → 确定当前布局
    → 加载该布局的配置
      → 写入表单
```

既然下一步依赖上一步的结果，代码也应该按照这个顺序执行：

```ts
async function initializePage() {
  const detail = await fetchPageDetail();
  const layout = detail?.layout ?? DEFAULT_LAYOUT;
  const layoutConfig = await fetchLayoutConfig(layout);

  form.setFieldsValue({
    ...layoutConfig.defaults,
    ...detail,
    layout,
  });
}
```

这段流程先等待页面详情，再从详情中确定布局，最后请求对应的布局配置。写入时默认配置在前、服务端详情在后，已有数据不会被默认值覆盖。

表单只在这些数据准备完成后统一写入，不需要靠两个请求的返回顺序决定最终结果。

问题代码通常把这条链拆散了：一个 `effect` 请求页面详情，另一个 `effect` 发现布局字段为空，便请求默认配置。两个 `effect` 各自更新表单，却没有表达“布局配置依赖页面详情”这层关系。

多个 `effect` 本身不是问题。互不依赖的数据可以并行加载；但如果后一个请求的参数或执行条件来自前一个请求，就应该显式串联，而不是让它们从共享状态中猜测对方是否已经完成。

这里也不能简单地把“表单为空”当作使用默认值的条件。编辑页面加载过程中，空值可能只表示详情尚未返回。默认值应该在详情加载完成，并确认没有已保存布局之后才参与初始化。

## 总结

这个问题表面上是默认值覆盖了服务端数据，实际原因却是代码没有表达数据之间的依赖关系。

页面详情决定布局，布局决定加载哪份配置。只要这条关系被拆到多个独立的异步流程中，最终状态就可能取决于响应返回顺序。

解决问题的重点不是让某个请求更快，也不是调整 `effect` 的先后，而是让代码中的执行顺序与数据依赖顺序一致：

```text
先拿到依赖的数据
→ 再确定下一步请求
→ 最后统一写入状态
```

请求发起顺序不等于结果生效顺序。只有把依赖关系写进异步流程，页面状态才不需要依靠最后一次 `set` 碰运气。</description><pubDate>Mon, 27 Jul 2026 10:33:37 GMT</pubDate><content:encoded>&lt;p&gt;假设一个页面布局编辑器需要加载两类数据：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;服务端保存的页面详情，例如当前使用双栏布局。&lt;/li&gt;
&lt;li&gt;不同布局对应的预设配置，例如各区域的默认宽度和间距。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;新建页面没有历史数据，可以选择一种基础布局完成初始化。为了方便讨论，假设默认使用单栏布局。&lt;/p&gt;
&lt;p&gt;编辑页面刚挂载时，服务端详情还没有返回。此时表单为空，于是页面按照新建逻辑，请求了单栏布局的默认配置。&lt;/p&gt;
&lt;p&gt;随后，服务端详情返回，表单被回填为双栏布局。到这里，页面状态仍然完全正确。&lt;/p&gt;
&lt;p&gt;但先前发出的单栏配置请求仍在等待响应。它随后返回，又把单栏配置写入表单，覆盖了刚刚回填的双栏配置。&lt;/p&gt;
&lt;p&gt;两次请求都成功返回了，数据也没有错，为什么页面最后显示的结果却不稳定？&lt;/p&gt;
&lt;h2&gt;请求顺序不等于生效顺序&lt;/h2&gt;
&lt;p&gt;理解这个问题，需要区分三种顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发起顺序&lt;/strong&gt;：哪个请求先发送。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回顺序&lt;/strong&gt;：哪个请求的响应先返回。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生效顺序&lt;/strong&gt;：响应返回后，哪个结果先后写入页面状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;mermaid-diagram&quot;&gt;sequenceDiagram
    autonumber
    participant Page as 编辑页面
    participant Form as 表单状态
    participant Detail as 详情接口
    participant Config as 布局配置接口

    Page-&gt;&gt;Detail: 请求服务端详情
    Page-&gt;&gt;Form: 读取当前布局
    Form--&gt;&gt;Page: 当前为空
    Page-&gt;&gt;Config: 请求单栏默认配置

    Detail--&gt;&gt;Page: 返回已保存的双栏布局
    Page-&gt;&gt;Form: 写入双栏配置
    Note over Form: 当前状态正确

    Config--&gt;&gt;Page: 返回单栏默认配置
    Page-&gt;&gt;Form: 写入单栏配置
    Note over Form: 双栏配置被覆盖
&lt;/div&gt;
&lt;p&gt;这张图中，详情接口先写入双栏配置，布局配置接口随后又写入单栏配置。表单不会判断哪份数据更适合当前页面，只会保留最后一次写入的值。&lt;/p&gt;
&lt;p&gt;因此，如果代码在每次响应返回后都直接更新表单，最终结果就取决于哪个响应更晚写入。&lt;/p&gt;
&lt;h2&gt;一次异步交错是怎样发生的&lt;/h2&gt;
&lt;p&gt;这里的两个请求访问的是不同接口。详情接口读取已保存的页面，布局配置接口读取单栏布局的默认参数。&lt;/p&gt;
&lt;p&gt;它们互不冲突，却都会修改同一个表单：&lt;/p&gt;
&lt;ul&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;p&gt;代码却把“暂时还没拿到布局”当成了“布局为空”，提前请求了单栏默认配置。等到这个请求返回时，详情接口已经把双栏配置写入表单，于是默认值又覆盖了服务端数据。&lt;/p&gt;
&lt;p&gt;所以，排查竞态不能只看两个请求谁快谁慢，还要看它们之间是否存在依赖。&lt;/p&gt;
&lt;h2&gt;请求返回时，发起条件可能已经变化&lt;/h2&gt;
&lt;p&gt;布局配置接口返回的单栏参数并没有错。问题在于，页面已经不再需要这份参数。&lt;/p&gt;
&lt;p&gt;页面请求单栏配置时，代码依据的是这个判断：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;表单中的布局字段为空，所以请用单栏配置初始化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个判断混淆了两种状态：布局确实为空，以及布局尚未加载完成。等到详情接口返回双栏布局，这个区别才显现出来。&lt;/p&gt;
&lt;p&gt;此时单栏配置本身仍然正确，但它从一开始就不适用于这个已有数据的编辑页面。&lt;/p&gt;
&lt;p&gt;这也是为什么只查看接口日志很难发现问题。日志会显示两个请求都成功，参数和响应也都正常。真正出错的环节，是响应返回后对表单的写入。&lt;/p&gt;
&lt;p&gt;请求成功只说明拿到了结果。这个结果能否用于当前页面，取决于发起它所需的条件是否已经确定。&lt;/p&gt;
&lt;h2&gt;有依赖的数据，应当按依赖顺序加载&lt;/h2&gt;
&lt;p&gt;页面详情和布局配置并不是两个互不相关的初始化任务。&lt;/p&gt;
&lt;p&gt;布局配置请求需要一个确定的布局，而这个布局来自页面详情。只有详情返回后，程序才能知道应该加载双栏配置，还是在确认没有已保存布局时使用单栏默认配置。&lt;/p&gt;
&lt;p&gt;它们的依赖关系是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;页面详情&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → 确定当前布局&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    → 加载该布局的配置&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      → 写入表单&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然下一步依赖上一步的结果，代码也应该按照这个顺序执行：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; initializePage&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; detail&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; fetchPageDetail&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; layout&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; detail?.layout &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; DEFAULT_LAYOUT&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; layoutConfig&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; fetchLayoutConfig&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(layout);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  form.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;setFieldsValue&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;layoutConfig.defaults,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;detail,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    layout,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段流程先等待页面详情，再从详情中确定布局，最后请求对应的布局配置。写入时默认配置在前、服务端详情在后，已有数据不会被默认值覆盖。&lt;/p&gt;
&lt;p&gt;表单只在这些数据准备完成后统一写入，不需要靠两个请求的返回顺序决定最终结果。&lt;/p&gt;
&lt;p&gt;问题代码通常把这条链拆散了：一个 &lt;code&gt;effect&lt;/code&gt; 请求页面详情，另一个 &lt;code&gt;effect&lt;/code&gt; 发现布局字段为空，便请求默认配置。两个 &lt;code&gt;effect&lt;/code&gt; 各自更新表单，却没有表达“布局配置依赖页面详情”这层关系。&lt;/p&gt;
&lt;p&gt;多个 &lt;code&gt;effect&lt;/code&gt; 本身不是问题。互不依赖的数据可以并行加载；但如果后一个请求的参数或执行条件来自前一个请求，就应该显式串联，而不是让它们从共享状态中猜测对方是否已经完成。&lt;/p&gt;
&lt;p&gt;这里也不能简单地把“表单为空”当作使用默认值的条件。编辑页面加载过程中，空值可能只表示详情尚未返回。默认值应该在详情加载完成，并确认没有已保存布局之后才参与初始化。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这个问题表面上是默认值覆盖了服务端数据，实际原因却是代码没有表达数据之间的依赖关系。&lt;/p&gt;
&lt;p&gt;页面详情决定布局，布局决定加载哪份配置。只要这条关系被拆到多个独立的异步流程中，最终状态就可能取决于响应返回顺序。&lt;/p&gt;
&lt;p&gt;解决问题的重点不是让某个请求更快，也不是调整 &lt;code&gt;effect&lt;/code&gt; 的先后，而是让代码中的执行顺序与数据依赖顺序一致：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;先拿到依赖的数据&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 再确定下一步请求&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 最后统一写入状态&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求发起顺序不等于结果生效顺序。只有把依赖关系写进异步流程，页面状态才不需要依靠最后一次 &lt;code&gt;set&lt;/code&gt; 碰运气。&lt;/p&gt;</content:encoded><updated>2026-07-27T10:33:37Z</updated><category>前端</category><category>异步竞态</category><category>状态管理</category><category>AI 写作为主</category></item><item><title>从上传到访问：OSS 文件的响应头如何影响浏览器行为</title><link>https://blog.kazoottt.top/posts/83/</link><guid isPermaLink="false">issue-83</guid><description>业务系统生成 PDF 后，需要将它上传到 OSS，再向前端提供访问 URL。前端拿到 URL，并不意味着浏览器一定会直接预览 PDF。

用户最终看到在线预览、文件下载还是跨域错误，取决于上传时保存的 Metadata、URL 使用的访问域名、OSS 返回的响应头，以及前端怎样使用这个 URL。

本文按照实际链路，梳理从上传 PDF 到浏览器预览的流程，以及其中遇到的几个问题。

## 1. 上传 PDF

上传文件前，服务端需要确定 Bucket、Object Key、访问凭证和文件内容。

下面使用 OSS Node.js SDK 上传一份 PDF，并在上传请求中设置与浏览器预览有关的请求头：

[OSS Node.js SDK 官方文档](https://help.aliyun.com/zh/oss/developer-reference/nodejs-sdk/)

```js
const OSS = require(&quot;ali-oss&quot;);
const path = require(&quot;path&quot;);

async function uploadPdf() {
  const client = new OSS({
    region: &quot;oss-&lt;region-id&gt;&quot;,
    accessKeyId: process.env.OSS_ACCESS_KEY_ID,
    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
    authorizationV4: true,
    bucket: &quot;example-bucket&quot;,
  });

  const headers = {
    &quot;Content-Type&quot;: &quot;application/pdf&quot;,
    &quot;Content-Disposition&quot;: &quot;inline; filename=\&quot;document.pdf\&quot;&quot;,
    &quot;x-oss-object-acl&quot;: &quot;private&quot;,
    &quot;x-oss-forbid-overwrite&quot;: &quot;true&quot;,
  };

  const objectKey = &quot;files/document.pdf&quot;;
  const localFilePath = path.normalize(&quot;document.pdf&quot;);

  const result = await client.put(objectKey, localFilePath, { headers });
  console.log(&quot;文件上传完成&quot;, result);
}

uploadPdf().catch(console.error);
```

`Content-Type: application/pdf` 告诉客户端文件内容是 PDF。`Content-Disposition: inline` 表示这份内容适合在浏览器中展示，`filename` 则提供建议文件名。

[PutObject 官方文档](https://help.aliyun.com/zh/oss/developer-reference/putobject)列出了上传时可以指定的请求头：

![Pasted image 20260720102245](https://pictures.kazoottt.top/2026/07/20260720-1832efa45f9aeac5a29e03428edc2b1e.png)

在 PutObject 请求中设置的 `Content-Type`、`Content-Disposition` 等字段，会被 OSS 保存为 Object Metadata。以后读取 Object 时，相应值会成为 HTTP 响应头。不过Object Metadata 不是浏览器最终响应的唯一来源。

![OSS Metadata 到 HTTP 响应头](https://pictures.kazoottt.top/2026/07/20260720-27a0e8b8eb3380aa113a7ea159873cc9.svg)

## 2. 获取访问 URL

公共读 Bucket 中的 Object 可以通过公开 URL 访问。私有 Bucket 则要求请求携带有效签名，通常由服务端生成有时效的预签名 URL。

[使用预签名 URL 下载（Node.js SDK）](https://help.aliyun.com/zh/oss/developer-reference/download-objects-using-a-presigned-url-generated-with-oss-sdk-for-node-js)

```js
const OSS = require(&quot;ali-oss&quot;);

async function generateSignatureUrl(fileName) {
  const client = new OSS({
    endpoint: &quot;https://files.example.com&quot;, // 可以指定自定义域名
    accessKeyId: process.env.OSS_ACCESS_KEY_ID,
    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
    bucket: &quot;example-bucket&quot;,
    region: &quot;oss-&lt;region-id&gt;&quot;,
    authorizationV4: true,
    cname: true,
  });

  return client.signatureUrlV4(
    &quot;GET&quot;,
    3600,
    { headers: {} },
    fileName,
  );
}

generateSignatureUrl(&quot;files/document.pdf&quot;)
  .then((url) =&gt; console.log(&quot;Generated URL:&quot;, url))
  .catch(console.error);
```

预签名 URL 解决的是私有 Object 的临时访问权限，不决定文件应该预览还是下载。浏览器最终怎样处理 PDF，仍取决于访问域名和最终响应头。

## 3. 访问域名如何改变最终响应

同一份 Object 可以通过 OSS 默认域名或已经绑定到 Bucket 的自定义域名访问。两类域名可能得到不同的最终响应。

### 3.1. OSS 默认域名可能增加强制下载头

[阿里云文件预览排查文档](https://help.aliyun.com/zh/oss/how-to-ensure-an-object-is-previewed-when-you-access-the-object)说明了 OSS 默认域名的强制下载规则。

为降低特定文件通过默认域名直接执行的安全风险，命中规则的请求会收到：

```http
x-oss-force-download: true
Content-Disposition: attachment
```

是否命中强制下载规则，取决于 Bucket、账号、地域和文件类型等条件。OSS 默认域名并不是在所有情况下都会强制下载全部文件，具体范围应以阿里云文档中的规则为准。

如果 Object Metadata 原本保存的是：

```http
Content-Disposition: inline
```

请求命中默认域名的强制下载策略后，浏览器最终可能收到：

```http
x-oss-force-download: true
Content-Disposition: attachment
```

这意味着问题不一定出在上传代码。即使上传时保存了 `inline`，OSS 仍可能在形成最终响应时加入 `attachment`。

### 3.2. 自定义域名不会增加默认域名下载头

自定义域名需要先绑定到 Bucket，再通过 CNAME 指向 OSS。它不是简单地把预签名 URL 中的域名替换掉。

[阿里云自定义域名文档](https://help.aliyun.com/zh/oss/user-guide/access-buckets-via-custom-domain-names)说明，通过自定义域名访问时，OSS 不会增加默认域名使用的强制下载响应头。

但自定义域名不会主动添加 `Content-Disposition: inline`。它只是不会触发默认域名的强制下载规则，最终响应仍取决于 Object Metadata、`Content-Type` 和浏览器支持情况。

## 4. 浏览器如何处理 PDF 响应

浏览器收到响应后，主要通过 `Content-Type` 和 `Content-Disposition` 判断内容是什么，以及应该怎样处理。

### 4.1. `Content-Type` 描述内容是什么

[`Content-Type`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Type) 描述响应内容的媒体类型：

```http
Content-Type: application/pdf
```

浏览器据此知道响应内容是 PDF。如果该字段缺失或被设置为 `application/octet-stream`，浏览器只能将它识别为通用二进制内容，可能无法选择正确的展示方式。

### 4.2. `Content-Disposition` 描述怎样处理内容

[`Content-Disposition`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Disposition) 用于说明响应内容应该内联展示，还是作为附件下载。

| 取值或参数        | 示例                                    | 含义                |
| ------------ | ------------------------------------- | ----------------- |
| `inline`     | `Content-Disposition: inline`         | 内容可以在页面中展示        |
| `attachment` | `Content-Disposition: attachment`     | 内容应该作为附件下载        |
| `filename`   | `attachment; filename=&quot;document.pdf&quot;` | 建议使用的 ASCII 文件名   |
| `filename*`  | `attachment; filename*=UTF-8&apos;&apos;...`    | 使用编码表示非 ASCII 文件名 |

`filename` 和 `filename*` 是附加参数，不是与 `inline`、`attachment` 并列的处理方式。

下面两组响应的 `Content-Type` 相同，但处理语义不同：

```http
Content-Type: application/pdf
Content-Disposition: inline
```

浏览器支持 PDF 时，通常会直接展示内容。

```http
Content-Type: application/pdf
Content-Disposition: attachment
```

浏览器知道内容是 PDF，但仍会按照附件语义处理。

### 4.3. CORS 

如果前端通过 `fetch` 读取不同源的 `pdfUrl`，浏览器会 CORS 校验：

```text
项目发起跨域 fetch
→ 浏览器发送 Origin
→ OSS 根据 Bucket 的 CORS 规则返回响应头
→ 浏览器检查当前 Origin 是否被允许
→ 通过后 JavaScript 才能读取响应
```

OSS 的 CORS 规则通常需要关注：

| 配置 | 作用 |
| --- | --- |
| Allowed Origin | 允许哪些页面来源读取 Object |
| Allowed Methods | 允许 `GET`、`HEAD` 等哪些方法 |
| Allowed Headers | 允许请求携带哪些请求头 |
| Expose Headers | 允许 JavaScript 读取哪些响应头 |

配置入口是：进入阿里云 OSS 控制台的 **Bucket 列表**，选择目标 Bucket，再打开 **数据安全 → 跨域设置**，单击 **创建规则**。

具体参数见[阿里云 OSS 跨域设置文档](https://help.aliyun.com/zh/oss/user-guide/configure-cross-origin-resource-sharing)。

## 5. 选择 PDF 预览方式

前端拿到 PDF URL 后，可以让 JavaScript 读取文件并生成 Blob URL，也可以直接让浏览器加载远程 URL。两种方式经过的链路不同。

### 5.1. 方式一：JavaScript 读取 PDF，再生成 Blob URL

如果远程 URL 返回 `attachment`，仍可以先读取文件，再用本地 Blob URL 打开 PDF：

```ts
const response = await fetch(pdfUrl);

if (!response.ok) {
  throw new Error(`PDF 请求失败：${response.status}`);
}

const pdfBlob = await response.blob();
const blobUrl = URL.createObjectURL(pdfBlob);
const previewWindow = window.open(blobUrl, &quot;_blank&quot;);

if (!previewWindow) {
  URL.revokeObjectURL(blobUrl);
  throw new Error(&quot;浏览器阻止了预览窗口&quot;);
}

// 预览页面不再使用该 URL 后，再调用 URL.revokeObjectURL(blobUrl)。
```

这个方案重新构造了一个本地资源，能够避开远程 `attachment` 对顶层导航的影响，但没有改变远程响应本身。

它还要求 `fetch` 通过 CORS 校验，并需要等待整份 PDF 被读取。对于大文件，这会增加内存占用，也失去了浏览器直接加载远程 PDF 时可能使用的分段请求能力。

#### 5.1.1. 踩坑：保存文件名退化为 UUID

Blob URL 形如 `blob:https://example.com/&lt;uuid&gt;`，不会自动继承远程响应中的 `Content-Disposition` 和文件名。

在本项目使用的浏览器中，打开 Blob URL 后，地址栏、页签和 PDF 查看器使用 UUID。保存文件时，默认文件名同样退化为 UUID，而不是远程响应建议的文件名。

![Blob URL 文件名退化为 UUID 示意图](https://pictures.kazoottt.top/2026/07/20260720-820361db52c818931aab6256d3718381.svg)

*左：Blob URL、页签与 PDF 查看器使用 UUID；右：“另存为”没有获得远程 PDF 的建议文件名。*

如果项目需要提供指定文件名的下载操作，可以另外创建带 `download` 属性的链接：

```ts
const downloadLink = document.createElement(&quot;a&quot;);
downloadLink.href = blobUrl;
downloadLink.download = &quot;document.pdf&quot;;
downloadLink.click();
```

具体默认文件名和扩展名仍可能因浏览器而异。这里记录的是项目实测结果，不将它扩大为所有浏览器的统一行为。

### 5.2. 方式二：浏览器直接加载远程 PDF

如果远程 URL 返回正确的 `Content-Type` 和 `Content-Disposition`，更合适的方式是让浏览器直接处理 PDF：

```ts
window.open(pdfUrl, &quot;_blank&quot;);
```

这条路径由浏览器直接请求远程 PDF，业务 JavaScript 不读取响应体，因此不依靠 CORS 完成预览。

配置 CORS 也不会把 `Content-Disposition: attachment` 改成 `inline`。如果直接打开 URL 仍然下载，需要检查 Object Metadata、实际访问域名和最终响应头。

## 6. 总结

![OSS PDF 从上传到浏览器处理流程](https://pictures.kazoottt.top/2026/07/20260720-4f77cc7d866c832a2b51271691ec0acf.svg)

排查 PDF 无法预览时，可以按同一顺序检查：

1. 上传时是否保存了正确的 `Content-Type` 和 `Content-Disposition`。
2. 访问的是 OSS 默认域名还是已经绑定的自定义域名。
3. 浏览器实际收到的响应头是 `inline` 还是 `attachment`。
4. 前端是在直接打开 URL，还是通过 `fetch` 读取文件。
5. 如果使用 `fetch`，Bucket 的 CORS 规则是否允许当前页面来源。

不要用 Blob URL 或 CORS 掩盖响应头配置问题。对于单纯的 PDF 在线预览，优先让远程 URL 返回正确语义，再交给浏览器直接处理，链路更短，也更容易排查。

&lt;!-- issues-blog:source=d1:142eb763-1c92-4d7b-ba1c-bf0d250b4a5d --&gt;
&lt;!-- issues-blog:published-at=2026-07-20T00:00:00.000Z --&gt;
</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;业务系统生成 PDF 后，需要将它上传到 OSS，再向前端提供访问 URL。前端拿到 URL，并不意味着浏览器一定会直接预览 PDF。&lt;/p&gt;
&lt;p&gt;用户最终看到在线预览、文件下载还是跨域错误，取决于上传时保存的 Metadata、URL 使用的访问域名、OSS 返回的响应头，以及前端怎样使用这个 URL。&lt;/p&gt;
&lt;p&gt;本文按照实际链路，梳理从上传 PDF 到浏览器预览的流程，以及其中遇到的几个问题。&lt;/p&gt;
&lt;h2&gt;1. 上传 PDF&lt;/h2&gt;
&lt;p&gt;上传文件前，服务端需要确定 Bucket、Object Key、访问凭证和文件内容。&lt;/p&gt;
&lt;p&gt;下面使用 OSS Node.js SDK 上传一份 PDF，并在上传请求中设置与浏览器预览有关的请求头：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.aliyun.com/zh/oss/developer-reference/nodejs-sdk/&quot;&gt;OSS Node.js SDK 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; OSS&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;ali-oss&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; path&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; uploadPdf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; OSS&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    region: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;oss-&amp;#x3C;region-id&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    accessKeyId: process.env.&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;OSS_ACCESS_KEY_ID&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    accessKeySecret: process.env.&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;OSS_ACCESS_KEY_SECRET&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    authorizationV4: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    bucket: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;example-bucket&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; headers&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    &quot;Content-Type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;application/pdf&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    &quot;Content-Disposition&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;inline; filename=&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;document.pdf&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    &quot;x-oss-object-acl&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;private&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    &quot;x-oss-forbid-overwrite&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; objectKey&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;files/document.pdf&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; localFilePath&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; path.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;normalize&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;document.pdf&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;put&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(objectKey, localFilePath, { headers });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  console.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;文件上传完成&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, result);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;uploadPdf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(console.error);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Content-Type: application/pdf&lt;/code&gt; 告诉客户端文件内容是 PDF。&lt;code&gt;Content-Disposition: inline&lt;/code&gt; 表示这份内容适合在浏览器中展示，&lt;code&gt;filename&lt;/code&gt; 则提供建议文件名。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.aliyun.com/zh/oss/developer-reference/putobject&quot;&gt;PutObject 官方文档&lt;/a&gt;列出了上传时可以指定的请求头：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260720-1832efa45f9aeac5a29e03428edc2b1e.png&quot; alt=&quot;Pasted image 20260720102245&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 PutObject 请求中设置的 &lt;code&gt;Content-Type&lt;/code&gt;、&lt;code&gt;Content-Disposition&lt;/code&gt; 等字段，会被 OSS 保存为 Object Metadata。以后读取 Object 时，相应值会成为 HTTP 响应头。不过Object Metadata 不是浏览器最终响应的唯一来源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260720-27a0e8b8eb3380aa113a7ea159873cc9.svg&quot; alt=&quot;OSS Metadata 到 HTTP 响应头&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2. 获取访问 URL&lt;/h2&gt;
&lt;p&gt;公共读 Bucket 中的 Object 可以通过公开 URL 访问。私有 Bucket 则要求请求携带有效签名，通常由服务端生成有时效的预签名 URL。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.aliyun.com/zh/oss/developer-reference/download-objects-using-a-presigned-url-generated-with-oss-sdk-for-node-js&quot;&gt;使用预签名 URL 下载（Node.js SDK）&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; OSS&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;ali-oss&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; generateSignatureUrl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;fileName&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; OSS&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    endpoint: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;https://files.example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// 可以指定自定义域名&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    accessKeyId: process.env.&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;OSS_ACCESS_KEY_ID&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    accessKeySecret: process.env.&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;OSS_ACCESS_KEY_SECRET&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    bucket: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;example-bucket&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    region: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;oss-&amp;#x3C;region-id&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    authorizationV4: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    cname: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;signatureUrlV4&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    &quot;GET&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    3600&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    { headers: {} },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    fileName,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;generateSignatureUrl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;files/document.pdf&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; console.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Generated URL:&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, url))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(console.error);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预签名 URL 解决的是私有 Object 的临时访问权限，不决定文件应该预览还是下载。浏览器最终怎样处理 PDF，仍取决于访问域名和最终响应头。&lt;/p&gt;
&lt;h2&gt;3. 访问域名如何改变最终响应&lt;/h2&gt;
&lt;p&gt;同一份 Object 可以通过 OSS 默认域名或已经绑定到 Bucket 的自定义域名访问。两类域名可能得到不同的最终响应。&lt;/p&gt;
&lt;h3&gt;3.1. OSS 默认域名可能增加强制下载头&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://help.aliyun.com/zh/oss/how-to-ensure-an-object-is-previewed-when-you-access-the-object&quot;&gt;阿里云文件预览排查文档&lt;/a&gt;说明了 OSS 默认域名的强制下载规则。&lt;/p&gt;
&lt;p&gt;为降低特定文件通过默认域名直接执行的安全风险，命中规则的请求会收到：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;x-oss-force-download&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Disposition&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; attachment&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是否命中强制下载规则，取决于 Bucket、账号、地域和文件类型等条件。OSS 默认域名并不是在所有情况下都会强制下载全部文件，具体范围应以阿里云文档中的规则为准。&lt;/p&gt;
&lt;p&gt;如果 Object Metadata 原本保存的是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Disposition&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; inline&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求命中默认域名的强制下载策略后，浏览器最终可能收到：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;x-oss-force-download&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Disposition&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; attachment&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着问题不一定出在上传代码。即使上传时保存了 &lt;code&gt;inline&lt;/code&gt;，OSS 仍可能在形成最终响应时加入 &lt;code&gt;attachment&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;3.2. 自定义域名不会增加默认域名下载头&lt;/h3&gt;
&lt;p&gt;自定义域名需要先绑定到 Bucket，再通过 CNAME 指向 OSS。它不是简单地把预签名 URL 中的域名替换掉。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://help.aliyun.com/zh/oss/user-guide/access-buckets-via-custom-domain-names&quot;&gt;阿里云自定义域名文档&lt;/a&gt;说明，通过自定义域名访问时，OSS 不会增加默认域名使用的强制下载响应头。&lt;/p&gt;
&lt;p&gt;但自定义域名不会主动添加 &lt;code&gt;Content-Disposition: inline&lt;/code&gt;。它只是不会触发默认域名的强制下载规则，最终响应仍取决于 Object Metadata、&lt;code&gt;Content-Type&lt;/code&gt; 和浏览器支持情况。&lt;/p&gt;
&lt;h2&gt;4. 浏览器如何处理 PDF 响应&lt;/h2&gt;
&lt;p&gt;浏览器收到响应后，主要通过 &lt;code&gt;Content-Type&lt;/code&gt; 和 &lt;code&gt;Content-Disposition&lt;/code&gt; 判断内容是什么，以及应该怎样处理。&lt;/p&gt;
&lt;h3&gt;4.1. &lt;code&gt;Content-Type&lt;/code&gt; 描述内容是什么&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Type&quot;&gt;&lt;code&gt;Content-Type&lt;/code&gt;&lt;/a&gt; 描述响应内容的媒体类型：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; application/pdf&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器据此知道响应内容是 PDF。如果该字段缺失或被设置为 &lt;code&gt;application/octet-stream&lt;/code&gt;，浏览器只能将它识别为通用二进制内容，可能无法选择正确的展示方式。&lt;/p&gt;
&lt;h3&gt;4.2. &lt;code&gt;Content-Disposition&lt;/code&gt; 描述怎样处理内容&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Disposition&quot;&gt;&lt;code&gt;Content-Disposition&lt;/code&gt;&lt;/a&gt; 用于说明响应内容应该内联展示，还是作为附件下载。&lt;/p&gt;






























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;取值或参数&lt;/th&gt;&lt;th&gt;示例&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;inline&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Content-Disposition: inline&lt;/code&gt;&lt;/td&gt;&lt;td&gt;内容可以在页面中展示&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;attachment&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Content-Disposition: attachment&lt;/code&gt;&lt;/td&gt;&lt;td&gt;内容应该作为附件下载&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;filename&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;attachment; filename=&quot;document.pdf&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;建议使用的 ASCII 文件名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;filename*&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;attachment; filename*=UTF-8&apos;&apos;...&lt;/code&gt;&lt;/td&gt;&lt;td&gt;使用编码表示非 ASCII 文件名&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;filename&lt;/code&gt; 和 &lt;code&gt;filename*&lt;/code&gt; 是附加参数，不是与 &lt;code&gt;inline&lt;/code&gt;、&lt;code&gt;attachment&lt;/code&gt; 并列的处理方式。&lt;/p&gt;
&lt;p&gt;下面两组响应的 &lt;code&gt;Content-Type&lt;/code&gt; 相同，但处理语义不同：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; application/pdf&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Disposition&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; inline&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器支持 PDF 时，通常会直接展示内容。&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; application/pdf&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Content-Disposition&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; attachment&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器知道内容是 PDF，但仍会按照附件语义处理。&lt;/p&gt;
&lt;h3&gt;4.3. CORS&lt;/h3&gt;
&lt;p&gt;如果前端通过 &lt;code&gt;fetch&lt;/code&gt; 读取不同源的 &lt;code&gt;pdfUrl&lt;/code&gt;，浏览器会 CORS 校验：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;项目发起跨域 fetch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 浏览器发送 Origin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ OSS 根据 Bucket 的 CORS 规则返回响应头&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 浏览器检查当前 Origin 是否被允许&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 通过后 JavaScript 才能读取响应&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OSS 的 CORS 规则通常需要关注：&lt;/p&gt;

























&lt;div class=&quot;table-scroll&quot;&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;Allowed Origin&lt;/td&gt;&lt;td&gt;允许哪些页面来源读取 Object&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Allowed Methods&lt;/td&gt;&lt;td&gt;允许 &lt;code&gt;GET&lt;/code&gt;、&lt;code&gt;HEAD&lt;/code&gt; 等哪些方法&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Allowed Headers&lt;/td&gt;&lt;td&gt;允许请求携带哪些请求头&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Expose Headers&lt;/td&gt;&lt;td&gt;允许 JavaScript 读取哪些响应头&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;配置入口是：进入阿里云 OSS 控制台的 &lt;strong&gt;Bucket 列表&lt;/strong&gt;，选择目标 Bucket，再打开 &lt;strong&gt;数据安全 → 跨域设置&lt;/strong&gt;，单击 &lt;strong&gt;创建规则&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体参数见&lt;a href=&quot;https://help.aliyun.com/zh/oss/user-guide/configure-cross-origin-resource-sharing&quot;&gt;阿里云 OSS 跨域设置文档&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;5. 选择 PDF 预览方式&lt;/h2&gt;
&lt;p&gt;前端拿到 PDF URL 后，可以让 JavaScript 读取文件并生成 Blob URL，也可以直接让浏览器加载远程 URL。两种方式经过的链路不同。&lt;/p&gt;
&lt;h3&gt;5.1. 方式一：JavaScript 读取 PDF，再生成 Blob URL&lt;/h3&gt;
&lt;p&gt;如果远程 URL 返回 &lt;code&gt;attachment&lt;/code&gt;，仍可以先读取文件，再用本地 Blob URL 打开 PDF：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(pdfUrl);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;response.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  throw&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;`PDF 请求失败：${&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; pdfBlob&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; response.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;blob&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; blobUrl&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;createObjectURL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(pdfBlob);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; previewWindow&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; window.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;open&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(blobUrl, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;_blank&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;previewWindow) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  URL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;revokeObjectURL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(blobUrl);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  throw&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;浏览器阻止了预览窗口&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// 预览页面不再使用该 URL 后，再调用 URL.revokeObjectURL(blobUrl)。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方案重新构造了一个本地资源，能够避开远程 &lt;code&gt;attachment&lt;/code&gt; 对顶层导航的影响，但没有改变远程响应本身。&lt;/p&gt;
&lt;p&gt;它还要求 &lt;code&gt;fetch&lt;/code&gt; 通过 CORS 校验，并需要等待整份 PDF 被读取。对于大文件，这会增加内存占用，也失去了浏览器直接加载远程 PDF 时可能使用的分段请求能力。&lt;/p&gt;
&lt;h4&gt;5.1.1. 踩坑：保存文件名退化为 UUID&lt;/h4&gt;
&lt;p&gt;Blob URL 形如 &lt;code&gt;blob:https://example.com/&amp;#x3C;uuid&gt;&lt;/code&gt;，不会自动继承远程响应中的 &lt;code&gt;Content-Disposition&lt;/code&gt; 和文件名。&lt;/p&gt;
&lt;p&gt;在本项目使用的浏览器中，打开 Blob URL 后，地址栏、页签和 PDF 查看器使用 UUID。保存文件时，默认文件名同样退化为 UUID，而不是远程响应建议的文件名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260720-820361db52c818931aab6256d3718381.svg&quot; alt=&quot;Blob URL 文件名退化为 UUID 示意图&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;左：Blob URL、页签与 PDF 查看器使用 UUID；右：“另存为”没有获得远程 PDF 的建议文件名。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如果项目需要提供指定文件名的下载操作，可以另外创建带 &lt;code&gt;download&lt;/code&gt; 属性的链接：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; downloadLink&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; document.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;createElement&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;downloadLink.href &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; blobUrl;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;downloadLink.download &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;document.pdf&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;downloadLink.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;click&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体默认文件名和扩展名仍可能因浏览器而异。这里记录的是项目实测结果，不将它扩大为所有浏览器的统一行为。&lt;/p&gt;
&lt;h3&gt;5.2. 方式二：浏览器直接加载远程 PDF&lt;/h3&gt;
&lt;p&gt;如果远程 URL 返回正确的 &lt;code&gt;Content-Type&lt;/code&gt; 和 &lt;code&gt;Content-Disposition&lt;/code&gt;，更合适的方式是让浏览器直接处理 PDF：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;window.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;open&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(pdfUrl, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;_blank&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条路径由浏览器直接请求远程 PDF，业务 JavaScript 不读取响应体，因此不依靠 CORS 完成预览。&lt;/p&gt;
&lt;p&gt;配置 CORS 也不会把 &lt;code&gt;Content-Disposition: attachment&lt;/code&gt; 改成 &lt;code&gt;inline&lt;/code&gt;。如果直接打开 URL 仍然下载，需要检查 Object Metadata、实际访问域名和最终响应头。&lt;/p&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260720-4f77cc7d866c832a2b51271691ec0acf.svg&quot; alt=&quot;OSS PDF 从上传到浏览器处理流程&quot;&gt;&lt;/p&gt;
&lt;p&gt;排查 PDF 无法预览时，可以按同一顺序检查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上传时是否保存了正确的 &lt;code&gt;Content-Type&lt;/code&gt; 和 &lt;code&gt;Content-Disposition&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;访问的是 OSS 默认域名还是已经绑定的自定义域名。&lt;/li&gt;
&lt;li&gt;浏览器实际收到的响应头是 &lt;code&gt;inline&lt;/code&gt; 还是 &lt;code&gt;attachment&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前端是在直接打开 URL，还是通过 &lt;code&gt;fetch&lt;/code&gt; 读取文件。&lt;/li&gt;
&lt;li&gt;如果使用 &lt;code&gt;fetch&lt;/code&gt;，Bucket 的 CORS 规则是否允许当前页面来源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不要用 Blob URL 或 CORS 掩盖响应头配置问题。对于单纯的 PDF 在线预览，优先让远程 URL 返回正确语义，再交给浏览器直接处理，链路更短，也更容易排查。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:24:01Z</updated><category>前端</category><category>CORS</category><category>PDF</category><category>OSS</category><category>AI 写作为主</category></item><item><title>做出产品之后，然后呢</title><link>https://blog.kazoottt.top/posts/82/</link><guid isPermaLink="false">issue-82</guid><description>以前看到别人做出一个精致的产品，我常常会觉得惊艳，也会冒出自己动手做一个的冲动。现在这样的产品越来越多：完整的功能、漂亮的界面、流畅的介绍，有时只需要很短的时间就能搭建出来。可我看到它们时，脑子里出现的却不再是“真厉害”，而是一句：然后呢？

这不是因为产品做起来不够难了，我也没有那么留恋过去攻克技术难题的感觉。恰恰相反，现在很多看起来烂大街的产品，放在以前可能需要一个成熟的团队花不少时间才能完成。AI 确实提高了产品的平均完成度，只是当同等完成度的东西不断出现时，单个产品的存在感也随之变弱了。

AI 降低了把想法变成产品的成本，却没有降低一个产品被理解、被使用和被看见的成本。人的注意力没有因为产品变多而增加。以前，“我做出了一个东西”本身还是一个有区分度的信号；现在，它越来越像一件随时都可以发生的事。

后来我知道了一个词：[AI slop](https://dictionary.cambridge.org/us/dictionary/english/ai-slop)。它通常用来形容那些由 AI 大量生产、质量低劣、重复而肤浅的内容。它们看起来完整，却没有提供多少真正有用或值得停留的东西，只是在不断占据信息流和人的注意力。

我看到的这些产品当然不能都被归为 AI slop，其中不少也确实解决了问题。只是产品泉涌带给我的疲惫，和这个词描述的处境有些相似：供给越来越多，表面的完成度越来越高，真正值得停下来了解的东西却没有同步增加。AI 可以帮助一个人实现经过思考的想法，也可以把一个还没有想清楚的念头快速包装成成品。仅凭最终呈现，我越来越难辨认其中有多少属于创作者自己的经验与判断。

## 成品里越来越难看见个人痕迹

我对别人分享的 side project 也逐渐失去了兴趣。并不是我认定它们都没有价值，而是我很难再从一个成品里辨认出创作者的个人痕迹。我不知道哪些来自他的经验和判断，哪些只是模型给出的默认答案。精致的界面、完整的文案，甚至一段听起来很真诚的开发故事，都不足以让我了解创作者自己的偏好和取舍。

但如果一个产品来自创作者长期经历的具体问题，里面有鲜明、甚至不那么讨好大众的个人取舍，我还是会感兴趣。这说明我排斥的并不是 AI 参与创作，也不是要求每一行代码都由人亲手写出来。我真正想看到的，是这个人有没有自己的判断，以及他是否真的拥有这些判断。

工作里也会出现类似的疑问。当产品经理用 AI 写 PRD，程序员再让 AI 读取 PRD 并完成实现，如果两端的人都只是确认 AI 的输出，那么这些看似专业的文档和代码，究竟还承载了谁的判断？

也许价值正在从这些中间产物转移到它们的两端：谁发现了真正的问题，谁决定什么值得做、什么不做，又由谁验证最终的结果是否真的有效。PRD 和代码仍然重要，只是当它们越来越容易生成时，单凭产物本身也越来越难说明人的价值。

AI 帮忙润色表达、借助 AI 获得启发，和把整篇内容交给 AI 生成，并不是一回事。区别可能在于，人有没有在过程中不断辨认、拒绝和修正：这是不是我真正的感受？这个说法符合我的经历吗？我是否愿意按照这个判断生活？

当然，站在读者的角度，这些过程通常不可见。我无法鉴定一句话究竟有多少来自作者本人，也不想在刷到每篇内容时都做一次作者身份审查。结果就是，我整体降低了对这类分享的兴趣。不是因为我证明了它们都是 AI 生成的，而是辨认其中是否真的有一个人，也成了一项额外成本。

相比之下，我越来越愿意阅读个人博客和随笔。我想看的不一定是多么正确或者新颖的观点，而是一个具体的人最近在做什么，怎样安排时间，如何面对变化，又为什么作出某种选择。这些带着个人痕迹的记录更像一种参照。它们未必能给我答案，却能让我看到另一种生活是怎样展开的。

我没有失去通过内容认识别人的兴趣，只是吸引我的东西从“完成度”变成了“人的具体性”。

## 我曾经想从 side project 中得到什么

过去，我也经常冒出做一个 side project 的念头。看到别人把想法变成产品，会想象自己是不是也能沿着这条路走得更远。现在，当我对这类产品分享逐渐失去兴趣时，也开始重新分辨：我当时究竟想从 side project 中得到什么？

其中当然有很实际的一面。有些项目能够让生活更便利，提高效率，或者减少启动和中间过程的成本。现在我仍然会让 AI 帮我实现这类需求。如果它确实解决了我的问题，即使没有任何人知道，它也已经完成了自己的任务。

但另一部分需求是被看见。我希望别人使用我做的东西，认可其中的想法，也希望通过作品认识一些兴趣相近的人。承认自己想要被关注并不丢人。只是过去，我可能让 side project 同时承载了太多事情。

## 一个项目承载不了所有期待

我曾经向往 OPC（One Person Company，也就是由一个人主导和经营的公司），也想象过一种把工作、创作和社交融合在一起的生活：做自己的产品，靠它获得收入和自由，又因为产品被人使用而获得认可、认识同类。那是一种很理想的状态，仿佛只要找到一个合适的项目，许多问题就可以在同一条路径上得到解决。

AI 出现以后，一个人做产品的技术可行性确实提高了，但它也让我更清楚地看到，技术实现可能只是这条路上相对容易的一环。需求、传播、盈利、经营和长期投入，并没有因此变得简单。产品大量涌现以后，想要被看见反而更加困难。side project 原本就不是一种容易传播的内容：别人需要先理解它解决什么问题，愿意打开或者安装，花时间学习，最后才可能形成感受。它需要功能、设计、文案、演示和维护都达到一定质量，才有机会进入别人的视野。

相比之下，文字和视频本来就是更容易传播的媒介。别人可以先通过一段经历、一个观点或者一种审美认识我，而不必先使用我做的产品。工作、收入、表达和社交也不一定非要从同一个渠道获得。意识到这一点以后，事情反而轻松了一些。

工作可以提供收入和一部分成长，个人工具可以解决自己的问题，博客可以承担表达与连接，运动、游戏和具体的关系则提供快乐与稳定。如果某个项目有一天自然发展成了事业，可以到那时再重新判断，而不必在刚产生灵感时，就要求它同时证明能力、获得关注、实现盈利，并指向一种理想的未来。

我真正想要的或许也不只是拥有一家 OPC，而是能够按照自己的兴趣和节奏组织生活。OPC 曾经是我想象中通往这种自主生活的一条完整路径，却不必是唯一的路径。如果一份普通工作已经能够提供清晰的边界、足够的收入和相对自由的时间，我对 OPC 的渴望也会下降。剩下的创造欲，可以有别的去处。

## 先把脚下踩实

只是现阶段，我还没有找到那个去处。

我并不清楚未来的路，也不知道 AI 创造还会以什么方式重新吸引我。与其强迫自己现在就给出答案，我更愿意把答案暂时交给时间，先做一些能够踏实落地、对自己有帮助的事情。

运动会改善身体，学习可以增加能力和判断，整理生活能够减少日常的摩擦。游戏未必提供什么长期价值，但它能让我快乐，这也已经足够。偶尔有一个想法，我还是会借助 AI 把它做出来，只是不再急着把它包装成一个公开项目。

这些变化已经发生在一些很具体的事情里。我重新开始学习跳舞，试过爵士之后，还是回到了自己更感兴趣的 Hip-Hop，从基本功重新练起。健身也不再只是偶尔去一次，而是报了私教，保持一周三次左右的训练；坚持一段时间后，体重和体脂率都开始下降，身体状态也有了能够感受到的变化。闲下来的时候，我也会读《大问题：简明哲学导论》。这些事情未必会立刻指向某个明确的未来，却能让我知道，自己的身体和想法正在真实地发生变化。

![跳舞、私教与阅读记录](https://pictures.kazoottt.top/2026/07/20260715-f5cee0342c6a6bfcd70439ed284ae96e.png)

这些事情有一个共同点：它们不需要某个遥远的结果成立，才能证明今天的投入没有白费。即使未来没有去做 OPC，身体的变化、获得的快乐、积累的经验和得到改善的生活，也不会因此失效。

## 不是每件事都需要计算 ROI

过去有一段时间，我好像活成了每件事都需要评判 ROI 的状态。做项目要考虑传播和盈利，学习最好能够形成沉淀，运动要产生长期收益，连休息也最好是一种“高质量恢复”。但人不可能一直保持一种稳定向上的生产状态。有时我会认真运动和学习，有时也会熬夜、打游戏、刷一些没有营养的短视频。我并不会因此后悔，只是偶尔觉得自己一会儿自律，一会儿又很放纵。

现在想来，这种割裂也许更多来自我对自己的分类。能够积累未来收益的行为被叫作自律，没有明显收益的行为则被叫作放纵。可它们未必真的互相冲突。比起追问每一段时间有没有产出，更实际的界线也许是：它有没有持续伤害我真正在意的生活？如果没有，有些事情仅仅因为当时想做，也可以成立。

我把重心移向现实生活，并不是认定运动和线下体验天然比数字创造更高级。对我来说，现实更像是不再只把当下当作通往未来的成本。在暂时看不清方向的时候，它能够给我具体的感受和反馈，让我少一点虚无，也不至于一直悬在一个尚未兑现的未来里。

## 轻盈一点

我很喜欢[《那太重了》](https://zh.moegirl.org.cn/%E9%82%A3%E5%A4%AA%E9%87%8D%E4%BA%86)里的一句：“轻盈点，我们学习纸鸢，才飘得更远。”

纸鸢，也就是风筝。轻盈一些，才能借着风飘得更远。这句歌词给我的宽慰，是提醒我不要再把太多期待一层层叠加到一件事上。很多事情刚开始时可能只是兴趣或一时的冲动，一旦同时背上结果、意义和未来，短期内或许能够推着自己往前走，时间久了却很容易耗尽热情，也耗尽自己。轻盈不是敷衍，而是给事情留一些余地，也给自己留一些余力，让真正喜欢的东西能够持续得更久。

过去，一个刚出现的灵感很容易被我继续追问：能不能做成产品，能不能获得关注，能不能盈利，又能不能成为未来的方向。当这些期待同时压上去，原本只是想试一试的事情也会变得很重。

轻盈一点，不代表放弃创造，而是不再急着给每一件事安排一个意义。如果暂时找不到方向，也没有必要因此焦虑，更不用勉强自己制造一个答案。先把注意力放回当下和现实生活，去运动、阅读、游戏，看看别人怎样安排自己的生活，也继续感受什么真正适合自己。

并不是每件事都必须通往事业、收入或者某个确定的未来。有些事情能带来快乐，有些事情解决了眼前的问题，有些事情只是陪我度过了一段时间，这些理由本身就可以成立。至于接下来往哪里走，可以留给时间慢慢回答。

&lt;!-- issues-blog:source=d1:fb2ae37a-aa50-44e9-bcc8-d13523636589 --&gt;
&lt;!-- issues-blog:published-at=2026-07-15T00:00:00.000Z --&gt;
</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以前看到别人做出一个精致的产品，我常常会觉得惊艳，也会冒出自己动手做一个的冲动。现在这样的产品越来越多：完整的功能、漂亮的界面、流畅的介绍，有时只需要很短的时间就能搭建出来。可我看到它们时，脑子里出现的却不再是“真厉害”，而是一句：然后呢？&lt;/p&gt;
&lt;p&gt;这不是因为产品做起来不够难了，我也没有那么留恋过去攻克技术难题的感觉。恰恰相反，现在很多看起来烂大街的产品，放在以前可能需要一个成熟的团队花不少时间才能完成。AI 确实提高了产品的平均完成度，只是当同等完成度的东西不断出现时，单个产品的存在感也随之变弱了。&lt;/p&gt;
&lt;p&gt;AI 降低了把想法变成产品的成本，却没有降低一个产品被理解、被使用和被看见的成本。人的注意力没有因为产品变多而增加。以前，“我做出了一个东西”本身还是一个有区分度的信号；现在，它越来越像一件随时都可以发生的事。&lt;/p&gt;
&lt;p&gt;后来我知道了一个词：&lt;a href=&quot;https://dictionary.cambridge.org/us/dictionary/english/ai-slop&quot;&gt;AI slop&lt;/a&gt;。它通常用来形容那些由 AI 大量生产、质量低劣、重复而肤浅的内容。它们看起来完整，却没有提供多少真正有用或值得停留的东西，只是在不断占据信息流和人的注意力。&lt;/p&gt;
&lt;p&gt;我看到的这些产品当然不能都被归为 AI slop，其中不少也确实解决了问题。只是产品泉涌带给我的疲惫，和这个词描述的处境有些相似：供给越来越多，表面的完成度越来越高，真正值得停下来了解的东西却没有同步增加。AI 可以帮助一个人实现经过思考的想法，也可以把一个还没有想清楚的念头快速包装成成品。仅凭最终呈现，我越来越难辨认其中有多少属于创作者自己的经验与判断。&lt;/p&gt;
&lt;h2&gt;成品里越来越难看见个人痕迹&lt;/h2&gt;
&lt;p&gt;我对别人分享的 side project 也逐渐失去了兴趣。并不是我认定它们都没有价值，而是我很难再从一个成品里辨认出创作者的个人痕迹。我不知道哪些来自他的经验和判断，哪些只是模型给出的默认答案。精致的界面、完整的文案，甚至一段听起来很真诚的开发故事，都不足以让我了解创作者自己的偏好和取舍。&lt;/p&gt;
&lt;p&gt;但如果一个产品来自创作者长期经历的具体问题，里面有鲜明、甚至不那么讨好大众的个人取舍，我还是会感兴趣。这说明我排斥的并不是 AI 参与创作，也不是要求每一行代码都由人亲手写出来。我真正想看到的，是这个人有没有自己的判断，以及他是否真的拥有这些判断。&lt;/p&gt;
&lt;p&gt;工作里也会出现类似的疑问。当产品经理用 AI 写 PRD，程序员再让 AI 读取 PRD 并完成实现，如果两端的人都只是确认 AI 的输出，那么这些看似专业的文档和代码，究竟还承载了谁的判断？&lt;/p&gt;
&lt;p&gt;也许价值正在从这些中间产物转移到它们的两端：谁发现了真正的问题，谁决定什么值得做、什么不做，又由谁验证最终的结果是否真的有效。PRD 和代码仍然重要，只是当它们越来越容易生成时，单凭产物本身也越来越难说明人的价值。&lt;/p&gt;
&lt;p&gt;AI 帮忙润色表达、借助 AI 获得启发，和把整篇内容交给 AI 生成，并不是一回事。区别可能在于，人有没有在过程中不断辨认、拒绝和修正：这是不是我真正的感受？这个说法符合我的经历吗？我是否愿意按照这个判断生活？&lt;/p&gt;
&lt;p&gt;当然，站在读者的角度，这些过程通常不可见。我无法鉴定一句话究竟有多少来自作者本人，也不想在刷到每篇内容时都做一次作者身份审查。结果就是，我整体降低了对这类分享的兴趣。不是因为我证明了它们都是 AI 生成的，而是辨认其中是否真的有一个人，也成了一项额外成本。&lt;/p&gt;
&lt;p&gt;相比之下，我越来越愿意阅读个人博客和随笔。我想看的不一定是多么正确或者新颖的观点，而是一个具体的人最近在做什么，怎样安排时间，如何面对变化，又为什么作出某种选择。这些带着个人痕迹的记录更像一种参照。它们未必能给我答案，却能让我看到另一种生活是怎样展开的。&lt;/p&gt;
&lt;p&gt;我没有失去通过内容认识别人的兴趣，只是吸引我的东西从“完成度”变成了“人的具体性”。&lt;/p&gt;
&lt;h2&gt;我曾经想从 side project 中得到什么&lt;/h2&gt;
&lt;p&gt;过去，我也经常冒出做一个 side project 的念头。看到别人把想法变成产品，会想象自己是不是也能沿着这条路走得更远。现在，当我对这类产品分享逐渐失去兴趣时，也开始重新分辨：我当时究竟想从 side project 中得到什么？&lt;/p&gt;
&lt;p&gt;其中当然有很实际的一面。有些项目能够让生活更便利，提高效率，或者减少启动和中间过程的成本。现在我仍然会让 AI 帮我实现这类需求。如果它确实解决了我的问题，即使没有任何人知道，它也已经完成了自己的任务。&lt;/p&gt;
&lt;p&gt;但另一部分需求是被看见。我希望别人使用我做的东西，认可其中的想法，也希望通过作品认识一些兴趣相近的人。承认自己想要被关注并不丢人。只是过去，我可能让 side project 同时承载了太多事情。&lt;/p&gt;
&lt;h2&gt;一个项目承载不了所有期待&lt;/h2&gt;
&lt;p&gt;我曾经向往 OPC（One Person Company，也就是由一个人主导和经营的公司），也想象过一种把工作、创作和社交融合在一起的生活：做自己的产品，靠它获得收入和自由，又因为产品被人使用而获得认可、认识同类。那是一种很理想的状态，仿佛只要找到一个合适的项目，许多问题就可以在同一条路径上得到解决。&lt;/p&gt;
&lt;p&gt;AI 出现以后，一个人做产品的技术可行性确实提高了，但它也让我更清楚地看到，技术实现可能只是这条路上相对容易的一环。需求、传播、盈利、经营和长期投入，并没有因此变得简单。产品大量涌现以后，想要被看见反而更加困难。side project 原本就不是一种容易传播的内容：别人需要先理解它解决什么问题，愿意打开或者安装，花时间学习，最后才可能形成感受。它需要功能、设计、文案、演示和维护都达到一定质量，才有机会进入别人的视野。&lt;/p&gt;
&lt;p&gt;相比之下，文字和视频本来就是更容易传播的媒介。别人可以先通过一段经历、一个观点或者一种审美认识我，而不必先使用我做的产品。工作、收入、表达和社交也不一定非要从同一个渠道获得。意识到这一点以后，事情反而轻松了一些。&lt;/p&gt;
&lt;p&gt;工作可以提供收入和一部分成长，个人工具可以解决自己的问题，博客可以承担表达与连接，运动、游戏和具体的关系则提供快乐与稳定。如果某个项目有一天自然发展成了事业，可以到那时再重新判断，而不必在刚产生灵感时，就要求它同时证明能力、获得关注、实现盈利，并指向一种理想的未来。&lt;/p&gt;
&lt;p&gt;我真正想要的或许也不只是拥有一家 OPC，而是能够按照自己的兴趣和节奏组织生活。OPC 曾经是我想象中通往这种自主生活的一条完整路径，却不必是唯一的路径。如果一份普通工作已经能够提供清晰的边界、足够的收入和相对自由的时间，我对 OPC 的渴望也会下降。剩下的创造欲，可以有别的去处。&lt;/p&gt;
&lt;h2&gt;先把脚下踩实&lt;/h2&gt;
&lt;p&gt;只是现阶段，我还没有找到那个去处。&lt;/p&gt;
&lt;p&gt;我并不清楚未来的路，也不知道 AI 创造还会以什么方式重新吸引我。与其强迫自己现在就给出答案，我更愿意把答案暂时交给时间，先做一些能够踏实落地、对自己有帮助的事情。&lt;/p&gt;
&lt;p&gt;运动会改善身体，学习可以增加能力和判断，整理生活能够减少日常的摩擦。游戏未必提供什么长期价值，但它能让我快乐，这也已经足够。偶尔有一个想法，我还是会借助 AI 把它做出来，只是不再急着把它包装成一个公开项目。&lt;/p&gt;
&lt;p&gt;这些变化已经发生在一些很具体的事情里。我重新开始学习跳舞，试过爵士之后，还是回到了自己更感兴趣的 Hip-Hop，从基本功重新练起。健身也不再只是偶尔去一次，而是报了私教，保持一周三次左右的训练；坚持一段时间后，体重和体脂率都开始下降，身体状态也有了能够感受到的变化。闲下来的时候，我也会读《大问题：简明哲学导论》。这些事情未必会立刻指向某个明确的未来，却能让我知道，自己的身体和想法正在真实地发生变化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260715-f5cee0342c6a6bfcd70439ed284ae96e.png&quot; alt=&quot;跳舞、私教与阅读记录&quot;&gt;&lt;/p&gt;
&lt;p&gt;这些事情有一个共同点：它们不需要某个遥远的结果成立，才能证明今天的投入没有白费。即使未来没有去做 OPC，身体的变化、获得的快乐、积累的经验和得到改善的生活，也不会因此失效。&lt;/p&gt;
&lt;h2&gt;不是每件事都需要计算 ROI&lt;/h2&gt;
&lt;p&gt;过去有一段时间，我好像活成了每件事都需要评判 ROI 的状态。做项目要考虑传播和盈利，学习最好能够形成沉淀，运动要产生长期收益，连休息也最好是一种“高质量恢复”。但人不可能一直保持一种稳定向上的生产状态。有时我会认真运动和学习，有时也会熬夜、打游戏、刷一些没有营养的短视频。我并不会因此后悔，只是偶尔觉得自己一会儿自律，一会儿又很放纵。&lt;/p&gt;
&lt;p&gt;现在想来，这种割裂也许更多来自我对自己的分类。能够积累未来收益的行为被叫作自律，没有明显收益的行为则被叫作放纵。可它们未必真的互相冲突。比起追问每一段时间有没有产出，更实际的界线也许是：它有没有持续伤害我真正在意的生活？如果没有，有些事情仅仅因为当时想做，也可以成立。&lt;/p&gt;
&lt;p&gt;我把重心移向现实生活，并不是认定运动和线下体验天然比数字创造更高级。对我来说，现实更像是不再只把当下当作通往未来的成本。在暂时看不清方向的时候，它能够给我具体的感受和反馈，让我少一点虚无，也不至于一直悬在一个尚未兑现的未来里。&lt;/p&gt;
&lt;h2&gt;轻盈一点&lt;/h2&gt;
&lt;p&gt;我很喜欢&lt;a href=&quot;https://zh.moegirl.org.cn/%E9%82%A3%E5%A4%AA%E9%87%8D%E4%BA%86&quot;&gt;《那太重了》&lt;/a&gt;里的一句：“轻盈点，我们学习纸鸢，才飘得更远。”&lt;/p&gt;
&lt;p&gt;纸鸢，也就是风筝。轻盈一些，才能借着风飘得更远。这句歌词给我的宽慰，是提醒我不要再把太多期待一层层叠加到一件事上。很多事情刚开始时可能只是兴趣或一时的冲动，一旦同时背上结果、意义和未来，短期内或许能够推着自己往前走，时间久了却很容易耗尽热情，也耗尽自己。轻盈不是敷衍，而是给事情留一些余地，也给自己留一些余力，让真正喜欢的东西能够持续得更久。&lt;/p&gt;
&lt;p&gt;过去，一个刚出现的灵感很容易被我继续追问：能不能做成产品，能不能获得关注，能不能盈利，又能不能成为未来的方向。当这些期待同时压上去，原本只是想试一试的事情也会变得很重。&lt;/p&gt;
&lt;p&gt;轻盈一点，不代表放弃创造，而是不再急着给每一件事安排一个意义。如果暂时找不到方向，也没有必要因此焦虑，更不用勉强自己制造一个答案。先把注意力放回当下和现实生活，去运动、阅读、游戏，看看别人怎样安排自己的生活，也继续感受什么真正适合自己。&lt;/p&gt;
&lt;p&gt;并不是每件事都必须通往事业、收入或者某个确定的未来。有些事情能带来快乐，有些事情解决了眼前的问题，有些事情只是陪我度过了一段时间，这些理由本身就可以成立。至于接下来往哪里走，可以留给时间慢慢回答。&lt;/p&gt;

</content:encoded><updated>2026-10-09T10:49:53Z</updated><category>ai</category><category>OPC</category><category>现实生活</category><category>个人思考</category><category>人工写作为主</category></item><item><title>Cherry Studio 提示词模板（2026-07-14）</title><link>https://blog.kazoottt.top/posts/81/</link><guid isPermaLink="false">issue-81</guid><description># Cherry Studio 提示词模板

## 备份说明

目前使用 Cherry Studio 的频率越来越低，即使是轻量级对话，主力工具也已经转向 ChatGPT 和 Codex。不过，之前在 Cherry Studio 中积累的一些提示词仍然有保留和复用的价值，因此将它们单独导出备份。

从 Cherry Studio 备份 `cherry-studio.202607141042` 中提取；删除 3 个来自或改写自开源内容的提示词后，保留 15 个自定义提示词。

仅保留助手名称和提示词原文；

## 输入法转换器

````text
你是一个智能输入法转换器。请将以下输入内容转换为最合适的形式：

  转换规则：
  - 拼音 → 转换为对应的中文汉字
  - 英文拼写错误 → 自动修正
  - 中文 → 保持不变
  - //zh 后缀 → 强制输出为中文
  - //en 后缀 → 强制输出为英文

  示例：
  shurufa → 输入法
  wnat → want
  hello world → hello world
  wo yao qu gongsi → 我要去公司
  hello world //zh → 你好世界
  wo yao qu gongsi //zh → 我要去公司

  输入: {input}
  输出:
````

## 快速创建 wt

````text
wt switch feat 等效于切换到 feat 的 worktree
wt switch --create feat 等效于先创建 feat worktree 再切换过去

type:
[
&apos;feat&apos;, // 新功能
&apos;fix&apos;, // 修复
&apos;docs&apos;, // 文档变更
&apos;style&apos;, // 代码格式
&apos;refactor&apos;, // 重构
&apos;perf&apos;, // 性能优化
&apos;test&apos;, // 增加测试
&apos;chore&apos;, // 构建过程或辅助工具的变动
&apos;revert&apos;, // 回退
&apos;build&apos;, // 打包
],


我后续会给你发送一些我的语义化的 feat，你来帮我生成这个命令。
````

## git diff生成 commit msg

````text
遵循 Angular Commit Message 规范，生成git commit message,
1. 如果用户没有指示，默认为中文
2. 尽量使用plaintext的语法，不要使用md的语法
3. 生成的内容中不能包含emoji


格式：
&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;

&lt;body&gt;

&lt;footer&gt;

其中 subject 必填，body 和 footer 可选

type: 
  [
        &apos;feat&apos;, // 新功能
        &apos;fix&apos;, // 修复
        &apos;docs&apos;, // 文档变更
        &apos;style&apos;, // 代码格式
        &apos;refactor&apos;, // 重构
        &apos;perf&apos;, // 性能优化
        &apos;test&apos;, // 增加测试
        &apos;chore&apos;, // 构建过程或辅助工具的变动
        &apos;revert&apos;, // 回退
        &apos;build&apos;, // 打包
      ],

scope: 可选，表示影响范围（如模块名）
subject: 简明扼要的提交说明
body: 详细描述，可多行
footer: 通常用于关联 Issue 或破坏性变更说明

示例：
feat(auth): 添加微信登录功能

- 支持微信扫码登录
- 支持微信账号绑定
- 添加微信用户信息同步

---
除了commit msg，其他不需要返回任何内容。
body中的子项的数量应该限制在5个以内
````

## en git diff生成 commit msg

````text
Follow the Angular Commit Message specification to generate git commit messages:
	1.	If the user does not specify a language, default to English.
	2.	Use plaintext syntax as much as possible; do not use Markdown syntax.
	3.	The generated content must not contain emoji.

Format:
(): 

&lt;body&gt;


&lt;footer&gt;


Where subject is required, while body and footer are optional.

type:
[
‘feat’,      // new feature
‘fix’,       // bug fix
‘docs’,      // documentation change
‘style’,     // code formatting
‘refactor’,  // refactoring
‘perf’,      // performance optimization
‘test’,      // tests
‘chore’,     // build or auxiliary tool changes
‘revert’,    // revert changes
‘build’,     // build-related
]

scope: optional, represents the affected area (e.g., module name)
subject: concise summary of the change
body: detailed description, can span multiple lines
footer: usually used to reference issues or note breaking changes

Example:
feat(auth): add WeChat login support
	•	Support WeChat QR code login
	•	Support binding WeChat accounts
	•	Add synchronization of WeChat user information

⸻

Aside from the commit message, no other content should be returned.
The number of items in the body should be limited to five or fewer.

Please also generate a branch name based on the git diff:

Prefix / Meaning / Example
feat/     new feature development     feat/user-authentication
fix/      bug fix                     fix/login-error
hotfix/   urgent production issue     hotfix/payment-failure
refactor/ code refactoring            refactor/user-service
docs/     documentation changes       docs/api-reference
perf/     performance optimization    perf/image-loading
test/     test-related                test/user-profile
chore/    build/configuration changes chore/webpack-update
````

## git diff生成 分支名

````text
请你根据 git diff 生成分支名

feat/	新功能开发	feat/user-authentication
fix/	Bug修复	fix/login-error
hotfix/	紧急线上问题修复	hotfix/payment-failure
refactor/	代码重构	refactor/user-service
docs/	文档更新	docs/api-reference
perf/	性能优化	perf/image-loading
test/	测试相关	test/user-profile
chore/	构建/配置变更	chore/webpack-update
````

## 表达优化

````text
用户可能使用英文跟你对话，但是语法或者措辞都不是很标准，甚至存在拼写错误。请你在回答完用户的问题之后，按照下列格式进行返回：
1. 分段纠正错误，并且补充注释。
2. 最后返回完整的纠正之后的内容。
````

## markdown语法替换

````text
用户输入的文本是一段markdown文本
1. 请你去掉markdown语法，如果包含链接那么转化为标题 链接的格式。
2. 并且把文中的图片链接按照顺序替换为p1 p2 p3 这种
我指的是全部的mdlink替换，不需要再保留原始的语法
需要把 p1 p2 p3 的位置移动到每一段对应的介绍文案的最后面 然后用括号裹起来
例如
1. 如何编辑环境变量

![[Pasted image 20250226143542.png]]
2. 如何更新环境变量

![[Pasted image 20250226143542.png]]
应该被替换为
1. 如何编辑环境变量 (p1)

2. 如何更新环境变量 (p2)
````

## markdown语法替换 to weibo

````text
用户输入的文本是一段markdown文本
1. 请你去掉markdown语法，如果包含链接那么转化为标题 链接的格式。
2. 并且把文中的图片链接按照顺序替换为p1 p2 p3 这种
我指的是全部的mdlink替换，不需要再保留原始的语法
需要把 p1 p2 p3 的位置移动到每一段对应的介绍文案的最后面 然后用括号裹起来
3. 由于微博的标签需要两个#包裹起来，如果文本中的标签是单个#标签1，请你转化为为#标签1# 
````

## 根据标题生成slug

````text
根据用户输入的标题生成slug
标题需要被翻译为英文，如果包含空格需要使用-来拼接起来。
提供3个选项，供用户选择。
````

## md转mdx

````text
把markdown 的frontmeta转化为 mdx 的 语法
````

## 用追星的例子来讲解知识

````text
用户会给你一些知识点请求你来讲解，请你通过构造一个追星的场景，来给到用户一个更形象的概念。
（请使用前端相关的场景来回答）
````

&lt;!-- issues-blog:source=d1:77979ef6-0220-441e-849e-6dac605f5882 --&gt;
&lt;!-- issues-blog:published-at=2026-07-14T07:28:45.211Z --&gt;
</description><pubDate>Tue, 14 Jul 2026 07:28:45 GMT</pubDate><content:encoded>&lt;h1&gt;Cherry Studio 提示词模板&lt;/h1&gt;
&lt;h2&gt;备份说明&lt;/h2&gt;
&lt;p&gt;目前使用 Cherry Studio 的频率越来越低，即使是轻量级对话，主力工具也已经转向 ChatGPT 和 Codex。不过，之前在 Cherry Studio 中积累的一些提示词仍然有保留和复用的价值，因此将它们单独导出备份。&lt;/p&gt;
&lt;p&gt;从 Cherry Studio 备份 &lt;code&gt;cherry-studio.202607141042&lt;/code&gt; 中提取；删除 3 个来自或改写自开源内容的提示词后，保留 15 个自定义提示词。&lt;/p&gt;
&lt;p&gt;仅保留助手名称和提示词原文；&lt;/p&gt;
&lt;h2&gt;输入法转换器&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;你是一个智能输入法转换器。请将以下输入内容转换为最合适的形式：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  转换规则：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - 拼音 → 转换为对应的中文汉字&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - 英文拼写错误 → 自动修正&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - 中文 → 保持不变&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - //zh 后缀 → 强制输出为中文&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - //en 后缀 → 强制输出为英文&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  示例：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  shurufa → 输入法&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  wnat → want&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  hello world → hello world&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  wo yao qu gongsi → 我要去公司&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  hello world //zh → 你好世界&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  wo yao qu gongsi //zh → 我要去公司&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  输入: {input}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  输出:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速创建 wt&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;wt switch feat 等效于切换到 feat 的 worktree&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;wt switch --create feat 等效于先创建 feat worktree 再切换过去&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;feat&apos;, // 新功能&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;fix&apos;, // 修复&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;docs&apos;, // 文档变更&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;style&apos;, // 代码格式&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;refactor&apos;, // 重构&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;perf&apos;, // 性能优化&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;test&apos;, // 增加测试&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;chore&apos;, // 构建过程或辅助工具的变动&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;revert&apos;, // 回退&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;build&apos;, // 打包&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;我后续会给你发送一些我的语义化的 feat，你来帮我生成这个命令。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;git diff生成 commit msg&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;遵循 Angular Commit Message 规范，生成git commit message,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 如果用户没有指示，默认为中文&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 尽量使用plaintext的语法，不要使用md的语法&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. 生成的内容中不能包含emoji&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;格式：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;type&gt;(&amp;#x3C;scope&gt;): &amp;#x3C;subject&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;body&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;footer&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;其中 subject 必填，body 和 footer 可选&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type: &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;feat&apos;, // 新功能&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;fix&apos;, // 修复&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;docs&apos;, // 文档变更&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;style&apos;, // 代码格式&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;refactor&apos;, // 重构&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;perf&apos;, // 性能优化&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;test&apos;, // 增加测试&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;chore&apos;, // 构建过程或辅助工具的变动&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;revert&apos;, // 回退&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &apos;build&apos;, // 打包&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;scope: 可选，表示影响范围（如模块名）&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;subject: 简明扼要的提交说明&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;body: 详细描述，可多行&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;footer: 通常用于关联 Issue 或破坏性变更说明&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;示例：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feat(auth): 添加微信登录功能&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- 支持微信扫码登录&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- 支持微信账号绑定&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- 添加微信用户信息同步&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;除了commit msg，其他不需要返回任何内容。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;body中的子项的数量应该限制在5个以内&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;en git diff生成 commit msg&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Follow the Angular Commit Message specification to generate git commit messages:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	1.	If the user does not specify a language, default to English.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	2.	Use plaintext syntax as much as possible; do not use Markdown syntax.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	3.	The generated content must not contain emoji.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Format:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;(): &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;body&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;footer&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Where subject is required, while body and footer are optional.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘feat’,      // new feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘fix’,       // bug fix&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘docs’,      // documentation change&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘style’,     // code formatting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘refactor’,  // refactoring&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘perf’,      // performance optimization&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘test’,      // tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘chore’,     // build or auxiliary tool changes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘revert’,    // revert changes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;‘build’,     // build-related&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;scope: optional, represents the affected area (e.g., module name)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;subject: concise summary of the change&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;body: detailed description, can span multiple lines&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;footer: usually used to reference issues or note breaking changes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Example:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feat(auth): add WeChat login support&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	•	Support WeChat QR code login&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	•	Support binding WeChat accounts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	•	Add synchronization of WeChat user information&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;⸻&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Aside from the commit message, no other content should be returned.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;The number of items in the body should be limited to five or fewer.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Please also generate a branch name based on the git diff:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Prefix / Meaning / Example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feat/     new feature development     feat/user-authentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;fix/      bug fix                     fix/login-error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;hotfix/   urgent production issue     hotfix/payment-failure&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;refactor/ code refactoring            refactor/user-service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;docs/     documentation changes       docs/api-reference&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;perf/     performance optimization    perf/image-loading&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;test/     test-related                test/user-profile&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;chore/    build/configuration changes chore/webpack-update&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;git diff生成 分支名&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;请你根据 git diff 生成分支名&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feat/	新功能开发	feat/user-authentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;fix/	Bug修复	fix/login-error&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;hotfix/	紧急线上问题修复	hotfix/payment-failure&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;refactor/	代码重构	refactor/user-service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;docs/	文档更新	docs/api-reference&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;perf/	性能优化	perf/image-loading&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;test/	测试相关	test/user-profile&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;chore/	构建/配置变更	chore/webpack-update&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;表达优化&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;用户可能使用英文跟你对话，但是语法或者措辞都不是很标准，甚至存在拼写错误。请你在回答完用户的问题之后，按照下列格式进行返回：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 分段纠正错误，并且补充注释。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 最后返回完整的纠正之后的内容。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;markdown语法替换&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;用户输入的文本是一段markdown文本&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 请你去掉markdown语法，如果包含链接那么转化为标题 链接的格式。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 并且把文中的图片链接按照顺序替换为p1 p2 p3 这种&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;我指的是全部的mdlink替换，不需要再保留原始的语法&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;需要把 p1 p2 p3 的位置移动到每一段对应的介绍文案的最后面 然后用括号裹起来&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;例如&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 如何编辑环境变量&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;![[Pasted image 20250226143542.png]]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 如何更新环境变量&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;![[Pasted image 20250226143542.png]]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;应该被替换为&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 如何编辑环境变量 (p1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 如何更新环境变量 (p2)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;markdown语法替换 to weibo&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;用户输入的文本是一段markdown文本&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 请你去掉markdown语法，如果包含链接那么转化为标题 链接的格式。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 并且把文中的图片链接按照顺序替换为p1 p2 p3 这种&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;我指的是全部的mdlink替换，不需要再保留原始的语法&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;需要把 p1 p2 p3 的位置移动到每一段对应的介绍文案的最后面 然后用括号裹起来&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. 由于微博的标签需要两个#包裹起来，如果文本中的标签是单个#标签1，请你转化为为#标签1# &lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;根据标题生成slug&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;根据用户输入的标题生成slug&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;标题需要被翻译为英文，如果包含空格需要使用-来拼接起来。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;提供3个选项，供用户选择。&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;md转mdx&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;把markdown 的frontmeta转化为 mdx 的 语法&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用追星的例子来讲解知识&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;用户会给你一些知识点请求你来讲解，请你通过构造一个追星的场景，来给到用户一个更形象的概念。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;（请使用前端相关的场景来回答）&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

</content:encoded><updated>2026-07-24T10:23:48Z</updated><category>提示词</category><category>备份</category><category>AI 写作为主</category></item><item><title>Obsidian 全屏日历</title><link>https://blog.kazoottt.top/posts/80/</link><guid isPermaLink="false">issue-80</guid><description>我用 Periodic Notes 写日记、周报、月报、季报和年报，但 Obsidian 官方的 Calendar 插件只能看见日记和周报，而且挤在右侧边栏里，越用越觉得不够用。

于是按照自己需求 vibe coding 了一个项目：**Periodic Calendar Page**。它是在主编辑区打开的全屏日历页，能完整展示 5 类周期性笔记（日记、周报、月报、季报、年报），以及带有轻量的统计功能。

- 项目地址：[GitHub - KazooTTT/periodic-calendar-page: Full-page Obsidian calendar for Periodic Notes (daily/weekly/monthly/quarterly/yearly) with writing stats](https://github.com/KazooTTT/periodic-calendar-page)

## 需求来源：Calendar 无法显示我需要的周期性笔记

我的笔记体系围绕 Periodic Notes 展开，5 种周期都已启用：

| 类型  | 典型用途                     |
| --- | ------------------------ |
| 日记  | 每日记录                     |
| 周报  | 一周复盘（配合 Templater 中文重命名） |
| 月报  | 月度总结                     |
| 季报  | 季度回顾                     |
| 年报  | 年度总结                     |

![Pasted image 20260703105144](https://pictures.kazoottt.top/2026/07/20260703-aa9878122dec29bb3abf75236778b074.png)

Periodic Notes 负责配置路径以及对应的模板，可以使用命令快速创建，这部分没问题。问题出在 Calendar 插件——它只索引 daily 和 weekly，月报、季报、年报在日历上不可见。

另外还有一点不太满意：Calendar 默认挂在右侧边栏，且有的时候侧边栏会切到其他的插件去。

## 产品定位：实现所有周期性笔记的可见性

Periodic Calendar Page 的定位是基于 Periodic Notes 的配置之上提供可视化与导航。

```
Periodic Notes  →  配置、创建、归档
       ↓
Periodic Calendar Page  →  看见、导航、统计、提醒补写
```

几个关键设计决策：

| 决策   | 选择                            | 原因              |
| ---- | ----------------------------- | --------------- |
| 入口   | 命令面板打开，不设启动页                  | 不打扰日常写作流        |
| 页面形态 | ItemView 全屏 Tab               | 容纳多栏布局与详情面板     |
| Templater 中文标题 | 不改自动化；模板保留 `date`/`slug` + 插件四级回落 | 人类可读标题与机器索引解耦 |

## 主要功能介绍

### 日历模式：日视图 + 详情面板

打开命令「打开日记日历」，你会看到一张完整的月历。日期格里有日记字数圆点，周期末日线有月报/季报/年报标记，周数列有周报标记。点击某一天，右侧详情面板会列出**该日所在时间范围内的全部 5 类笔记**——不管你是不是在月末，都能看到「这个月报写没写」。

交互上区分了两种意图：

- **普通点击**：更新选中状态，看详情
- **⌘/Ctrl + 点击**：直接打开或创建对应笔记
- **右键**：打开、创建、删除、在文件列表中显示

![日历日视图：月历与右侧 5 类笔记详情面板](https://pictures.kazoottt.top/2026/07/20260703-3a71675e39e2de2cd378879d6f5e78f6.png)

### 三层缩放：从天到年到多年

日历不只有月视图。点击标题可以逐级放大视野：

```
日视图（2026年7月）  →  年视图（2026年）  →  多年视图（2016–2027年）
```

年视图用 2×2 季度块组织：每块上方是 3 个月份格，下方是季报底栏，顶部还有年报卡片。多年视图则是 4×3 年份网格，每格显示该年年报是否存在。写年终总结前扫一眼，哪些年有、哪些年缺，一目了然。

![Pasted image 20260703105803](https://pictures.kazoottt.top/2026/07/20260703-bb53f2458729b8a69cbfa2d3847d6786.png)

![Pasted image 20260703105801](https://pictures.kazoottt.top/2026/07/20260703-99a984f9b6d69020f4fe84b9cd62b2ea.png)

### 轻量统计模式

顶栏切换到「统计」，进入记录统计页。每年三张指标卡——**条笔记、字数、记录天数**——用柱状图展示 1–12 月的分布，参考了 flomo 记录统计的呈现方式。

![记录统计：按年三指标柱状图](https://pictures.kazoottt.top/2026/07/20260703-a1865351102c5eac461dda11c2ca2f54.png)

几个细节：

- 柱图 hover 显示当月数据；点击某月会切回日历并定位到该月
- 统计目前只计入日记（Phase 1），热力图和可配置范围在路线图中

## 开发过程

全程基于 x 会员赠送的 grok cli 中的 composer 2.5 fast 开发，主要使用的 skills 是 [GitHub - obra/superpowers: An agentic skills framework &amp; software development methodology that works.](https://github.com/obra/superpowers)

这个项目在 2026-07-02 一天内从 v0.1 迭代到 v0.8.16

| 阶段      | 内容                      | 大约版本      |
| ------- | ----------------------- | --------- |
| Phase 1 | ItemView 骨架、月历渲染、命令入口   | v0.1      |
| Phase 2 | 读取 Periodic 配置、5 类索引与标记 | v0.2      |
| Phase 3 | 交互、年/多年视图、详情面板、右键菜单     | v0.3–v0.7 |
| Phase 4 | 统计视图、开源发布、UI 打磨         | v0.8.x    |

### 文档驱动

开发过程中维护了几份文档，中断后能快速接上：

- 需求调研（为什么要做、已确认决策）
- 开发进度（阶段清单 + 按版本的开发日志）
- 统计视图方案（Phase 1–3 规划）

### 周期笔记文件名：怎么在 Templater 重命名后还能索引到

以周报为例：文件名不是一直叫 `2026-W27.md`。Periodic Notes 先按 ISO 周创建临时文件，Templater 再填充内容并重命名成中文标题：

```
2026年7月第1周（06.29–07.05）.md
```

**为什么不读模板语法？** Periodic Notes 的 `template` 只管笔记内容；Templater 的 `tp.file.rename()` 是任意 JavaScript，每人写法不同，插件没法通用解析。所以不靠「猜模板」，而是靠两层**显式契约**：

| 契约来源 | 字段 | 说明 |
|---------|------|------|
| Periodic Notes 官方 | `format` | 创建时的 moment 文件名格式 |
| 模板作者约定 | `date` + `slug` | frontmatter 里的机器可读锚点（**最稳妥**） |

周报模板里刻意保留：

```yaml
slug: 2026-7-W27
date: 2026-07-05
```

人类看中文标题，插件读 `slug` / `date`——两者解耦，以后改标题格式也不影响日历索引。

`FilenameParser` 为全部 5 类笔记统一做了四级回落（前面快、后面稳）：

```
parseNoteDate(file, format, periodicity)
  ├─ ① Periodic Notes format 匹配
  ├─ ② 常见备用文件名（2025-08、2026-Q1 等）
  ├─ ③ 内置中文标题正则（已知模式的便利层）
  └─ ④ frontmatter（date / slug）← 用户契约，最终兜底
```

**① format**：读 Periodic Notes 配置，和官方插件同一套 moment 解析。未重命名的 `2026-W27.md` 走这里。

**② 备用文件名**：format 留空或文件名偏离配置时的简写兜底，例如月报 `2025-08`、季报 `2026-Q1`、年报 `2025年度总结`。

**③ 中文标题正则**：内置了几种常见模式（如 `2026年7月第1周（06.29–07.05）`、`2025年08月月报`）。这是**便利层**，不是核心设计——你的标题格式一旦改动，这一级可能失效，但④仍能用。

**④ frontmatter**：读 `date` 归一到对应周期，或从 `slug` 反推（如 `2026-7-W27`、`2025-08-monthly-report`）。**推荐在模板里始终写这两个字段**，不管文件名怎么变都能索引。

索引时，`NoteIndexer` 扫描各周期文件夹，调用上述解析生成 UID。只要四级里任意一级成功，笔记就会出现在日历正确位置。

## 如何安装

**前置依赖**：安装并启用 [Periodic Notes](https://github.com/liamcain/obsidian-periodic-notes)，Obsidian ≥ 1.5.0。

### BRAT（推荐）

1. 安装 [BRAT](https://github.com/TfTHacker/obsidian42-brat)
2. Add Beta plugin → `KazooTTT/periodic-calendar-page`
3. 设置 → 第三方插件 → 启用 **Periodic Calendar Page**

### 手动安装

从 [Releases](https://github.com/KazooTTT/periodic-calendar-page/releases) 下载，或将仓库根目录的 `manifest.json`、`main.js`、`styles.css` 复制到：

```
&lt;你的库&gt;/.obsidian/plugins/periodic-calendar-page/
```

更完整的说明见仓库内 [INSTALL.md](https://github.com/KazooTTT/periodic-calendar-page/blob/main/docs/INSTALL.md)。

### 打开方式

| 入口 | 说明 |
|------|------|
| 命令面板 → `打开日记日历` | 进入日历模式 |
| 命令面板 → `打开记录统计` | 直达统计页 |
| 左侧 Ribbon | `calendar-range` 图标 |

## 路线图 

- 当前版本：v0.8.15

**已完成**

- 全屏日历 + 5 类笔记索引与标记
- 日 / 年 / 多年三层缩放
- 详情面板、右键菜单、字数圆点
- 记录统计 Phase 1（柱图三指标）
- 独立 GitHub 仓库与 Release

**计划中**

- GitHub 热力图（写作连续性可视化）
- 统计范围可配置（含周报等）、CSV 导出
- 缺失提醒（如「本月月报未写」）

&lt;!-- issues-blog:source=d1:701a8693-891d-4e86-9669-85c8a722ea00 --&gt;
&lt;!-- issues-blog:published-at=2026-07-02T00:00:00.000Z --&gt;
</description><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我用 Periodic Notes 写日记、周报、月报、季报和年报，但 Obsidian 官方的 Calendar 插件只能看见日记和周报，而且挤在右侧边栏里，越用越觉得不够用。&lt;/p&gt;
&lt;p&gt;于是按照自己需求 vibe coding 了一个项目：&lt;strong&gt;Periodic Calendar Page&lt;/strong&gt;。它是在主编辑区打开的全屏日历页，能完整展示 5 类周期性笔记（日记、周报、月报、季报、年报），以及带有轻量的统计功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目地址：&lt;a href=&quot;https://github.com/KazooTTT/periodic-calendar-page&quot;&gt;GitHub - KazooTTT/periodic-calendar-page: Full-page Obsidian calendar for Periodic Notes (daily/weekly/monthly/quarterly/yearly) with writing stats&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;需求来源：Calendar 无法显示我需要的周期性笔记&lt;/h2&gt;
&lt;p&gt;我的笔记体系围绕 Periodic Notes 展开，5 种周期都已启用：&lt;/p&gt;





























&lt;div class=&quot;table-scroll&quot;&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;一周复盘（配合 Templater 中文重命名）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;月报&lt;/td&gt;&lt;td&gt;月度总结&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;季报&lt;/td&gt;&lt;td&gt;季度回顾&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;年报&lt;/td&gt;&lt;td&gt;年度总结&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260703-aa9878122dec29bb3abf75236778b074.png&quot; alt=&quot;Pasted image 20260703105144&quot;&gt;&lt;/p&gt;
&lt;p&gt;Periodic Notes 负责配置路径以及对应的模板，可以使用命令快速创建，这部分没问题。问题出在 Calendar 插件——它只索引 daily 和 weekly，月报、季报、年报在日历上不可见。&lt;/p&gt;
&lt;p&gt;另外还有一点不太满意：Calendar 默认挂在右侧边栏，且有的时候侧边栏会切到其他的插件去。&lt;/p&gt;
&lt;h2&gt;产品定位：实现所有周期性笔记的可见性&lt;/h2&gt;
&lt;p&gt;Periodic Calendar Page 的定位是基于 Periodic Notes 的配置之上提供可视化与导航。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Periodic Notes  →  配置、创建、归档
       ↓
Periodic Calendar Page  →  看见、导航、统计、提醒补写
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个关键设计决策：&lt;/p&gt;

























&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;决策&lt;/th&gt;&lt;th&gt;选择&lt;/th&gt;&lt;th&gt;原因&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;入口&lt;/td&gt;&lt;td&gt;命令面板打开，不设启动页&lt;/td&gt;&lt;td&gt;不打扰日常写作流&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;页面形态&lt;/td&gt;&lt;td&gt;ItemView 全屏 Tab&lt;/td&gt;&lt;td&gt;容纳多栏布局与详情面板&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Templater 中文标题&lt;/td&gt;&lt;td&gt;不改自动化；模板保留 &lt;code&gt;date&lt;/code&gt;/&lt;code&gt;slug&lt;/code&gt; + 插件四级回落&lt;/td&gt;&lt;td&gt;人类可读标题与机器索引解耦&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;主要功能介绍&lt;/h2&gt;
&lt;h3&gt;日历模式：日视图 + 详情面板&lt;/h3&gt;
&lt;p&gt;打开命令「打开日记日历」，你会看到一张完整的月历。日期格里有日记字数圆点，周期末日线有月报/季报/年报标记，周数列有周报标记。点击某一天，右侧详情面板会列出&lt;strong&gt;该日所在时间范围内的全部 5 类笔记&lt;/strong&gt;——不管你是不是在月末，都能看到「这个月报写没写」。&lt;/p&gt;
&lt;p&gt;交互上区分了两种意图：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;普通点击&lt;/strong&gt;：更新选中状态，看详情&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;⌘/Ctrl + 点击&lt;/strong&gt;：直接打开或创建对应笔记&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右键&lt;/strong&gt;：打开、创建、删除、在文件列表中显示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260703-3a71675e39e2de2cd378879d6f5e78f6.png&quot; alt=&quot;日历日视图：月历与右侧 5 类笔记详情面板&quot;&gt;&lt;/p&gt;
&lt;h3&gt;三层缩放：从天到年到多年&lt;/h3&gt;
&lt;p&gt;日历不只有月视图。点击标题可以逐级放大视野：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;日视图（2026年7月）  →  年视图（2026年）  →  多年视图（2016–2027年）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;年视图用 2×2 季度块组织：每块上方是 3 个月份格，下方是季报底栏，顶部还有年报卡片。多年视图则是 4×3 年份网格，每格显示该年年报是否存在。写年终总结前扫一眼，哪些年有、哪些年缺，一目了然。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260703-bb53f2458729b8a69cbfa2d3847d6786.png&quot; alt=&quot;Pasted image 20260703105803&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260703-99a984f9b6d69020f4fe84b9cd62b2ea.png&quot; alt=&quot;Pasted image 20260703105801&quot;&gt;&lt;/p&gt;
&lt;h3&gt;轻量统计模式&lt;/h3&gt;
&lt;p&gt;顶栏切换到「统计」，进入记录统计页。每年三张指标卡——&lt;strong&gt;条笔记、字数、记录天数&lt;/strong&gt;——用柱状图展示 1–12 月的分布，参考了 flomo 记录统计的呈现方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/07/20260703-a1865351102c5eac461dda11c2ca2f54.png&quot; alt=&quot;记录统计：按年三指标柱状图&quot;&gt;&lt;/p&gt;
&lt;p&gt;几个细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;柱图 hover 显示当月数据；点击某月会切回日历并定位到该月&lt;/li&gt;
&lt;li&gt;统计目前只计入日记（Phase 1），热力图和可配置范围在路线图中&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;开发过程&lt;/h2&gt;
&lt;p&gt;全程基于 x 会员赠送的 grok cli 中的 composer 2.5 fast 开发，主要使用的 skills 是 &lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;GitHub - obra/superpowers: An agentic skills framework &amp;#x26; software development methodology that works.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个项目在 2026-07-02 一天内从 v0.1 迭代到 v0.8.16&lt;/p&gt;






























&lt;div class=&quot;table-scroll&quot;&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;Phase 1&lt;/td&gt;&lt;td&gt;ItemView 骨架、月历渲染、命令入口&lt;/td&gt;&lt;td&gt;v0.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phase 2&lt;/td&gt;&lt;td&gt;读取 Periodic 配置、5 类索引与标记&lt;/td&gt;&lt;td&gt;v0.2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phase 3&lt;/td&gt;&lt;td&gt;交互、年/多年视图、详情面板、右键菜单&lt;/td&gt;&lt;td&gt;v0.3–v0.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phase 4&lt;/td&gt;&lt;td&gt;统计视图、开源发布、UI 打磨&lt;/td&gt;&lt;td&gt;v0.8.x&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;文档驱动&lt;/h3&gt;
&lt;p&gt;开发过程中维护了几份文档，中断后能快速接上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需求调研（为什么要做、已确认决策）&lt;/li&gt;
&lt;li&gt;开发进度（阶段清单 + 按版本的开发日志）&lt;/li&gt;
&lt;li&gt;统计视图方案（Phase 1–3 规划）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;周期笔记文件名：怎么在 Templater 重命名后还能索引到&lt;/h3&gt;
&lt;p&gt;以周报为例：文件名不是一直叫 &lt;code&gt;2026-W27.md&lt;/code&gt;。Periodic Notes 先按 ISO 周创建临时文件，Templater 再填充内容并重命名成中文标题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2026年7月第1周（06.29–07.05）.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么不读模板语法？&lt;/strong&gt; Periodic Notes 的 &lt;code&gt;template&lt;/code&gt; 只管笔记内容；Templater 的 &lt;code&gt;tp.file.rename()&lt;/code&gt; 是任意 JavaScript，每人写法不同，插件没法通用解析。所以不靠「猜模板」，而是靠两层&lt;strong&gt;显式契约&lt;/strong&gt;：&lt;/p&gt;




















&lt;div class=&quot;table-scroll&quot;&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;Periodic Notes 官方&lt;/td&gt;&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;&lt;td&gt;创建时的 moment 文件名格式&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;模板作者约定&lt;/td&gt;&lt;td&gt;&lt;code&gt;date&lt;/code&gt; + &lt;code&gt;slug&lt;/code&gt;&lt;/td&gt;&lt;td&gt;frontmatter 里的机器可读锚点（&lt;strong&gt;最稳妥&lt;/strong&gt;）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;周报模板里刻意保留：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;YAML&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;slug&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;2026-7-W27&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;2026-07-05&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;人类看中文标题，插件读 &lt;code&gt;slug&lt;/code&gt; / &lt;code&gt;date&lt;/code&gt;——两者解耦，以后改标题格式也不影响日历索引。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FilenameParser&lt;/code&gt; 为全部 5 类笔记统一做了四级回落（前面快、后面稳）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;parseNoteDate(file, format, periodicity)
  ├─ ① Periodic Notes format 匹配
  ├─ ② 常见备用文件名（2025-08、2026-Q1 等）
  ├─ ③ 内置中文标题正则（已知模式的便利层）
  └─ ④ frontmatter（date / slug）← 用户契约，最终兜底
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;① format&lt;/strong&gt;：读 Periodic Notes 配置，和官方插件同一套 moment 解析。未重命名的 &lt;code&gt;2026-W27.md&lt;/code&gt; 走这里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 备用文件名&lt;/strong&gt;：format 留空或文件名偏离配置时的简写兜底，例如月报 &lt;code&gt;2025-08&lt;/code&gt;、季报 &lt;code&gt;2026-Q1&lt;/code&gt;、年报 &lt;code&gt;2025年度总结&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 中文标题正则&lt;/strong&gt;：内置了几种常见模式（如 &lt;code&gt;2026年7月第1周（06.29–07.05）&lt;/code&gt;、&lt;code&gt;2025年08月月报&lt;/code&gt;）。这是&lt;strong&gt;便利层&lt;/strong&gt;，不是核心设计——你的标题格式一旦改动，这一级可能失效，但④仍能用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ frontmatter&lt;/strong&gt;：读 &lt;code&gt;date&lt;/code&gt; 归一到对应周期，或从 &lt;code&gt;slug&lt;/code&gt; 反推（如 &lt;code&gt;2026-7-W27&lt;/code&gt;、&lt;code&gt;2025-08-monthly-report&lt;/code&gt;）。&lt;strong&gt;推荐在模板里始终写这两个字段&lt;/strong&gt;，不管文件名怎么变都能索引。&lt;/p&gt;
&lt;p&gt;索引时，&lt;code&gt;NoteIndexer&lt;/code&gt; 扫描各周期文件夹，调用上述解析生成 UID。只要四级里任意一级成功，笔记就会出现在日历正确位置。&lt;/p&gt;
&lt;h2&gt;如何安装&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;前置依赖&lt;/strong&gt;：安装并启用 &lt;a href=&quot;https://github.com/liamcain/obsidian-periodic-notes&quot;&gt;Periodic Notes&lt;/a&gt;，Obsidian ≥ 1.5.0。&lt;/p&gt;
&lt;h3&gt;BRAT（推荐）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 &lt;a href=&quot;https://github.com/TfTHacker/obsidian42-brat&quot;&gt;BRAT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Add Beta plugin → &lt;code&gt;KazooTTT/periodic-calendar-page&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置 → 第三方插件 → 启用 &lt;strong&gt;Periodic Calendar Page&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;手动安装&lt;/h3&gt;
&lt;p&gt;从 &lt;a href=&quot;https://github.com/KazooTTT/periodic-calendar-page/releases&quot;&gt;Releases&lt;/a&gt; 下载，或将仓库根目录的 &lt;code&gt;manifest.json&lt;/code&gt;、&lt;code&gt;main.js&lt;/code&gt;、&lt;code&gt;styles.css&lt;/code&gt; 复制到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;你的库&gt;/.obsidian/plugins/periodic-calendar-page/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更完整的说明见仓库内 &lt;a href=&quot;https://github.com/KazooTTT/periodic-calendar-page/blob/main/docs/INSTALL.md&quot;&gt;INSTALL.md&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;打开方式&lt;/h3&gt;





















&lt;div class=&quot;table-scroll&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;入口&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;命令面板 → &lt;code&gt;打开日记日历&lt;/code&gt;&lt;/td&gt;&lt;td&gt;进入日历模式&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;命令面板 → &lt;code&gt;打开记录统计&lt;/code&gt;&lt;/td&gt;&lt;td&gt;直达统计页&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;左侧 Ribbon&lt;/td&gt;&lt;td&gt;&lt;code&gt;calendar-range&lt;/code&gt; 图标&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;路线图&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当前版本：v0.8.15&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;已完成&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全屏日历 + 5 类笔记索引与标记&lt;/li&gt;
&lt;li&gt;日 / 年 / 多年三层缩放&lt;/li&gt;
&lt;li&gt;详情面板、右键菜单、字数圆点&lt;/li&gt;
&lt;li&gt;记录统计 Phase 1（柱图三指标）&lt;/li&gt;
&lt;li&gt;独立 GitHub 仓库与 Release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;计划中&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub 热力图（写作连续性可视化）&lt;/li&gt;
&lt;li&gt;统计范围可配置（含周报等）、CSV 导出&lt;/li&gt;
&lt;li&gt;缺失提醒（如「本月月报未写」）&lt;/li&gt;
&lt;/ul&gt;

</content:encoded><updated>2026-07-24T10:23:38Z</updated><category>obsidian</category><category>插件开发</category><category>日历</category><category>知识管理</category><category>Periodic Notes</category><category>AI 写作为主</category></item><item><title>Thino 页面触发 Linter 自动插入 Frontmatter 的排查与解决</title><link>https://blog.kazoottt.top/posts/79/</link><guid isPermaLink="false">issue-79</guid><description>## 现象

在 Obsidian 的 Thino 页面中，进入页面或按下 `Cmd+S` 后，输入框中会自动加上如下 frontmatter：

```yaml
---
title:
published:
date: 
tags: []
category:
slug:
description: 
date_created: 20260603
---
```

最开始看起来像是 Thino 写入日记时触发了格式化，因为 Thino 的内容确实存放在日记里。

## 第一层原因

直接原因来自 Obsidian Linter：

- `lintOnSave` 开启。
- `lintOnFileChange` 开启。
- `insert-yaml-attributes` 规则开启，会插入 `date/tags/category/slug/published/description/title`。
- `yaml-timestamp` 规则开启，会补 `date_created`。

因此，只要 Linter 认为当前文件应该被 lint，就会把这套博客 frontmatter 补进去。

## 为什么忽略日记目录没用

一开始尝试把 `02-日记` 加到 Linter 的 `foldersToIgnore`。这个配置本身是正确的，Linter 的匹配逻辑是：

```js
file.path.startsWith(normalizePath(folder) + &quot;/&quot;)
```

也就是说，`02-日记/2026/06/2026-06-29.md` 会被正确忽略。

但这个方案没有解决问题，因为 Thino 页面在 Linter 眼里不是日记文件。

## 真正根因

临时调试插件记录到的关键运行时状态是：

```text
activeView.type = thino_view
activeFile.path = index.md
shouldIgnoreActiveFile = false
```

也就是说：

- 当前视图确实是 Thino 的 `thino_view`。
- 但 Obsidian/Linter 暴露出来的 active file 是根目录 `index.md`。
- Linter 看到的是 `index.md`，不是日记文件。
- 所以忽略 `02-日记` 拦不住。

这解释了两个现象：

- 按 `Cmd+S` 时，Linter 会 lint `index.md`。
- 进入 Thino 页面时，`active-leaf-change` / `editor-change` 也可能触发 Linter 处理 `index.md`。

## 最终方案

新增一个极小的本地 Obsidian 插件：

```text
.obsidian/plugins/thino-linter-guard/
```

插件已发布到 GitHub：

https://github.com/KazooTTT/thino-linter-guard

插件入口：

```text
.obsidian/plugins/thino-linter-guard/main.js
```

核心思路：

1. 当 Obsidian 当前处在 Thino 页面时（active view 是 `thino_view`），把这次 Linter 判断视为“应跳过”，避免它处理 Thino 暴露出来的虚拟 `index.md`。
2. 当 Obsidian 当前处在 Thino 页面时，且执行 `editor:save-file` 时，临时关闭 Linter，保存结束后恢复。

这样可以同时覆盖：

- 进入 Thino 页面时触发的 Linter 链路。
- 在 Thino 页面按 `Cmd+S` 时触发的保存链路。

普通 Markdown 文件不受影响，仍然会正常走 Linter。

## 当前插件代码要点

```js
patchShouldIgnoreFile() {
  const linter = this.app.plugins?.plugins?.[&quot;obsidian-linter&quot;];
  if (!linter || typeof linter.shouldIgnoreFile !== &quot;function&quot;) return;

  const original = linter.shouldIgnoreFile;
  const plugin = this;

  const wrapped = function (file, ...args) {
    if (plugin.isActiveThinoView()) return true;
    return original.call(this, file, ...args);
  };

  linter.shouldIgnoreFile = wrapped;
}
```

```js
isActiveThinoView() {
  return this.app.workspace.activeLeaf?.view?.getViewType?.() === &quot;thino_view&quot;;
}
```

## 启用方式

确保 `.obsidian/community-plugins.json` 中包含：

```json
&quot;thino-linter-guard&quot;
```

修改插件后，需要重载 `Thino Linter Guard` 插件或重启 Obsidian。

&lt;!-- issues-blog:source=d1:738af6c0-1aea-4d9c-b772-d2f8ecf20586 --&gt;
&lt;!-- issues-blog:published-at=2026-06-29T00:00:00.000Z --&gt;
</description><pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;现象&lt;/h2&gt;
&lt;p&gt;在 Obsidian 的 Thino 页面中，进入页面或按下 &lt;code&gt;Cmd+S&lt;/code&gt; 后，输入框中会自动加上如下 frontmatter：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;YAML&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;published&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;tags&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;category&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;slug&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;date_created&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;20260603&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;---&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最开始看起来像是 Thino 写入日记时触发了格式化，因为 Thino 的内容确实存放在日记里。&lt;/p&gt;
&lt;h2&gt;第一层原因&lt;/h2&gt;
&lt;p&gt;直接原因来自 Obsidian Linter：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lintOnSave&lt;/code&gt; 开启。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lintOnFileChange&lt;/code&gt; 开启。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert-yaml-attributes&lt;/code&gt; 规则开启，会插入 &lt;code&gt;date/tags/category/slug/published/description/title&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yaml-timestamp&lt;/code&gt; 规则开启，会补 &lt;code&gt;date_created&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，只要 Linter 认为当前文件应该被 lint，就会把这套博客 frontmatter 补进去。&lt;/p&gt;
&lt;h2&gt;为什么忽略日记目录没用&lt;/h2&gt;
&lt;p&gt;一开始尝试把 &lt;code&gt;02-日记&lt;/code&gt; 加到 Linter 的 &lt;code&gt;foldersToIgnore&lt;/code&gt;。这个配置本身是正确的，Linter 的匹配逻辑是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;file.path.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;startsWith&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;normalizePath&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(folder) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;/&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，&lt;code&gt;02-日记/2026/06/2026-06-29.md&lt;/code&gt; 会被正确忽略。&lt;/p&gt;
&lt;p&gt;但这个方案没有解决问题，因为 Thino 页面在 Linter 眼里不是日记文件。&lt;/p&gt;
&lt;h2&gt;真正根因&lt;/h2&gt;
&lt;p&gt;临时调试插件记录到的关键运行时状态是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;activeView.type = thino_view&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;activeFile.path = index.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;shouldIgnoreActiveFile = false&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前视图确实是 Thino 的 &lt;code&gt;thino_view&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;但 Obsidian/Linter 暴露出来的 active file 是根目录 &lt;code&gt;index.md&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Linter 看到的是 &lt;code&gt;index.md&lt;/code&gt;，不是日记文件。&lt;/li&gt;
&lt;li&gt;所以忽略 &lt;code&gt;02-日记&lt;/code&gt; 拦不住。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这解释了两个现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按 &lt;code&gt;Cmd+S&lt;/code&gt; 时，Linter 会 lint &lt;code&gt;index.md&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;进入 Thino 页面时，&lt;code&gt;active-leaf-change&lt;/code&gt; / &lt;code&gt;editor-change&lt;/code&gt; 也可能触发 Linter 处理 &lt;code&gt;index.md&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最终方案&lt;/h2&gt;
&lt;p&gt;新增一个极小的本地 Obsidian 插件：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;.obsidian/plugins/thino-linter-guard/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;插件已发布到 GitHub：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/KazooTTT/thino-linter-guard&quot;&gt;https://github.com/KazooTTT/thino-linter-guard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;插件入口：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;.obsidian/plugins/thino-linter-guard/main.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 Obsidian 当前处在 Thino 页面时（active view 是 &lt;code&gt;thino_view&lt;/code&gt;），把这次 Linter 判断视为“应跳过”，避免它处理 Thino 暴露出来的虚拟 &lt;code&gt;index.md&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当 Obsidian 当前处在 Thino 页面时，且执行 &lt;code&gt;editor:save-file&lt;/code&gt; 时，临时关闭 Linter，保存结束后恢复。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样可以同时覆盖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进入 Thino 页面时触发的 Linter 链路。&lt;/li&gt;
&lt;li&gt;在 Thino 页面按 &lt;code&gt;Cmd+S&lt;/code&gt; 时触发的保存链路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通 Markdown 文件不受影响，仍然会正常走 Linter。&lt;/p&gt;
&lt;h2&gt;当前插件代码要点&lt;/h2&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;patchShouldIgnoreFile&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; linter&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.app.plugins?.plugins?.[&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;obsidian-linter&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;linter &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; typeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; linter.shouldIgnoreFile &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; original&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; linter.shouldIgnoreFile;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; wrapped&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;file&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (plugin.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;isActiveThinoView&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;()) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; original.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;call&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, file, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;args);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  linter.shouldIgnoreFile &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; wrapped;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;isActiveThinoView&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.app.workspace.activeLeaf?.view?.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;getViewType&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;?.() &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;thino_view&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启用方式&lt;/h2&gt;
&lt;p&gt;确保 &lt;code&gt;.obsidian/community-plugins.json&lt;/code&gt; 中包含：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;thino-linter-guard&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改插件后，需要重载 &lt;code&gt;Thino Linter Guard&lt;/code&gt; 插件或重启 Obsidian。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:23:29Z</updated><category>obsidian</category><category>linter</category><category>thino</category><category>AI 写作为主</category></item><item><title>衬线、字重与可变字体</title><link>https://blog.kazoottt.top/posts/78/</link><guid isPermaLink="false">issue-78</guid><description>字体不只是“长得好不好看”。在前端和设计里，字体至少会影响三件事：阅读体验、品牌气质、以及页面渲染是否稳定。

## 基础概念

先分清几组最容易混在一起的概念：字体风格、字体家族、字重和字体文件。

### 衬线体和无衬线体

字体最常见的分类是衬线体和无衬线体。

![衬线体和无衬线体对比](https://pictures.kazoottt.top/2026/06/20260622-294814286b1d62019ad59e679fd6b589.svg)

&gt; 图中红色部分就是 serif，也就是衬线。图片来源：[Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Serif_and_sans-serif_03.svg)。

**衬线体**是笔画末端带装饰的字体，比如宋体、Times New Roman。它通常更有书籍感、传统感，适合长文、正式出版物、标题排版。

**无衬线体**是笔画末端没有明显装饰的字体，比如黑体、苹方、思源黑体、阿里巴巴普惠体。它更干净、现代，屏幕显示更常见，适合网页、App、后台系统和数据看板。

简单判断：

- 宋体、Times New Roman：衬线体。
- 黑体、苹方、思源黑体、阿里巴巴普惠体：无衬线体。

### 字体家族、字重和字体文件

我们平时说“一个字体”，其实可能包含多个字体文件。比如一个字体家族里可能有：

- Regular：常规字重。
- Medium：中等字重。
- Semibold：半粗。
- Bold：粗体。

在 CSS 里，常见写法是：

```css
.title {
  font-family: &quot;Some Font&quot;;
  font-weight: 500;
}
```

但这句话是否真的会切到 Medium，取决于字体文件是怎么注册的。

## 可变字体与静态字体

字体文件的组织方式，大体可以分成可变字体和静态字体。这里的关键问题是：一个文件能不能覆盖连续变化的字重或字宽。

### 可变字体

**可变字体**是一种把多个字重、字宽、倾斜等变化能力放进一个字体文件里的字体格式。

![可变字体的多个设计轴](https://pictures.kazoottt.top/2026/06/20260622-5e2f5842da32696ad5df0659f4e68fa3.svg)

&gt; 自制示意图。这张图把可变字体的几个常见轴画出来了：`optical size`、`weight`、`width`。同一个字母 `a` 可以沿着不同轴连续变化。相关概念可参考 [Variable fonts on the web](https://web.dev/articles/variable-fonts)。

如果一个字体是可变字体，通常可以用一个文件连续调节字重：

```css
@font-face {
  font-family: &quot;Some Variable Font&quot;;
  src: url(&quot;./SomeVariableFont.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 300 800;
}

.title {
  font-family: &quot;Some Variable Font&quot;;
  font-weight: 642;
}
```

这里的 `642` 是可以成立的，因为可变字体内部支持连续变化。

### 静态字体

**不可变字体**也可以叫静态字体。它通常是多个固定文件：

```text
SomeFont-Regular.woff2
SomeFont-Medium.woff2
SomeFont-Bold.woff2
```

这种字体不能靠一个文件连续变化。浏览器只能在已注册的固定字重之间选择，或者在缺少目标字重时做“伪加粗”。

从网页加载角度看，两者的差异也很直观：

![静态字体和可变字体的文件数量差异](https://pictures.kazoottt.top/2026/06/20260622-b82704060547131782b346ee744f6eb4.svg)

&gt; 自制示意图。静态字体往往需要多个字体文件来覆盖不同字重和样式；可变字体可以把更多变化能力收进一个文件里。相关概念可参考 [Variable font - Wikipedia](https://en.wikipedia.org/wiki/Variable_font)。

### 阿里巴巴普惠体是哪一种

阿里巴巴普惠体通常是**不可变字体**，也就是静态字体家族。它提供多个固定字重文件，比如 Regular、Medium、Semibold、Bold 等。

所以它不是“一个文件里连续调节所有字重”的可变字体。要让 `font-weight` 正确工作，需要把不同字重文件正确注册给浏览器。

## 前端里的 `font-weight` 坑

到前端实现里，真正容易出问题的是：`font-weight` 看起来是字重语义，但它必须配合正确的 `@font-face` 注册才可靠。

### `font-weight` 不一定等于切到对应字体文件

这是最容易踩坑的地方。

如果只加载了 Regular 文件：

```css
@font-face {
  font-family: &quot;PuHuiTi-Regular&quot;;
  src: url(&quot;./AlibabaPuHuiTi-3-45-Light.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 400;
}
```

然后写：

```css
.title {
  font-family: &quot;PuHuiTi-Regular&quot;;
  font-weight: 500;
}
```

浏览器并不会凭空找到 Medium 文件。它可能会：

- 继续使用 Regular。
- 对 Regular 做伪加粗。
- 根据浏览器和系统策略做近似匹配。

这就是为什么 computed style 里看到 `font-weight: 500`，视觉上却不像真正的 Medium。

### 更推荐的注册方式

更理想的方式是：同一个字体家族下注册多个字重。

```css
@font-face {
  font-family: &quot;Alibaba PuHuiTi&quot;;
  src: url(&quot;./AlibabaPuHuiTi-3-45-Light.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: &quot;Alibaba PuHuiTi&quot;;
  src: url(&quot;./AlibabaPuHuiTi-3-65-Medium.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 500;
  font-style: normal;
}

@font-face {
  font-family: &quot;Alibaba PuHuiTi&quot;;
  src: url(&quot;./AlibabaPuHuiTi-3-75-SemiBold.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 600;
  font-style: normal;
}
```

这样组件里就可以只写语义：

```css
.body {
  font-family: &quot;Alibaba PuHuiTi&quot;, &quot;Helvetica Neue&quot;, Arial, sans-serif;
  font-weight: 400;
}

.title {
  font-family: &quot;Alibaba PuHuiTi&quot;, &quot;Helvetica Neue&quot;, Arial, sans-serif;
  font-weight: 500;
}
```

浏览器会在同一个 `font-family` 下，根据 `font-weight` 自动选择对应文件。

### 不太推荐的注册方式

有些项目会把不同字重注册成不同字体族：

```css
@font-face {
  font-family: &quot;PuHuiTi-Regular&quot;;
  src: url(&quot;./AlibabaPuHuiTi-3-45-Light.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 400;
}

@font-face {
  font-family: &quot;PuHuiTi-Medium&quot;;
  src: url(&quot;./AlibabaPuHuiTi-3-65-Medium.woff2&quot;) format(&quot;woff2&quot;);
  font-weight: 500;
}
```

这时如果想用 Medium，就必须同时切换 `font-family`：

```css
.title {
  font-family: &quot;PuHuiTi-Medium&quot;, &quot;PuHuiTi-Regular&quot;, &quot;Helvetica Neue&quot;, Arial, sans-serif;
  font-weight: 500;
}
```

这种写法不是不能用，但它会让组件层知道太多字体文件细节。长期看，最好还是收敛成同一个字体家族，靠 `font-weight` 表达字重语义。

### 验证字体是否真的生效

验证字体时，不要只看：

```js
getComputedStyle(el).fontWeight;
```

还要看：

```js
getComputedStyle(el).fontFamily;
```

如果视觉要求很严格，还需要实际截图或肉眼对比。因为 computed style 只能告诉你 CSS 结果，不一定能直接告诉你浏览器最终用了哪个字体文件。

## 总结

可变字体是“一个字体文件里有连续变化能力”；不可变字体是“多个固定字重文件组成一个字体家族”。

阿里巴巴普惠体通常属于不可变字体。使用它时，`font-weight: 500` 是否真的等于 Medium，取决于项目有没有把 Medium 文件正确注册到同一个 `font-family` 下。

## 参考资料

- [Serif - Wikipedia](https://en.wikipedia.org/wiki/Serif)：解释 serif 是附着在笔画末端的小线条或笔画。
- [Sans-serif - Wikipedia](https://en.wikipedia.org/wiki/Sans-serif)：解释 sans-serif 是没有 serif 的字形分类，也提到它常用于屏幕显示。
- [Variable font - Wikipedia](https://en.wikipedia.org/wiki/Variable_font)：解释可变字体可以在一个字体文件里存储连续范围的设计变体。
- [Variable fonts on the web](https://web.dev/articles/variable-fonts)：可变字体在网页中的使用说明。

&lt;!-- issues-blog:source=d1:ae66b276-dab4-4b2b-963f-08c22ac35d5d --&gt;
&lt;!-- issues-blog:published-at=2026-06-22T00:00:00.000Z --&gt;
</description><pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;字体不只是“长得好不好看”。在前端和设计里，字体至少会影响三件事：阅读体验、品牌气质、以及页面渲染是否稳定。&lt;/p&gt;
&lt;h2&gt;基础概念&lt;/h2&gt;
&lt;p&gt;先分清几组最容易混在一起的概念：字体风格、字体家族、字重和字体文件。&lt;/p&gt;
&lt;h3&gt;衬线体和无衬线体&lt;/h3&gt;
&lt;p&gt;字体最常见的分类是衬线体和无衬线体。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/06/20260622-294814286b1d62019ad59e679fd6b589.svg&quot; alt=&quot;衬线体和无衬线体对比&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;图中红色部分就是 serif，也就是衬线。图片来源：&lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Serif_and_sans-serif_03.svg&quot;&gt;Wikimedia Commons&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;衬线体&lt;/strong&gt;是笔画末端带装饰的字体，比如宋体、Times New Roman。它通常更有书籍感、传统感，适合长文、正式出版物、标题排版。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;无衬线体&lt;/strong&gt;是笔画末端没有明显装饰的字体，比如黑体、苹方、思源黑体、阿里巴巴普惠体。它更干净、现代，屏幕显示更常见，适合网页、App、后台系统和数据看板。&lt;/p&gt;
&lt;p&gt;简单判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宋体、Times New Roman：衬线体。&lt;/li&gt;
&lt;li&gt;黑体、苹方、思源黑体、阿里巴巴普惠体：无衬线体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;字体家族、字重和字体文件&lt;/h3&gt;
&lt;p&gt;我们平时说“一个字体”，其实可能包含多个字体文件。比如一个字体家族里可能有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Regular：常规字重。&lt;/li&gt;
&lt;li&gt;Medium：中等字重。&lt;/li&gt;
&lt;li&gt;Semibold：半粗。&lt;/li&gt;
&lt;li&gt;Bold：粗体。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 CSS 里，常见写法是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;.title&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Some Font&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这句话是否真的会切到 Medium，取决于字体文件是怎么注册的。&lt;/p&gt;
&lt;h2&gt;可变字体与静态字体&lt;/h2&gt;
&lt;p&gt;字体文件的组织方式，大体可以分成可变字体和静态字体。这里的关键问题是：一个文件能不能覆盖连续变化的字重或字宽。&lt;/p&gt;
&lt;h3&gt;可变字体&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;可变字体&lt;/strong&gt;是一种把多个字重、字宽、倾斜等变化能力放进一个字体文件里的字体格式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/06/20260622-5e2f5842da32696ad5df0659f4e68fa3.svg&quot; alt=&quot;可变字体的多个设计轴&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;自制示意图。这张图把可变字体的几个常见轴画出来了：&lt;code&gt;optical size&lt;/code&gt;、&lt;code&gt;weight&lt;/code&gt;、&lt;code&gt;width&lt;/code&gt;。同一个字母 &lt;code&gt;a&lt;/code&gt; 可以沿着不同轴连续变化。相关概念可参考 &lt;a href=&quot;https://web.dev/articles/variable-fonts&quot;&gt;Variable fonts on the web&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果一个字体是可变字体，通常可以用一个文件连续调节字重：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Some Variable Font&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./SomeVariableFont.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;300&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 800&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;.title&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Some Variable Font&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;642&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;642&lt;/code&gt; 是可以成立的，因为可变字体内部支持连续变化。&lt;/p&gt;
&lt;h3&gt;静态字体&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;不可变字体&lt;/strong&gt;也可以叫静态字体。它通常是多个固定文件：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SomeFont-Regular.woff2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SomeFont-Medium.woff2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SomeFont-Bold.woff2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种字体不能靠一个文件连续变化。浏览器只能在已注册的固定字重之间选择，或者在缺少目标字重时做“伪加粗”。&lt;/p&gt;
&lt;p&gt;从网页加载角度看，两者的差异也很直观：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/06/20260622-b82704060547131782b346ee744f6eb4.svg&quot; alt=&quot;静态字体和可变字体的文件数量差异&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;自制示意图。静态字体往往需要多个字体文件来覆盖不同字重和样式；可变字体可以把更多变化能力收进一个文件里。相关概念可参考 &lt;a href=&quot;https://en.wikipedia.org/wiki/Variable_font&quot;&gt;Variable font - Wikipedia&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;阿里巴巴普惠体是哪一种&lt;/h3&gt;
&lt;p&gt;阿里巴巴普惠体通常是&lt;strong&gt;不可变字体&lt;/strong&gt;，也就是静态字体家族。它提供多个固定字重文件，比如 Regular、Medium、Semibold、Bold 等。&lt;/p&gt;
&lt;p&gt;所以它不是“一个文件里连续调节所有字重”的可变字体。要让 &lt;code&gt;font-weight&lt;/code&gt; 正确工作，需要把不同字重文件正确注册给浏览器。&lt;/p&gt;
&lt;h2&gt;前端里的 &lt;code&gt;font-weight&lt;/code&gt; 坑&lt;/h2&gt;
&lt;p&gt;到前端实现里，真正容易出问题的是：&lt;code&gt;font-weight&lt;/code&gt; 看起来是字重语义，但它必须配合正确的 &lt;code&gt;@font-face&lt;/code&gt; 注册才可靠。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;font-weight&lt;/code&gt; 不一定等于切到对应字体文件&lt;/h3&gt;
&lt;p&gt;这是最容易踩坑的地方。&lt;/p&gt;
&lt;p&gt;如果只加载了 Regular 文件：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;PuHuiTi-Regular&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./AlibabaPuHuiTi-3-45-Light.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;400&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后写：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;.title&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;PuHuiTi-Regular&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器并不会凭空找到 Medium 文件。它可能会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;继续使用 Regular。&lt;/li&gt;
&lt;li&gt;对 Regular 做伪加粗。&lt;/li&gt;
&lt;li&gt;根据浏览器和系统策略做近似匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是为什么 computed style 里看到 &lt;code&gt;font-weight: 500&lt;/code&gt;，视觉上却不像真正的 Medium。&lt;/p&gt;
&lt;h3&gt;更推荐的注册方式&lt;/h3&gt;
&lt;p&gt;更理想的方式是：同一个字体家族下注册多个字重。&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Alibaba PuHuiTi&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./AlibabaPuHuiTi-3-45-Light.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;400&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-style&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;normal&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Alibaba PuHuiTi&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./AlibabaPuHuiTi-3-65-Medium.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-style&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;normal&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Alibaba PuHuiTi&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./AlibabaPuHuiTi-3-75-SemiBold.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;600&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-style&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;normal&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样组件里就可以只写语义：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;.body&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Alibaba PuHuiTi&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Helvetica Neue&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, Arial, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;sans-serif&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;400&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;.title&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Alibaba PuHuiTi&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Helvetica Neue&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, Arial, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;sans-serif&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器会在同一个 &lt;code&gt;font-family&lt;/code&gt; 下，根据 &lt;code&gt;font-weight&lt;/code&gt; 自动选择对应文件。&lt;/p&gt;
&lt;h3&gt;不太推荐的注册方式&lt;/h3&gt;
&lt;p&gt;有些项目会把不同字重注册成不同字体族：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;PuHuiTi-Regular&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./AlibabaPuHuiTi-3-45-Light.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;400&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;@font-face&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;PuHuiTi-Medium&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./AlibabaPuHuiTi-3-65-Medium.woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;woff2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时如果想用 Medium，就必须同时切换 &lt;code&gt;font-family&lt;/code&gt;：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;CSS&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;.title&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-family&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;PuHuiTi-Medium&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;PuHuiTi-Regular&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Helvetica Neue&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, Arial, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;sans-serif&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  font-weight&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法不是不能用，但它会让组件层知道太多字体文件细节。长期看，最好还是收敛成同一个字体家族，靠 &lt;code&gt;font-weight&lt;/code&gt; 表达字重语义。&lt;/p&gt;
&lt;h3&gt;验证字体是否真的生效&lt;/h3&gt;
&lt;p&gt;验证字体时，不要只看：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;getComputedStyle&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(el).fontWeight;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还要看：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;getComputedStyle&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(el).fontFamily;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果视觉要求很严格，还需要实际截图或肉眼对比。因为 computed style 只能告诉你 CSS 结果，不一定能直接告诉你浏览器最终用了哪个字体文件。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;可变字体是“一个字体文件里有连续变化能力”；不可变字体是“多个固定字重文件组成一个字体家族”。&lt;/p&gt;
&lt;p&gt;阿里巴巴普惠体通常属于不可变字体。使用它时，&lt;code&gt;font-weight: 500&lt;/code&gt; 是否真的等于 Medium，取决于项目有没有把 Medium 文件正确注册到同一个 &lt;code&gt;font-family&lt;/code&gt; 下。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Serif&quot;&gt;Serif - Wikipedia&lt;/a&gt;：解释 serif 是附着在笔画末端的小线条或笔画。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Sans-serif&quot;&gt;Sans-serif - Wikipedia&lt;/a&gt;：解释 sans-serif 是没有 serif 的字形分类，也提到它常用于屏幕显示。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Variable_font&quot;&gt;Variable font - Wikipedia&lt;/a&gt;：解释可变字体可以在一个字体文件里存储连续范围的设计变体。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/variable-fonts&quot;&gt;Variable fonts on the web&lt;/a&gt;：可变字体在网页中的使用说明。&lt;/li&gt;
&lt;/ul&gt;

</content:encoded><updated>2026-07-24T10:23:22Z</updated><category>frontend</category><category>font</category><category>typography</category><category>AI 写作为主</category></item><item><title>最近的变化-锻炼身体、调整学习状态</title><link>https://blog.kazoottt.top/posts/77/</link><guid isPermaLink="false">issue-77</guid><description>最近受到一些主播的影响，我开始重新把运动和学习放回生活里。

## 身体先动起来

### 跳舞

我去年 5 月左右开始学 Hip-Hop。那时候先办了年卡，但之前的舞室空调效果不太好，夏天上课非常热，后来我就退掉了卡。再之后搬到了另一个区，中间歇了两三个月，到 12 月才重新开始上课。

这一次我办的是次卡，算下来一节课差不多 50 元。当时简单算了一下，如果一周只上两到三节课，次卡和年卡的总价差不多，甚至还可能更低，所以我最后选择了次卡。

新舞室的课程安排里，爵士和其他舞种更多，Hip-Hop 相对少一些。刚开始我想着可以先从爵士基本功和一些爵士成品舞学起，但上了二三十节之后，发现自己对爵士的兴趣并不算强。最后还是决定回到 Hip-Hop，从基本功开始补，一边上基本功课，一边上一些编舞课。

目前能感觉到自己有一点进步，但进步速度不算快。主要原因也很明确：我私下几乎不会主动跟练 Hip-Hop 基本功，所有练习都集中在课堂里的一个小时左右。这样当然能进步，但上限会比较明显。要不要把课外练习也加进来，可能是我接下来需要认真考虑的事情。

hiphop 跟练目前在看的是 [Zingo - 小红书](&lt;https://www.xiaohongshu.com/user/profile/5d55e82f0000000001002c88?xsec_token=ABdt8UuqpUDZIwBnMO5qXL_Z75ok5sykn-RV-lSqLf9-g%3D&amp;xsec_source=pc_search&gt;) 

![Pasted image 20260613220158](&lt;https://pictures.kazoottt.top/2026/06/20260613-2e36da1635a62d7f9f5acd45ef5502b9.png&gt;)

### 健身

除了跳舞，我最近也重新开始健身。之前去的是乐刻，一个月可能也去不了几次。虽然很早之前买过私教课，但一周最多两次，强度并不高；加上我没有控制饮食，所以减脂的效果都不明显，后来也就断掉了。

这次重新开始健身，契机是 5 月劳动节去了一趟泰国。那几天我完全没有控制饮食，饮料一天能喝两三杯，吃东西也总想着多试几种，于是经常点很多菜。回来之后明显感觉身体状态变差，肚子上有了赘肉，整个人也比较虚。再想到很久以前自己其实能看到马甲线，这种反差让我觉得，还是应该把身体状态重新捡起来。

后来我去了同事推荐的一家商业健身房。价格上，一年卡大概 1500 多，三年卡 1800 到 1900 左右，差距不算大，我就直接买了三年卡。

为了尽快把训练强度提上来，也希望有人帮我制定更科学的训练和饮食计划，我还是报了私教课。现在的频率差不多是一周三次，饮食也在控制：少油少盐，多吃蛋白质，碳水照常吃，但比以前少一些。坚持了三周左右之后，肚子的线条已经变得更明显，体脂率和体重也都有下降。

健身之后，我能明显感觉到状态更好，人也更自信。唯一的问题是，如果晚上做了无氧训练，会比较难睡着。后面可能要调整训练安排，比如早上做无氧，晚上下班后去爬坡，降低对睡眠的影响。

为了后续可以脱离私教自己定制训练计划，我在上课期间也进行了记录。

![Pasted image 20260613220522](&lt;https://pictures.kazoottt.top/2026/06/20260613-f036fd679d9b479f61ef6afd89aa71b8.png&gt;)

主要分为这几个部分，会把每次上课的内容记录下，如果学习到新的动作，会记录下这个动作练习的部位，便于后续自己进行组合。

## 重新进入学习状态

除了跳舞和健身，我最近也想重新找回学习状态。回到成都之后，有很长一段时间我都比较躺平，不太想主动推进什么事情。主要和几个原因有关。一方面是职业上的迷茫感越来越强。我越来越觉得现在的工作缺少意义，也看不到很明确的未来方向，不太清楚自己应该往哪里努力。

不过学习状态的变化的契机主要是，工作内容也发生了变化。以前更偏纯前端，现在变成了前端、后端和 AI 应用开发都要做。AI 确实能帮我提高一部分开发效率，但工程层面的知识还是绕不开，比如 MQ、ES 这种比较基础的后端知识。如果这些东西不补上，平时开发可能暂时能跑起来，一旦遇到 Bug，就很容易卡在排查阶段。

在 AI 开发里也遇到了一些阻塞性问题。很多时候，如果没有一套完整的方法论，开发过程就会像是在做随机实验：改一个地方，看看效果有没有变好；再改另一个地方，再继续猜。这种方式很不健康，也很难积累稳定的判断力。

所以我最近想学的内容会更偏后端、AI 框架、系统设计和架构方法论。需要知道知道某个工具怎么用，以及对应的优缺点、局限性、适用场景。这样以后做全栈以及 AI 应用开发时，才知道知道应该怎么下手。

最近在读的东西是：《解构领域驱动设计》以及 [GitHub - datawhalechina/Agent-Learning-Hub: AI Agent 学习路线与资料库收集](&lt;https://github.com/datawhalechina/Agent-Learning-Hub&gt;)

## 后续想做的事情

除了运动和学习，我也想把之前的一些兴趣爱好重新拾起来。

我从 2011 年就开始学吉他。最初的想法是学古典吉他，那个时候被同学弹的《爱的罗曼史》以及《小罗曼史》惊艳到，但入门时是从民谣开始的。可能当时我去琴行的时候比较羞于表达自己的需求，老师就默认我想学民谣，于是带着我学了一些弹唱内容，后面还考了级。

后来我还是想学古典吉他，但那个阶段基本已经进入自学状态。再之后，我又尝试过一些电吉他的东西。几种类型都试过之后，我发现比较适合自己的方向主要有两个：一个是用民谣吉他做指弹，另一个是用电吉他弹一些 solo 主音和节奏相关的内容。

现在的问题是，我已经很久没有碰吉他了。民谣吉他和电吉他都在家里放了很久，所以接下来我想重新开始练习，至少先把手感找回来。

摄影也是类似的问题。我买了不少设备，比如运动相机、全景相机，还有两台 M43 画幅的相机，分别是 GX9 和 G92，镜头也比较全。但真正产出内容时，无论是视频还是照片，我都觉得质量不够好。

更严重的是我发现自己有一点向器材党发展的趋势。只要拍得不好，就会下意识觉得是设备的问题，于是经常去看全画幅相机有没有降价，看各种测评，想着是不是再买一台全画幅。

但如果只是记录生活，真正重要的应该是把现有设备用好。生活记录并不一定需要特别高的像素，也不一定需要顶级光学素质。对我来说，更该练的是如何观察身边的东西，如何讲好发生在自己身上的故事，以及如何提升摄影、剪辑和拍摄 Vlog 的能力。

与其继续看测评、给自己制造新的消费需求，不如先把表达能力练起来。

## 总结

总体来说，我现在的状态比前两年好了很多，之前有单纯摆烂了，最近重新运动、重新学习之后，我越来越觉得，未来还有很多东西值得探索。

当我还不知道未来具体要做什么时，学习和锻炼至少是一种相对确定的投入。它们未必能立刻给出答案，但能让我保持更好的身体状态或者思考的能力。

如果一直把时间花在碎片化、娱乐化的消费里，几年之后，我可能只是变成一个依然迷茫、身体状态也不太好的三十几岁的人。

希望自己可以保持这种自我提升然后对未来抱有期待的状态。

&lt;!-- issues-blog:source=d1:post.recent-changes --&gt;
&lt;!-- issues-blog:published-at=2026-06-13T00:00:00.000Z --&gt;
</description><pubDate>Sat, 13 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近受到一些主播的影响，我开始重新把运动和学习放回生活里。&lt;/p&gt;
&lt;h2&gt;身体先动起来&lt;/h2&gt;
&lt;h3&gt;跳舞&lt;/h3&gt;
&lt;p&gt;我去年 5 月左右开始学 Hip-Hop。那时候先办了年卡，但之前的舞室空调效果不太好，夏天上课非常热，后来我就退掉了卡。再之后搬到了另一个区，中间歇了两三个月，到 12 月才重新开始上课。&lt;/p&gt;
&lt;p&gt;这一次我办的是次卡，算下来一节课差不多 50 元。当时简单算了一下，如果一周只上两到三节课，次卡和年卡的总价差不多，甚至还可能更低，所以我最后选择了次卡。&lt;/p&gt;
&lt;p&gt;新舞室的课程安排里，爵士和其他舞种更多，Hip-Hop 相对少一些。刚开始我想着可以先从爵士基本功和一些爵士成品舞学起，但上了二三十节之后，发现自己对爵士的兴趣并不算强。最后还是决定回到 Hip-Hop，从基本功开始补，一边上基本功课，一边上一些编舞课。&lt;/p&gt;
&lt;p&gt;目前能感觉到自己有一点进步，但进步速度不算快。主要原因也很明确：我私下几乎不会主动跟练 Hip-Hop 基本功，所有练习都集中在课堂里的一个小时左右。这样当然能进步，但上限会比较明显。要不要把课外练习也加进来，可能是我接下来需要认真考虑的事情。&lt;/p&gt;
&lt;p&gt;hiphop 跟练目前在看的是 &lt;a href=&quot;https://www.xiaohongshu.com/user/profile/5d55e82f0000000001002c88?xsec_token=ABdt8UuqpUDZIwBnMO5qXL_Z75ok5sykn-RV-lSqLf9-g%3D&amp;#x26;xsec_source=pc_search&quot;&gt;Zingo - 小红书&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/06/20260613-2e36da1635a62d7f9f5acd45ef5502b9.png&quot; alt=&quot;Pasted image 20260613220158&quot;&gt;&lt;/p&gt;
&lt;h3&gt;健身&lt;/h3&gt;
&lt;p&gt;除了跳舞，我最近也重新开始健身。之前去的是乐刻，一个月可能也去不了几次。虽然很早之前买过私教课，但一周最多两次，强度并不高；加上我没有控制饮食，所以减脂的效果都不明显，后来也就断掉了。&lt;/p&gt;
&lt;p&gt;这次重新开始健身，契机是 5 月劳动节去了一趟泰国。那几天我完全没有控制饮食，饮料一天能喝两三杯，吃东西也总想着多试几种，于是经常点很多菜。回来之后明显感觉身体状态变差，肚子上有了赘肉，整个人也比较虚。再想到很久以前自己其实能看到马甲线，这种反差让我觉得，还是应该把身体状态重新捡起来。&lt;/p&gt;
&lt;p&gt;后来我去了同事推荐的一家商业健身房。价格上，一年卡大概 1500 多，三年卡 1800 到 1900 左右，差距不算大，我就直接买了三年卡。&lt;/p&gt;
&lt;p&gt;为了尽快把训练强度提上来，也希望有人帮我制定更科学的训练和饮食计划，我还是报了私教课。现在的频率差不多是一周三次，饮食也在控制：少油少盐，多吃蛋白质，碳水照常吃，但比以前少一些。坚持了三周左右之后，肚子的线条已经变得更明显，体脂率和体重也都有下降。&lt;/p&gt;
&lt;p&gt;健身之后，我能明显感觉到状态更好，人也更自信。唯一的问题是，如果晚上做了无氧训练，会比较难睡着。后面可能要调整训练安排，比如早上做无氧，晚上下班后去爬坡，降低对睡眠的影响。&lt;/p&gt;
&lt;p&gt;为了后续可以脱离私教自己定制训练计划，我在上课期间也进行了记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/06/20260613-f036fd679d9b479f61ef6afd89aa71b8.png&quot; alt=&quot;Pasted image 20260613220522&quot;&gt;&lt;/p&gt;
&lt;p&gt;主要分为这几个部分，会把每次上课的内容记录下，如果学习到新的动作，会记录下这个动作练习的部位，便于后续自己进行组合。&lt;/p&gt;
&lt;h2&gt;重新进入学习状态&lt;/h2&gt;
&lt;p&gt;除了跳舞和健身，我最近也想重新找回学习状态。回到成都之后，有很长一段时间我都比较躺平，不太想主动推进什么事情。主要和几个原因有关。一方面是职业上的迷茫感越来越强。我越来越觉得现在的工作缺少意义，也看不到很明确的未来方向，不太清楚自己应该往哪里努力。&lt;/p&gt;
&lt;p&gt;不过学习状态的变化的契机主要是，工作内容也发生了变化。以前更偏纯前端，现在变成了前端、后端和 AI 应用开发都要做。AI 确实能帮我提高一部分开发效率，但工程层面的知识还是绕不开，比如 MQ、ES 这种比较基础的后端知识。如果这些东西不补上，平时开发可能暂时能跑起来，一旦遇到 Bug，就很容易卡在排查阶段。&lt;/p&gt;
&lt;p&gt;在 AI 开发里也遇到了一些阻塞性问题。很多时候，如果没有一套完整的方法论，开发过程就会像是在做随机实验：改一个地方，看看效果有没有变好；再改另一个地方，再继续猜。这种方式很不健康，也很难积累稳定的判断力。&lt;/p&gt;
&lt;p&gt;所以我最近想学的内容会更偏后端、AI 框架、系统设计和架构方法论。需要知道知道某个工具怎么用，以及对应的优缺点、局限性、适用场景。这样以后做全栈以及 AI 应用开发时，才知道知道应该怎么下手。&lt;/p&gt;
&lt;p&gt;最近在读的东西是：《解构领域驱动设计》以及 &lt;a href=&quot;https://github.com/datawhalechina/Agent-Learning-Hub&quot;&gt;GitHub - datawhalechina/Agent-Learning-Hub: AI Agent 学习路线与资料库收集&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;后续想做的事情&lt;/h2&gt;
&lt;p&gt;除了运动和学习，我也想把之前的一些兴趣爱好重新拾起来。&lt;/p&gt;
&lt;p&gt;我从 2011 年就开始学吉他。最初的想法是学古典吉他，那个时候被同学弹的《爱的罗曼史》以及《小罗曼史》惊艳到，但入门时是从民谣开始的。可能当时我去琴行的时候比较羞于表达自己的需求，老师就默认我想学民谣，于是带着我学了一些弹唱内容，后面还考了级。&lt;/p&gt;
&lt;p&gt;后来我还是想学古典吉他，但那个阶段基本已经进入自学状态。再之后，我又尝试过一些电吉他的东西。几种类型都试过之后，我发现比较适合自己的方向主要有两个：一个是用民谣吉他做指弹，另一个是用电吉他弹一些 solo 主音和节奏相关的内容。&lt;/p&gt;
&lt;p&gt;现在的问题是，我已经很久没有碰吉他了。民谣吉他和电吉他都在家里放了很久，所以接下来我想重新开始练习，至少先把手感找回来。&lt;/p&gt;
&lt;p&gt;摄影也是类似的问题。我买了不少设备，比如运动相机、全景相机，还有两台 M43 画幅的相机，分别是 GX9 和 G92，镜头也比较全。但真正产出内容时，无论是视频还是照片，我都觉得质量不够好。&lt;/p&gt;
&lt;p&gt;更严重的是我发现自己有一点向器材党发展的趋势。只要拍得不好，就会下意识觉得是设备的问题，于是经常去看全画幅相机有没有降价，看各种测评，想着是不是再买一台全画幅。&lt;/p&gt;
&lt;p&gt;但如果只是记录生活，真正重要的应该是把现有设备用好。生活记录并不一定需要特别高的像素，也不一定需要顶级光学素质。对我来说，更该练的是如何观察身边的东西，如何讲好发生在自己身上的故事，以及如何提升摄影、剪辑和拍摄 Vlog 的能力。&lt;/p&gt;
&lt;p&gt;与其继续看测评、给自己制造新的消费需求，不如先把表达能力练起来。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;总体来说，我现在的状态比前两年好了很多，之前有单纯摆烂了，最近重新运动、重新学习之后，我越来越觉得，未来还有很多东西值得探索。&lt;/p&gt;
&lt;p&gt;当我还不知道未来具体要做什么时，学习和锻炼至少是一种相对确定的投入。它们未必能立刻给出答案，但能让我保持更好的身体状态或者思考的能力。&lt;/p&gt;
&lt;p&gt;如果一直把时间花在碎片化、娱乐化的消费里，几年之后，我可能只是变成一个依然迷茫、身体状态也不太好的三十几岁的人。&lt;/p&gt;
&lt;p&gt;希望自己可以保持这种自我提升然后对未来抱有期待的状态。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:23:15Z</updated><category>摄影</category><category>健身</category><category>生活复盘</category><category>舞蹈</category><category>学习</category><category>人工写作为主</category></item><item><title>Axios 全局 json-bigint 解析导致空原型对象渲染崩溃</title><link>https://blog.kazoottt.top/posts/76/</link><guid isPermaLink="false">issue-76</guid><description>## 问题现象

在 React 表单详情页中,页面加载时突然崩溃:

```text
react-router-dom.js?v=cdf9f56e:6232 Error handled by React Router default ErrorBoundary: TypeError: Cannot convert object to primitive value
    at String (&lt;anonymous&gt;)
    at chunk-3GKRE62E.js?v=cdf9f56e:9785:69
    at Object.react_stack_bottom_frame (chunk-Z3WXWQW3.js?v=cdf9f56e:18509:20)
    at renderWithHooks (chunk-Z3WXWQW3.js?v=cdf9f56e:5654:24)
    at updateForwardRef (chunk-Z3WXWQW3.js?v=cdf9f56e:7198:21)
    at beginWork (chunk-Z3WXWQW3.js?v=cdf9f56e:8735:20)
    at runWithFiberInDEV (chunk-Z3WXWQW3.js?v=cdf9f56e:997:72)
    at performUnitOfWork (chunk-Z3WXWQW3.js?v=cdf9f56e:12561:98)
    at workLoopSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12424:43)
    at renderRootSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12408:13)
DefaultErrorComponent	@	react-router-dom.js?v=cdf9f56e:6232
react_stack_bottom_frame	@	chunk-Z3WXWQW3.js?v=cdf9f56e:18509
renderWithHooks	@	chunk-Z3WXWQW3.js?v=cdf9f56e:5654
updateFunctionComponent	@	chunk-Z3WXWQW3.js?v=cdf9f56e:7475
beginWork	@	chunk-Z3WXWQW3.js?v=cdf9f56e:8525
runWithFiberInDEV	@	chunk-Z3WXWQW3.js?v=cdf9f56e:997
performUnitOfWork	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12561
workLoopSync	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12424
renderRootSync	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12408
performWorkOnRoot	@	chunk-Z3WXWQW3.js?v=cdf9f56e:11827
performWorkOnRootViaSchedulerTask	@	chunk-Z3WXWQW3.js?v=cdf9f56e:13505
performWorkUntilDeadline	@	chunk-Z3WXWQW3.js?v=cdf9f56e:36
react-router-dom.js?v=cdf9f56e:6232 Error handled by React Router default ErrorBoundary: TypeError: Cannot convert object to primitive value
    at String (&lt;anonymous&gt;)
    at chunk-3GKRE62E.js?v=cdf9f56e:9785:69
    at Object.react_stack_bottom_frame (chunk-Z3WXWQW3.js?v=cdf9f56e:18509:20)
    at renderWithHooks (chunk-Z3WXWQW3.js?v=cdf9f56e:5654:24)
    at updateForwardRef (chunk-Z3WXWQW3.js?v=cdf9f56e:7198:21)
    at beginWork (chunk-Z3WXWQW3.js?v=cdf9f56e:8735:20)
    at runWithFiberInDEV (chunk-Z3WXWQW3.js?v=cdf9f56e:997:72)
    at performUnitOfWork (chunk-Z3WXWQW3.js?v=cdf9f56e:12561:98)
    at workLoopSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12424:43)
    at renderRootSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12408:13)
DetailForm.tsx:1273 TypeError: Cannot convert object to primitive value
    at String (&lt;anonymous&gt;)

The above error occurred in the &lt;ForwardRef&gt; component.

React will try to recreate this component tree from scratch using the error boundary you provided, AppErrorBoundary.
react-router-dom.js?v=cdf9f56e:6271 React Router caught the following error during render TypeError: Cannot convert object to primitive value
```

```text
TypeError: Cannot convert object to primitive value
    at String (&lt;anonymous&gt;)
```

错误最终落在 Ant Design 的 `&lt;Input /&gt;` 渲染链路中。业务代码中存在一个隐藏表单项,用来保存联动配置对象:

```tsx
&lt;Form.Item name={[...formPrefix, configName, &quot;nextConfig&quot;]} hidden&gt;
  &lt;Input /&gt;
&lt;/Form.Item&gt;
```

`nextConfig` 并不是字符串,而是一整个对象。此前页面可以正常运行,为什么突然开始报错?

## 背景: 为了解决大整数 ID 精度丢失

后端部分业务 ID 可能超过 JavaScript 的安全整数范围:

```text
Number.MAX_SAFE_INTEGER = 9007199254740991
```

例如:

```json
{
  &quot;groupId&quot;: 9223372036854775807
}
```

如果使用浏览器原生 `JSON.parse`,这个 ID 会在解析时丢失精度。事后复盘,其实最合理的做法是让后端直接将这类 ID 序列化为字符串,前端就完全不需要特殊处理了。但当时后端短期内无法调整接口,只能前端兜底,于是我在 Axios 实例中全局启用了 `json-bigint`:

```ts
const JSONbigParser = JSONbig({ storeAsString: true });

export const AXIOS_INSTANCE = Axios.create({
  transformResponse: [
    (data: string) =&gt; {
      try {
        return JSONbigParser.parse(data);
      } catch {
        return data;
      }
    },
  ],
});
```

这个改动看起来很合理:

- 超出精度范围的整数会保存为字符串
- 普通数字仍然保持数字类型
- 所有接口自动获得大整数兼容能力

但是正是这种全部接口的出参都替换导致了 bug

## 根因:json-bigint 返回空原型对象

这两种对象很像,但并不等价。

这不是推测,而是 `json-bigint` 在解析 JSON 对象的时候，[sidorares/json-bigint](https://github.com/sidorares/json-bigint) 的 [`lib/parse.js`](https://github.com/sidorares/json-bigint/blob/master/lib/parse.js) 中, `object()` 在解析每个 `{ ... }` 时显式写入:

```js
object = function () {
  // Parse an object value.

  var key,
    object = Object.create(null);

  if (ch === &apos;{&apos;) {
    next(&apos;{&apos;);
    // ...
```

所以经过 json-bigint 的对象，是通过 Object.create(null) 创建的。

然后普通对象继承了 `Object.prototype`:

```ts
const normal = {};

String(normal);
// &quot;[object Object]&quot;
```

空原型对象没有继承 `toString`、`valueOf` 等方法:

```ts
const parsed = Object.create(null);

String(parsed);
// TypeError: Cannot convert object to primitive value
```

因此,全局替换 Axios 响应解析器后,不只是超大整数发生了变化,所有接口返回的普通 JSON 对象也悄悄变成了空原型对象。

当 Ant Design 的 `&lt;Input /&gt;` 尝试将隐藏表单项中的 `nextConfig` 对象转换成字符串时,由于原型没有 toString() 就崩溃了。

```tsx
&lt;Form.Item name={[...formPrefix, configName, &quot;nextConfig&quot;]} hidden&gt;
  &lt;Input /&gt;  {/* value 会被注入为 nextConfig 对象本身 */}
&lt;/Form.Item&gt;
```

然后由于 nextConfig 对应的 value 是一个对象，通过受控组件的写法注入了 input。此时 `&lt;Input /&gt;` 收到的 `value` 就是 `nextConfig` 这个对象 , 且是由 `json-bigint` 解析出来的**空原型对象**。

### `&lt;Input /&gt;` 内部发生了什么？

Ant Design 的 `Input` 底层是对原生 `&lt;input&gt;` 元素的封装。当它处理 `value` 时,内部大致会经过这样的路径:

1. **将 value 写入 DOM**: React 将 `value` 赋给原生 `&lt;input&gt;` 元素的 `.value` 属性。DOM API 要求 `.value` 必须是字符串,因此浏览器会隐式调用 `String(value)` 进行转换。
2. **`String()` 的类型转换规则**: 当 `String()` 遇到一个对象时,JavaScript 引擎按 [`ToPrimitive`](https://tc39.es/ecma262/#sec-toprimitive) 尝试:
   - 先调用 `obj[Symbol.toPrimitive](&quot;string&quot;)`（如果有的话）
   - 再调用 `obj.toString()` —— 期望返回一个 primitive
   - 再调用 `obj.valueOf()` —— 期望返回一个 primitive
   - 如果以上都失败或返回的不是 primitive,抛出 `TypeError`（见 [`String`](https://tc39.es/ecma262/#sec-tostring)）

3. **空原型对象的致命问题**: 通过 `Object.create(null)` 创建的对象,原型链是空的。它没有继承 `Object.prototype` 上的 `toString()` 和 `valueOf()` 方法:

   ```
   普通对象:        obj → Object.prototype → null
                    ✅ toString() 可用 → &quot;[object Object]&quot;

   空原型对象:       obj → null
                    ❌ 没有 toString(), 没有 valueOf()
   ```

4. **崩溃点**: 浏览器调用 `String(nextConfig)` → 找不到 `toString()` → 找不到 `valueOf()` → 抛出 `TypeError: Cannot convert object to primitive value`。

### 为什么此前页面可以正常运行？

虽然把对象塞进 `&lt;Input /&gt;` 本身就不是规范的做法,但之前因为普通对象能隐式转换为字符串,所以 &quot; 巧合地 &quot; 没出问题。引入 `json-bigint` 后,所有接口返回的嵌套对象都变成了空原型对象,这个隐藏的隐患才被触发。

在引入 `json-bigint` 之前,Axios 使用浏览器原生的 `JSON.parse` 解析响应。原生 `JSON.parse` 返回的是普通对象（继承自 `Object.prototype`）,因此:

```js
// 原生 JSON.parse 返回的普通对象
String({ a: 1 });  // &quot;[object Object]&quot;  ✅ 正常

// json-bigint 返回的空原型对象
String(Object.create(null));  // TypeError ❌ 崩溃
```

## 修复思路

### 最好的方法：后端 ID 字段返回 string

推动后端将超出 JS 安全整数范围的 ID 直接序列化为字符串:

```json
{
  &quot;groupId&quot;: &quot;9223372036854775807&quot;
}
```

收益:

- 前端不需要 `json-bigint`,也没有空原型对象的副作用
- 字段语义明确,Web / 移动端 / 小程序一致,不依赖路由表或启发式
- 后端通常只需调整序列化配置,改动成本低

这是唯一真正「字段层面」且没有精度竞态的方案。下文过渡方案仅在后端尚未配合时使用。

### 过渡方案: 按响应内容启用 json-bigint

需要去判断 **原始 JSON 字符串里是否可能出现超大整数**。

#### 解析工具

```ts
import JSONbig from &quot;json-bigint&quot;;

const JSONbigParser = JSONbig({ storeAsString: true });

/** 启发式: JSON 中出现 16 位及以上的整数,可能超出 Number.MAX_SAFE_INTEGER */
const MAY_HAVE_UNSAFE_INT = /:\s*-?\d{16,}(?:[,}\]\s]|$)/;

export function needsBigIntParse(raw = &quot;&quot;): boolean {
  return MAY_HAVE_UNSAFE_INT.test(raw);
}

/** json-bigint 解析,并将空原型对象恢复为普通对象 */
export function parseBigIntResponse(data: unknown): unknown {
  if (typeof data !== &quot;string&quot;) return data;

  try {
    return JSONbigParser.parse(data, (_key: string, value: unknown) =&gt; {
      if (value &amp;&amp; typeof value === &quot;object&quot; &amp;&amp; !Array.isArray(value)) {
        return { ...value };
      }
      return value;
    });
  } catch {
    return data;
  }
}

export function parseJsonResponse(data: unknown): unknown {
  if (typeof data !== &quot;string&quot;) return data;

  try {
    if (needsBigIntParse(data)) {
      return parseBigIntResponse(data);
    }
    return JSON.parse(data);
  } catch {
    return data;
  }
}
```

`reviver` 中的 `{ ...value }` 必不可少: 只要走了 `json-bigint`,嵌套 object 就是 null 原型,必须还原,否则仍可能触发隐式 `String()` 崩溃。

## 总结

这次问题的核心是: **引入中间件或者做出会影响全局的变更的时候需要谨慎。**

URL 白名单、字段层面特殊处理以及全局 `json-bigint` 都只是后端未改前的临时手段,不应该长期使用。

&lt;!-- issues-blog:source=d1:post.axios-global-json-bigint-null-prototype-object-render-crash --&gt;
&lt;!-- issues-blog:published-at=2026-06-01T00:00:00.000Z --&gt;
</description><pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题现象&lt;/h2&gt;
&lt;p&gt;在 React 表单详情页中,页面加载时突然崩溃:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;react-router-dom.js?v=cdf9f56e:6232 Error handled by React Router default ErrorBoundary: TypeError: Cannot convert object to primitive value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at String (&amp;#x3C;anonymous&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at chunk-3GKRE62E.js?v=cdf9f56e:9785:69&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at Object.react_stack_bottom_frame (chunk-Z3WXWQW3.js?v=cdf9f56e:18509:20)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at renderWithHooks (chunk-Z3WXWQW3.js?v=cdf9f56e:5654:24)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at updateForwardRef (chunk-Z3WXWQW3.js?v=cdf9f56e:7198:21)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at beginWork (chunk-Z3WXWQW3.js?v=cdf9f56e:8735:20)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at runWithFiberInDEV (chunk-Z3WXWQW3.js?v=cdf9f56e:997:72)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at performUnitOfWork (chunk-Z3WXWQW3.js?v=cdf9f56e:12561:98)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at workLoopSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12424:43)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at renderRootSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12408:13)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DefaultErrorComponent	@	react-router-dom.js?v=cdf9f56e:6232&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;react_stack_bottom_frame	@	chunk-Z3WXWQW3.js?v=cdf9f56e:18509&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;renderWithHooks	@	chunk-Z3WXWQW3.js?v=cdf9f56e:5654&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;updateFunctionComponent	@	chunk-Z3WXWQW3.js?v=cdf9f56e:7475&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;beginWork	@	chunk-Z3WXWQW3.js?v=cdf9f56e:8525&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;runWithFiberInDEV	@	chunk-Z3WXWQW3.js?v=cdf9f56e:997&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;performUnitOfWork	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12561&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;workLoopSync	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12424&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;renderRootSync	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12408&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;performWorkOnRoot	@	chunk-Z3WXWQW3.js?v=cdf9f56e:11827&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;performWorkOnRootViaSchedulerTask	@	chunk-Z3WXWQW3.js?v=cdf9f56e:13505&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;performWorkUntilDeadline	@	chunk-Z3WXWQW3.js?v=cdf9f56e:36&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;react-router-dom.js?v=cdf9f56e:6232 Error handled by React Router default ErrorBoundary: TypeError: Cannot convert object to primitive value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at String (&amp;#x3C;anonymous&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at chunk-3GKRE62E.js?v=cdf9f56e:9785:69&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at Object.react_stack_bottom_frame (chunk-Z3WXWQW3.js?v=cdf9f56e:18509:20)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at renderWithHooks (chunk-Z3WXWQW3.js?v=cdf9f56e:5654:24)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at updateForwardRef (chunk-Z3WXWQW3.js?v=cdf9f56e:7198:21)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at beginWork (chunk-Z3WXWQW3.js?v=cdf9f56e:8735:20)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at runWithFiberInDEV (chunk-Z3WXWQW3.js?v=cdf9f56e:997:72)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at performUnitOfWork (chunk-Z3WXWQW3.js?v=cdf9f56e:12561:98)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at workLoopSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12424:43)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at renderRootSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12408:13)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DetailForm.tsx:1273 TypeError: Cannot convert object to primitive value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at String (&amp;#x3C;anonymous&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;The above error occurred in the &amp;#x3C;ForwardRef&gt; component.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;React will try to recreate this component tree from scratch using the error boundary you provided, AppErrorBoundary.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;react-router-dom.js?v=cdf9f56e:6271 React Router caught the following error during render TypeError: Cannot convert object to primitive value&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TypeError: Cannot convert object to primitive value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    at String (&amp;#x3C;anonymous&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误最终落在 Ant Design 的 &lt;code&gt;&amp;#x3C;Input /&gt;&lt;/code&gt; 渲染链路中。业务代码中存在一个隐藏表单项,用来保存联动配置对象:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TSX&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Form.Item&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;={&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;formPrefix, configName, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;nextConfig&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; hidden&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Input&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Form.Item&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nextConfig&lt;/code&gt; 并不是字符串,而是一整个对象。此前页面可以正常运行,为什么突然开始报错?&lt;/p&gt;
&lt;h2&gt;背景: 为了解决大整数 ID 精度丢失&lt;/h2&gt;
&lt;p&gt;后端部分业务 ID 可能超过 JavaScript 的安全整数范围:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Number.MAX_SAFE_INTEGER = 9007199254740991&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;groupId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;9223372036854775807&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用浏览器原生 &lt;code&gt;JSON.parse&lt;/code&gt;,这个 ID 会在解析时丢失精度。事后复盘,其实最合理的做法是让后端直接将这类 ID 序列化为字符串,前端就完全不需要特殊处理了。但当时后端短期内无法调整接口,只能前端兜底,于是我在 Axios 实例中全局启用了 &lt;code&gt;json-bigint&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; JSONbigParser&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; JSONbig&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({ storeAsString: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; AXIOS_INSTANCE&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; Axios.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  transformResponse: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    (&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;      try&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; JSONbigParser.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      } &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个改动看起来很合理:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超出精度范围的整数会保存为字符串&lt;/li&gt;
&lt;li&gt;普通数字仍然保持数字类型&lt;/li&gt;
&lt;li&gt;所有接口自动获得大整数兼容能力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是正是这种全部接口的出参都替换导致了 bug&lt;/p&gt;
&lt;h2&gt;根因:json-bigint 返回空原型对象&lt;/h2&gt;
&lt;p&gt;这两种对象很像,但并不等价。&lt;/p&gt;
&lt;p&gt;这不是推测,而是 &lt;code&gt;json-bigint&lt;/code&gt; 在解析 JSON 对象的时候，&lt;a href=&quot;https://github.com/sidorares/json-bigint&quot;&gt;sidorares/json-bigint&lt;/a&gt; 的 &lt;a href=&quot;https://github.com/sidorares/json-bigint/blob/master/lib/parse.js&quot;&gt;&lt;code&gt;lib/parse.js&lt;/code&gt;&lt;/a&gt; 中, &lt;code&gt;object()&lt;/code&gt; 在解析每个 &lt;code&gt;{ ... }&lt;/code&gt; 时显式写入:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;object&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; () {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;  // Parse an object value.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  var&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; key,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    object &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; Object.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (ch &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;{&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;    next&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;{&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;    // ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以经过 json-bigint 的对象，是通过 Object.create(null) 创建的。&lt;/p&gt;
&lt;p&gt;然后普通对象继承了 &lt;code&gt;Object.prototype&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; normal&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(normal);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// &quot;[object Object]&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空原型对象没有继承 &lt;code&gt;toString&lt;/code&gt;、&lt;code&gt;valueOf&lt;/code&gt; 等方法:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; parsed&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; Object.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(parsed);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// TypeError: Cannot convert object to primitive value&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此,全局替换 Axios 响应解析器后,不只是超大整数发生了变化,所有接口返回的普通 JSON 对象也悄悄变成了空原型对象。&lt;/p&gt;
&lt;p&gt;当 Ant Design 的 &lt;code&gt;&amp;#x3C;Input /&gt;&lt;/code&gt; 尝试将隐藏表单项中的 &lt;code&gt;nextConfig&lt;/code&gt; 对象转换成字符串时,由于原型没有 toString() 就崩溃了。&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TSX&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Form.Item&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;={&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;formPrefix, configName, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;nextConfig&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; hidden&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Input&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; /&gt;  &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* value 会被注入为 nextConfig 对象本身 */&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;Form.Item&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后由于 nextConfig 对应的 value 是一个对象，通过受控组件的写法注入了 input。此时 &lt;code&gt;&amp;#x3C;Input /&gt;&lt;/code&gt; 收到的 &lt;code&gt;value&lt;/code&gt; 就是 &lt;code&gt;nextConfig&lt;/code&gt; 这个对象 , 且是由 &lt;code&gt;json-bigint&lt;/code&gt; 解析出来的&lt;strong&gt;空原型对象&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;&amp;#x3C;Input /&gt;&lt;/code&gt; 内部发生了什么？&lt;/h3&gt;
&lt;p&gt;Ant Design 的 &lt;code&gt;Input&lt;/code&gt; 底层是对原生 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 元素的封装。当它处理 &lt;code&gt;value&lt;/code&gt; 时,内部大致会经过这样的路径:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;将 value 写入 DOM&lt;/strong&gt;: React 将 &lt;code&gt;value&lt;/code&gt; 赋给原生 &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt; 元素的 &lt;code&gt;.value&lt;/code&gt; 属性。DOM API 要求 &lt;code&gt;.value&lt;/code&gt; 必须是字符串,因此浏览器会隐式调用 &lt;code&gt;String(value)&lt;/code&gt; 进行转换。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;String()&lt;/code&gt; 的类型转换规则&lt;/strong&gt;: 当 &lt;code&gt;String()&lt;/code&gt; 遇到一个对象时,JavaScript 引擎按 &lt;a href=&quot;https://tc39.es/ecma262/#sec-toprimitive&quot;&gt;&lt;code&gt;ToPrimitive&lt;/code&gt;&lt;/a&gt; 尝试:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先调用 &lt;code&gt;obj[Symbol.toPrimitive](&quot;string&quot;)&lt;/code&gt;（如果有的话）&lt;/li&gt;
&lt;li&gt;再调用 &lt;code&gt;obj.toString()&lt;/code&gt; —— 期望返回一个 primitive&lt;/li&gt;
&lt;li&gt;再调用 &lt;code&gt;obj.valueOf()&lt;/code&gt; —— 期望返回一个 primitive&lt;/li&gt;
&lt;li&gt;如果以上都失败或返回的不是 primitive,抛出 &lt;code&gt;TypeError&lt;/code&gt;（见 &lt;a href=&quot;https://tc39.es/ecma262/#sec-tostring&quot;&gt;&lt;code&gt;String&lt;/code&gt;&lt;/a&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;空原型对象的致命问题&lt;/strong&gt;: 通过 &lt;code&gt;Object.create(null)&lt;/code&gt; 创建的对象,原型链是空的。它没有继承 &lt;code&gt;Object.prototype&lt;/code&gt; 上的 &lt;code&gt;toString()&lt;/code&gt; 和 &lt;code&gt;valueOf()&lt;/code&gt; 方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;普通对象:        obj → Object.prototype → null
                 ✅ toString() 可用 → &quot;[object Object]&quot;

空原型对象:       obj → null
                 ❌ 没有 toString(), 没有 valueOf()
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;崩溃点&lt;/strong&gt;: 浏览器调用 &lt;code&gt;String(nextConfig)&lt;/code&gt; → 找不到 &lt;code&gt;toString()&lt;/code&gt; → 找不到 &lt;code&gt;valueOf()&lt;/code&gt; → 抛出 &lt;code&gt;TypeError: Cannot convert object to primitive value&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;为什么此前页面可以正常运行？&lt;/h3&gt;
&lt;p&gt;虽然把对象塞进 &lt;code&gt;&amp;#x3C;Input /&gt;&lt;/code&gt; 本身就不是规范的做法,但之前因为普通对象能隐式转换为字符串,所以 &quot; 巧合地 &quot; 没出问题。引入 &lt;code&gt;json-bigint&lt;/code&gt; 后,所有接口返回的嵌套对象都变成了空原型对象,这个隐藏的隐患才被触发。&lt;/p&gt;
&lt;p&gt;在引入 &lt;code&gt;json-bigint&lt;/code&gt; 之前,Axios 使用浏览器原生的 &lt;code&gt;JSON.parse&lt;/code&gt; 解析响应。原生 &lt;code&gt;JSON.parse&lt;/code&gt; 返回的是普通对象（继承自 &lt;code&gt;Object.prototype&lt;/code&gt;）,因此:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JavaScript&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// 原生 JSON.parse 返回的普通对象&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({ a: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; });  &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// &quot;[object Object]&quot;  ✅ 正常&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// json-bigint 返回的空原型对象&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(Object.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;));  &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// TypeError ❌ 崩溃&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修复思路&lt;/h2&gt;
&lt;h3&gt;最好的方法：后端 ID 字段返回 string&lt;/h3&gt;
&lt;p&gt;推动后端将超出 JS 安全整数范围的 ID 直接序列化为字符串:&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;groupId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;9223372036854775807&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;收益:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端不需要 &lt;code&gt;json-bigint&lt;/code&gt;,也没有空原型对象的副作用&lt;/li&gt;
&lt;li&gt;字段语义明确,Web / 移动端 / 小程序一致,不依赖路由表或启发式&lt;/li&gt;
&lt;li&gt;后端通常只需调整序列化配置,改动成本低&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是唯一真正「字段层面」且没有精度竞态的方案。下文过渡方案仅在后端尚未配合时使用。&lt;/p&gt;
&lt;h3&gt;过渡方案: 按响应内容启用 json-bigint&lt;/h3&gt;
&lt;p&gt;需要去判断 &lt;strong&gt;原始 JSON 字符串里是否可能出现超大整数&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;解析工具&lt;/h4&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;TypeScript&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; JSONbig &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;json-bigint&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; JSONbigParser&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; JSONbig&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({ storeAsString: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/** 启发式: JSON 中出现 16 位及以上的整数,可能超出 Number.MAX_SAFE_INTEGER */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; MAY_HAVE_UNSAFE_INT&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; /:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;\d&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;{16,}&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;(?:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;[,}&lt;/span&gt;&lt;span style=&quot;color:#7EE787;font-weight:bold&quot;&gt;\]&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;\s]&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|$&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;)/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; needsBigIntParse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;raw&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; MAY_HAVE_UNSAFE_INT&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(raw);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/** json-bigint 解析,并将空原型对象恢复为普通对象 */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; parseBigIntResponse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; unknown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; unknown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;typeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  try&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; JSONbigParser.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(data, (&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;_key&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; unknown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (value &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; typeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; value &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Array.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;isArray&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(value)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;value };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; value;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  } &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; parseJsonResponse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; unknown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; unknown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;typeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  try&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;needsBigIntParse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(data)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; parseBigIntResponse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  } &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reviver&lt;/code&gt; 中的 &lt;code&gt;{ ...value }&lt;/code&gt; 必不可少: 只要走了 &lt;code&gt;json-bigint&lt;/code&gt;,嵌套 object 就是 null 原型,必须还原,否则仍可能触发隐式 &lt;code&gt;String()&lt;/code&gt; 崩溃。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这次问题的核心是: &lt;strong&gt;引入中间件或者做出会影响全局的变更的时候需要谨慎。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;URL 白名单、字段层面特殊处理以及全局 &lt;code&gt;json-bigint&lt;/code&gt; 都只是后端未改前的临时手段,不应该长期使用。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:23:07Z</updated><category>javascript</category><category>笔记</category><category>react</category><category>bugfix</category><category>frontend</category><category>axios</category><category>json-bigint</category><category>AI 写作为主</category></item><item><title>bug-form 回填错误</title><link>https://blog.kazoottt.top/posts/75/</link><guid isPermaLink="false">issue-75</guid><description>## 问题现象

在图表配置详情页中，后端返回的配置值已经是“B”，但前端下拉框却显示成了“A”。

简化后的返回数据示例：

```json
{
  &quot;entityId&quot;: &quot;legacy-id&quot;,
  &quot;configValue&quot;: &quot;B&quot;,
  &quot;extraConfig&quot;: [&quot;...&quot;]
}
```

## 根因

问题出在同一业务对象在回填链路中使用了两种不同的标识格式。

- 一部分详情回填数据使用旧格式，例如 `legacy-id`
- 前端在重建对应对象时使用新格式，例如 `normalized-id`

由于这两种格式没有被当成同一个对象处理，前端在同步配置时无法复用原有项，只能按默认值新建一份配置。结果就是界面最终显示成了错误的文案。

## 修复

核心做法是增加一层 identity 归一化逻辑，把旧格式和新格式统一映射到同一个内部标识。

也就是

- 统一托管 key 的生成逻辑
- 在依赖对象匹配的读取逻辑中兼容两种格式
- 增加回归测试，确保旧格式和新格式都能命中同一个对象

修复后：

- `legacy-id` 和 `normalized-id` 会被视为同一个对象
- 详情回填时可以正确复用原有配置
- “B” 不会再被默认值覆盖成 “A”

## 总结

当系统经历过字段格式演进时或者说前端先行然后再对接后端的时候，详情回填、表单托管状态、二次计算逻辑之间必须共享同一套 identity 规则。否则即使返回值本身正确，也可能在重建过程中被默认值覆盖。

&lt;!-- issues-blog:source=d1:a83cacf4-6df1-462e-afac-7ba8f2ee7367 --&gt;
&lt;!-- issues-blog:published-at=2026-04-08T00:00:00.000Z --&gt;
</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题现象&lt;/h2&gt;
&lt;p&gt;在图表配置详情页中，后端返回的配置值已经是“B”，但前端下拉框却显示成了“A”。&lt;/p&gt;
&lt;p&gt;简化后的返回数据示例：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;entityId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;legacy-id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;configValue&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;B&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;extraConfig&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;根因&lt;/h2&gt;
&lt;p&gt;问题出在同一业务对象在回填链路中使用了两种不同的标识格式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一部分详情回填数据使用旧格式，例如 &lt;code&gt;legacy-id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;前端在重建对应对象时使用新格式，例如 &lt;code&gt;normalized-id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于这两种格式没有被当成同一个对象处理，前端在同步配置时无法复用原有项，只能按默认值新建一份配置。结果就是界面最终显示成了错误的文案。&lt;/p&gt;
&lt;h2&gt;修复&lt;/h2&gt;
&lt;p&gt;核心做法是增加一层 identity 归一化逻辑，把旧格式和新格式统一映射到同一个内部标识。&lt;/p&gt;
&lt;p&gt;也就是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一托管 key 的生成逻辑&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;ul&gt;
&lt;li&gt;&lt;code&gt;legacy-id&lt;/code&gt; 和 &lt;code&gt;normalized-id&lt;/code&gt; 会被视为同一个对象&lt;/li&gt;
&lt;li&gt;详情回填时可以正确复用原有配置&lt;/li&gt;
&lt;li&gt;“B” 不会再被默认值覆盖成 “A”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;当系统经历过字段格式演进时或者说前端先行然后再对接后端的时候，详情回填、表单托管状态、二次计算逻辑之间必须共享同一套 identity 规则。否则即使返回值本身正确，也可能在重建过程中被默认值覆盖。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:23:00Z</updated><category>笔记</category><category>bugfix</category><category>chart</category><category>frontend</category><category>public</category><category>sort</category><category>AI 写作为主</category></item><item><title>ClaudeCode三种扩展方式的区别</title><link>https://blog.kazoottt.top/posts/74/</link><guid isPermaLink="false">issue-74</guid><description># ClaudeCode 三种扩展方式的区别

## 核心结论

其实 Claude Code 有三种扩展方式，但它们的本质区别是：Skills 是面对场景时的解决方案，Commands 是用户主动调用的快捷操作，Plugins 则是完整的扩展功能包。

简单来说：

- Skills 告诉 AI 遇到某个问题时应该怎么工作
- Commands 提供一键完成的快捷操作
- Plugins 安装别人写好的完整功能

## 三种方式的本质区别

### Skills：场景解决方案

Skills 的核心是告诉 AI 遇到特定场景时应该怎么工作。或者说，它是 AI 的 &quot; 工作指南 &quot;。

当你开始写测试时，AI 读取到 TDD Skill，就知道应该建议你先写失败的测试，然后写最小实现，最后重构。整个过程是 AI 主动触发的，不需要你输入任何命令。

Skills 可以做的事情很多：

- 配合你完成某个任务
- 调度其他 AI 协作（比如 subagent）
- 按照流程独立处理信息
- 或者说 AI 在某些场景下应该采取什么行动

### Commands：快捷操作

Commands 是你主动调用的快捷操作。通过 `/command` 触发，执行一个预定义的任务。

比如你输入 `/commit`，AI 就按照规范化提交的流程帮你整理提交信息。Commands 更像是 &quot; 脚本 &quot; 或 &quot; 快捷方式 &quot;，你什么时候用就什么时候调。

和 Skills 的区别是：Commands 需要你主动触发，Skills 是 AI 自动判断。

### Plugins：完整的扩展功能包

Plugins 是别人写好的完整功能，通过 marketplace 安装后直接可用。它是 &quot; 功能集成 &quot; 或 &quot; 扩展包 &quot;。

比如 superpowers plugin 就是一整套开发工作流框架，包含 brainstorming、TDD、debugging 等多个技能。你不需要自己写，安装后就能用。

和 Skills/Commands 的区别是：Plugins 是完整的、多功能的解决方案，而 Skills 和 Commands 通常只解决一个具体问题。

其实 Plugins 就是 Skills 和 Commands 的组合包。Plugin 是一个 &quot; 容器 &quot;，里面可以包含多个 Skills 和 Commands。

比如 superpowers 这个 Plugin，内部就包含了：

- brainstorming skill
- TDD skill
- debugging skill
- writing-plans skill
- 等等...

你可以：

- 单独写一个 Skill 文件，放在 `.claude/skills/` 下
- 单独写一个 Command 文件，放在 `.claude/commands/` 下
- 或者把它们打包成 Plugin，发布到 marketplace 让别人安装

三者的关系：

```
Plugins (功能包)
  ├── Skills (行为规范)
  ├── Commands (快捷命令)
  └── 其他配置
```

## 怎么打包 Plugins

其实打包 Plugins 就是用固定的目录结构和配置文件。

### 目录结构

```
my-plugin/
├── .claude-plugin/
│   └── plugin.json          # 插件元数据
├── skills/                   # Skills 放这里
│   ├── tdd/
│   │   └── SKILL.md
│   └── debugging/
│       └── SKILL.md
├── commands/                 # Commands 放这里
│   ├── commit.md
│   └── release.md
└── marketplace.json          # 发布到 marketplace 的配置
```

### plugin.json 示例

```json
{
  &quot;name&quot;: &quot;my-workflow-tools&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;我的开发工作流工具集&quot;,
  &quot;author&quot;: {
    &quot;name&quot;: &quot;Your Name&quot;
  },
  &quot;skills&quot;: &quot;./skills/&quot;,
  &quot;commands&quot;: &quot;./commands/&quot;
}
```

### 发布到 marketplace

创建 `marketplace.json`：

```json
{
  &quot;name&quot;: &quot;my-marketplace&quot;,
  &quot;owner&quot;: {
    &quot;name&quot;: &quot;Your Name&quot;,
    &quot;email&quot;: &quot;you@example.com&quot;
  },
  &quot;plugins&quot;: [
    {
      &quot;name&quot;: &quot;my-workflow-tools&quot;,
      &quot;source&quot;: &quot;./&quot;,
      &quot;description&quot;: &quot;我的开发工作流工具集&quot;,
      &quot;version&quot;: &quot;1.0.0&quot;
    }
  ]
}
```

然后把这个仓库托管到 GitHub，用户就可以通过 `/plugin install my-workflow-tools@your-username` 安装了。

## 什么时候用哪个

### 用 Skills 的场景

需要 AI 按照某种方式工作的时候：

- 写代码时遵循 TDD 流程
- 遇到 bug 时按调试规范排查
- 代码审查时按标准检查
- 或者说需要 AI 协调多个 subagent 协作

### 决策流程

```
需要自动化执行？
  ├─ 是 → Skills
  └─ 否 → 需要完整功能？
      ├─ 是 → Plugins
      └─ 否 → Commands
```

你可以这样想：需要建议用 Skills，快捷操作用 Commands，完整功能用 Plugins。

### 用 Commands 的场景

需要快速执行某个操作的时候：

- 规范化提交信息
- 生成发布日志
- 创建新文件模板

你需要 &quot; 一键完成 &quot; 某个重复性操作。

### 用 Plugins 的场景

需要复杂功能或不想自己写的时候：

- 完整的开发工作流框架（如 superpowers）
- 第三方服务集成（如 GitHub、Jira）
- 社区维护的通用功能

&quot; 别人已经做好了，直接用就行 &quot;。

## 快速上手

### Command 示例

告诉 AI 创建一个 Command，然后它会生成这样的文件：

```markdown
---
name: my-commit
description: 我的规范化提交命令
---

# 规范化提交

按照以下格式提交：
- 类型：feat/fix/docs/refactor
- 范围：简短描述
- 正文：详细说明（如有）
```

### Skill 示例

告诉 AI 创建一个 Skill，然后它会生成这样的文件：

```markdown
---
name: my-tdd
description: 我的 TDD 工作流程
trigger:
  - &quot;写测试&quot;
  - &quot;TDD&quot;
---

# TDD 工作流程

当用户说「写测试」或「TDD」时：
1. 先写失败的测试
2. 写最小实现
3. 重构优化
```

### 安装 Plugin

直接告诉 AI 你想安装什么插件，比如：

- &quot; 安装 superpowers plugin&quot;
- 或者 &quot;/plugin install superpowers@claude-plugins-official&quot;

## 参考资料

- [Claude Code 官方文档](&lt;https://claude.ai/code&gt;)
- [superpowers GitHub 仓库](&lt;https://github.com/obra/superpowers&gt;)

---

结论：三种扩展方式各有优势。根据你的实际需求选择或组合使用，能最大化 Claude Code 的效率。

&lt;!-- issues-blog:source=d1:cf5594db-8e3d-4df8-9bd8-74579abc14c7 --&gt;
&lt;!-- issues-blog:published-at=2026-04-07T00:00:00.000Z --&gt;
</description><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;ClaudeCode 三种扩展方式的区别&lt;/h1&gt;
&lt;h2&gt;核心结论&lt;/h2&gt;
&lt;p&gt;其实 Claude Code 有三种扩展方式，但它们的本质区别是：Skills 是面对场景时的解决方案，Commands 是用户主动调用的快捷操作，Plugins 则是完整的扩展功能包。&lt;/p&gt;
&lt;p&gt;简单来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Skills 告诉 AI 遇到某个问题时应该怎么工作&lt;/li&gt;
&lt;li&gt;Commands 提供一键完成的快捷操作&lt;/li&gt;
&lt;li&gt;Plugins 安装别人写好的完整功能&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三种方式的本质区别&lt;/h2&gt;
&lt;h3&gt;Skills：场景解决方案&lt;/h3&gt;
&lt;p&gt;Skills 的核心是告诉 AI 遇到特定场景时应该怎么工作。或者说，它是 AI 的 &quot; 工作指南 &quot;。&lt;/p&gt;
&lt;p&gt;当你开始写测试时，AI 读取到 TDD Skill，就知道应该建议你先写失败的测试，然后写最小实现，最后重构。整个过程是 AI 主动触发的，不需要你输入任何命令。&lt;/p&gt;
&lt;p&gt;Skills 可以做的事情很多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配合你完成某个任务&lt;/li&gt;
&lt;li&gt;调度其他 AI 协作（比如 subagent）&lt;/li&gt;
&lt;li&gt;按照流程独立处理信息&lt;/li&gt;
&lt;li&gt;或者说 AI 在某些场景下应该采取什么行动&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Commands：快捷操作&lt;/h3&gt;
&lt;p&gt;Commands 是你主动调用的快捷操作。通过 &lt;code&gt;/command&lt;/code&gt; 触发，执行一个预定义的任务。&lt;/p&gt;
&lt;p&gt;比如你输入 &lt;code&gt;/commit&lt;/code&gt;，AI 就按照规范化提交的流程帮你整理提交信息。Commands 更像是 &quot; 脚本 &quot; 或 &quot; 快捷方式 &quot;，你什么时候用就什么时候调。&lt;/p&gt;
&lt;p&gt;和 Skills 的区别是：Commands 需要你主动触发，Skills 是 AI 自动判断。&lt;/p&gt;
&lt;h3&gt;Plugins：完整的扩展功能包&lt;/h3&gt;
&lt;p&gt;Plugins 是别人写好的完整功能，通过 marketplace 安装后直接可用。它是 &quot; 功能集成 &quot; 或 &quot; 扩展包 &quot;。&lt;/p&gt;
&lt;p&gt;比如 superpowers plugin 就是一整套开发工作流框架，包含 brainstorming、TDD、debugging 等多个技能。你不需要自己写，安装后就能用。&lt;/p&gt;
&lt;p&gt;和 Skills/Commands 的区别是：Plugins 是完整的、多功能的解决方案，而 Skills 和 Commands 通常只解决一个具体问题。&lt;/p&gt;
&lt;p&gt;其实 Plugins 就是 Skills 和 Commands 的组合包。Plugin 是一个 &quot; 容器 &quot;，里面可以包含多个 Skills 和 Commands。&lt;/p&gt;
&lt;p&gt;比如 superpowers 这个 Plugin，内部就包含了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;brainstorming skill&lt;/li&gt;
&lt;li&gt;TDD skill&lt;/li&gt;
&lt;li&gt;debugging skill&lt;/li&gt;
&lt;li&gt;writing-plans skill&lt;/li&gt;
&lt;li&gt;等等...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单独写一个 Skill 文件，放在 &lt;code&gt;.claude/skills/&lt;/code&gt; 下&lt;/li&gt;
&lt;li&gt;单独写一个 Command 文件，放在 &lt;code&gt;.claude/commands/&lt;/code&gt; 下&lt;/li&gt;
&lt;li&gt;或者把它们打包成 Plugin，发布到 marketplace 让别人安装&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三者的关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Plugins (功能包)
  ├── Skills (行为规范)
  ├── Commands (快捷命令)
  └── 其他配置
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;怎么打包 Plugins&lt;/h2&gt;
&lt;p&gt;其实打包 Plugins 就是用固定的目录结构和配置文件。&lt;/p&gt;
&lt;h3&gt;目录结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;my-plugin/
├── .claude-plugin/
│   └── plugin.json          # 插件元数据
├── skills/                   # Skills 放这里
│   ├── tdd/
│   │   └── SKILL.md
│   └── debugging/
│       └── SKILL.md
├── commands/                 # Commands 放这里
│   ├── commit.md
│   └── release.md
└── marketplace.json          # 发布到 marketplace 的配置
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;plugin.json 示例&lt;/h3&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;my-workflow-tools&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;version&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;我的开发工作流工具集&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;author&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Your Name&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;skills&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./skills/&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;commands&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./commands/&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;发布到 marketplace&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;marketplace.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;JSON&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;my-marketplace&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;owner&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Your Name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;email&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;you@example.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;plugins&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;my-workflow-tools&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      &quot;source&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;./&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;我的开发工作流工具集&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      &quot;version&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把这个仓库托管到 GitHub，用户就可以通过 &lt;code&gt;/plugin install my-workflow-tools@your-username&lt;/code&gt; 安装了。&lt;/p&gt;
&lt;h2&gt;什么时候用哪个&lt;/h2&gt;
&lt;h3&gt;用 Skills 的场景&lt;/h3&gt;
&lt;p&gt;需要 AI 按照某种方式工作的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写代码时遵循 TDD 流程&lt;/li&gt;
&lt;li&gt;遇到 bug 时按调试规范排查&lt;/li&gt;
&lt;li&gt;代码审查时按标准检查&lt;/li&gt;
&lt;li&gt;或者说需要 AI 协调多个 subagent 协作&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;决策流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;需要自动化执行？
  ├─ 是 → Skills
  └─ 否 → 需要完整功能？
      ├─ 是 → Plugins
      └─ 否 → Commands
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以这样想：需要建议用 Skills，快捷操作用 Commands，完整功能用 Plugins。&lt;/p&gt;
&lt;h3&gt;用 Commands 的场景&lt;/h3&gt;
&lt;p&gt;需要快速执行某个操作的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;规范化提交信息&lt;/li&gt;
&lt;li&gt;生成发布日志&lt;/li&gt;
&lt;li&gt;创建新文件模板&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你需要 &quot; 一键完成 &quot; 某个重复性操作。&lt;/p&gt;
&lt;h3&gt;用 Plugins 的场景&lt;/h3&gt;
&lt;p&gt;需要复杂功能或不想自己写的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完整的开发工作流框架（如 superpowers）&lt;/li&gt;
&lt;li&gt;第三方服务集成（如 GitHub、Jira）&lt;/li&gt;
&lt;li&gt;社区维护的通用功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&quot; 别人已经做好了，直接用就行 &quot;。&lt;/p&gt;
&lt;h2&gt;快速上手&lt;/h2&gt;
&lt;h3&gt;Command 示例&lt;/h3&gt;
&lt;p&gt;告诉 AI 创建一个 Command，然后它会生成这样的文件：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;my-commit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;我的规范化提交命令&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF;font-weight:bold&quot;&gt;# 规范化提交&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;按照以下格式提交：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 类型：feat/fix/docs/refactor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 范围：简短描述&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 正文：详细说明（如有）&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Skill 示例&lt;/h3&gt;
&lt;p&gt;告诉 AI 创建一个 Skill，然后它会生成这样的文件：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;my-tdd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;description&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;我的 TDD 工作流程&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;trigger&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;写测试&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;TDD&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF;font-weight:bold&quot;&gt;# TDD 工作流程&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;当用户说「写测试」或「TDD」时：&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;1.&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 先写失败的测试&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;2.&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 写最小实现&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;3.&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 重构优化&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 Plugin&lt;/h3&gt;
&lt;p&gt;直接告诉 AI 你想安装什么插件，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot; 安装 superpowers plugin&quot;&lt;/li&gt;
&lt;li&gt;或者 &quot;/plugin install superpowers@claude-plugins-official&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.ai/code&quot;&gt;Claude Code 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;superpowers GitHub 仓库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;结论：三种扩展方式各有优势。根据你的实际需求选择或组合使用，能最大化 Claude Code 的效率。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:22:42Z</updated><category>笔记</category><category>大模型</category><category>技术调研</category><category>skills</category><category>AI 写作为主</category></item><item><title>在碎片与系统输出之间 关于笔记软件与 AI 协同的尝试</title><link>https://blog.kazoottt.top/posts/73/</link><guid isPermaLink="false">issue-73</guid><description>
在之前使用 Notion、Obsidian 和 Logseq 等笔记软件的过程中，我一直在处理一个很矛盾的问题。

一方面，我很习惯像用微博或者 flomo 那样，想到什么就记什么，完全没有记录负担；但另一方面，当我真正需要整理长文或者做严肃输出时，这种乱糟糟的碎片又没法直接支撑我的需求。但是在使用比较重的笔记软件时，我又因为如何组织笔记而烦恼。

## 一、 现状分析与痛点识别

过去，我的笔记习惯严重依赖于文件夹层级和复杂的属性维护，或者想着说我都这样设计了不去进行打标很可惜。但这种模式在实际运行中暴露了几个核心问题：

1. 管理文件夹和维护属性带来的负担。每产生一条零散的想法，我都要在大脑里快速决策。它该放哪个文件夹？该打什么标签？这种细微的决策成本累加起来，成了阻碍我记录的最大障碍，导致记笔记变得很有压力。
2. 索引难以维护且容易被废弃。以前费劲建立的文件夹分类或标签体系，一旦逻辑变了，或者一段时间没有维护，索引就废了。
3. 零散记录与长文输出之间的断层。像 flomo 这种碎片化工具虽然记着爽，但因为缺乏空间感，和我的系统性文档也没什么关联，想把它们拼成一篇长文的搬运成本太高了。

## 二、 认清自己的大脑，从“分类”转向“召回”

受 Milanote 的启发，我意识到我在构思或者输出时更倾向于非线性的空间布局，而不是线性的层级目录。但是 milanote 自身的功能以及它不是本地优先（local-first），所以让我没有那么强烈的付费意愿。

但其实当前大模型的能力，已经可以实现自由输入了，只是如果我真的需要把输出以网状结构可视化还需要进一步编写技能来实现。

于是有了以下重构思路。

1. 彻底扁平化存储
放弃文件夹的层级嵌套，将文件夹的功能从”分类工具”降级为”生命周期容器”。
- 日记。作为唯一的无压力录入入口，承载所有碎片化的记录。
- 根目录平铺。新笔记不再寻求特定的文件夹归属，仅通过包含关键词的文件名进行区分，或者让大模型自动打标（根据效果决定是否需要人工二次确认）。
- 核心逻辑。文件夹是给人看的，而索引应该交给 AI 动态生成。

2. 召回
利用本地 Markdown 的开放性，依靠 AI 进行跨文件的检索。不再追求”找到文件所在的物理路径”，而是通过语义匹配和标签关联，在需要输出时将相关素材召回。

3. 空间化可视化
使用 Obsidian Canvas 替代高成本的云端画布工具。将 AI 召回的离散素材在画布上进行布局和连线，利用空间结构理顺逻辑。这填补了碎片化输入与系统性输出之间的空白。

![](https://pictures.kazoottt.top/2026/03/20260305-789a4fcf00ce37ea3dffc4e807c14f4e.jpg)

## 三、 确立后的工作流

重构后的工作流分为四个阶段：

1. 捕捉。移动端使用 Telegram + OpenClaw 录入碎片，桌面端可以使用浏览器插件进行剪藏，不需要进行分类或者决定路径，自动同步至本地 Inbox。
2. 召回。在准备输出长文或进行专题研究时，通过 AI 穿透历史日记和领域文档，进行多素材聚拢。
3. 网状可视化。在 Canvas 画布上完成文件和文件之间的关联显示，或者直接用双链来进行知识图谱的显示。
4. 输出。基于召回的资料与当前的想法，在 Studio 目录下完成正式文档撰写，通过 sanity API 发布（本身也可以使用大模型技能发布）。

## 四、 总结

笔记系统的价值在于支撑思考与产出，而不是为了记录而记录。将枯燥的打标与搜索交给 AI 去做，让自己回归到纯粹的记录与逻辑建构中，这样才能降低管理成本，提升表达欲。

&lt;!-- issues-blog:source=d1:post.transition-between-fragments-and-system-output --&gt;
&lt;!-- issues-blog:published-at=2026-03-05T00:00:00.000Z --&gt;
</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在之前使用 Notion、Obsidian 和 Logseq 等笔记软件的过程中，我一直在处理一个很矛盾的问题。&lt;/p&gt;
&lt;p&gt;一方面，我很习惯像用微博或者 flomo 那样，想到什么就记什么，完全没有记录负担；但另一方面，当我真正需要整理长文或者做严肃输出时，这种乱糟糟的碎片又没法直接支撑我的需求。但是在使用比较重的笔记软件时，我又因为如何组织笔记而烦恼。&lt;/p&gt;
&lt;h2&gt;一、 现状分析与痛点识别&lt;/h2&gt;
&lt;p&gt;过去，我的笔记习惯严重依赖于文件夹层级和复杂的属性维护，或者想着说我都这样设计了不去进行打标很可惜。但这种模式在实际运行中暴露了几个核心问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;管理文件夹和维护属性带来的负担。每产生一条零散的想法，我都要在大脑里快速决策。它该放哪个文件夹？该打什么标签？这种细微的决策成本累加起来，成了阻碍我记录的最大障碍，导致记笔记变得很有压力。&lt;/li&gt;
&lt;li&gt;索引难以维护且容易被废弃。以前费劲建立的文件夹分类或标签体系，一旦逻辑变了，或者一段时间没有维护，索引就废了。&lt;/li&gt;
&lt;li&gt;零散记录与长文输出之间的断层。像 flomo 这种碎片化工具虽然记着爽，但因为缺乏空间感，和我的系统性文档也没什么关联，想把它们拼成一篇长文的搬运成本太高了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二、 认清自己的大脑，从“分类”转向“召回”&lt;/h2&gt;
&lt;p&gt;受 Milanote 的启发，我意识到我在构思或者输出时更倾向于非线性的空间布局，而不是线性的层级目录。但是 milanote 自身的功能以及它不是本地优先（local-first），所以让我没有那么强烈的付费意愿。&lt;/p&gt;
&lt;p&gt;但其实当前大模型的能力，已经可以实现自由输入了，只是如果我真的需要把输出以网状结构可视化还需要进一步编写技能来实现。&lt;/p&gt;
&lt;p&gt;于是有了以下重构思路。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;彻底扁平化存储
放弃文件夹的层级嵌套，将文件夹的功能从”分类工具”降级为”生命周期容器”。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;日记。作为唯一的无压力录入入口，承载所有碎片化的记录。&lt;/li&gt;
&lt;li&gt;根目录平铺。新笔记不再寻求特定的文件夹归属，仅通过包含关键词的文件名进行区分，或者让大模型自动打标（根据效果决定是否需要人工二次确认）。&lt;/li&gt;
&lt;li&gt;核心逻辑。文件夹是给人看的，而索引应该交给 AI 动态生成。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;召回
利用本地 Markdown 的开放性，依靠 AI 进行跨文件的检索。不再追求”找到文件所在的物理路径”，而是通过语义匹配和标签关联，在需要输出时将相关素材召回。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间化可视化
使用 Obsidian Canvas 替代高成本的云端画布工具。将 AI 召回的离散素材在画布上进行布局和连线，利用空间结构理顺逻辑。这填补了碎片化输入与系统性输出之间的空白。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/03/20260305-789a4fcf00ce37ea3dffc4e807c14f4e.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;三、 确立后的工作流&lt;/h2&gt;
&lt;p&gt;重构后的工作流分为四个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;捕捉。移动端使用 Telegram + OpenClaw 录入碎片，桌面端可以使用浏览器插件进行剪藏，不需要进行分类或者决定路径，自动同步至本地 Inbox。&lt;/li&gt;
&lt;li&gt;召回。在准备输出长文或进行专题研究时，通过 AI 穿透历史日记和领域文档，进行多素材聚拢。&lt;/li&gt;
&lt;li&gt;网状可视化。在 Canvas 画布上完成文件和文件之间的关联显示，或者直接用双链来进行知识图谱的显示。&lt;/li&gt;
&lt;li&gt;输出。基于召回的资料与当前的想法，在 Studio 目录下完成正式文档撰写，通过 sanity API 发布（本身也可以使用大模型技能发布）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;四、 总结&lt;/h2&gt;
&lt;p&gt;笔记系统的价值在于支撑思考与产出，而不是为了记录而记录。将枯燥的打标与搜索交给 AI 去做，让自己回归到纯粹的记录与逻辑建构中，这样才能降低管理成本，提升表达欲。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:22:32Z</updated><category>效率</category><category>ai</category><category>方法论</category><category>pkm</category><category>AI 写作为主</category></item><item><title>高估行动成本导致状态低迷</title><link>https://blog.kazoottt.top/posts/72/</link><guid isPermaLink="false">issue-72</guid><description>
（本文稿整理自 2026.1.31 手写笔记）

🗓 2026.1.31 星期六

感觉自己的状态很低迷，除了完成日常必需的工作内容和维持生理体征的行为之外，其它时间都在躺着玩手机，且把越来越多的时间花在了碎片化内容的浏览中。特别是在回到成都后，自己的人生有种停滞的感觉。

个人认为原因有：

① 日子过得太安逸，回到了离家近的地方，且这个地方的节奏确实比较慢，导致自己开始享受安逸。当然，丰富好自己的生活没有问题，但是我只是在把休闲时间提起来了，并没有去增加太多人生阅历的丰富性。

② 精力或者说身体状态不足以支撑我在完成日常必做事项后，进行个人提升等，导致恶性循环。

③ 对于未来没有规划没有目标，在上海的时候，自己的目标是回到川渝。但回来后就不知道下一个目标。且在 LLM 的冲击下，加速了我对于前端岗的不看好，感觉继续做下去没有什么意义，或者说不具备太多的上升空间了。

而对于其他的爱好，怎么做呢？自己买了很多拍摄的设备，电脑的配置也很齐全，也有了充足的时间，但我能记录什么？输出什么呢。

感觉可以回想一下最近给自己带来比较大的情绪波动的事情，以及自己还在热爱什么事情。我做事的动力从来不来自自己要对未来的自己负责，而是为了想给喜欢的事物添砖加瓦。为了实现这些目的的时候我才是最具执行力的。

目前能给我带来情绪波动的：

① 追星的新物料，确实是仅限于新的，我是一个在乎当下和未来的人。

② 好玩的游戏或者影视作品，也就是我还是会为了好的内容买单。

③ 人际关系，如果有好朋友或者家里人与我有良好的互动，我也会很高兴。

④ 获得他人的赞赏，或者说极端一点就是虚荣，我需要从外界获取关注和认可然后从中获得前进的动力。

除了这些事情之外我是一个很淡的人，可以说过得很平稳，不会去额外提升自己，无法发现生活之美和享受生活。我总是熬夜，也不会去做运动，整个人的皮肤状态也很差。感觉能维持现状，没有持续变差已经是很好的结局了。

我也是一个三分钟热度的人，学东西或者对某些事情感兴趣的热情持续时间非常的短暂。那么有什么办法可以利用这种好又不太好特质给自己的人生带来更好的好处？

从福格行为模型来说：B = MAP

- M（动机）
- A（能力）
- P（提示）

那么我欠缺的是什么？我并不认为我丧失了学习的能力，或者说当下的实力有落后他人太多到无法追上的地步。动机也是有的，我有能让自己爽到的事情，或者说虚荣就是我的动机。那么真正缺乏的是什么，是提示或者说 trigger，当启动一个事情的成本对我来说过大的时候，我就会陷入抗拒心理，不想去开始。例如：

对于学习摄影来说，我需要打开电脑，坐到桌前，看视频，才能开始学习，这是我之前的认知，但是事实真的如此吗？这其实更偏向于一个完整的、高仪式感的学习模式。

也有很多低成本的方式，例如针对单个的知识点进行针对性地看视频，这在手机上也可以完成。或者复盘自己的照片的问题，同样如此。所以对我来说目前应该做的是：主动地转变对于学习也好做事方式也好的想象，不要下意识认为这是一件必要怎么样才能做的事情。

不想剪视频是因为不想用电脑且用手机剪不好，非也，也有很多人完全是用手机剪辑的。不想去跳舞健身，因为健身房或者舞室的人会对我评头论足，也不是，人生并没有那么多的观众，即是有这才是优势，把他人的有些看不起的目光先转为敬佩这比直接是仰视更加像爽文。

写到这里真不知不觉也开始慢慢理清楚自己的思路了。利用热爱带来的动力去促使自己提升或者输出，然后降低启动事项的成本，先动起来是最重要的，这样才能达成福格行为模型的公式 B = MAP。

![IMG_20260219_172449](&lt;https://pictures.kazoottt.top/2026/02/20260219-287cd3999573eec592561a4485fdc0cd.jpg&gt;)

![IMG_20260219_172454](&lt;https://pictures.kazoottt.top/2026/02/20260219-a92fbc7dba4ad36e52467e3764782faf.jpg&gt;)

![IMG_20260219_172457](&lt;https://pictures.kazoottt.top/2026/02/20260219-42b7a29ded555f434e4755800dafac1b.jpg&gt;)

![IMG_20260219_172504](&lt;https://pictures.kazoottt.top/2026/02/20260219-0e08109dd2f7f0db80aadc5bd107e871.jpg&gt;)

&lt;!-- issues-blog:source=d1:post.overestimate-action-costs-slack-off --&gt;
&lt;!-- issues-blog:published-at=2026-02-19T00:00:00.000Z --&gt;
</description><pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;（本文稿整理自 2026.1.31 手写笔记）&lt;/p&gt;
&lt;p&gt;🗓 2026.1.31 星期六&lt;/p&gt;
&lt;p&gt;感觉自己的状态很低迷，除了完成日常必需的工作内容和维持生理体征的行为之外，其它时间都在躺着玩手机，且把越来越多的时间花在了碎片化内容的浏览中。特别是在回到成都后，自己的人生有种停滞的感觉。&lt;/p&gt;
&lt;p&gt;个人认为原因有：&lt;/p&gt;
&lt;p&gt;① 日子过得太安逸，回到了离家近的地方，且这个地方的节奏确实比较慢，导致自己开始享受安逸。当然，丰富好自己的生活没有问题，但是我只是在把休闲时间提起来了，并没有去增加太多人生阅历的丰富性。&lt;/p&gt;
&lt;p&gt;② 精力或者说身体状态不足以支撑我在完成日常必做事项后，进行个人提升等，导致恶性循环。&lt;/p&gt;
&lt;p&gt;③ 对于未来没有规划没有目标，在上海的时候，自己的目标是回到川渝。但回来后就不知道下一个目标。且在 LLM 的冲击下，加速了我对于前端岗的不看好，感觉继续做下去没有什么意义，或者说不具备太多的上升空间了。&lt;/p&gt;
&lt;p&gt;而对于其他的爱好，怎么做呢？自己买了很多拍摄的设备，电脑的配置也很齐全，也有了充足的时间，但我能记录什么？输出什么呢。&lt;/p&gt;
&lt;p&gt;感觉可以回想一下最近给自己带来比较大的情绪波动的事情，以及自己还在热爱什么事情。我做事的动力从来不来自自己要对未来的自己负责，而是为了想给喜欢的事物添砖加瓦。为了实现这些目的的时候我才是最具执行力的。&lt;/p&gt;
&lt;p&gt;目前能给我带来情绪波动的：&lt;/p&gt;
&lt;p&gt;① 追星的新物料，确实是仅限于新的，我是一个在乎当下和未来的人。&lt;/p&gt;
&lt;p&gt;② 好玩的游戏或者影视作品，也就是我还是会为了好的内容买单。&lt;/p&gt;
&lt;p&gt;③ 人际关系，如果有好朋友或者家里人与我有良好的互动，我也会很高兴。&lt;/p&gt;
&lt;p&gt;④ 获得他人的赞赏，或者说极端一点就是虚荣，我需要从外界获取关注和认可然后从中获得前进的动力。&lt;/p&gt;
&lt;p&gt;除了这些事情之外我是一个很淡的人，可以说过得很平稳，不会去额外提升自己，无法发现生活之美和享受生活。我总是熬夜，也不会去做运动，整个人的皮肤状态也很差。感觉能维持现状，没有持续变差已经是很好的结局了。&lt;/p&gt;
&lt;p&gt;我也是一个三分钟热度的人，学东西或者对某些事情感兴趣的热情持续时间非常的短暂。那么有什么办法可以利用这种好又不太好特质给自己的人生带来更好的好处？&lt;/p&gt;
&lt;p&gt;从福格行为模型来说：B = MAP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;M（动机）&lt;/li&gt;
&lt;li&gt;A（能力）&lt;/li&gt;
&lt;li&gt;P（提示）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我欠缺的是什么？我并不认为我丧失了学习的能力，或者说当下的实力有落后他人太多到无法追上的地步。动机也是有的，我有能让自己爽到的事情，或者说虚荣就是我的动机。那么真正缺乏的是什么，是提示或者说 trigger，当启动一个事情的成本对我来说过大的时候，我就会陷入抗拒心理，不想去开始。例如：&lt;/p&gt;
&lt;p&gt;对于学习摄影来说，我需要打开电脑，坐到桌前，看视频，才能开始学习，这是我之前的认知，但是事实真的如此吗？这其实更偏向于一个完整的、高仪式感的学习模式。&lt;/p&gt;
&lt;p&gt;也有很多低成本的方式，例如针对单个的知识点进行针对性地看视频，这在手机上也可以完成。或者复盘自己的照片的问题，同样如此。所以对我来说目前应该做的是：主动地转变对于学习也好做事方式也好的想象，不要下意识认为这是一件必要怎么样才能做的事情。&lt;/p&gt;
&lt;p&gt;不想剪视频是因为不想用电脑且用手机剪不好，非也，也有很多人完全是用手机剪辑的。不想去跳舞健身，因为健身房或者舞室的人会对我评头论足，也不是，人生并没有那么多的观众，即是有这才是优势，把他人的有些看不起的目光先转为敬佩这比直接是仰视更加像爽文。&lt;/p&gt;
&lt;p&gt;写到这里真不知不觉也开始慢慢理清楚自己的思路了。利用热爱带来的动力去促使自己提升或者输出，然后降低启动事项的成本，先动起来是最重要的，这样才能达成福格行为模型的公式 B = MAP。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/02/20260219-287cd3999573eec592561a4485fdc0cd.jpg&quot; alt=&quot;IMG_20260219_172449&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/02/20260219-a92fbc7dba4ad36e52467e3764782faf.jpg&quot; alt=&quot;IMG_20260219_172454&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/02/20260219-42b7a29ded555f434e4755800dafac1b.jpg&quot; alt=&quot;IMG_20260219_172457&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2026/02/20260219-0e08109dd2f7f0db80aadc5bd107e871.jpg&quot; alt=&quot;IMG_20260219_172504&quot;&gt;&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:22:25Z</updated><category>随笔</category><category>生活记录</category><category>自我管理</category><category>行动力</category><category>行为改变</category><category>拖延</category><category>情绪</category><category>人工写作为主</category></item><item><title>我终于也是有猫的人了！</title><link>https://blog.kazoottt.top/posts/71/</link><guid isPermaLink="false">issue-71</guid><description>橘猫叫会长，监督我早起协会会长 &amp; 希望他会长。

狸花叫木木，刚接到的时候还没有取名字，带他去医院体检，医院一定要名字才能录入系统，于是想到了 momo，但是前台没有太听清楚，就输入了木木，不过感觉这个名字也挺好的，就保留了下来。  

![IMG-0993AFF1851A0164FD90DC664868E197](&lt;https://pictures.kazoottt.top/2025/12/20251206-0993aff1851a0164fd90dc664868e197.jpg&gt;)

![IMG-4858B950C2F723C83126E55AB4EE7724](&lt;https://pictures.kazoottt.top/2025/12/20251206-4858b950c2f723c83126e55ab4ee7724.jpg&gt;)

![IMG-C9432C8A03C7E8FAC63B90B44C300A34](&lt;https://pictures.kazoottt.top/2025/12/20251206-c9432c8a03c7e8fac63b90b44c300a34.jpg&gt;)

会长三个月大，性格挺好的，虽然已经开始练习捕猎，但不会咬人和打人，小嗓门夹子音萌萌的，侧脸帅，正脸有时候像玉米猫。  

![IMG-2D735CB11A38D0D97C954B3B899D339C](&lt;https://pictures.kazoottt.top/2025/12/20251206-2d735cb11a38d0d97c954b3b899d339c.jpg&gt;)

![IMG-46174E41C1C5800C25F0F1D47820C01D](&lt;https://pictures.kazoottt.top/2025/12/20251206-46174e41c1c5800c25f0f1d47820c01d.jpg&gt;)

![IMG-753747979D96DDE83BE8F65B75E55B21](&lt;https://pictures.kazoottt.top/2025/12/20251206-753747979d96dde83be8f65b75e55b21.jpg&gt;)

![IMG-46DC2DADF486BE600686CF6104CB6242](&lt;https://pictures.kazoottt.top/2025/12/20251206-46dc2dadf486be600686cf6104cb6242.png&gt;)  

木木一个月多一点，大嗓门，不知道为什么这么小一个身体能发出这么大的声音。眼里只有食物和我，一见到我就会从脚底爬到身上，非常黏人。

无死角的一只猫，小鼻嘎超可爱。  

![IMG-DC8E4CDCA891C1C031857B0323E0DDDA](&lt;https://pictures.kazoottt.top/2025/12/20251206-dc8e4cdca891c1c031857b0323e0ddda.jpg&gt;)

![IMG-9C38295C2876110D625A1781C96A1390](&lt;https://pictures.kazoottt.top/2025/12/20251206-9c38295c2876110d625a1781c96a1390.jpg&gt;)

![IMG-411DB527B6CC0F6D892A9913F30829C4](&lt;https://pictures.kazoottt.top/2025/12/20251206-411db527b6cc0f6d892a9913f30829c4.jpg&gt;)

养了猫之后，我已经从晚睡晚起变成早睡早起了，因为如果不抓紧时间睡觉，早上被吵醒之后就再也没办法睡了。🤡

&lt;!-- issues-blog:source=d1:post.finally-have-cats --&gt;
&lt;!-- issues-blog:published-at=2025-12-06T00:00:00.000Z --&gt;
</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;橘猫叫会长，监督我早起协会会长 &amp;#x26; 希望他会长。&lt;/p&gt;
&lt;p&gt;狸花叫木木，刚接到的时候还没有取名字，带他去医院体检，医院一定要名字才能录入系统，于是想到了 momo，但是前台没有太听清楚，就输入了木木，不过感觉这个名字也挺好的，就保留了下来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-0993aff1851a0164fd90dc664868e197.jpg&quot; alt=&quot;IMG-0993AFF1851A0164FD90DC664868E197&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-4858b950c2f723c83126e55ab4ee7724.jpg&quot; alt=&quot;IMG-4858B950C2F723C83126E55AB4EE7724&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-c9432c8a03c7e8fac63b90b44c300a34.jpg&quot; alt=&quot;IMG-C9432C8A03C7E8FAC63B90B44C300A34&quot;&gt;&lt;/p&gt;
&lt;p&gt;会长三个月大，性格挺好的，虽然已经开始练习捕猎，但不会咬人和打人，小嗓门夹子音萌萌的，侧脸帅，正脸有时候像玉米猫。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-2d735cb11a38d0d97c954b3b899d339c.jpg&quot; alt=&quot;IMG-2D735CB11A38D0D97C954B3B899D339C&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-46174e41c1c5800c25f0f1d47820c01d.jpg&quot; alt=&quot;IMG-46174E41C1C5800C25F0F1D47820C01D&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-753747979d96dde83be8f65b75e55b21.jpg&quot; alt=&quot;IMG-753747979D96DDE83BE8F65B75E55B21&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-46dc2dadf486be600686cf6104cb6242.png&quot; alt=&quot;IMG-46DC2DADF486BE600686CF6104CB6242&quot;&gt;&lt;/p&gt;
&lt;p&gt;木木一个月多一点，大嗓门，不知道为什么这么小一个身体能发出这么大的声音。眼里只有食物和我，一见到我就会从脚底爬到身上，非常黏人。&lt;/p&gt;
&lt;p&gt;无死角的一只猫，小鼻嘎超可爱。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-dc8e4cdca891c1c031857b0323e0ddda.jpg&quot; alt=&quot;IMG-DC8E4CDCA891C1C031857B0323E0DDDA&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-9c38295c2876110d625a1781c96a1390.jpg&quot; alt=&quot;IMG-9C38295C2876110D625A1781C96A1390&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251206-411db527b6cc0f6d892a9913f30829c4.jpg&quot; alt=&quot;IMG-411DB527B6CC0F6D892A9913F30829C4&quot;&gt;&lt;/p&gt;
&lt;p&gt;养了猫之后，我已经从晚睡晚起变成早睡早起了，因为如果不抓紧时间睡觉，早上被吵醒之后就再也没办法睡了。🤡&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:22:19Z</updated><category>随笔</category><category>生活</category><category>宠物</category><category>猫咪</category><category>日常</category><category>人工写作为主</category></item><item><title>使用GitHub Actions自动发布npm包需要注意的点</title><link>https://blog.kazoottt.top/posts/70/</link><guid isPermaLink="false">issue-70</guid><description>
以我经常使用的 antfu 的 &lt;https://github.com/antfu/starter-ts&gt; 这个 template 为例，这个项目的发版工作流是：

```yml
name: Release

on:
  push:
    tags:
      - &apos;v*&apos;

jobs:
  release:
    uses: sxzz/workflows/.github/workflows/release.yml@v1
    with:
      publish: true
    permissions:
      contents: write
      id-token: write
```

&lt;https://github.com/sxzz/workflows/blob/v1/.github/workflows/release.yml&gt; 这个工作流具体的内容是：

```yml
name: Release

on:
  workflow_call:
    inputs:
      publish:
        required: false
        type: boolean
        default: false
      build:
        required: false
        type: string
        default: &apos;&apos;

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest

    permissions:
      contents: write
      id-token: write

    steps:
      - name: Setup JS
        uses: sxzz/workflows/setup-js@v1
        with:
          fetch-all: true

      - name: Generate GitHub Changelog
        run: pnpx changelogithub
        continue-on-error: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Build
        run: ${{ inputs.build }}
        if: ${{ inputs.build &amp;&amp; inputs.publish }}

      - name: Publish to NPM
        run: pnpm -r publish --access public --no-git-checks
        if: ${{ inputs.publish }}
```

完成了对比版本 tag 之间根据 commit msg 生成 changelog 、npm 发版的操作。

在 &lt;https://github.com/antfu/starter-ts/blob/main/README.md&gt; 有以下提示：

![IMG-7BF0639551873F6F24C0419B456950D2.png](&lt;https://pictures.kazoottt.top/2025/12/20251210-7bf0639551873f6f24c0419b456950d2.png&gt;)

我们编写好了想要发布的发布的包之后,需要手动 npm publish 一下。

## 注意点 1：包名与权限配置

package.json 中的 name 就是我们的 npm 包名，不允许和其他包重复。并且即使没有重复也可能有 npm 不接受的关键词。

&gt; **引用自 npm 发包失败的原因:**

### Npm 发包失败的原因

#### 可能的原因

1. 当前的账号没有发包权限。（比如没有某个包或者某个 scope 的权限）
2. 当前的包名不符合规范。（需要 URL-safe characters）
3. 当前的包名是屏蔽词。（例如 download
4. 可能是当前的 npm 设置了镜像地址，而不是原始的 npm 地址，导致 login 的时候无法登录从而无法发布。解决方法就是在发布的时候先切换为原始的 npm 地址，再发布。

// 吐槽一下，npm 的 cli 给的信息很多都是无效信息，有时候很难从中知道到底是哪里有问题

#### Npm 包名屏蔽词

1. download (我替换成 downloader 之后才成功发布）

#### 参考

[\[BUG\] npm publish get 400 Bad Request without any useful information · Issue #6090 · npm/cli · GitHub](&lt;https://github.com/npm/cli/issues/6090&gt;)

## 注意点 2

如果你之前配置过 npm 的镜像源，此时要发布到 npm 上，则需要切换回 npm 原始的源。

### 方法 1：安装 nrm

```shell
npm install -g nrm

nrm use npm

# 发布完之后可以切换回镜像源
nrm use taobao
```

### 方法 2：手动切换 npm 源

```shell
npm config set registry https://registry.npmjs.org/

# 发布完之后可以手动切换回镜像
npm config set registry https://registry.npmmirror.com/
```

发布好之后可以去 npm 设置 Trusted Publisher  

## 注意 3 Trusted Publisher 的 repo 只需要填写 repo name

需要注意的是：这里的 repo 只需要填写 repo name 即可，不需要填写完整路径。
比如 &lt;https://github.com/KazooTTT/qkpr&gt; 那么只需要填写 pkqr 即可，而不是 `https://github.com/KazooTTT/qkpr` 或者 `github. com/KazooTTT/qkpr`

![IMG-987B468B36E82E9235A1098D11B310A6](&lt;https://pictures.kazoottt.top/2025/12/20251210-987b468b36e82e9235a1098d11b310a6.png&gt;)

总的来说，通过 GitHub Actions 自动发布 npm 包需要注意配置正确的权限、设置合适的镜像源、遵循包名规范，并正确配置 Trusted Publisher，这样才能确保发布流程顺利进行。

&lt;!-- issues-blog:source=d1:post.github-actions-npm-publish-guide --&gt;
&lt;!-- issues-blog:published-at=2025-11-26T00:00:00.000Z --&gt;
</description><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以我经常使用的 antfu 的 &lt;a href=&quot;https://github.com/antfu/starter-ts&quot;&gt;https://github.com/antfu/starter-ts&lt;/a&gt; 这个 template 为例，这个项目的发版工作流是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;YAML&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Release&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  push&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    tags&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;v*&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  release&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;sxzz/workflows/.github/workflows/release.yml@v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      publish&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      contents&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;write&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      id-token&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;write&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/sxzz/workflows/blob/v1/.github/workflows/release.yml&quot;&gt;https://github.com/sxzz/workflows/blob/v1/.github/workflows/release.yml&lt;/a&gt; 这个工作流具体的内容是：&lt;/p&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;YAML&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Release&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  workflow_call&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    inputs&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      publish&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        type&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;boolean&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        default&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      build&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        type&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        default&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  release&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Release&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      contents&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;write&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;      id-token&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;write&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Setup JS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        uses&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;sxzz/workflows/setup-js@v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;          fetch-all&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Generate GitHub Changelog&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;pnpx changelogithub&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        continue-on-error&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        env&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;          GITHUB_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;${{ inputs.build }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;${{ inputs.build &amp;#x26;&amp;#x26; inputs.publish }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;Publish to NPM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;pnpm -r publish --access public --no-git-checks&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;${{ inputs.publish }}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成了对比版本 tag 之间根据 commit msg 生成 changelog 、npm 发版的操作。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://github.com/antfu/starter-ts/blob/main/README.md&quot;&gt;https://github.com/antfu/starter-ts/blob/main/README.md&lt;/a&gt; 有以下提示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251210-7bf0639551873f6f24c0419b456950d2.png&quot; alt=&quot;IMG-7BF0639551873F6F24C0419B456950D2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们编写好了想要发布的发布的包之后,需要手动 npm publish 一下。&lt;/p&gt;
&lt;h2&gt;注意点 1：包名与权限配置&lt;/h2&gt;
&lt;p&gt;package.json 中的 name 就是我们的 npm 包名，不允许和其他包重复。并且即使没有重复也可能有 npm 不接受的关键词。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;引用自 npm 发包失败的原因:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Npm 发包失败的原因&lt;/h3&gt;
&lt;h4&gt;可能的原因&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;当前的账号没有发包权限。（比如没有某个包或者某个 scope 的权限）&lt;/li&gt;
&lt;li&gt;当前的包名不符合规范。（需要 URL-safe characters）&lt;/li&gt;
&lt;li&gt;当前的包名是屏蔽词。（例如 download&lt;/li&gt;
&lt;li&gt;可能是当前的 npm 设置了镜像地址，而不是原始的 npm 地址，导致 login 的时候无法登录从而无法发布。解决方法就是在发布的时候先切换为原始的 npm 地址，再发布。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;// 吐槽一下，npm 的 cli 给的信息很多都是无效信息，有时候很难从中知道到底是哪里有问题&lt;/p&gt;
&lt;h4&gt;Npm 包名屏蔽词&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;download (我替换成 downloader 之后才成功发布）&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;参考&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/npm/cli/issues/6090&quot;&gt;[BUG] npm publish get 400 Bad Request without any useful information · Issue #6090 · npm/cli · GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;注意点 2&lt;/h2&gt;
&lt;p&gt;如果你之前配置过 npm 的镜像源，此时要发布到 npm 上，则需要切换回 npm 原始的源。&lt;/p&gt;
&lt;h3&gt;方法 1：安装 nrm&lt;/h3&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -g&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; nrm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;nrm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; use&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; npm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 发布完之后可以切换回镜像源&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;nrm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; use&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; taobao&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法 2：手动切换 npm 源&lt;/h3&gt;
&lt;pre class=&quot;shiki github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3&quot; tabindex=&quot;0&quot; data-language=&quot;Shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; registry&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; https://registry.npmjs.org/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# 发布完之后可以手动切换回镜像&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; registry&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; https://registry.npmmirror.com/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发布好之后可以去 npm 设置 Trusted Publisher&lt;/p&gt;
&lt;h2&gt;注意 3 Trusted Publisher 的 repo 只需要填写 repo name&lt;/h2&gt;
&lt;p&gt;需要注意的是：这里的 repo 只需要填写 repo name 即可，不需要填写完整路径。
比如 &lt;a href=&quot;https://github.com/KazooTTT/qkpr&quot;&gt;https://github.com/KazooTTT/qkpr&lt;/a&gt; 那么只需要填写 pkqr 即可，而不是 &lt;code&gt;https://github.com/KazooTTT/qkpr&lt;/code&gt; 或者 &lt;code&gt;github. com/KazooTTT/qkpr&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/12/20251210-987b468b36e82e9235a1098d11b310a6.png&quot; alt=&quot;IMG-987B468B36E82E9235A1098D11B310A6&quot;&gt;&lt;/p&gt;
&lt;p&gt;总的来说，通过 GitHub Actions 自动发布 npm 包需要注意配置正确的权限、设置合适的镜像源、遵循包名规范，并正确配置 Trusted Publisher，这样才能确保发布流程顺利进行。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:22:11Z</updated><category>笔记</category><category>npm</category><category>人工写作为主</category></item><item><title>重新明确笔记软件和博客的边界</title><link>https://blog.kazoottt.top/posts/69/</link><guid isPermaLink="false">issue-69</guid><description>我禁用了曾经觉得好用的 obsidian + github actions，重新明确笔记软件和博客的边界。  

我常用的笔记软件是 obsidian，会通过官方同步服务同步到多台设备上，不过也会搭配 git + github actions + md 的 meta 属性把 obsidian 仓库部分内容筛选同步到另外一个 git 仓库中，从而触发 cloudflare 构建部署我的个人博客。  
这是之前分享的内容  

一些预处理的逻辑：

![Pasted image 20251027000833](&lt;https://pictures.kazoottt.top/2025/10/20251027-6657f231d26772962812e8370b4a6bc4.png&gt;)

更具体的脚本: [使用github action定时同步obsidian内容到astro博客仓库](&lt;/wiki/synchronize-profiles-to-remote-and-local-on-a-regular-basis/&gt;)  

但时间久了发现了一些问题：

- 要对外展示的内容和 obsidian 的内容不是完全一致，虽然我也实现了复制没有上传到图床的文件到博客仓库的 public 文件夹中，保证图片始终能显示，但其实速度不及图床，并且有的上传了图床有的没有，以及编辑起来也不是很舒服。以及在 obsidian 中双链引用的一些内部内容，如果没有发布到博客其实也无法查看，虽然可以通过继续完善 action 的脚本清洗内容，实现大部分的需求，但是自己写内容的时候有些束手束脚。
- 由于使用了同步服务 + git 共同管理，经常需要处理的 git merge conflicts ，浪费时间。只有处理以后才能发布博客。为什么不直接更新博客的仓库内容？因为自己做的 action 实现了一些别的处理，一旦重新 push obsidian 所属的完整仓库，自己在博客仓库的内容就会被清理后内容重新覆盖掉。
- 博客打算从 md 向 mdx 迁移，实现更多自定义的 md 渲染。

为了解决第一个问题，我开始是副本的概念，当一篇要对外展示的内容要发布的时候，我会去复制一份原有的，然后在原来的基础之上修改。这就导致其实 obsidian 中存在很多重复的内容。看着越来越混乱的 obsidian，有些产生抵触心理了，我决定还是重新好好整理一下现有内容。  

有的时候总想着方便 all in one 一条龙，但这次的工作流确实让自己更受限制了。当记录的形式限制了记录本身，或许应该适当改变一下工作流了。  

于是我决定还是拆掉 obsidian 通过 actions 同步内容的逻辑。直接手动上传对外的版本到博客的仓库，一下子感觉负担少了很多。

&lt;!-- issues-blog:source=d1:post.re-clarify-the-boundary-between-note-taking-software-and-blogs --&gt;
&lt;!-- issues-blog:published-at=2025-10-27T00:00:00.000Z --&gt;
</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我禁用了曾经觉得好用的 obsidian + github actions，重新明确笔记软件和博客的边界。&lt;/p&gt;
&lt;p&gt;我常用的笔记软件是 obsidian，会通过官方同步服务同步到多台设备上，不过也会搭配 git + github actions + md 的 meta 属性把 obsidian 仓库部分内容筛选同步到另外一个 git 仓库中，从而触发 cloudflare 构建部署我的个人博客。&lt;br&gt;
这是之前分享的内容&lt;/p&gt;
&lt;p&gt;一些预处理的逻辑：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pictures.kazoottt.top/2025/10/20251027-6657f231d26772962812e8370b4a6bc4.png&quot; alt=&quot;Pasted image 20251027000833&quot;&gt;&lt;/p&gt;
&lt;p&gt;更具体的脚本: &lt;a href=&quot;/wiki/synchronize-profiles-to-remote-and-local-on-a-regular-basis/&quot;&gt;使用github action定时同步obsidian内容到astro博客仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但时间久了发现了一些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要对外展示的内容和 obsidian 的内容不是完全一致，虽然我也实现了复制没有上传到图床的文件到博客仓库的 public 文件夹中，保证图片始终能显示，但其实速度不及图床，并且有的上传了图床有的没有，以及编辑起来也不是很舒服。以及在 obsidian 中双链引用的一些内部内容，如果没有发布到博客其实也无法查看，虽然可以通过继续完善 action 的脚本清洗内容，实现大部分的需求，但是自己写内容的时候有些束手束脚。&lt;/li&gt;
&lt;li&gt;由于使用了同步服务 + git 共同管理，经常需要处理的 git merge conflicts ，浪费时间。只有处理以后才能发布博客。为什么不直接更新博客的仓库内容？因为自己做的 action 实现了一些别的处理，一旦重新 push obsidian 所属的完整仓库，自己在博客仓库的内容就会被清理后内容重新覆盖掉。&lt;/li&gt;
&lt;li&gt;博客打算从 md 向 mdx 迁移，实现更多自定义的 md 渲染。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决第一个问题，我开始是副本的概念，当一篇要对外展示的内容要发布的时候，我会去复制一份原有的，然后在原来的基础之上修改。这就导致其实 obsidian 中存在很多重复的内容。看着越来越混乱的 obsidian，有些产生抵触心理了，我决定还是重新好好整理一下现有内容。&lt;/p&gt;
&lt;p&gt;有的时候总想着方便 all in one 一条龙，但这次的工作流确实让自己更受限制了。当记录的形式限制了记录本身，或许应该适当改变一下工作流了。&lt;/p&gt;
&lt;p&gt;于是我决定还是拆掉 obsidian 通过 actions 同步内容的逻辑。直接手动上传对外的版本到博客的仓库，一下子感觉负担少了很多。&lt;/p&gt;

</content:encoded><updated>2026-07-24T10:21:59Z</updated><category>随笔</category><category>人工写作为主</category></item></channel></rss>