Obsidian 目录新鲜度着色
让 Obsidian 左侧文件树自己说话:扫一眼就知道哪篇是今天写的、哪篇放了一周、哪些还没读、哪些是云盘冲突垃圾。
产出物只有一个 CSS 片段文件,靠一个 Python 脚本每日重算。
目录着色的特点(本技能的设计核心)
一句话:这套着色不是「给文件夹配个固定颜色」,而是让整棵目录树随时间自己变色——谁新、谁旧、谁没读、谁是垃圾,扫一眼文件树就知道,且不需要任何人手动维护颜色。
四个特征速览:
| 特征 | 普通目录着色 | 本方案 |
|---|---|---|
| 颜色随时间变化 | 不变,配好就固定 | 每天重算,今天最艳 → 7 天褪灰 → 回落分类色 |
| 文件夹是否着色 | 只染文件夹,或只染文件 | 文件夹继承子树最新笔记日期,折叠也看得见 |
| 日期从哪来 | 多为 mtime(云盘同步会污染) | frontmatter → 文件名 → mtime 三级回退 + 防伪 |
| 多套配色打架 | 靠手动调启用顺序,别人一改就失效 | :not(#id) 拿到 id 位,压过任意类数量,与顺序无关 |
| 新目录会不会漏染 | 会,硬编码清单永远追不上新建的目录,且无人察觉 | 每次对账,未登记目录按名称哈希自动派色并点名报告 |
| 颜色之间会不会撞 | 不管,配完能显示就算完 | 每次算 CIELAB ΔE 体检,撞车直接点名(见第 16 节) |
| 日期本身可不可信 | 不管,mtime 说新就是新 | 两级判定 mtime 可信度(按目录 + 按时间突发),不可信就换证据链;没证据就撤离着色(见第 19、23 节) |
| 污染换个分布还认得吗 | 不管 | 认得——同步跨目录刷也能抓:不看目录看时分(60 分钟内跨 2 目录 ≥10 篇),见第 23 节 |
| 别人写的目录色坏了吗 | 不管,各管各的 | 扫 snippets 全目录查四类结构隐患(回落层自相矛盾/子串误伤/漏染/非法转义),见第 20 节 |
| 配色改了能不能留住 | 改源码,升级就冲掉 | 外部 folder_colors.json + --pin-folders 一键固化,升级不冲掉(见第 21 节) |
| 染的是不是真东西 | 不管,写进 CSS 就算数 | 死选择器体检:拿真实路径撞每一个选择器,语法合法但永远匹配不到的直接点名(见第 22 节) |
| 谁压过谁 | 靠片段加载顺序碰运气 | 级联顺序体检:按「元素 × 通道」实算每条规则的胜负,高优先级层被后面的层压掉就点名(见第 24 节) |
| 只在暗色/高对比下坏 | 查不到,浅色下看着好好的 | 三个通道分别算(常规/暗色/高对比),实测揪出高对比下 3 篇未读被覆盖(见第 24 节) |
| 嵌套目录染不染 | 多数方案只染顶层,二级三级目录标题是灰的 | 逐层显式枚举 + 继承父色,覆盖率实测 22.1% → 100%(见第 25 节) |
| 自检的分母对不对 | 没人问过;只统计顶层目录,覆盖率永远 100% | 覆盖率体检把目录分成已覆盖/刻意弱化/漏染三类分别计数(见第 25 节) |
| 污染了还继续谎报吗 | 会,提示归提示,颜色照样错 | 不会——检测到即免疫,宁可无色不可谎报(实测 309 篇 → 47 篇) |
| 自检靠不靠谱 | 没有自检 | 自检本身逐篇实测特异性、不写死常量;输入不变则结论不变(见第 17 节) |
| 最终显示成什么颜色 | 靠肉眼扫一眼,看不出也就认了 | 终局颜色体检:按浏览器层叠算法逐篇算出实际渲染色,与设计意图对账(见第 26 节) |
| 只在某个场景下坏一半 | 查不到,浅色下看着好好的 | 浅色常规 / 高对比两个场景分别模拟,实测揪出 12 个文件夹标题没被加深(见第 26 节) |
| 颜色真的看得见吗 | 不管,color 写进 CSS 就算数 | 渲染弱化体检:opacity 是乘在整个元素上的,别人一句裸声明就能冲淡你;折算后算对比度再点名(见第 27 节) |
| 弱化/归档区怎么表达 | 用更淡的灰色,淡到看不见为止 | 语义交给 opacity 通道,基础色保持站得住;「弱化是相对弱化,不是消失」(见第 27 节) |
| 开关会不会越权 | 没有开关概念 | 会,且最难发现:一个「不参与新鲜度」的开关被当成「整篇不参与着色」,13 篇笔记(含 4 篇冲突文件)漏染、漏报 57%(见第 28 节) |
| 出问题怎么发现 | 看不出来 | 语法自检 + 注册校验 + 陈旧度审计 + 配色体检 + 日期可信度 + 量化告警(分「实际/潜在」) |
下面逐条展开。
1. 颜色会过期 —— 这是它和普通着色最本质的区别
绝大多数 Obsidian 配色方案是静态的:给某个文件夹指定一个颜色,然后永远不变。
本方案的颜色是时间的函数:
| 距今天数 | 表现 |
|---|---|
| D0(今天) | 最鲜艳的橙红 #C2410C,加粗 + 淡橙背景 + 左侧色条 |
| D1 → D4 | 每天淡一档,橙 → 浅橙 → 藕色 |
| D5 → D6 | 转灰,热度褪去 |
| D7 及更早 | 不再着新鲜度色,自动回落到所在目录的分类色 |
也就是说:今天的笔记明天会自动变淡,一周后自动"沉底"变回分类色。
不需要任何人手动改颜色、打标签、移动文件 —— 颜色随时间自己演进,这是静态配色做不到的。
2. 三层语义叠加在同一行文件名上
一个文件名的最终颜色,是三层信息的合成结果,互不涂花:
- 分类色(底层,常驻) —— 它属于哪个目录:工作业务蓝、投资理财红、出海 TikTok 青、古文经典紫……
- 新鲜度色(中层,7 天有效) —— 它有多新:D0 最艳 → D6 近灰
- 状态色(顶层,优先级最高) —— 它的状态:未读(紫底高亮)、云盘冲突(红 + 删除线)
第 8 天起中层失效,文件自然落回底层分类色 —— 新鲜度是覆盖在分类色之上的一层"临时热度",过期就自动揭掉。
3. 文件夹会继承子树里最新的那篇笔记
不只看文件,目录本身也着色:一个目录的日期 = 它子树内所有笔记里最新的那个日期。
所以即使目录是折叠状态,你也能一眼看出「今天哪个分类动过」——D0 的目录标题是橙红加粗带左侧色条的。
(同类方案只染文件不染文件夹,折叠起来就全瞎了。)
v2.13 起逐层计算:每一层目录都按自己的子树算,父子可以显示不同档位——
父目录最新笔记是 3 天前、某个子目录今天刚动过,就显示「父 D3 / 子 D0」,
一眼看出新东西落在哪一层。此前只算顶层目录。
子目录的分类色则继承最近的有色祖先(05-古文经典/道德经 沿用古籍紫),
不另派新色——53 个嵌套目录各派一色必然撞车,而且它们本来就不是新分类。详见第 25 节。
4. 日期三级回退 + 防伪,抗云盘污染
日期来源按可信度排序:
- frontmatter 的
日期 / 更新 / 创建 / created / date / updated - 文件名里的日期(
xxx-2026-10-04.md) - 文件 mtime(最后手段)
为什么 mtime 排最后:天翼云盘 / 坚果云 / iCloud 同步会把 mtime 刷成同步时间,用 mtime 判新鲜度必然失真。
防伪:文件名里的日期若与 mtime 相差超过 FILENAME_DATE_MAX_SKEW(默认 400 天),判定为「正文引用的历史日期」而非笔记日期(比如古文笔记文件名里写的朝代年份),自动降级用 mtime,不会把一篇老笔记误染成"今天"。
> ⚠️ 这条防伪规则有个隐含前提,v2.8 在真实库上撞破了(详见第 19 节):
> 它是拿 mtime 当尺子去量文件名日期的。一旦 mtime 本身被云盘同步污染,尺子就是弯的——
> 2026-10-20 实测,06-有道云导入 里 221 篇 2015 年的微信收藏被同步刷成 mtime 2026-10-19,
> 于是 221 个真实日期全被判成「引用日期」丢弃,整批 2015 年的笔记被染成"昨天写的最鲜艳色"。
> v2.8 起:某目录的 mtime 一旦被认定不可信,这把弯尺就不再使用,文件名日期直接采信。
未来日期单列:frontmatter 日期晚于今天的(计划中的笔记)不再被当成"陈旧"静默丢弃,单独统计为「未来/计划」,不着新鲜度色。
5. 精确匹配,不靠子串猜
生成的每条规则都是精确到单个文件的选择器:
.nav-file-title[data-path="01-工作业务/工作业务MOC.md"] { ... }
而不是同类方案常用的子串匹配 [data-path*="2026-10-12"] —— 后者会把任何路径里含这串日期的文件都染上今天的颜色,包括恰好在文件名里引用了历史日期的笔记。
6. 选择器特异性加固 —— 用 id 位取胜,不靠堆类名(v2.3 重写)
这是多套配色共存的库里最容易踩的坑:
> 当两个片段都用了 !important 时,!important 互相抵消,
> 胜负由选择器特异性决定,与片段加载顺序无关。
6.1 v2.0~v2.2 的做法:堆类名(能用,但笨重)
在目标元素自身上重复类名与属性选择器(CSS 合法写法,不改变匹配范围):
.nav-file-title.nav-file-title.nav-file-title[data-path="X"][data-path="X"][data-path="X"] .tree-item-inner
特异性堆到「类 3 + 属性 3 + 后代类 1 = 7」,能赢,但代价是选择器极长:同一个库里 CSS 从 36KB 涨到 90KB。
6.2 v2.3 的做法::not(#id) 直接拿 id 位
CSS 规范里 :not() 取其参数中最高的特异性。所以:
.nav-file-title[data-path="X"]:not(#kb-scope) .tree-item-inner
:not(#kb-scope) 贡献 1 个 id,于是特异性是 (1,3,0)。而 CSS 比较特异性是按位比较——先比 id 位,id 多的一律赢,与后面有多少个类无关。
| 片段 | 选择器 | 特异性 (id-类-元素) |
|---|---|---|
| 同类方案·动态层 | .nav-files-container .nav-file-title[data-path*="…"] .tree-item-inner |
0-4-0 |
| 同类方案·冲突层 | .nav-files-container .nav-files-container .nav-file-title[…] .tree-item-inner |
0-5-0 |
| 本方案 v2.2(堆类名) | .nav-file-title×3 + [data-path]×3 + .tree-item-inner |
0-7-0 |
本方案 v2.3(:not(#id)) |
.nav-file-title[data-path="X"]:not(#kb-scope) .tree-item-inner |
1-3-0 |
| 假设对手堆到 20 个类 | … | 0-20-0 ← 仍然输给 1-3-0 |
收益:既把胜负从"比谁类名多"变成"比谁有 id 位"——对手无论怎么堆类名都追不上,又让 CSS 体积下降约 23%(实测 90,291 → 69,335 字节)。
要求只有一条:#kb-scope 这个 id 在 Obsidian DOM 里不存在(我们从不添加它),所以 :not() 恒真,纯为抬特异性而存在。想改名字改 BOOST_ID_NAME。
6.3 顺带修掉一个隐性 bug:特异性不能「求和」
v2.2 用「类名数 + 属性数 + id 数」求和来判断胜负。引入 :not(#id) 之后这个模型立刻失真:本方案 (1,3,0) 求和只有 4,对手 (0,5,0) 求和是 5,求和会误判对手赢,而真实规则是本方案赢。
所以 v2.3 改成三元组按位比较(spec_tuple() / spec_gt()):
(1,3,0) > (0,5,0) → True ← id 位优先
(0,4,0) > (1,0,0) → False
同时修掉「属性值里的点号被当成类名」:选择器里 data-path="a/b.md" 的 .md 不是类名,不先抹掉会把 1-3-0 误报成 1-4-0。
6.4 配套的三道保险
- 写完后回读
appearance.json,确认本片段确实启用且在末位(顺序是双保险,不是唯一依赖) - 竞品体检:扫描其他已启用片段里针对文件树的规则,按三元组比较,发现高于本片段就点名告警
- 输出里直接打印
选择器特异性: 1-3-0,一眼可核
7. 幂等写入,不制造无谓的云盘同步
每次运行先比对生成内容与磁盘现有 CSS(比较时剔除"生成时间"这一行)。
内容没变就不写盘 —— 对一个 60KB+ 的片段文件,避免每天无谓改动触发云盘同步风暴和 Obsidian 重载。
输出会明确显示 内容无变化,跳过写入。
一个容易漏的细节:跳过写入 ≠ 跳过体检。陈旧片段体检是纯只读的,即使内容没变化也会照常输出——否则"今天恰好没写文件"的那天反而看不到旧片段停更的告警。
8. 零插件、零笔记改动、自愈
- 不安装任何 Obsidian 插件。社区插件(如 File Explorer Color / Supercharged Links)都只能手动给单个文件或文件夹指定颜色,做不到"按日期自动渐变",装了只会引入 CSS 冲突。
- 不修改任何
.md内容,不写 frontmatter,只读。 - 每次执行整体重写 CSS,不存在规则残留;文件增删后重跑一次即可,规则自动跟上。
9. 主题与无障碍
- 暗色双通道:通道 A 用
body.theme-dark(Obsidian 主题自己的类,权威);通道 B 用@media (prefers-color-scheme: dark)+body:not(.theme-light),兜底那些不写.theme-dark类的第三方主题。背景色自动反转,不会出现浅色块刺眼。 - 高对比模式(
prefers-contrast: more)下淡色档自动加深,保证可读 - 动画受
prefers-reduced-motion约束,用户要求减少动效时自动关闭
10. 生成前自检 + 生成后校验
生成前:用状态机扫描注释开合,能抓出「注释提前闭合 + 裸 * 行」这类隐性损坏——只数 /* 和 */ 的个数是查不出来的(个数依然配对,但规则已被解析器吞掉)。同类方案的自动生成片段就出过这个事故,实测能准确报错到第 2/3/4 行。校验不通过直接中止写入,原 CSS 保持不动。
生成后:回读 appearance.json 校验启用状态与顺序,并体检竞品片段特异性。
11. 「颜色会过期」的另一半 —— 绝对日期型片段会静默失效(v2.1 新增)
第 1 节说「颜色是时间的函数」,这里补上容易被忽略的反面:如果片段把日期硬编码进选择器,它就变成了一次性产物。
/* 绝对日期型:这条规则只在 2026-10-12 当天有意义 */
.nav-file-title[data-path*="2026-10-12"] .tree-item-inner { color: #C2410C; }
这类片段必须靠生成方每天重跑。一旦生成方停更,后果是:
- CSS 没有任何语法错误(语法自检查不出来)
- 片段仍然启用、仍在
enabledCssSnippets里 - 但规则里的日期整体滑出窗口,颜色悄悄没了,或更糟——把最鲜艳的「今天色」错染到过期日期上
用户只会觉得「今天的新笔记怎么没颜色」,很难联想到是另一套配色停更了。
实测案例(真实库,2026-10-14):
| 项 | 值 |
|---|---|
| 片段 | teleagent-dynamic-colors.css(某同类方案的动态层) |
| 硬编码日期 | 2026-10-06 ~ 2026-10-12,共 7 条 |
| 文件 mtime | 2026-10-12 01:00,此后未再生成 |
| 实际状态 | 5/7 条规则仍在 7 天窗口内(看着像在工作),但最鲜艳的「今天色」已错染到 2 天前的笔记;10-13、10-14 的笔记无规则匹配 |
注意这里的陷阱:只判断「窗口内是否还有规则」会漏判——它还有 5 条规则在生效,肉眼完全看不出异常。
正确判据:绝对日期型片段的规则里必须包含今天。不含今天 → 即判陈旧,按 今天 - 最新规则日期 报告过期天数。本技能已实现为 audit_stale_snippets(),每次运行与 --check 都会输出告警。
本方案为什么不受影响:生成的 CSS 里没有任何绝对日期,只有「相对新鲜度」D0~D6,且每天整体重算。所以哪怕某天漏跑,也只是颜色整体滞后一天,不会出现「规则全部失配」的静默失效。
12. 告警不是形容词,是篇数(v2.2 新增)
发现别人的片段停更了,只说「今天色错染在 3 天前的笔记上」,用户仍然没法决策——20 篇被染错和 1 篇被染错,处置优先级完全不同。
所以告警必须落到本库真实篇数上。做法是先把全库笔记按日期做直方图,再与陈旧片段的规则日期交叉统计,一次算出两个数字:
本库实际受影响:20 篇 2026-10-12 的笔记被错染成「今天色」,今天的 3 篇笔记无规则匹配
- 错染篇数 = 全库里路径包含该日期字符串的笔记数(口径必须和竞品一致:它用的是
data-path*="2026-10-12"子串匹配,所以得按路径子串数,而不是按笔记自身日期数)。它能和同一次运行的Dn分档互相印证:停更 3 天 → 对应 D3 档的 20 篇,两个数字对得上说明口径没跑偏。 - 漏染篇数 = 今天的笔记数,也就是输出里的
D0篇数——本该最鲜艳,实际完全没颜色。
真实库连续三天的实测(同一个停更片段):
| 观测日 | 停更天数 | 窗口内剩余规则 | 错染篇数 | 当天漏染 |
|---|---|---|---|---|
| 10-13 | 1 天 | 6/7 | 少 | 少 |
| 10-14 | 2 天 | 5/7 | 中 | 中 |
| 10-15 | 3 天 | 4/7 | 20 篇 | 3 篇 |
「窗口内剩余规则」每天减少却始终不为 0——这正是它难被发现的原因:只要还有规则落在窗口内,文件树看起来就"像是有颜色"。
有了具体篇数,处置标准就能量化:错染 + 漏染超过 10 篇就停用该片段;个位数可以等生成方恢复。
13. 但「篇数」还得分「实际」和「潜在」(v2.3 修正)
上一节的算法有个隐含前提:只有那一个片段在生效。真实库里往往几套配色并存,这个前提不成立,于是 v2.2 会虚报。
2026-10-16 的真实库实测:
| 口径 | 篇数 | 说明 |
|---|---|---|
| v2.2 报的「实际受影响」 | 20 篇 | 按"笔记日期 = 2026-10-12"统计 |
| 竞品真实命中(路径含该日期) | 16 篇 | 它用的是子串匹配,口径应该按路径算 |
| 其中被本片段覆盖并压制 | 16 篇 | 本片段 (1,3,0) > 竞品 (0,1,0),浏览器采用本片段的颜色 |
| 实际受影响 | 0 篇 | 一条都没真被染错 |
同一个库、同一天,v2.2 说"20 篇被错染",实际一篇都没有——因为本片段对每一篇都发了更高特异性的规则,竞品那条根本轮不上。
所以 v2.3 把告警拆成两个数:
- 实际影响(effective) = 竞品命中 − 被本片段压住的。这是现在正在发生的。
- 潜在风险(potential) = 竞品命中总数。这是"本片段若被停用"会发生什么。
两个数不一致时,告警级别从 [告警] 自动降为 [提示],措辞也改掉:
[提示] 片段 teleagent-dynamic-colors 为「绝对日期型」,最新规则日期 2026-10-12 已过去 4 天…
本库命中 16 篇,但全部被本片段压制(特异性 1-3-0 > 0-1-0),实际 0 篇受影响;
若本片段被停用,这 16 篇会立刻错染成「今天色」
为什么值得较这个真:告警一旦天天喊狼来了,用户就会整段忽略——而真出事那天(比如本片段被别的工具从启用清单里挤掉)恰恰是最需要被看见的。分级之后的规则很简单:
| 输出级别 | 含义 | 该做什么 |
|---|---|---|
[提示] |
旧片段停更了,但被本片段压着,当前无害 | 不用动,记着「本片段是唯一的压制者」这件事 |
[告警] |
有笔记真的被染错了(本片段没覆盖到) | 按篇数处置:>10 篇停用旧片段,个位数等生成方恢复 |
--strict 也随之调整:只在 level=告警 时才以退出码 2 结束。被压制的陈旧片段不该让定时任务天天红。
配套的两个开关:
# 定时任务把"别人的片段停更"当真错误抛出(陈旧时退出码 2)
python scripts/refresh_note_colors.py --vault "<vault>" --check --strict
不加 --strict 不影响退出码,避免误伤既有自动化。--json 的 stale_report 同时输出结构化告警(片段名 / 最新规则日期 / 过期天数 / 窗口内剩余规则数 / 错染篇数 / 漏染篇数),可直接喂看板。
14. 随时可做的只读体检(v2.1 新增)
python scripts/refresh_note_colors.py --vault "<vault>" --check
不写 CSS、不动 appearance.json,只输出:统计 + 片段注册状态 + 竞品特异性告警 + 陈旧片段告警。
怀疑颜色不对时先跑这个,比直接重跑安全——不会产生任何副作用。
15. 目录配色清单会「漏染」——而且没人会发现(v2.4 新增)
前面 14 节都在讲颜色怎么变化,这一节讲颜色根本没被派上的情况——这是所有硬编码配色方案的通病,也是本方案 v2.4 之前自己身上的一个真实 bug。
15.1 病根:目录色是一张手写清单
不管是本方案的 FOLDER_COLORS,还是同类方案的静态层,目录色都是一张手写的映射表:
FOLDER_COLORS = {"00-收件箱": "#D97706", "01-工作业务": "#2563EB", ...}
知识库是会长的。新建一个目录,若没人回头改这张表,代码里那句 if fd not in FOLDER_COLORS: continue 就会静默跳过——不报错、不告警、不写日志。
15.2 真实库的实测代价(2026-10-17)
| 未登记目录 | 笔记数 | 占比 |
|---|---|---|
WX同步 |
11 篇 | 6.5% |
SkillHub |
6 篇 | 3.6% |
| 合计 | 17 篇 | 10.1% |
v2.4 首次运行就把它俩揪了出来,自动派色为 WX同步 → #0F766E、SkillHub → #7C2D12。
这两个目录长期无分类色,而且同类方案的静态层同样漏掉了它们——说明这不是谁写错了,是「硬编码清单」这个做法本身的结构性缺陷。
15.3 为什么「没颜色」比想象的严重
不是少个颜色这么轻。回顾第 2 节的三层语义:第 8 天起新鲜度色失效,文件要回落到分类色。分类色不存在意味着:
- 这批笔记在 7 天内是有热度的(新鲜度色生效)
- 第 8 天直接掉到 Obsidian 默认灰,而不是掉到自己的分类色
- 于是它们和「根目录散落的临时文件」混成一片 —— 分类色作为长期身份标识的作用完全失效
更麻烦的是时间越久越难发现:新建目录的头几天,里面的笔记都是新的,新鲜度色盖着,看着挺正常;等热度退了才发现"这一片怎么灰扑扑的",但那时已经说不清是哪天开始的。
15.4 解法:每次扫描都对账 + 自动派色
v2.4 起不再静默跳过。每次运行把实际扫到的顶层目录与配色清单对账:
未登记配色目录: 2 个 / 涉及 17 篇 —— 已自动派色(建议补进 FOLDER_COLORS 固化)
SkillHub: 6 篇 -> #7C2D12
WX同步: 11 篇 -> #0F766E
自动派色有三个必须守住的约束:
- 稳定可复现 —— 而且不能依赖"这批目录有几个"。
第一版写成「按遍历顺序逐个取,占用了就顺移下一个」,看着合理,实测一验就露馅:新建或删掉另一个未登记目录,其余目录的颜色会整体平移。正确做法是两轮——先在候选色表上按目录名 SHA1 哈希直接取(每个目录独立算,别的目录来去都不影响它),只有真撞色的才顺移。回归验证:
两目录: {SkillHub: #7C2D12, WX同步: #0F766E}
四目录: {SkillHub: #7C2D12, WX同步: #0F766E, 新目录A: …, 新目录B: …}
稳定性: WX同步 True | SkillHub True
- 不与已登记色撞车 —— 派色前排除
FOLDER_COLORS里已占用的值。 - 更不能与语义色撞车 —— 排除集还要并上
FRESH_COLORS、FRESH_COLORS_HICONTRAST、未读色、冲突色、根目录色。这是实测踩出来的:初版把SkillHub派成了#C2410C,正好是 D0「今天」的橙红,于是"这个目录是橙红的"和"这篇是今天写的"两种语义撞在一棵树上,反而制造误导。
生成的 CSS 里也会标注来源,方便日后回查:
/* SkillHub(自动派色:本目录未登记配色) */
--json 另有结构化字段 uncolored_folders(目录 / 篇数 / 颜色),可喂看板做长期趋势。
15.5 自动派色只是止血,不是终点
自动色能保证不漏染,但不能保证好看或语义贴切(WX同步 拿到玫红纯属哈希结果)。所以它定位是兜底 + 告警:
- 看到报告里出现未登记目录,顺手补进
FOLDER_COLORS就固化了 - 想彻底关掉自动派色:设
AUTO_FOLDER_COLOR = False,退回"只染已登记目录"(不推荐,等于重新接受漏染)
这一节的通用教训:凡是「手写清单 + 静默 continue」的组合,都会随数据增长悄悄失效。要么对账后告警,要么干脆不让它静默。本方案之前的 12~14 节都在处理别人的片段静默失效,这一节处理的是自己的。
16. 颜色写对了,不等于「能看出来」(v2.5 新增)
前 15 节解决的是「颜色有没有写进 CSS」。这一节解决的是更隐蔽的一层:每个颜色单看都合理、CSS 语法完全合法、片段正常启用、颜色值也没写错——但两个颜色放在一起人眼分不开,整套着色就白做了。
16.1 三种撞车,全部静默
| 类型 | 判定 | 后果 |
|---|---|---|
| A 回落撞车 | 目录分类色 ≈ 新鲜度色阶某档(ΔE<20) | 第 8 天新鲜度失效、回落到分类色时肉眼看不出变化,用户以为这篇还新鲜 —— 等于新鲜度着色静默失效 |
| B 目录互撞 | 两个目录的分类色 ΔE<20 | 分类色失去辨识作用,等于没分类 |
| C 状态色撞车 | 未读色/冲突色 ≈ 某目录色(ΔE<12) | 该目录下的状态标记被"淹没" |
A 类最阴,因为它和第 1 节「颜色会过期」直接对冲:本方案的全部价值在于「颜色过期时你能看出来」,而回落撞车让过期的那一刻恰好不发生视觉变化——你设计的信号,被自己选的颜色抹掉了。
16.2 实测数据(2026-10-18,171 篇)
给目录色逐个算 ΔE76 后,揪出 4 处真撞车,全部已换色:
| 目录 | 原色 | 撞上 | ΔE | 新色 |
|---|---|---|---|---|
Clippings |
#B45309 |
D1 #D4621F |
9.8 | #854D0E |
Excalidraw |
#9333EA |
05-古文经典 #7C3AED |
7.1 | #115E59 |
00-收件箱 |
#D97706 |
D2 #E07B39 |
14.5 | #CA8A04 |
02-投资理财 |
#DC2626 |
D0 #C2410C |
18.8 | #9F1239 |
换色后仍保留原语义(暖=待处理 / 红=投资 / 褐=剪藏 / 青=画布),只是把距离拉开。修复后整库体检归零。
16.3 防虚警:只对"真会发生"的撞车报警
这一步比算法本身更重要——告警一旦天天刷屏,人就会习惯性忽略,体检器也就白做了(和第 13 节把「命中篇数」拆成「实际/潜在」是同一个纪律)。两条过滤:
- 回落撞车只判参与新鲜度着色的目录。
04-模板、90-归档、Templates在EXCLUDE_FRESHNESS里,永远不经历 D0~D6,算出来 ΔE 也很小(90-归档 vs D6 只有 7.5),但报了就是假警报——它们压根不会"回落"。 - 刻意同色组显式声明。模板/归档/工具脚本本来就都属于「弱化区,灰、不抢眼」,彼此不需要互相区分。不声明的话这几个灰阶会稳定刷出 6 条互撞告警,全是真数值、全是假问题。用一行配置把意图写明:
python
INTENTIONAL_SAME_GROUP = {"04-模板", "Templates", "90-归档", "tools"}
显式声明意图,比让体检器闭嘴、或假装没看见都诚实。
淡色档的低对比度(D2~D6 在浅色主题下 1.99~2.97:1)也只报一行区间摘要而非逐档 5 行——「逐级变淡」是设计意图,靠 prefers-contrast 高对比档兜底,但同样不能让它每天刷屏。所有逐档数值仍保留在 --json 的 palette_report 里,要查随时能查。
16.4 两个踩坑(都是修 bug 时自己造的)
坑 1:把候选池榨干,反而制造出同色。 给自动派色加 ΔE 过滤时,直接按 ΔE≥20 对全部保留色过滤。但保留色有 30+ 个(14 个目录色 + 7 档新鲜度 + 7 档高对比 + 未读/冲突/根/全局),要求与每一个都拉开 ΔE20 几乎不可能,候选池被榨到只剩 1 个槽位。池子一小于待派目录数,那条「撞了就顺移」的兜底就永远顺移到同一个颜色——SkillHub 和 WX同步 拿到同一个 #A21CAF。修撞车反倒制造了更严重的撞车。
坑 2:池子依赖批次,把稳定性搞没了。 改成「池子够分就停」后,池大小依赖 len(names),于是同一个目录在单目录批次和多目录批次下拿到不同颜色(WX同步 单独派 #A21CAF、四个一起派 #15803D),等于把第 15.4 节好不容易建立的稳定性又退回去。
结论(通用):候选池只能取决于已占用色集合,绝不能取决于这批要派几个;宁可把阈值放宽到 ΔE12,也绝不能让两个目录共用同一颜色。两条硬保证冲突时的优先级——唯一性 > 稳定性:共用色 = 分类直接失效,比"某个目录换了个颜色"严重得多。有限色板下「绝对稳定 + 绝对唯一」不可兼得(鸽笼原理),别硬凑,想清楚在文档里写明白。
16.5 怎么读报告
配色体检: 发现 1 处撞车
[提示] D2~D6 淡色档在浅色主题下对比度 1.99~2.97:1(低于 3:1) —— 「逐级变淡」是设计意图…
- 只出现这一行 = 健康,淡色档是有意设计
- 出现「回落撞车」= 该改
FOLDER_COLORS,否则这批笔记的新鲜度信号是假的 --strict下「回落撞车(告警级)」与陈旧片段同类,都会让进程以退出码 2 结束,可接流水线--json的palette_report给出结构化字段(类型/级别/目录/色值/ΔE),可喂看板做长期趋势
这一节的通用教训:配色方案的正确性不止「颜色值写对」,还有「颜色之间分得开」。前者有语法校验兜底,后者没有任何现成工具会告诉你——必须自己算感知距离,而且必须对「什么算真问题」做诚实界定,否则体检器自己会变成新的噪声源。
17. 告警会自己翻转 —— 覆盖口径与 !important 的两处真 bug(v2.6 新增)
前 16 节都在往里加检查。这一节反过来:检查本身出了错,而且错得让人信以为真。
2026-10-19 的例行刷新里,同一批 16 篇笔记、一个字没改,陈旧告警却从昨天的「实际 0 篇受影响」自动升级成了「16 篇受影响」。追下去发现三处问题,全部已修。
17.1 分类色缺 !important,回落被竞品整层压掉(真 bug,有实证)
第 1 节承诺「第 8 天回落到所在目录的分类色」。实测发现:这层回落从来没真正生效过。
v8 加目录分类色时只写了裸 color: #15803D,没带 !important。而竞品片段 teleagent-dynamic-colors 的绝对日期规则是带 !important 的:
/* 竞品:带 !important,特异性 (0,4,0) */
.nav-files-container .nav-file-title[data-path*="2026-10-12"] .tree-item-inner {
color: rgba(255, 68, 68, 1) !important;
}
/* 我方 v8/v9:裸 color,特异性再高也没用 */
.tree-item.nav-folder[data-path="WX同步"] .tree-item.nav-file .tree-item-inner {
color: #15803D;
}
!important 面前特异性无效——这是第 6 节那条「靠特异性取胜」的前提条件:只有双方都带 !important 时,才轮到特异性说话。我方分类色没带,直接判负。
后果非常具体:凡是路径里含某个日期、且已过 7 天的笔记,本该显示分类色(绿/青/紫),实际一直显示竞品那条过期的红。2026-10-19 实测 16 篇(05-古文经典、02-投资理财、WX同步 等)。用户以为看到了「回落」,看到的是别人的残影。
修法是给目录级规则补 !important,优先级顺序不变:
| 层 | 选择器特异性 | 说明 |
|---|---|---|
| 未读 / 冲突 / 新鲜度 / 根目录 | 1-3-0(:not(#kb-scope) 拿 id 位) |
id 位压过一切纯类组合 |
| 目录分类色(本次加固) | 0-6-0 + !important |
压过竞品 0-4-0,但仍低于文件级 id 位 |
| 竞品动态层 | 0-4-0 + !important |
被上面两层压住 |
17.2 覆盖口径太窄,告警自己翻转(比 bug 更危险)
v7 建立的 covered(本片段发过规则的笔记集合)口径是 新鲜度 + 未读 + 冲突 + 根目录,漏了「被目录分类色覆盖的文件」。
于是出现这样一条曲线:
| 日期 | 竞品最新规则日期 | 陈旧天数 | 告警级别 | 报告篇数 |
|---|---|---|---|---|
| 10-18 | 2026-10-12 | 6 天 | 提示 | 实际 0 篇(16 篇命中全被压制) |
| 10-19 | 2026-10-12 | 7 天 | 告警 | 实际 16 篇(0 篇被压制) |
笔记一篇没变,告警级别自己翻了。原因:这批笔记第 7 天从 D6 桶里掉出来,不再算「新鲜度覆盖」,于是被判成「本片段没管它」。可它们明明还被分类色管着——只是分类色没被计入口径。
这类 bug 比 17.1 更危险,因为它生产假结论:运维看到「告警升级」会去查库,查不到任何变化,几次之后就不再信这条告警了。
修法:把分类色覆盖的文件计入 covered。
17.3 竞品特异性改为实测,并逐篇比较
原来审计里对手特异性是拍脑袋写死的:
their_spec = (0, 1, 0) # 只数了 [data-path*="…"] 这一个属性位
真实选择器是 .nav-files-container .nav-file-title[data-path*="…"] .tree-item-inner = (0,4,0)。低估对手会得出「我们稳赢」的自我安慰——换个更强的竞品片段立刻翻车。
改成从 CSS 里把含该日期的选择器抠出来实测,并且逐篇比较(文件级 1-3-0、目录级 0-6-0 混用,不能拿一个统一值代表):
their_spec = _max_sel_spec(css, latest_str) or (0, 1, 0)
suppressed = sum(1 for r in hit
if r in covered_spec and spec_gt(covered_spec[r], their_spec))
回归验证(同一批 16 篇,改变对手强度):
| 对手特异性 | 压制篇数 | 实际受影响 | 告警级别 |
|---|---|---|---|
| 0-4-0(真实值) | 16 | 0 | 提示 |
| 0-6-0 | 0 | 16 | 告警 |
| 0-8-0 | 0 | 16 | 告警 |
改之前(写死 0-1-0)三种情况都会报「16 篇被压制、0 篇受影响」——结论恒真,等于没检查。
踩坑:_max_sel_spec 初版按字符扫描,把竞品文件里坏掉的注释(第 2~4 行是没被 /* */ 包住的裸文本,恰好含日期)当成选择器解析,量出 (0,4,16) 这种荒唐值(裸单词被当成元素选择器)。修正:先 re.sub 去掉成对注释,再逐行跳过以 * 开头的残留行。教训:解析别人的文件时,"先假设它是坏的"要写进代码,不能只在脑子里假设。
17.4 新增:mtime 簇污染检测 —— 50 篇"假新鲜"(2026-10-19 实测)
当天报告里 D1(1 天前)突然冒出 58 篇,而 D0 只有 3 篇、D2 只有 2 篇。查来源:58 篇里 50 篇的日期只来自 mtime,其中 05-古文经典 子目录独占 30 篇——全是《道德经》《论语》《孙子兵法》这类根本不可能是昨天写的古文笔记。
成因:天翼云盘 eCloud 重新拉取 / 整目录复制 / 解压覆盖,会把大批老文件的 mtime 统一刷成同一天。脚本照章办事(mtime 是三级回退的最后一级),于是把一批老笔记染成「昨天写的最鲜艳色」。全程没有任何报错,肉眼看只是"怎么一片橙"。
这类污染在元数据层面查不出来——文件确实是被改过。只能靠形状识别:
> 同一天 + 日期来源纯 mtime + 文件名里也没有日期 + 成批出现
于是新增 audit_mtime_clusters(),阈值 MTIME_CLUSTER_MIN = 15。实测抓到:
日期可信度: 发现 1 处疑似 mtime 污染
[提示] 2026-10-18(1 天前)有 50 篇笔记的日期**只**来自文件 mtime、文件名里也没有日期,
疑为云盘同步/批量触碰导致的 mtime 污染(05-古文经典 30 篇、(根目录) 5 篇、tools 4 篇);
这批笔记实际可能并不新,新鲜度色仅供参考(本函数只报告,不擅自改判)
只报告、不擅自改判,是刻意的:真的当天批量导入 50 篇新笔记也会长成这个形状,机器分不清"同步刷了 mtime"和"真的导入了 50 篇"。给不出可靠判断时就别动手——把数字摆出来,判断权留给人。这也顺带回答了一个长期指标:mtime 来源占比应该持续下降(本库 51/178,仍偏高),补 frontmatter 日期 才是根治办法。
--json 新增 mtime_clusters / mtime_cluster_warnings 两个字段,可喂看板做趋势。
17.5 怎么读这一节的报告
片段校验: 校验通过:已启用且在末位
[提示] 片段 teleagent-dynamic-colors 为「绝对日期型」,最新规则日期 2026-10-12 已过去 7 天…
;本库命中 16 篇,但全部被本片段压制(逐篇比较,我方最弱 0-6-0 > 对方 0-4-0),实际 0 篇受影响
;若本片段被停用,这 16 篇会立刻错染成「今天色」
- 「逐篇比较,我方最弱 X > 对方 Y」是关键——说明两个数都是实测的,不是写死的
- 仍是「提示」而非「告警」= 我们确实压住了,
--strict不会因此退出码 2 - 「若本片段被停用」那句是潜在风险,提醒别轻易关掉本片段
这一节的通用教训:给系统加自检,和给系统加功能一样会引入 bug,而且自检的 bug 更贵——功能错了你会在界面上看见,自检错了你会看见一个"看起来很专业"的错误结论并据此行动。所以自检本身至少要满足两条:① 结论能被独立复现(这里是"逐篇实测特异性"而不是拍脑袋常量);② 输入没变时结论不能自己变(17.2 那条翻转曲线就是最好的反例)。
18. 关系图谱也按目录分类着色(v2.7 新增)
前 17 节都在讲左侧文件树着色。这一节补上另一个界面:关系图谱(Graph View)——用户要求"查看关系图谱中也需要颜色分类",于是把目录分类色同步到图谱节点。
18.1 为什么不能靠加 CSS
文件树是普通 DOM,CSS color 直接生效。图谱节点是 canvas 渲染,CSS 打不进去;节点颜色只能由 .obsidian/graph.json 里的 colorGroups 机制控制。所以这条需求不能再加 CSS 片段,得给 refresh_note_colors.py 新增"生成 graph.json color groups"的能力。
18.2 graph.json 的 colorGroups 格式(实测官方格式)
{
"colorGroups": [
{ "query": "path:01-工作业务/", "color": { "a": 1, "rgb": 2450411 } }
]
}
三个最容易踩的坑:
| 坑 | 说明 |
|---|---|
| rgb 是十进制整数,不是 hex 字符串 | #2563EB 要存成 int("2563EB", 16) = 2450411。手改这文件时几乎人人踩 |
path: 是子串匹配 |
path:01 会匹配 01-工作业务/x.md 也会匹配 a/01-b.md。必须尾部加 / 锚定:path:01-工作业务/ |
| first-match-wins | 数组从上到下,第一个匹配的组染色,后面的不再看。顺序重要 |
18.3 只染分类色,不掺新鲜度
图谱节点只染分类色(FOLDER_COLORS + 自动派色),不跟 D0~D6 新鲜度渐变。理由:图谱是用来看结构的,再叠新鲜度会让节点颜色每天变、且 canvas 上渐变不如文件树直观,反而过载。分类色是"这个节点属于哪个目录"的长期身份,正合适。
18.4 保留用户手建的颜色组
graph.json 里可能已经有用户自己建的组(按 tag:#重要、[status:done] 等)。本方案不覆盖这些组,只管理"按目录分类"那一类:
- 区分"我们的组"和"用户的组":colorGroup 项里没有 name 字段,所以用 query 集合判据——凡是 query 命中我们本次生成的
path:<目录>/集合的算 ours(替换为本次最新色),其余算 theirs(原样保留)。 - 顺序:用户组排在前,我们的目录组排在后。first-match-wins 下用户组优先——符合"用户主动建的强调应覆盖底层目录色"的自然预期。
- 其他图谱设置原样保留:
collapse-filter/search/showTags/ 力学参数等一个不动,只改写colorGroups字段。
18.5 与文件树同口径、同色值、同幂等
- 同口径:图谱排除
EXCLUDE_FRESHNESS目录(模板/归档),与文件树build_css的分类色输出范围一致。 - 同色值:直接复用
FOLDER_COLORS的 hex,转成十进制 rgb 写入。 - 同幂等:整段
colorGroups未变化则跳过写入,与 CSS 片段同一纪律,不制造云盘同步抖动。
18.6 与 --check / --no-register 的关系
--check只读体检会顺带回读 graph.json,报告"11 个目录组已就位(另有 N 个用户组)";若发现某个目录组颜色过期会[告警]。--no-register同时跳过 graph.json(与 appearance.json 同进退,保持"最小副作用"语义)。- 想完全关掉图谱着色:设
GRAPH_COLOR_GROUPS = False。
18.7 怎么读报告
图谱着色: 已写入 11 个目录分类色组(保留 1 个用户组)
图谱着色: 内容无变化,跳过写入(11 个目录组,保留 1 个用户组) ← 幂等
校验通过:11 个目录组已就位(另有 1 个用户组) ← --check
[告警] 图谱 colorGroups 与当前目录配色不一致:颜色过期 1 个目录组 ← 有人改了 graph.json
实测验证(最小测试 vault,3 篇笔记 + 1 个预置用户组):生成 11 个目录组且保留用户组、第二次运行幂等跳过、手动改坏某目录 rgb 后 --check 准确报"颜色过期"、正式生成修正回正确值且不重复追加。
19. 宁可无色,不可谎报 —— mtime 污染免疫(v2.8 核心)
19.1 问题:提示写得再准,颜色本身还是错的
v2.6 加了 mtime 簇污染检测,会告诉你"某目录 232 篇笔记的日期只来自 mtime、集中在同一天,疑似云盘污染"。
但那一版只提示、不改判——理由是"真的当天批量导入也会长这个形状,机器分不清"。
这个理由站得住,可代价在 2026-10-20 变得无法接受:
| v2.7(只提示) | v2.8(免疫) | |
|---|---|---|
| 扫描 | 419 篇 | 419 篇 |
| 7 天内着色 | 309 篇(74%) | 47 篇(11%) |
| D1 一天 | 235 篇 | 3 篇 |
| CSS 体积 | 241 KB | 55 KB |
419 篇里 309 篇挂着新鲜度色,D1 单独占 235 篇——新鲜度信号被噪声彻底淹没。
更糟的是这批"新鲜"笔记里,221 篇其实是 2015 年的微信收藏。
一个"我知道这批可能不准"的提示,并不能阻止界面继续向你撒谎。
19.2 病根:防伪规则拿 mtime 当尺子
第 4 节那条文件名日期防伪,逻辑是「文件名日期与 mtime 差超过 400 天 → 判定为引用日期,丢弃」。
它默认 mtime 是准的。云盘一刷,这个前提就没了:
06-有道云导入/微信收藏/视频 2015-04-25 1208.md
v2.7: 文件名日期 2015-04-25 距 mtime 2026-10-19 超过 400 天
-> 判为「引用日期」丢弃 -> 用 mtime -> 2015 年的收藏染成「昨天写的」
v2.8: 该目录 mtime 已被证伪 -> 弯尺停用 -> 直接采信 2015-04-25
被这条规则误伤的正是最有证据的那批笔记——它们明明在文件名里写着日期。
反倒是没有任何日期证据的笔记,v2.7 敢直接采信 mtime。证据链整个是反的。
19.3 判据:按目录,不按全库
某目录下「没有 frontmatter 日期」的笔记,mtime 高度集中在同一天
且篇数 ≥ MTIME_POLLUTION_MIN(15)
且占该目录笔记总数 ≥ MTIME_POLLUTION_RATIO(60%)
→ 该目录 mtime 不可信
为什么按目录判:云盘重拉通常一次一个目录。实测同一次同步里
06-有道云导入 236/236=100%、05-古文经典 30/31=97%,
而 tools 4 篇、根目录 5 篇只是零星几篇。按全库判会把后面这几个正常目录一起误伤。
为什么排除 frontmatter 笔记:有 frontmatter 日期的笔记根本不看 mtime,压根不受污染影响,
把它们算进簇里只会稀释比例、导致真污染被漏判。
19.4 免疫后怎么办:换证据,不是换猜测
判定不可信后,脚本不会去猜一个日期,而是回到证据链:
| 证据 | 处置 |
|---|---|
frontmatter 日期 等字段 |
照常采信(本来就最高优先级) |
| 文件名里的日期 | 直接采信,不再用 mtime 这把弯尺去量它 |
| 什么都没有 | 判定「日期未知」→ 不参与新鲜度着色,回落到分类色 |
第三条是本节的价值观:没有证据就不表态。无色(分类色)是一个诚实的"我不知道",
而谎报 D1 是一个错误的"这是昨天写的"。两者在界面上都是有色块,但后者会让人做错决定。
实测 06-有道云导入 236 篇:221 篇纠正回真实日期(全部掉出 7 天窗口)、11 篇免着色、4 篇另有 frontmatter。
05-古文经典 31 篇:30 篇免着色。
19.5 三条退出通道(自动判断必须可被推翻)
自动判断最怕"我想让它染但它不染,还找不到开关"。v2.8 留了三层:
--trust-mtime—— 一次性关总闸,退回 v2.7 的"只提示不改判"MTIME_IMMUNITY = False—— 永久关总闸(脚本顶部)MTIME_TRUST_FOLDERS = {"某目录"}—— 精确放行:这个目录的 mtime 永远可信
(适合你确实每天整批导入的目录)
运行报告会逐目录点名:哪个目录、多少篇、mtime 都在哪天、占比多少、纠正几篇、免着色几篇。
不静默。
19.6 一个刻意的妥协:不动「今天」的簇
同一天冒出一大批 mtime,有两种都讲得通的解释:被同步刷了,或者你今天真的整批导入了。
昨天/前天的簇只有前一种解释;今天的簇两种都有可能——
而如果是真的导入,抑制掉就丢掉了货真价实的新鲜度信号。
所以 MTIME_IMMUNITY_SKIP_TODAY = True:今天的簇不下手。
好在污染躲不掉——明天它变成 D1 的同类簇,那时判据就只剩一种解释了。
代价是污染识别晚一天,换来真实导入不会被误杀。
19.7 这一节的通用教训
- 提示 ≠ 修复。给系统加一个"我知道这里有问题"的告警很容易,但告警不会改变输出;
输出错了,告警写得越准确,越显得系统在明知故犯。 - 别用一把可能弯的尺子去量别人。任何"以 X 为基准判断 Y"的规则,都要先问 X 本身可信吗。
- 没有证据就别表态。宁可输出一个中性的"未知",也不要输出一个看着正常但错误的结论。
- 自动判断必须留退出通道,并且要在报告里说清楚怎么退。
20. 目录着色的结构体检 —— 不只查自己,也查别人怎么把目录色写坏的(v2.9 核心)
前面十九节查的都是「本片段的颜色对不对」。v2.9 补上另一半:同一个 snippets 目录里,所有片段的目录着色写得对不对。
为什么要往外接这一层。目录着色有个很不像 bug 的特点:它坏起来基本不报错。
CSS 语法合法、片段正常启用、颜色也确实写进了文件,只是你眼睛看到的是错的。
学同库的 TeleAgent 两层片段时这一点看得很清楚——它坏的地方,全都不是"报错",而是"没报错"。
audit_snippet_hygiene() 扫 snippets 目录下的所有片段(不只已启用的),查四类隐患:
| 编号 | 隐患 | 判据 | 实测(本库 423 篇) |
|---|---|---|---|
| H1 | 回落层自相矛盾 | 同一套配色里「分类色没写 !important」+「日期色写了 !important」 |
30 篇 |
| H2 | 文件名级子串误伤 | [data-path*="index.md"] 这类以文件名做子串 |
2 条规则、各误伤 1 篇 |
| H3 | 目录漏染 | 顶层目录不在任何片段的目录清单里 | 7 个目录 / 264 篇(本片段已兜底) |
| H4 | 非法 CSS 转义 | 属性值含 \0 等数字转义 |
1 条规则整体失效 |
H1 值得单独说:它是「自己跟自己打架」,不是跟我打架
TeleAgent 的分层是对的:静态层给目录分类色,动态层给 7 天新鲜度色。
但它的第一层写的是裸 color,没有 !important,而动态层是带 !important 的。
!important 面前特异性不作数。于是:
- 命中动态层的笔记,分类色永远赢不了——所谓「第 8 天回落到分类色」在这批笔记上从未发生
- 更要命的是动态层把日期硬编码进选择器(
[data-path*="2026-10-12"]),生成器 2026-10-12 停更后日期就冻结了 - 两者叠加的后果:这批笔记被永久钉死在 2026-10-12 那天的颜色上,一天都不会再变
这里的关键认知是:这跟我没关系。即使把本片段整个删掉,那 30 篇也是错的。
v2.3~v2.6 一直在解决"我和对手谁压过谁",H1 说的是另一件事——对手自己的两层也在打架。
所以体检报告把它标成 [提示] 而不是 [告警]:本片段已用 1-3-0 接管,当前不实际生效;
只有当你停用本片段、或它自己停用了才会显现。报告里这句话是写明的,不含糊。
> 顺带一条通用教训:v2.6 修过"分类色漏写 !important"这个 bug(第 17 节),
> 当时以为是本片段的疏漏。H1 说明这是一整类方案的通病——
> 分层配色里,只要有一层带 !important、另一层不带,分层就名存实亡。
H2/H3/H4:小问题,但都是"没人会去看"的那类
- H2:
[data-path*="index.md"]的本意是给根目录索引文件加斜体青色,实测把tools/_index.md一起染了;
[data-path*="知识看板.md"]则把00-知识看板.md染了——而它想匹配的知识看板.md在库里根本不存在。
子串匹配的成本就在这里:你以为在说"根目录那个 index",实际在说"全库所有名字里含 index.md 的东西"。 - H3:v2.4 只能看见"本片段清单里没有的目录"。H3 换成全局视角——其他片段也救不了的目录有哪些。
- H4:
[data-path*="^\04-模板"]里的\0后跟数字是非法 CSS 转义,整条选择器被判无效,
那条规则从来没生效过,也不会有任何提示。
怎么读这段报告
目录着色体检: 发现 4 处结构隐患
[提示] 回落层自相矛盾:30 篇笔记同时命中「无 !important 的目录分类色」和
「带 !important 的日期色」(teleagent-color-coding(分类层) vs
teleagent-dynamic-colors(日期层) 30 篇)……
- 全部是
[提示]= 本片段已接管,不用动手,但它告诉你"别人的片段停更后会变成什么样" - 出现
[告警]= 本片段没启用(或没接管),真有笔记被染错,按篇数处置 --json里对应hygiene_report,每条带type/level/notes/samples
21. 配色可以固化 —— 让「建议补进 FOLDER_COLORS」不再是一句空话(v2.9)
v2.4 到 v2.8 有个一直没解决的死循环:报告每天写「未登记目录已自动派色(建议补进 FOLDER_COLORS 固化)」,
但 FOLDER_COLORS 写在脚本源码里。
- 改源码 → 下次升级脚本要重新改一遍
- 不改 → 这批目录(实测 258 篇,占全库 61%)永远只能靠自动派色兜底,且颜色可能随旁人变动而平移
一个无法一键执行的建议,等于没建议。 v2.9 把固化落到脚本同目录的 folder_colors.json:
{
"_说明": "未登记目录的固化配色…本表优先于脚本内的 FOLDER_COLORS。升级脚本不会覆盖本文件。",
"06-有道云导入": "#A21CAF",
"SkillHub": "#3F6212",
"WX同步": "#1E3A8A",
"images": "#5D1E8C"
}
- 脚本启动时自动合并(外部表优先,可手工改色),
_开头的键当注释忽略 --pin-folders把本次自动派色结果追加进去,只补不覆盖已有条目- 升级脚本不会冲掉这个文件——固化结果和脚本版本解耦了
固化顺带暴露了一个此前看不见的问题:自动派出来的颜色,从来没进过配色体检。
它们不在 FOLDER_COLORS 里,audit_palette() 压根看不到;一旦固化进去,体检立刻报了 8 处目录互撞(含 2 处 [告警])。
换句话说,v2.4~v2.8 的自动派色一直在派体检通不过的颜色,只是没人查得到。
修法是两头一起改:
- 候选色板从 12 色扩到 26 色——已占用色(13 分类色 + 7 档新鲜度 + 7 档高对比 + 4 状态色)有 31 个,
12 色根本凑不出"离所有占用色都 ≥20"的候选,旧逻辑会一路放宽到"只要不重复就行"(实测派到 ΔE 6.2 的紫)。 - 选池从「阈值阶梯放宽」改成「最远点贪心」——每次挑一个离已占用色和已入选色都最远的颜色,
且只收确实拉得开的(≥DE_THRESHOLD),收不够MIN_AUTO_POOL_FLOOR个才降标准。
结果只由已占用色决定,不随这批要派几个目录变化,v2.4 的稳定性不受影响。 - 报告里直接报最近邻 ΔE,撞了就点名撞的是谁,方便手改:
未登记配色目录: 4 个 / 涉及 258 篇 —— 已自动派色(建议补进 FOLDER_COLORS 固化)
06-有道云导入: 236 篇 -> #A21CAF [最近邻 05-古文经典 #7C3AED ΔE31.8]
SkillHub: 10 篇 -> #3F6212 [最近邻 05-每日回顾 #16A34A ΔE35.6]
WX同步: 11 篇 -> #1E3A8A [最近邻 01-工作业务 #2563EB ΔE34.2]
images: 1 篇 -> #5B21B6 [最近邻 05-古文经典 #7C3AED ΔE18.7,偏近,建议手改]
实测效果(同一批 4 个目录、258 篇,一字未改):
| 指标 | v2.8 | v2.9 |
|---|---|---|
| 未登记目录最近邻 ΔE 最小值 | 10.4(#0F766E vs Excalidraw) |
18.7 |
| 固化后配色体检撞车数 | 9 处(含 2 处 [告警]) |
1 处(设计意图,非撞车) |
| 未登记目录两两 ΔE 最小值 | 12.4 | 24.9 |
一个诚实的取舍:色相空间就那么大,"离 31 个已占用色都 ≥20"的候选只找到 4 个。
池子小 → 哈希碰撞 → 顺移 → 同一个目录在不同批次可能拿到不同颜色。
v2.4 曾把"稳定性"看得极重,v2.9 的判断是:稳定性现在由 folder_colors.json 保证,不必再让算法硬扛——
先保证颜色可用(不撞车),再让用户一键固化把颜色钉死。跑一次 --pin-folders 即可。
22. 染的是不是真东西 —— 死选择器体检(v2.10 核心)
前 12 版查的都是「CSS 写对了没有」:语法自校验、片段注册、特异性对抗、陈旧日期、配色撞车。
但目录着色还有一类故障,以上五道检查一道也查不出来:
> 选择器完全合法,CSS 解析通过,片段也好好启用着,只是它要匹配的东西在库里根本不存在。
于是你既看不到报错,也看不到异常,只会看到一个非常容易被忽略的现象——
那个目录的标题没有分类色。
实测来源。 TeleAgent 静态层给 04-模板 的文件夹标题写了两条选择器:
.nav-folder-title[data-path*="/04-模板"] .tree-item-inner, /* 前导斜杠 */
.nav-folder-title[data-path*="^\04-模板"] .tree-item-inner, /* 锚点 + 转义 */
.nav-file-title [data-path*="04-模板/"] .tree-item-inner { /* 这条是好的 */
color: #7788aa;
}
两条文件夹规则在本库命中数都是 0。结果就是 04-模板 这个目录标题永远没有分类色,
而它下面 8 篇文件却照常有色——目录配色只坏了一半,正是肉眼最难发现的那种坏法。
判据:不靠语法理论推断,直接拿真实路径去撞。
语法理论这次骗了人:v2.9 曾据" \0 是非法转义"推出"整条规则失效",
拿真实 CSS 解析器一跑,prelude errors = 0,\04 是合法转义,规则活得好好的。
所以 v2.10 只认命中数——0 命中即死,并给出四种死因:
| 死因 | 例子 | 说明 |
|---|---|---|
| 前导斜杠 | [data-path*="/04-模板"] |
Obsidian 的 data-path 是库内相对路径,永不带前导斜杠 |
| 正则锚点 | [data-path*="^04-模板"] |
*= 是子串匹配,^ $ 会被当普通字符 |
| CSS 转义 | [data-path*="^\04-模板"] |
实际参与匹配的字符与字面看到的不同 |
| 目标不存在 | [data-path="99-不存在的目录"] |
目录改名或删除后残留的旧规则 |
跑起来长这样(本库实测,2 处都是真问题):
死选择器体检: 发现 2 处永不生效的选择器
[提示] 片段 teleagent-color-coding 的文件夹标题选择器 [data-path*="/04-模板"]
在本库命中 0 条 —— 前导斜杠:Obsidian 的 data-path 是库内相对路径,
永不带前导斜杠;意图目录 04-模板 下 8 篇仍会被本片段接管
[提示] 片段 teleagent-color-coding 的文件夹标题选择器 [data-path*="^\04-模板"]
在本库命中 0 条 —— 含正则锚点 ^ / $:*= 是子串匹配,锚点会被当普通字符;
意图目录 04-模板 下 8 篇仍会被本片段接管
三个假警报,都是自己造的,也都修了(23 条噪声 → 2 条真问题):
- 拿剪过枝的
rows当语料。rows会按EXCLUDE_FRESHNESS整棵剪掉04-模板、90-归档,
但这两个目录在 Obsidian 文件树里照样显示、照样被着色。
结果是它们全被报成"死选择器"。死选择器的定义是"在 Obsidian 里匹配不到",
语料就必须是文件树本身——v2.10 改成直接遍历文件系统,只排除点目录。 - 把目录属性误判成文件属性。
.nav-folder[data-path="01-工作业务"] .nav-file-title
的属性明明挂在 folder 上,初版看见这行里有nav-file-title就当文件选择器,
拿文件语料去撞,把自己 12 条目录规则全报成死。
修法:data-path属于紧挨着它的那个元素,取属性前最后一个复合选择器来定性。 - 精确匹配被当成标记词漏掉。
[data-path="99-不存在"]因为没有斜杠、
不像路径,被"标记型选择器"的过滤规则吃掉。修法:data-path="X"(精确匹配)天生就是路径,
命中 0 只可能是路径写错,不必再猜它是不是未读这类标记词。
标记型选择器要放过。 [data-path*="未读"] 命中 0,只说明当前没有这类文件,不是写错了。
只有"看起来像个路径"的才值得报,否则天天刷屏,告警就没人看了。
也体检自己。 本片段的选择器是从真实路径生成的 data-path="...",理论上是 0 死选择器——
但"理论上"三个字,正是前面十二版反复出事的地方,所以自己也一起撞。
23. 日期只告诉你「同一天」,时分告诉你「同一件事」—— 突发级 mtime 免疫(v2.11 核心)
v2.8 加了 mtime 污染免疫,判据是按目录:同一目录里 ≥15 篇同天、且占该目录 ≥60% → 该目录 mtime 不可信。
在 2026-10-20 那次整目录重拉里它很准(06-有道云导入 236/236、05-古文经典 30/31)。
但云盘同步不是按目录挑的——它一次触碰整棵库,落到哪个目录全看当时同步了什么。
实测漏网(2026-10-22):2026-10-18 被刷的 46 篇散在 6 个目录,最多的一个目录只有 4 篇。
目录级判据「≥15 篇且占 60%」一条都没命中,于是 16 篇老笔记被染成 D4 亮橙,占 D4 总数 22 篇的 73%:
| 被误染的 | 内容 |
|---|---|
(根目录) 4 篇 |
dashboard.md、dashboard-2.md、未命名.md、概念A.md |
tools/ 4 篇 |
三个「接入说明」+ _index.md |
05-读书笔记/ 3 篇 |
欢迎.md、Agent.md、执行报告 |
01-工作业务/ 2 篇、02-投资理财/ 2 篇、00-收件箱/ 1 篇 |
使用说明、课程精要、老墨跟踪 |
更该反省的是:这一版自己的「日期可信度」体检已经把 16 篇点名报出来了,
可它只提示、不改判——颜色照样是错的。这正是 v2.8 用来批评 v2.7 的那句话:
告警不改变输出,等于明知故犯。 上一版修好的 bug,换个分布就回来了。
换一个不受目录结构影响的指纹:时间。
人一天写几十篇不会挤在同一小时,同步/批量脚本会。实测:
| 那天 | 纯 mtime 篇数 | 时间分布 |
|---|---|---|
| 2026-10-18 | 46 | 43 篇落在 21:52~21:55(3 分钟) |
| 2026-10-19 | 232 | 222 篇落在 09:34~09:59 |
日期只告诉你"同一天",时分告诉你"同一件事"。
判据(detect_mtime_bursts()):对「没有 frontmatter 日期」的笔记,
同一天内按时序做滑动窗口,窗口 ≤MTIME_BURST_WINDOW_MIN(60) 分钟、
≥MTIME_BURST_MIN(10) 篇、且跨 ≥MTIME_BURST_MIN_FOLDERS(2) 个目录 → 判为同一次批量触碰,整批免疫。
为什么要求跨 ≥2 个目录 —— 这是刻意收窄的口子:
单目录内的突发有可能是你真的一小时写了十几篇,那种情况交给目录级判据(它还要 60% 占比,误伤更小)。
反过来,单目录、低占比的同步(300 篇的目录被刷了 20 篇)两种判据都抓不到。
宁可漏判,不可把真实写作判成污染——漏判只是少染几篇,误判是把新鲜度信号整片掐掉。
两级判据互补,缺一就漏:
目录级(v2.8) 同目录 ≥15 篇 且 占比 ≥60% → 整目录重拉
突发级(v2.11) 跨 ≥2 目录 ≥10 篇 且 ≤60 分钟 → 跨目录整批触碰
都抓不到 单目录、低占比的小批量同步 → 刻意留口子,走 --trust-mtime 兜
效果(同一批 433 篇,一字未改):
| 指标 | v2.10 | v2.11 |
|---|---|---|
| 7 天内着色 | 53 | 37 |
| D4(4 天前) | 22 | 6(16 篇假新鲜撤掉) |
| 日期来源 mtime | 27 | 11 |
| 日期未知(免着色) | 41 | 57 |
| 「日期可信度」告警 | 1 条(16 篇) | 0 条(已被处置,不再重复报) |
| CSS 体积 | 60,355 字节 | 51,527 字节 |
回归(合成库,三组各置不同时刻):单目录 12 篇同刻 → 不免疫;跨 2 目录 12 篇同刻 → 免疫;
跨 2 目录 8 篇(不足 10)→ 不免疫。三种都与设计一致。
同一批数据跑 --trust-mtime 得 315 篇(D3 一天 235 篇)——总闸有效,也顺便说明污染的真实规模。
通用教训:一条判据按空间(目录)分组,就一定漏掉跨空间的事件。
不是上一版没修好,是判据的维度不够。补第二个维度时要先想清楚它会误伤谁,
再决定是加阈值收窄,还是留一条退出通道。
24. 谁压过谁 —— 级联顺序体检(v2.12 核心)
前面 23 节查的全是「规则写对了没有」:语法自检、片段注册、特异性对抗、陈旧审计、配色撞车、死选择器。
漏掉一个维度:特异性相同、都带 !important 时,由源码顺序决胜。
本片段内部恰恰全是同特异性:文件级规则一律 .nav-file-title[data-path="X"]:not(#kb-scope) .tree-item-inner = 1-3-0,
目录级一律 0-6-0,全部带 !important。也就是说层与层之间完全靠顺序分胜负,顺序一错,优先级阶梯就整段翻转,
而 CSS 语法自检、片段注册校验、特异性体检一个都查不出来。
实测故障(2026-10-23,本库 436 篇):无障碍高对比档(旧第 7 节)原本排在「未读/冲突」之后。
它是「新鲜度档的加深版」,本意只是让淡色档在 prefers-contrast: more 下能看清,
可排到最后之后,它连未读也一起盖了:
| 被覆盖的未读笔记 | 新鲜度档 | 高对比色 | 未读色(本该显示) |
|---|---|---|---|
01-工作业务/LLM Wiki架构精读-查询协议与prompt落盘-2026-10-18.md |
D5 | #44403C |
紫色未读高亮 |
01-工作业务/WorkBuddy驱动Obsidian自进化-三步法融合-2026-10-18.md |
D5 | #44403C |
紫色未读高亮 |
01-工作业务/知识库判断层升级-我的判断强制-2026-10-20.md |
D3 | #7C2D12 |
紫色未读高亮 |
为什么难发现:浅色、正常对比度下一切正常——只有开了系统「提高对比度」偏好的人,
才会永远看不到未读标记。他不会去怀疑配色脚本,只会以为自己没标未读。
这和 v2.11 批评过的「告警不改变输出」是同一类病:只在你不看的那条路径上坏。
修法不是改颜色,是改位置:高对比档是新鲜度档的变体,必须紧跟它要修正的那一层,
放在最后反而会越过所有更高优先级的层。已上移为 3.7 节,排在 3.5 文件夹新鲜度之后、4 未读之前。
新增 audit_cascade_order():
对「每个元素 × 每个通道」:
收集命中它的所有规则
按 (是否 !important, 特异性三元组, 源码顺序) 取胜出者
胜出者的层级 必须 == 这批规则里的最高层级,否则点名
两个实现要点,都是实测踩出来的:
- 通道是叠加关系,不是互斥。
@media (prefers-contrast: more)里的规则不会关掉常规规则,
所以「高对比模式」这个上下文里参与竞争的既有常规层也有高对比层。
初版按通道各自独立取候选集 → 高对比通道里只剩新鲜度一条规则 → 永远查不出冲突(体检自己静默失效)。 - 计数按元素,不按选择器。 同一条规则会被
_file_sel拆成 4 个选择器变体(兼容不同 Obsidian 版本 DOM),
按选择器累加会让「3 篇未读」虚报成「12 处命中」。
回归:真实顺序 0 条;把层级整体倒排(合成故障)→ 报出 18 类冲突。
--check --strict 会把这类冲突计进退出码,定时任务可直接接告警。
通用教训:分层配色里,「高优先级」不是一个属性,是一个位置。
只要同层同特异性,谁写在后面谁就是高优先级——写文档时说「未读优先级最高」没用,
得让未读真的排在后面,而且要有体检盯住这个顺序,否则下一次有人往末尾追加一段就又翻了。
(v2.6 修过同类的「分层里一层带 !important 另一层不带 → 分层名存实亡」,
v2.12 是它的另一半:分层顺序错了,分层同样名存实亡。)
25. 该染的目录染了没有 —— 目录覆盖率体检(v2.13 核心)
前面 24 节查的全是「规则写对了没有」。
这一节查的是一件更基础的事:该染的目录,染了没有——也就是分母。
实测(2026-10-24,本库 461 篇):
| 口径 | v2.12 及以前(只染顶层) | v2.13(嵌套继承) |
|---|---|---|
| 全库目录数 | 71(顶层 18 / 嵌套 53) | 71 |
| 目录覆盖率 | 15/68 = 22.1% | 68/68 = 100% |
| 没有分类色的目录 | 53 个 | 0 |
| 涉及笔记(含后代) | 528 篇 | 0 |
| 文件夹新鲜度 | 9 个(只有顶层) | 12 个(逐层) |
| CSS 体积 | 74,770 字符 | 86,677 字符(+16%) |
表现形态(和 v2.10 查出的死选择器同一个形状,只是这次是批量的):
父目录标题有色 → 它下面的文件也有色 → 中间那层文件夹标题是灰的。
05-古文经典(紫)下面挂着 古文名篇、孙子兵法、孟子、庄子、论语、道德经 六个灰色子目录,
而这六个目录里的笔记全是紫色。只看父层和文件层都正常,只有展开到中间层才露出破绽。
病根有两层,第二层比第一层更值得记:
- v3 为了治子串误伤,把
[data-path*="X"]换成精确匹配[data-path="X"]——
这个决定本身是对的(子串会误伤同名文件,v2.9 实测 2 处)。
但换成精确匹配后,目录清单仍然只枚举顶层(folders = {r.split("/")[0] ...}),
精确匹配 + 只枚举顶层 = 嵌套目录一条规则都没有。 - v2.4 起的「未登记目录」自检,分母也只有顶层目录。
于是覆盖率天天算出 100%,而实际上 53 个目录没染。
自检全绿不等于全覆盖,前提是分母要包含全部对象。
这是本技能 13 个版本里藏得最深的一个 bug——不是因为难查,而是因为从来没人问过分母对不对。
修法取 TeleAgent 静态层的优点,但不沿用它的手段:
它的 [data-path*="00-收件箱"] 是子串匹配,嵌套目录(05-古文经典/道德经)天然含父名,
所以覆盖率天然是 100%——这是它比我们强的地方。
但子串会误伤([data-path*="index.md"] 染到 tools/_index.md)。
v2.13 的做法是 显式枚举所有层级的目录路径,每个目录单独发一条精确选择器:
/* 继承自 05-古文经典(6 个子目录) */
.nav-folder-title[data-path="05-古文经典/古文名篇"]:not(#kb-scope),
.tree-item.nav-folder[data-path="05-古文经典/古文名篇"] > .tree-item-inner,
.nav-folder-title[data-path="05-古文经典/古文名篇"], ... { color: #7C3AED !important; }
颜色为什么是「继承最近的有色祖先」而不是「给每个子目录另派一色」:
- 语义对:
05-古文经典/道德经就是古文经典下的一层,不是一个新分类。 - 不新增颜色:53 个嵌套目录各派一色,26 色板必然撞车(鸽笼原理,v2.9 已论证)。
- 视觉成块:父子同色在文件树里形成连续色带,比几十种杂色更好读。
新增 audit_folder_coverage(),把目录分成三类分开算——这一步不能省,
否则又会像 v2.5 那样被自己的假警报淹没:
| 类别 | 判据 | 本库 |
|---|---|---|
| 刻意弱化 | 路径含 EXCLUDE_FRESHNESS 段(04-模板/90-归档/tools 及其子树),设计上就是灰的 |
3 个 |
| 已覆盖 | 顶层登记色 + 嵌套继承色 | 68 个 |
| 漏染 | 剩下的,才是真问题 | 0 个 |
顺带修掉两个自己造的坑:
- 点目录被染色:
05-读书笔记/.weread-cache这类点目录 Obsidian 文件树根本不显示,
初版却给它发了规则。这个 bug 当场被 v2.10 的死选择器体检抓住
([告警] 片段 知识库目录着色 的文件夹标题选择器 ... 在本库命中 0 条)——
前面 24 节做的体检在这里第一次逮到了自己,算是交叉验证。 - 空目录漏染:顶层目录
日记暂时一篇 md 都没有,folders由笔记反推所以没它,
而它的配色早就登记在FOLDER_COLORS里。分母要跟「看得见的东西」对齐,
故顶层目录改为「笔记反推 ∪ 磁盘实际存在的顶层目录」。
文件夹新鲜度也一并逐层:以前只有顶层目录算「子树最新笔记」,
现在 05-每日回顾、05-古文经典/古文名篇 各自按自己的子树算,
父显示 D3、子显示 D0 是正确信息——一眼看出新笔记落在哪一层。
回归:同一批 461 篇一字未改,--no-nested 退回 v2.12 行为 → 覆盖率 22.1%、53 个目录漏染;
默认 → 100%。
通用教训:一个「覆盖率」类指标,先问分母是谁。
分母写错时,指标不是不准确,而是精确地误导——它每天都在报 100%,
报得越稳定,越没人会去怀疑它。
26. 颜色不是写出来的,是层叠算出来的 —— 终局颜色体检(v2.14 核心)
前面 25 节查的全是「规则」:语法对不对、注册没有、特异性够不够、顺序排没排反、
覆盖率到没到 100%、选择器死没死、颜色撞不撞。
没有一条回答用户真正在问的问题:这篇笔记最后到底显示成什么颜色?
为什么必须单独查一遍:层叠的结果不等于任何一条规则的意图。
v2.12 那个坑就是现成的例子——无障碍高对比档原本排在未读之后,
两者特异性相同、都带 !important,于是开了系统高对比的人看不到未读标记,而浅色模式下一切正常。
顺序体检能抓到它,是因为我们恰好登记了这两层;
终局模拟不需要预先知道哪两层会打架,它直接算结果再和期望比。
模拟用的就是浏览器的四条规则(不是近似):
- 带
!important的赢过不带的 - 同
!important比特异性三元组(按位比较,不能求和——v2.6 已论证求和会失真) - 还相同则源码顺序靠后的赢
@media里的规则是在对应场景下额外参与,不是替换常规规则(通道是叠加关系)
参与模拟的只有 appearance.json 里启用的片段,按启用顺序拼接;
没启用的 CSS 根本不会被 Obsidian 加载,算进去等于自己吓自己。
实测(2026-10-25,本库 467 篇 / 68 个目录):体检一次就揪出真 bug。
| 场景 | 对象 | 期望(设计意图) | 实际(层叠算出来) |
|---|---|---|---|
| 高对比模式 | 01-工作业务(D0 文件夹) |
#9A3412(加深档) |
#C2410C(普通档) |
| 高对比模式 | 05-读书笔记/每日荐书(D1) |
#9A3412 |
#D4621F |
| 高对比模式 | tools、tools/商品化、tools/商品化/发货包(D4) |
#8A5A3B |
#EFA87E |
| 高对比模式 | images(D5) |
#57534E |
#C9A79B |
共 12 个文件夹标题。病根很具体:v2.12 把高对比档从末尾上移到未读之前时,只搬了文件那一段,
3.7 节里全是 _file_sel(),一个 _folder_title_sel() 都没有。
后果是「D2~D6 淡色档靠 prefers-contrast 兜底」这句话对文件夹标题压根不成立——
同一个目录,文件标题加深了、文件夹标题还是淡灰,对比度约 2:1,正是高对比模式要消灭的东西。
修法也只是补发一段(文件夹新鲜度 3.5 与本段同为 1-2-0 且都带 !important,靠 3.7 在后取胜)。
--explain <路径>:把「为什么是这个颜色」摊开
目录着色最常被问的一句话是「这篇怎么是这个色」。以前只能靠人肉推理,现在直接打印:
[explain] 05-读书笔记/每日荐书/今日荐书-2026-10-24.md(file)
场景 浅色常规:期望 #0891B2(分类色(05-读书笔记)) / 实际 #0891B2
0-6-0 胜出 imp=True 知识库目录着色 color=#0891B2 [.tree-item.nav-folder[data-path="05-读书笔记"] ...]
0-3-0 imp=True 知识库目录着色 color=#0891B2 [.nav-folder[data-path="05-读书笔记"] .nav-file-title]
0-3-0 imp=False teleagent-color-coding color=#e056a0 [.nav-file-title[data-path*="05-读书笔记/"] ...]
一眼看出:竞品那条也命中了,但它没带 !important,就算特异性一样也直接判负。
只有真正那条决胜规则标「胜出」——同色的其它规则只是陪跑,全标会让人以为有 3 条同时生效。
三个自造的坑(都是实测撞出来的,比算法本身更值得记):
| # | 坑 | 后果 | 修正 |
|---|---|---|---|
| 1 | 祖先集合写成 range(1, len(parts)) |
01-工作业务/a.md 的祖先算出空集,它唯一的父目录反而丢了 → 分类色后代规则一条都命中不了 → 224 篇被误报「实际无色」 |
range(1, len(parts) + 1) |
| 2 | 文件自身没进祖先集 | .nav-file-title[data-path="X"] .tree-item-inner 这类写法里,带 data-path 的是 .tree-item-inner 的父元素,只查祖先目录就永远匹配不上 → 文件级规则全灭,分类色后代规则(0-3-0)反而压过新鲜度/未读(1-3-0)→ 134 处误报 |
祖先集 ∪ {自身路径} |
| 3 | 体检读磁盘上的 CSS | --check 与「内容无变化跳过写入」这两种情况下读到的都是上一版产物,刚修好的问题继续报,等于体检在验旧货 |
audit_final_color(rows, stats, css) 传入本次生成的 CSS |
回归四组(同一批 467 篇,验证体检不是空转通过):
| 构造 | 期望 | 实测 |
|---|---|---|
| 原样 | 0 处 | 0 处 |
| 去掉整个高对比档 | 大量文件 + 文件夹 | 70 处(D0~D6 文件 58 + 文件夹 12) |
| 去掉未读层 | 未读篇数 | 12 处(6 篇未读 × 2 场景) |
| 只去掉高对比的文件夹段 | 12 处 | 1 处(删段脚本只切到 1 条,方向正确) |
通用教训:一条体检只要查的是「规则」,就永远查不出「规则之间的关系」造成的故障。
规则和结果之间隔着一整套层叠算法,而那套算法有四个决胜顺位,任何一个都可能让写对的东西不生效。
要么模拟结果,要么承认有一部分故障你永远看不见。
27. 颜色对了 ≠ 看得见 —— 渲染层弱化体检(v2.15 核心)
第 26 节好不容易做到了「逐篇算出实际渲染色」,但它比对的仍然是 CSS 里的 color 值。
v2.15 要问的是下一层:这个颜色到了眼睛里还剩多少。
CSS 里能把一个颜色变淡的从来不止 color 本身 —— opacity 是乘在整个元素上的,谁写的都算。
你只接管 color、不接管 opacity,别人一句裸声明就能把你的分类色冲淡一半,
而所有体检都在比对 color 值,算出来「实际 == 意图」,全绿。
实测(2026-10-26):TeleAgent 静态层给三个目录写了弱化透明度,全部不带 !important:
| 目录 | 它的 opacity | 我们的分类色 | 冲淡后实际显示 | 白底对比度 |
|---|---|---|---|---|
| 00-收件箱 | 0.8 | #CA8A04 | #D5A136 | 2.34:1 |
| 90-归档 | 0.6 | #A1A1AA(旧) | #BBBBC2 | 1.91:1 |
| 04-模板 | 0.7 | #9CA3AF(旧) | #AEB4BD | 2.09:1 |
对照 WCAG:正文 4.5:1、大字 3:1。1.91:1 等于没有颜色 —— 用户看到的是一片灰,
而「终局颜色体检」还在报告「每篇的实际显示色都等于设计意图」。
判据就两步:把 alpha 折算进颜色,再算对比度。
blend(fg, bg, alpha) # opacity 乘在整个元素上 = 前景按 alpha 与背景混成实色
contrast_ratio(blend(color, BG, alpha), BG)
不折算,配色体检算出的对比度就是自欺欺人。
处置:把 opacity 通道收归自己(新增 CSS 第 9 节「弱化通道接管」)。
思路来自 TeleAgent —— 色相说「这是什么」,透明度说「重不重要」,两个通道解耦,
比把所有语义挤在一个 color 通道里强。但手段不学它:值写进 WEAKEN_OPACITY、进体检、
可 --no-weaken 一键退回。没设计过弱化值的目录一律接管为 opacity: 1,撤销别人的误伤。
两个自造的坑(都是新体检当场抓出来的):
| # | 坑 | 实测 | 修法 |
|---|---|---|---|
| 1 | 弱化值拍脑袋 | 初版写 0.82 / 0.72,被同一版新增的体检判死:#9CA3AF@0.82 → 2.09:1、#A1A1AA@0.72 → 1.91:1 | 弱化是相对弱化,不是消失。改更深的基础色(#6B7280 / #71717A)+ 更保守的 opacity(0.88 / 0.82),体检归零 |
| 2 | 开关只管生成、不管体检 | --no-weaken 关掉接管后,体检仍假设「我们声明过 opacity=1」,外部冲淡被算成已接管 → 关掉开关照样全绿 |
took_over 必须同时受 WEAKEN_ENABLED 约束;开关要管「生成」和「体检」两头,否则体检验的是一个不存在的产物 |
顺带修的一处:收件箱脉冲动画原本用 opacity: 0.75 → 1 做呼吸。
v2.15 起 opacity 归配色系统管,动画的 !important 和接管的 !important 必然打架,必有一伤。
改用 text-shadow 呼吸:视觉不变,但完全不碰 color / opacity 两个已被占用的通道。
动画不该占用你正在管理的视觉通道。
回归两组(同一批 492 篇):
| 构造 | 期望 | 实测 |
|---|---|---|
| 默认(已接管) | 通过 | 通过 |
--no-weaken |
报出被冲淡的目录 | 2 处:00-收件箱 2.34:1、90-归档 2.31:1,并注明「本片段未接管」 |
通用教训:查 CSS 值的体检,永远查不出「作用在值之上的东西」。
color 之上还有 opacity、filter、mix-blend-mode、主题的透明背景,
每多一层,你写进去的颜色就离用户看到的更远一点。
28. 一个开关干了两件事 —— 语义越权(v2.15 第二个核心)
这是比第 27 节更老的问题,藏了 13 个版本。
有个常量叫 EXCLUDE_FRESHNESS,字面意思是「不参与新鲜度着色」。
但 collect() 里用它是这么写的:
if any(s in rel.split("/") for s in EXCLUDE_FRESHNESS):
dn[:] = [] # 整棵子树从扫描里消失
continue
一个管新鲜度的开关,被当成「整篇笔记不参与着色」用了 ——
04-模板 / Templates / 90-归档 三个目录的笔记连分类色、未读色、冲突色都不发。
实测后果(2026-10-26,本库):
| 项 | 修复前 | 修复后 | 说明 |
|---|---|---|---|
| 扫描笔记 | 479 篇 | 492 篇 | 13 篇此前根本没进扫描 |
| 云盘冲突文件 | 3 篇 | 7 篇 | 漏报 57% |
| 目录覆盖率分母 | 68 | 71 | 三个可见目录此前被算进「刻意弱化不参与」 |
其中最要命的是冲突文件:那 4 篇漏掉的正是要用户去删的云盘同步垃圾,
染不上色就永远发现不了、永远删不掉。而报告天天写着「冲突文件 3 篇」,没人会去核对。
为什么 13 个版本没人发现 —— 三个条件同时成立,缺一不可:
- 颜色其实早就登记了:这三目录在
FOLDER_COLORS里有色(#9CA3AF / #9CA3AF / #A1A1AA),
只因循环里一句if fd in EXCLUDE_FRESHNESS: continue整条规则不发; - 所有体检都按同一个开关剪枝:覆盖率、配色、级联、终局颜色,
全都先if k not in EXCLUDE_FRESHNESS—— 分母里没有它们,结果恒绿; - 形态是「整体没有」而不是「一半没有」:不像死选择器那种「文件有色、目录灰」的错半拍,
一整片区域干干净净,看着像「本来就没配色」。
修法:拆成两个开关,各管一件事。
| 开关 | 含义 | 行为 |
|---|---|---|
EXCLUDE_ALL_COLOR |
Obsidian 文件树根本不显示的地方(.obsidian / .trash / .git / .workbuddy) |
整棵排除,不扫描 |
EXCLUDE_FRESHNESS |
日期对它没意义(模板、归档) | 照常扫描,照常发分类色 / 未读 / 冲突色,只是跳过新鲜度 |
通用教训:一个开关的语义边界,就是它的名字承诺的范围。
一旦被调用方拿去当另一个意思用,而体检又复用了同一个开关做剪枝,
那么这个越权就永远不会被发现 —— 因为检查者和被检查者共用同一副有色眼镜。
体检的分母不能来自被体检对象的同一个开关(第 25 节「分母错是精确误导」的又一个实例,
这次错的不是目录层级,是语义层级)。
何时使用
- 用户想让 Obsidian 文件树 / 侧边栏按新旧着色、显示"哪篇是新写的"
- 用户要求刷新 / 更新知识库目录颜色、更新 7 天新鲜度
- 需要标记未读笔记、标记云盘同步冲突文件
- 同库已有多套配色片段、颜色互相打架时
- 定时任务里每日刷新一次颜色
操作流程
1. 定位库根
缺省回退:脚本放在 <库根>/tools/ 下时可省略 --vault,自动取脚本上级目录的上一级。
显式指定(推荐):
python scripts/refresh_note_colors.py --vault "C:/path/to/vault"
2. 先干跑看统计
python scripts/refresh_note_colors.py --vault "<vault>" --check
推荐用 --check(只读体检)而非 --dry-run:除了统计,还会顺带报告片段注册状态、竞品特异性、陈旧片段告警,且完全不写盘。
确认扫描篇数、7 天内着色篇数、未读 / 冲突篇数、日期来源分布、选择器特异性是否合理。
3. 正式生成
python scripts/refresh_note_colors.py --vault "<vault>"
产出 <库根>/.obsidian/snippets/知识库目录着色.css,并自动在 appearance.json 的 enabledCssSnippets 中启用且置末位。
只生成 CSS 不动配置:加 --no-register。
给流水线消费的机器可读统计:加 --json(含结构化 stale_report)。
定时任务里想让「别人的片段停更」冒出来,用 --check --strict(陈旧时退出码 2)。
4. 输出一行结果
共扫描 N 篇,7 天内着色 M 篇,未读 K 篇。
失败时如实报告原因,不要静默跳过。
硬边界(不可违反)
- 禁止手动编辑生成的
.obsidian/snippets/知识库目录着色.css—— 下次刷新整体覆盖,手改必然丢失。要改规则就改脚本顶部的可调参数。 - 禁止为此安装任何 Obsidian 插件 —— 社区插件做不到按日期自动渐变,装了只会引入冲突。
- 不修改任何笔记内容 —— 只读 frontmatter 与文件名,绝不回写笔记。
- 不删除其他工具的片段 —— 同库若有其他工具的颜色片段,保留启用,只调整顺序让本片段置末位;冲突交给特异性解决。
- 写入范围(供安全审核参考) —— 脚本只在目标 vault 的
.obsidian/目录内写入,具体四个文件: .obsidian/snippets/知识库目录着色.css(生成的 CSS 片段).obsidian/appearance.json(注册片段到enabledCssSnippets并置末位).obsidian/graph.json(关系图谱colorGroups,v2.7 起)folder_colors.json(目录配色表外部化,仅--pin-folders时写入 vault 根)
读取范围:笔记的 frontmatter / 文件名 / mtime、同目录其他 CSS 片段、上述 .obsidian 配置文件。不联网、不上传任何数据、不执行外部命令、不删除任何文件、不修改笔记内容。所有写操作幂等,内容无变化时跳过写入。
可调参数(脚本顶部)
| 参数 | 作用 | 默认 |
|---|---|---|
FADE_DAYS |
新鲜度周期(天) | 7 |
FRESH_COLORS |
D0–D6 色阶 | 橙红 → 灰 |
FOLDER_COLORS |
目录 → 分类色映射 | 见脚本 |
FOLDER_FRESHNESS |
文件夹是否按子树最新笔记着色 | True |
FOLDER_FRESHNESS_TOP_LEVEL_ONLY |
只染顶层目录 / 逐层子目录都染 | True |
FILENAME_DATE_MAX_SKEW |
文件名日期与 mtime 的最大允许偏差(天) | 400 |
BOOST_ID |
用 :not(#id) 加固特异性(False 退回堆类名) |
True |
BOOST_ID_NAME |
:not() 里那个确保不存在的 id 名 |
kb-scope |
BOOST_CLASS_REPEAT |
仅 BOOST_ID=False 时生效:类名重复次数 |
3 |
BOOST_ATTR_REPEAT |
仅 BOOST_ID=False 时生效:属性重复次数 |
3 |
IDEMPOTENT_WRITE |
内容无变化时跳过写盘 | True |
INBOX_PULSE |
收件箱脉冲动画 | True |
TREAT_INBOX_AS_UNREAD |
收件箱一律视为未读 | True |
EXCLUDE_ALL_COLOR |
整棵排除、不扫描的目录(Obsidian 根本不显示) | .obsidian / .trash / .git / .workbuddy / .codebuddy |
EXCLUDE_FRESHNESS |
只跳过新鲜度、分类色/未读/冲突照常的目录(v2.15 起与上面彻底分开) | 模板 / Templates / 归档 |
WEAKEN_OPACITY |
弱化区自己声明的 opacity(v2.15)——值进体检,不拍脑袋 | 90-归档 0.82 / 04-模板 0.88 / Templates 0.88 |
WEAKEN_ENABLED |
弱化通道总闸:接管自己的 + 撤销别人的(v2.15) | True |
ATTENUATION_AUDIT |
渲染弱化体检:把 opacity 折算进颜色再算对比度 | True |
ATTENUATION_MIN_CONTRAST |
折算后对比度低于此值即报告「实际看不清」 | 2.6 |
UNREAD_FALSE |
判定为"未读"的 frontmatter 取值 | 否/false/no/未读/0/n |
GLOBAL_FILES |
根目录索引 / 日志类文件名 | index / log / README / AGENTS 等 |
AUTO_FOLDER_COLOR |
未登记目录自动派色(v2.4) | True(关掉即恢复静默漏染) |
AUTO_PALETTE |
自动派色的候选调色板 | 26 色(v2.9 从 12 色扩到 26 色) |
MIN_AUTO_POOL |
自动派色候选池容量上限(固定值,不随批次变化) | 8 |
MIN_AUTO_POOL_FLOOR |
池子下限:候选不够好也至少收这么多,保证有得派 | 4 |
FOLDER_COLORS_FILE |
外部目录配色表(优先于 FOLDER_COLORS,升级不覆盖) |
tools/folder_colors.json |
DE_THRESHOLD |
ΔE 低于此值判为「视觉难区分」 | 20.0 |
DE_WARN |
ΔE 低于此值判为「严重撞车」 | 12.0 |
INTENTIONAL_SAME_GROUP |
刻意同色组(弱化区),不参与互撞判定 | 模板 / 归档 / tools |
MTIME_CLUSTER_MIN |
同一天纯 mtime 来源笔记达到此数才提示疑似污染(v2.6) | 15 |
MTIME_IMMUNITY |
mtime 污染免疫总闸(v2.8);关掉退回"只提示不改判" | True |
MTIME_POLLUTION_MIN |
同目录同一天纯 mtime 笔记达此数才算污染簇 | 15 |
MTIME_POLLUTION_RATIO |
且占该目录笔记总数不低于此比例 | 0.6 |
MTIME_TRUST_FOLDERS |
mtime 永远可信的目录白名单 | 空 |
MTIME_IMMUNITY_SKIP_TODAY |
不对「日期就是今天」的簇下手(留给真实导入) | True |
MTIME_BURST_MIN |
一个时间窗内达此篇数才算「突发」(v2.11) | 10 |
MTIME_BURST_WINDOW_MIN |
判定为「同一次」的时间窗(分钟) | 60 |
MTIME_BURST_MIN_FOLDERS |
突发至少跨几个目录才认(1 = 单目录也免疫,误伤更大) | 2 |
GRAPH_COLOR_GROUPS |
是否把目录分类色同步到关系图谱 graph.json(v2.7) | True(只染分类色,保留用户组) |
NESTED_FOLDER_COLOR |
嵌套目录是否也着分类色(v2.13,关掉退回只染顶层) | True |
NESTED_FOLDER_NEED_NOTES |
是否只给「子树内有笔记」的嵌套目录发规则 | False(连附件目录一起染,覆盖率才是真的) |
NESTED_FOLDER_FRESHNESS |
嵌套目录是否逐层算文件夹新鲜度(父子可不同档) | True |
FINAL_COLOR_AUDIT |
终局颜色体检总闸(v2.14) | True(关掉不影响生成,只少一道体检) |
--explain <路径> |
摊开某篇笔记的全部候选规则与决胜者(v2.14) | — |
--as-folder |
配合 --explain,按「文件夹标题」而不是文件来算 |
— |
> BOOST_ID=True 时特异性已在 id 位,正常情况不需要再调任何参数——对手堆多少类名都追不上。
> 只有在极端场景(对手也用了 id 选择器)才需要额外处理;BOOST_ID=False 是回退开关,退回 v2.2 的堆类名写法。
未读与冲突的判定口径
- 未读:frontmatter
已读: 否(或read: false等)→ 文件名含「未读」→ 位于00-收件箱/(可用INBOX_UNREAD_IGNORE排除说明页)。三者依次兜底。 - 冲突:文件名含「冲突文件」(天翼云盘 / 坚果云同步冲突产物),删除线 + 整行弱化,提示清理,优先级最高。
排错
详见 references/着色规则与冲突排查.md。最常见几类问题:
- 终局颜色体检报「实际显示色与设计意图不符」:先看 --explain <路径> 打出来的决胜规则属于哪个片段——是自己某一层漏发(如 v2.14 的高对比文件夹段),还是被别人的片段压住(看 imp 与特异性三元组)。
- 颜色部分不对(最隐蔽) —— 多半是特异性被别的
!important片段压过。先看输出的选择器特异性是不是1-x-0;不是则说明BOOST_ID被关了。是1-x-0还被压,说明对手也用了 id 选择器,此时再考虑调BOOST_CLASS_REPEAT/BOOST_ATTR_REPEAT。 - 颜色完全没生效 ——
appearance.json的enabledCssSnippets里没有本片段。用--no-register跑完再手动启用。 - 新鲜度失真 —— 大量笔记靠 mtime 判日期(云盘同步污染)。看统计里
mtime占比是否过高,是的话逐步补 frontmatter 的日期字段。 - AI 工作文件被误着色 —— 确认
.workbuddy/.codebuddy在EXCLUDE_FRESHNESS里。 - 校验失败中止写入 —— 多半是改脚本时注释写坏了,看报错行号。
- 今天的笔记没颜色,但别的日期有 —— 典型的「绝对日期型片段停更」。看输出里
为「绝对日期型」那一行,先看级别再决定动不动手:
-[提示]= 被本片段压着,当前 0 篇受影响,不用动
-[告警]= 真有笔记被染错,按篇数处置:合计超过 10 篇就停用该片段,个位数可以等生成方恢复
详见第 11、12、13 节。 - 某个目录整片没颜色 / 一批笔记第 8 天后变成默认灰 —— 该目录没登记配色。看输出里的
未登记配色目录一行(v2.4 起会自动派色并列出目录与篇数)。v2.9 起固化不用改源码:跑一次--pin-folders,结果写进tools/folder_colors.json;想换色直接编辑那个 JSON。详见第 15、21 节。 - 某一档(比如 D1)篇数突然暴涨 —— 先看有没有
mtime 免疫或mtime 突发免疫那两段。v2.8 起整目录重拉已被自动处理,v2.11 起跨目录批量触碰也一并处理:被点名的批次不再用 mtime 定日期,暴涨会自己消失。若仍有残留暴涨,看日期可信度一行——那是单目录、低占比的小批量同步(两种判据刻意留的口子),根治是给这些笔记补 frontmatter日期;想调灵敏度改MTIME_CLUSTER_MIN/MTIME_POLLUTION_RATIO/MTIME_BURST_MIN/MTIME_BURST_MIN_FOLDERS。详见第 19、23 节。
8b. 某个整目录的文件全都没了新鲜度色 —— 先看mtime 免疫是不是点名了它。若这批笔记确实是你今天刚导入的,用--trust-mtime跑一次,或把该目录加进MTIME_TRUST_FOLDERS。详见第 19.5 节。 - 陈旧告警的篇数/级别自己变了,但笔记没动 —— 检查是不是
covered口径问题(v2.6 已修:分类色覆盖的文件现已计入)。若仍出现,用--json看stale_report的miscolored_effective/miscolored_suppressed两个字段是否自洽。详见第 17.2 节。 --json没有任何输出 / 流水线拿到空结果 —— v2.9 前json.dumps不认datetime.date,untrusted_folders里的日期字段一序列化就抛TypeError,整段结构化输出全丢;而人肉跑(不带--json)看起来完全正常,属于"只有流水线知道、人不知道"的静默故障。v2.9 已用default=str兜底。教训:给机器看的出口,必须单独跑一次验证,别默认它跟人看的输出同源。- 报告里出现「回落层自相矛盾」 —— 说明同库有另一套片段的分类色漏写了
!important,被它自己带!important的日期层压住,这批笔记的"第 8 天回落"不会发生。标[提示]时本片段已接管、不用动;标[告警]时按篇数处置(>10 篇就停用那个片段)。详见第 20 节。 - 关系图谱节点没按目录染色 —— 图谱是 canvas 渲染,CSS 管不到,靠
.obsidian/graph.json的colorGroups。先跑--check看"图谱着色"那行:报"已就位"说明 graph.json 正确(去 Obsidian 图谱设置里确认 colorGroups 面板没被手动重置);报"颜色过期/缺失"说明 graph.json 被改过,正式跑一次即可修正。完全不想染色相图谱设GRAPH_COLOR_GROUPS=False。详见第 18 节。 - 某个目录的标题没有分类色,但它下面的文件有色 —— 典型的死选择器:规则语法合法、片段也启用,就是选择器匹配不到任何东西。看输出里
死选择器体检那一段,它会点名片段、选择器、死因和意图目录下有多少篇。若是别人的片段,正确做法是修那个片段(或让它别启用),而不是在本片段里加颜色盖过去——盖过去等于把"这条规则已经坏了"这件事也一起盖住。详见第 22 节。 - 死选择器体检突然报出一大片 —— 先确认语料没错:v2.10 起语料直接遍历文件系统、不再用剪过枝的
rows。如果仍成片报错,多半是有人改了脚本里的属性归属判断(data-path属于紧挨着它的元素),或把EXCLUDE_FRESHNESS里的目录名写进了选择器。用--json看dead_selectors里的reason字段分类核对。 - 一批散在多个目录的老笔记被染成鲜艳色 —— 典型的跨目录同步污染:每个目录都凑不够「≥15 篇且占 60%」,目录级判据一条都抓不到(实测一次漏掉散在 6 个目录的 16 篇)。v2.11 起看
mtime 突发免疫那段,它会点名日期、起止时刻、篇数与涉及的目录。若这批确实是你刚整批导入的,用--trust-mtime跑一次。详见第 23 节。 - 某个目录/批次的文件全都没了新鲜度色,但它们是新的 —— 先看
mtime 免疫或mtime 突发免疫有没有点名。被点名的批次改从 frontmatter/文件名取日期,两者都没有就判定「日期未知」而撤回着色(宁可无色,不可谎报)。根治是补 frontmatter日期;临时放行用--trust-mtime或MTIME_TRUST_FOLDERS。详见第 19、23 节。 - 未读/冲突标记在别人电脑上好好的,在某些人那儿不显示 —— 先看
级联顺序体检那一行。若无告警,问清楚对方是不是开了系统的「提高对比度」或暗色偏好:v2.12 前高对比档排在未读之后,同特异性同!important时由顺序决胜,未读会被新鲜度色整段盖掉(实测 3 篇)。v2.12 已把高对比档上移为 3.7 节。若体检本身报「通过」却仍不显示,确认 CSS 里3.7 无障碍排在4. 未读之前。详见第 24 节。 - 父目录和文件都有色,中间那层子目录标题是灰的 —— 典型的嵌套目录漏染:目录清单只枚举顶层,二级三级目录一条规则都没有。看输出里
目录覆盖率那一行:v2.13 起默认逐层补染(实测 22.1% → 100%)。若仍不是 100%,看[告警]点名的是哪些目录——多半是新目录落进了EXCLUDE_FRESHNESS(刻意弱化)或点目录(Obsidian 不显示)。想退回旧行为用--no-nested。详见第 25 节。 - 报告说「冲突文件 3 篇」,磁盘上一搜却有 7 篇 —— v2.15 前的语义越权:
EXCLUDE_FRESHNESS(名为"不参与新鲜度")被当成"整篇不参与着色"用,模板/归档目录整棵不进扫描,那 4 篇冲突文件连删除线都没有。v2.15 已拆成EXCLUDE_ALL_COLOR(真正不扫描)与EXCLUDE_FRESHNESS(只跳过新鲜度)。自检方法:报告里的冲突文件数应等于文件名含"冲突文件"的实际文件数。详见第 28 节。 - 颜色明明写对了,看着却灰蒙蒙一片 —— 先看
渲染弱化体检那一行。opacity是乘在整个元素上的,同库另一个片段一句裸opacity: 0.6就能把你的分类色冲淡 40%(实测 90-归档 掉到 1.91:1,等于没颜色)。v2.15 起本片段会用第 9 节接管 opacity;若你用了--no-weaken,体检会点名"被谁冲淡、本片段未接管"。注意弱化不等于消失:自己调WEAKEN_OPACITY时,体检会当场验对比度。详见第 27 节。 - 收件箱脉冲动画不动了 / 弱化值不生效 —— 两者抢同一个
opacity通道且都带!important,必有一伤。v2.15 已把脉冲改为text-shadow呼吸,彻底让出color/opacity两个通道。若你自己加了别的动画,别再动这两个属性。详见第 27 节。
与同类方案的差异(对照 TeleAgent 两层片段)
同库常见的 TeleAgent 配色分两层:静态层 teleagent-color-coding.css(目录分类色 + 未读 + 冲突 + 收件箱脉冲)、动态层 teleagent-dynamic-colors.css(7 天渐变)。本方案取其长、补其短:
| 维度 | TeleAgent | 本方案 |
|---|---|---|
| 日期来源 | 只认文件名里的日期字符串 | frontmatter → 文件名 → mtime 三级回退 |
| 匹配方式 | [data-path*="2026-10-12"] 子串,会误伤含该日期的任意路径 |
精确到单文件的 data-path="...",不误伤 |
| 文件夹着色 | 只染文件,折叠后看不见热度 | 文件夹继承子树最新笔记日期,折叠也可见 |
| 日期防伪 | 无,文件名里的历史日期会被当今天 | 与 mtime 偏差超阈值即判为引用日期,自动降级 |
| 未来日期 | 无处理,被当陈旧丢弃 | 单独统计为「未来/计划」 |
| 选择器特异性 | 冲突层 0-5-0、动态层 0-4-0(纯类) |
1-3-0(含 id 位),压过任意类数量,不依赖加载顺序,且体积还小 23% |
| CSS 自检 | 无(其动态层第 2 行起注释已损坏,规则被吞) | 状态机校验,不通过拒绝写入 |
| 日期表达 | 绝对日期:日期硬编码进选择器,停更即静默失效 | 相对新鲜度 D0~D6,无绝对日期,漏跑只滞后一天 |
| 陈旧自检 | 无(其动态层停更 2 天后仍在"假装工作") | audit_stale_snippets() 按"规则是否含今天"判定并告警 |
| 告警粒度 | — | 直接给出错染 / 漏染的具体篇数(实测 16 篇命中 / 0 篇漏染),可量化决策 |
| 告警真伪 | — | 区分实际影响与潜在风险:被本片段压住的只报 [提示],真染错才报 [告警] |
| 目录覆盖 | 硬编码清单,未登记目录静默跳过(实测漏 2 个目录 18 篇) | 每次对账 + 名称哈希自动派色 + 点名报告,不漏染 |
| 嵌套目录 | 子串匹配天然覆盖子目录(这是它的优点),代价是误伤同名文件 | 逐层显式枚举 + 继承父色:精确匹配不误伤,覆盖率实测 22.1% → 100%(v2.13) |
| 覆盖率的分母 | 无此概念 | audit_folder_coverage() 分「已覆盖/刻意弱化/漏染」三类计数,分母含全部层级(v2.13) |
| 配色可辨性 | 无检测(03-出海TikTok #ff9f43 与其橙系新鲜度色同族;04-模板/Templates 完全同色) |
每次算 ΔE 体检三类撞车,实测揪出 4 处并换色 |
| 回落可感知 | 不检测,回落撞车会让「过期」看不出来 | A 类撞车判定 + 只判参与新鲜度着色的目录(不虚报) |
| 告警噪声 | — | 刻意同色组显式声明、淡色档合并为一行,不刷屏 |
| 日期可信度 | 无(mtime 被云盘刷了照样当新的) | 按目录判定 mtime 可信度:先检测污染簇,认定不可信后换证据链,实测纠正 221 篇、免着色 41 篇 |
| 污染后怎么办 | 无(只能眼看着全树变橙) | 免疫:换证据链 + 无证据即撤回着色,宁可无色不谎报;留 3 条退出通道 |
| 自检是否可靠 | — | 对手特异性逐篇实测(不写死常量)、覆盖口径含分类色,输入不变则结论不变;并给自检本身做回归验证 |
| 自己的静默失效 | 同类方案与其动态层都无自检 | v2.4 把「对账」、v2.5 把「色差体检」也用在自己身上,不只体检别人 |
| 回落层是否自洽 | 静态层裸 color、动态层 !important,自己的两层打架,30 篇被永久钉死在硬编码日期 |
v2.9 起全局体检 H1 会点名,且本片段分类色统一带 !important |
| 子串是否误伤 | [data-path*="index.md"] 连带染到 tools/_index.md;目标文件其实不存在 |
全部用精确 data-path="...",并体检他人片段 |
| 配色能否固化 | 只能改生成方源码 | 外部 folder_colors.json + --pin-folders,升级不冲掉 |
| 自动派色质量 | 无对应能力(未登记目录直接不染) | 26 色板 + 最远点贪心选池 + 报最近邻 ΔE,最近邻 10.4→18.7 |
| 选择器染的是不是真东西 | 不查;其静态层给 04-模板 写的两条文件夹规则命中数均为 0(前导斜杠 + 锚点转义),该目录标题从未上色,8 篇文件却有色 |
死选择器体检:拿真实路径撞每个选择器,0 命中即点名并给死因;v2.10 修掉自己一处同类误报(v2.9 曾据"非法转义"误判整条规则失效) |
| 层与层谁赢 | 未读层 .nav-files-container .nav-file-title[data-path*="未读"] .tree-item-inner = 0-4-0,动态层日期规则也是 0-4-0,双方都带 !important → 纯靠片段加载顺序分胜负;动态层是后生成的、通常排在后面,未读会被日期色压掉(冲突层 0-5-0 多一个类才幸免) |
高优先级层固定排在后面,且 v2.12 起有 audit_cascade_order() 按「元素 × 通道」实算每条规则胜负,被压掉就点名;高对比档曾越过未读(实测 3 篇),已上移为 3.7 节 |
| 暗色主题 | 只给 2 个目录写了暗色覆盖(00-收件箱、90-归档),其余 7 个目录在暗色主题下仍用为浅底调的配色 |
双通道全量覆盖(.theme-dark 权威通道 + prefers-color-scheme 兜底通道),背景色反转而非沿用浅色值 |
| 无障碍高对比 | 无 | prefers-contrast 独立加深档;v2.12 起该档排在未读/冲突之前,不会盖掉状态层(见第 24 节) |
| 最终显示成什么颜色 | 不查,写进 CSS 就算数 | 终局颜色体检:按层叠四顺位逐篇算实际渲染色并与设计意图对账,实测揪出 12 个文件夹标题没被高对比档兜住(v2.14) |
| 能不能解释「为什么是这个色」 | 不能,只能肉眼猜 | --explain <路径> 摊开全部候选规则与那条决胜规则(v2.14) |
| 弱化通道(opacity) | 思路对、手段错:用 opacity 表达"重不重要"(色相说是什么、透明度说重不重要,两通道解耦)确实比挤在一个 color 通道强;但值过狠(90-归档 0.6 → 实测 1.91:1,等于没颜色)、写在别人片段里、且自己无体检 |
取其思路、弃其手段:语义收归自己的 WEAKEN_OPACITY,值进体检(初版 0.82/0.72 当场被自己判死),未设计过的一律接管为 opacity:1 撤销误伤,可 --no-weaken 一键退回(v2.15) |
| opacity 会不会冲淡别人 | 会,且无声:一句裸 opacity 就能把同库其他片段的分类色冲淡 20%~40%,而对方所有体检都在比对 color 值,全绿 |
渲染弱化体检:把 alpha 折算进颜色(blend)再算对比度,被谁冲淡、接管了没有,逐目录点名(v2.15) |
| 开关语义 | — | v2.15 前自己踩过:EXCLUDE_FRESHNESS(名为"不参与新鲜度")被当成"整篇不参与着色",13 篇笔记含 4 篇冲突文件漏染、报告漏报 57%;已拆为 EXCLUDE_ALL_COLOR + EXCLUDE_FRESHNESS(见第 28 节) |
| 可接流水线 | 无 | --strict 陈旧时退出码 2;--json 输出结构化 stale_report/hygiene_report/dead_selectors(v2.9 修掉 date 序列化崩溃) |
| 写盘策略 | 每天无条件重写 | 幂等,内容无变化不写盘 |
| 暗色主题 | 仅 .theme-dark |
双通道,含系统偏好兜底 |
| 片段共存 | 带 !important,会压制他人 |
带 !important + 特异性加固 + 保留对手 + 竞品体检 |
| 高对比 / 动效 | 部分 | prefers-contrast + prefers-reduced-motion |