07 · Frontmatter 与保存协议
抽取和模板编译只产生字符串;真正的“耐久保存”还要处理属性类型、YAML、文件名、目标 vault、追加语义与浏览器限制。
一、保存前的四份编译产物
对一个 Template,popup/API 会分别编译:
noteNameFormat → noteName
path → target path(浏览器路径中按流程处理)
properties[*] → typed property values
noteContentFormat → Markdown body不能只编译 body,因为标题、路径和每个 property 都可以引用同一套变量、selector 与 filters。
二、Property 的最小模型
interface Property {
id?: string
name: string
value: string
type?: string
}类型可以来自 property 自身,也可由 settings 的 propertyTypes 映射补充。formatPropertyValue(value, type, templateValue) 在生成 YAML 前做领域格式化。
常见类型考虑:
- text:普通字符串;
- number:避免作为带引号文本;
- checkbox/boolean:规范 true/false;
- date/datetime:保持 Obsidian 可识别格式;
- list/multitext:拆成 YAML sequence;
- tags:清理与列表语义;
- link:保留 wiki/URL 需要的字符串形式。
三、Frontmatter 生成
浏览器主流程与环境无关 API 都有 frontmatter helper,核心目标一致:
---
title: "Example"
authors:
- Alice
- Bob
published: 2026-08-01
draft: false
---生成器必须处理:
- key 中的特殊字符;
- string 是否需要 quote;
- 多行文本;
- 数组与空数组;
- 数字/布尔值不被错误字符串化;
- 空 property 的保留策略;
- 与模板原始值/类型的联合判断。
项目选择集中生成,而不是让模板作者手写 ---,因为集中生成更容易保证 YAML 正确。
四、noteName 的双层清理
API 先编译 noteNameFormat,再调用 sanitizeFileName(),空结果回退 Untitled。下载动作还会替换 /\?%*:|"<> 等字符。
这里区分两个目标:
- Obsidian note name:受 vault 路径与平台规则影响;
- 浏览器下载文件名:必须满足本地文件系统和 Downloads API。
模板里的 safe_name 让用户显式控制,最终保存层仍保底清理,形成防御纵深。
五、三种主保存行为
SaveBehavior:
addToObsidian
saveFile
copyToClipboardpopup.ts:1278 的 determineMainAction() 根据 settings 决定主按钮;其他动作可放进 secondary menu。
1. Add to Obsidian
obsidian-note-creator.ts 负责构建 Obsidian URL 并打开。
2. Save file
通过浏览器下载能力创建 .md,适合未安装 Obsidian或希望手动归档的用户。
3. Copy to clipboard
优先现代 clipboard,受限环境可通过 content script textarea + execCommand('copy') 降级。
六、Obsidian URI 不是单一 create
Template.behavior 支持六种语义:
create
append-specific
append-daily
prepend-specific
prepend-daily
overwrite保存层需要把它们映射到 Obsidian 能理解的 URI/CLI 参数,而不是在浏览器中直接写 vault 文件。
数据通常包括:
vault
path / note name
content
behavior
silent/focus preferenceURI 方案的优势是浏览器扩展不需要文件系统权限;限制是 URL 长度、编码、焦点切换与本地 Obsidian 版本能力。
七、Clipboard 作为可靠桥
tryClipboardWrite(fileContent, obsidianUrl) 暗示长内容或浏览器限制下的两步策略:
- 把完整内容写入剪贴板;
- 打开携带控制参数的 Obsidian URL;
- Obsidian 侧再读取/落盘。
剪贴板不是最终存储,而是浏览器 sandbox 与桌面应用之间的传输通道。
八、silentOpen 与用户焦点
剪藏工具频繁把用户从浏览器拉到 Obsidian 会打断工作。silentOpen 表达的是交互策略:尽可能完成保存但不抢焦点。
这类设置看似 UI 偏好,实际要一直传播到 URI/CLI 的最末层,因此被纳入 Settings 与 save function 参数。
九、Popup 的保存时序
handleClipObsidian() 大致执行:
读取当前 DOM fields
→ 获取/确认 template
→ compile note name, properties, content
→ generate frontmatter
→ assemble fileContent
→ saveToObsidian(...)
→ incrementStat(addToObsidian)
→ addHistoryEntry(url/title/vault/path)
→ update UI success state / close if configured编译发生在点击时,而不只发生在 popup 初始化时。原因是用户可能编辑 note name、properties、prompt response 或正文预览。
十、历史与统计为什么分开存
stats在storage.sync:只有小型计数,适合跨设备同步;history在storage.local:最多 1000 条,含 URL/title/path,体量大且隐私敏感,不适合同步配额。
incrementStat() 先加载 settings、增加计数、保存;若有 URL,再写 history。Reader mode 也使用同一统计管线。
十一、CLI 保存出口
CLI 调用 clip() 后有三种出口:
stdout 默认,适合 shell pipe
--output <file> Node fs 写入
--open Obsidian CLI 或 URI--uri 强制 URI,--silent 控制 focus,--vault 覆盖 template vault。优先级是显式 CLI 参数高于模板默认值。
十二、API 的 ClipResult
环境无关 API 不替调用方保存,只返回完整中间结果:
interface ClipResult {
noteName: string
frontmatter: string
content: string
fullContent: string
properties: Property[]
variables: Record<string, string>
}为什么同时返回拆分和合并结果?
fullContent可直接写文件;frontmatter/content便于调用方二次组合;properties/variables便于调试、预览或审计模板。
十三、失败语义
| 场景 | 处理 |
|---|---|
| 模板局部错误 | 记录并尽量给部分输出 |
| noteName 为空 | Untitled |
| clipboard API 失败 | content script/legacy copy 降级 |
| URI 打开失败 | 抛出/展示保存错误,不应假报成功 |
| history 写失败 | 不应反向破坏已完成的笔记保存 |
| 下载文件名非法 | 最终 sanitize |
| CLI fetch 非 2xx | stderr + exit 1 |
| 模板目录无匹配 | 列出扫描数量 + exit 1 |
保存动作应尽量是“主事务优先、遥测次要”。
十四、隐私边界
保存链路可能处理完整页面 HTML、API key、vault 名、历史 URL。源码体现的边界:
- 笔记正文默认本地处理;
- 只有 Interpreter 开启且模板含 prompt 时才把上下文发给选定模型;
- history 留在 local;
- provider key 存于扩展 sync settings,需要用户理解同步风险;
- debug logs 在 production 通过 global define 被裁剪。
模板设计者尤其要注意 fullHtml 作为 prompt context 可能包含页面上不希望发送的内容。
十五、可改进点与取舍
从架构角度,保存层未来可进一步抽象成 adapter:
CompiledNote
→ ObsidianUriWriter
→ BrowserDownloadWriter
→ ClipboardWriter
→ NodeFileWriter当前实现已在函数层分离,但浏览器 UI 仍知道较多动作细节。是否继续抽象取决于是否增加新目标(REST API、Git、移动 share sheet 等);没有新目标时,过早接口化也会增加理解成本。
十六、本章检查点
- 能说出模板的四份独立编译产物;
- 能解释 typed property 为什么必须先于 YAML;
- 能区分 create/append/prepend/daily 等行为;
- 能说明 sync stats 与 local history 的划分;
- 能解释 API 为什么只返回 ClipResult 而不保存;
- 能识别 Interpreter 是保存链路中唯一可能向外发送正文的功能。