Files
tamabox/.workbuddy/memory/2026-10-20.md
T

291 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-10-20
## 01:10 shiroko 主题补「空气层」
- 用户问「还有没有其他元素,贴合主题又不突兀」。结论:场景压白 38% 后整页亮度只有 235~246,
再摆物件(书包/粉笔/纸飞机)一定会像贴纸,所以走**纯渐变造光**路线。
- 做了 4 个候选截图(A 纯窗光 / B 白散景 / C 光环符号 / D 组合),用户选 D。
⚠️ **B 是反面教材**:白色散景叠在已经压白的场景上,实测差异 0.14 级,肉眼完全不可见。
要"提亮"必须先有暗部,真正起作用的是「底部压深 + 暖色光斑」的组合。
- 最终 `--tb-deco` 后 4 槽:左窗光柱(112deg)、右侧 120px 暖散景(88% 30%)、
脚下 560×170 地面光斑(28% 94%)、底部 24% 压深。亮度差 3~9 级。
- 验证手段(本轮**模型读不了图**,全靠数值):留白区差分(与候选 D 差 0.0)、
分区亮度表、横向扫描相邻像素差 max 4(判色阶断层)、PNG 体积比对。
- ⚠️ 造预览页两个坑:① 覆盖块必须包在 `<style>` 里,裸写会被当正文渲染,整页布局炸掉;
② `--tb-deco` 各层之间**漏逗号**会让整条声明失效 → 人物和所有背景层全没,且各变体看起来"完全一样"。
以后先 diff 两个变体的 CSS 文本再截图。
- `StaticVersion` → `20261020a`;提交 `fb94b8c` 已推送。文档 `docs/theme-shiroko.md` 加「空气层」一节。
## 08:30 重画光环
- 用户反馈「光环变形太奇怪,而且中间没有扣干净」。根因两条:
① AI 画的环本身歪(bbox 291,93–584,232,比头还宽、偏移到左边);
② 环内是**封闭白孔**(面积 5190px 的包围区域),四角 floodfill 进不去 → 抠完环里糊着一团白。
- 做法:非白像素从环上一点漫水拿到独立连通域(与头发不连通,头部顶端在 y≈265,环只到 232),
整块抹白;**先把 alpha 建好,再在 RGBA 上画新环**——环心天然就是透明的,不用给封闭孔补洞。
- 新环参数:中心 (515,160)、100×34、线宽 10、-7° 倾斜、4 倍超采样后 LANCZOS 缩小、
外加 45% 模糊柔光;颜色 (127,211,220),压白 16% 后实测环心 (164,220,224)。
- 坑:擦除矩形右边界要绕开**灰色狼耳**(x>630,y 190-260,颜色 (139,140,146) 不是青色),
差点一起抹掉。判据是连通域 bbox 而不是"上方全是空白"。
- 验证:alpha 字符图确认环闭合、环心 alpha=0;渲染后页面里青色像素 bbox (201,467)-(271,649),
环在 y 468-486,头顶约 505,间距正常。
- `StaticVersion` → `20261020b`,图片 `?v=20261020b`;提交 `2e9bd6b` 已推送。
## 09:40 部署反馈三修 + 自行车/步枪
- **狼耳顶端被削平**:上一版用矩形 `(281,83)-(594,242)` 抹旧光环,右下角正好切到左耳尖
(耳尖在 y≈185、x 420-480)。改成只抹光环的**非白连通域**(bbox 291,93-584,232,与头发不连通)。
- **手内侧 / 大腿内侧白没抠干净**:那是**闭合**的白色区域,四角 floodfill 进不去。
加封闭白孔检测(>=246 且不与边缘连通、面积>=40)整块掏成透明。
⚠️ 阈值必须 >=251 才能当"背景":背景恒为 (253,253,253),而**白袜子是 237**,
用 246 当背景阈值会把袜子一起吃掉(第一版就吃穿了一只袜子)。
- **人物像站在课桌上**:原场景前景全是课桌,根本没有地面。重出场景:
前景留空地、课桌退到右后方,人物脚下才是地板。
- **自行车 + 步枪**:独立素材 `props/bike.webp`(镜像成车头朝右、与人同向、压白 26%、
自带接触阴影),放在**人物身后偏左**。deco 层顺序 = 画在上面的在前,所以第一层是人、
第三层是车,车天然被她挡住。
- ⚠️ **`background-position` 的百分比是相对 `(定位区宽 - 图片宽)`**,不是相对视口宽。
人(117px)和车(397px)用同一个 `calc(50% - 901px)` 会差出 280px,车直接跑出屏幕只剩一片轮圈。
按各自宽度反解后:人 `calc(50% - 524px)`、车 `calc(50% - 612px)`。
- ⚠️ 道具**不能烘焙进背景**:背景走 `cover`,视口一变裁切量就变,和固定 px 的人物必然分家。
- 场景重出后色调用「逐通道均值/方差对齐旧场景」保持同一雾度;移动端从同图竖裁,避免两个房间。
- `StaticVersion` → `20261020d`;提交 `15aebd6` 已推送。
## 09:55 素材规格书
- 用户打算换工具自己生成素材 → 写了 `docs/theme-shiroko-assets-spec.md`:
画布/导出/显示尺寸、比例(人 1:3.41、道具 1.53:1、背景 16:9)、雾度(人 16%、车 26%、场景 30~38%+模糊 1.2~1.4px)、
构图硬约束(左侧 22% 前景必须是空地,否则又回到"站在课桌上")、换图清单与定位公式。
- 配色给的是**压白后的显示值** + 反算公式 `原色=(显示值-40.8)/0.84`,并特别标注袜子要用 `#ededed`
而不是纯白(纯白会和背景粘连,抠图必然吃穿)。
## 10:40 主题生成器(dev/theme-builder)
- 单文件 `dev/theme-builder/index.html`:上传背景/人物/道具 → 拖拽调位置 → 导出 zip,
解压到项目根即可新增主题。不联网、不起服务、图片全在浏览器本地转 WebP。
- 预览用**和线上同一套机制**渲染(pattern + deco 6 层 + page-image,size/position 对齐),
不是另画示意图;拖出来的「左沿像素」反解成 `calc(50% - Npx)`,换视口不跑位。
- 导出包:`static/css/themes/<slug>.css` + `static/images/themes/<slug>/*.webp` + `INSTALL-<slug>.txt`。
CSS 同时写浅色/深色两块,10 个必需令牌齐全——**实测通过 `TestThemeCSSComplete`**。
- zip 自己实现(STORE + CRC32,约 40 行),不引 CDN;`unzip -t` 无错。
- 踩坑:① 面板里 `c.text2` 写成 undefined(light 对象里才算出来的派生色,要用 `light.text2`);
② `contain` 模式如果不按内容裁画布,会留大片透明边 → 导出宽高比全错;
③ 预览容器 `grid-template-columns:340px 1fr` 会被 1512px 的 frame 撑破 → `minmax(0,1fr)`;
④ 无头 Chrome 下 `canvas.toBlob` 的回调在 `--dump-dom` 前不触发 → 全部改 `toDataURL` 同步化,
自检才能跑(`?selftest=1` 输出 CSS/zip 字节,`?demo=1` 载入示例素材)。
- ⚠️ 预览里深色必须走「暗色渐变 + deco none」,否则会拿白天场景图压深底(和导出结果不一致)。
## 11:05 主题生成器重做:真实页面画布 + 按页面作用域
用户原话:旧生成器「只针对当前遇到的问题,需要更通用」「示例页面和我的项目完全不一样」
「想针对箱主页面、问题页面、问题列表、设置页面分别配置」。
**机制层(新)**
- `internal/context.pageScope()` 按路径归类,`<html data-page-scope>`:
`box`(/_/{domain}) / `question`(/_/{domain}/{id}) / `admin`(/user/questions,/user/votes*)
/ `profile`(/user/*) / `vote`(/vote/{token})。首页与 `/explore` 返回了值但**不加载主题 css**,
所以生成器里不列它们(列了就是骗人)。
- ⚠️ 深色覆盖必须是**三重选择器** `html[data-theme="dark"][data-theme-style][data-page-scope]`:
浅色作用域块特异性 0,2,1 与深色块相同、且写在后面,二重选择器会被它盖掉。
- 装饰槽位 **6 → 10**(base.css 背景 8 → 12 层 = pattern1 + deco10 + image1)。
`TestDecoSlotCount` 逐文件逐声明检查,变异验证有效(写 6 个会 FAIL)。
**生成器(重写)**
- 画板 = iframe 加载 `outputs/preview-*.html`(真模板渲染)。**必须起本地服务**:
file:// 下父页面读不到 iframe 的 DOM(即使同目录),预览只能退回示意图。
`dev/theme-builder/serve.py` 把项目根当静态目录(只读)。
- 每个作用域独立「整页背景 + 最多 10 层装饰」,默认跟随全局;不产覆盖块就自动回退。
- 图层 = 图片 / 径向光斑 / 线性光;9 宫格锚点 + 像素偏移;图片层可在画板上直接拖。
- ⚠️ 位置统一 `calc(0%|50%|100% ± Npx)` 两值:百分比是相对 (容器 − 图片)。
**不要** `left/bottom` 关键字混搭 —— 「水平居中 + 垂直贴底」是三值语法,非法时整条声明作废
→ 图层静默掉到左上角(这是之前踩过的坑,这次直接规避)。
- ⚠️ 配色只让填 6 个基础色,另 4 个必需令牌派生:`surface-2` 往 **text** 混(浅色下变深、
深色下变亮,方向自动对),`text-secondary/muted` 往 bg 混,`primary-weak` 加 alpha。
漏了 `--tb-primary-weak` 时 `TestThemeCSSComplete` 只报「缺少」,页面看着没问题,很容易混过去。
- 自检钩子:`?dump=css` / `?dump=zip`(写进 `<pre id="dump">`),配合 `--dump-dom` 回归。
**验证**(都是实测,不是推断)
- 生成器导出的 CSS 放进 `static/css/themes/` → `TestThemeCSSComplete` + `TestDecoSlotCount` 双绿;
删掉深色块做变异 → FAIL,说明测试真的在查。
- 真实箱主页计算样式:image/size/position/repeat/attachment **各 12 层**,前几层尺寸与 shiroko 一致
(auto 400 / 96x13 / auto 259 / 100%×3),确认扩槽没打乱顺序。
- 临时主题 `zztest` 验证:box 页有人物、question 页 `--tb-deco: none×10` 后人物消失;
不写覆盖块时回退全局(人物仍在)。
**顺手发现**:`.gitignore` 写的是 `./outputs`(无效写法),导致 247 个 outputs 文件被跟踪,
每次跑 `TestGenPreview` 都会产生一堆 M。没擅自改(会让人以为要清理历史产物),留待用户决定。
## 12:20 outputs 不再纳入版本控制
- 根因:`.gitignore` 里写的是 `./outputs`,**gitignore 不认 `./` 前缀**,整条规则等于失效,
247 个产物(预览快照、截图、历史 review 报告)一直被跟踪,每次跑 `TestGenPreview` 都产生一堆 M。
- 改成 `/outputs/` + `git rm -r --cached outputs`:索引删除 247 个,**磁盘文件一个没动**。
`git check-ignore -v outputs/x` 确认命中。
- 另两条同样无效的规则没动:`./workbuddy`(目录根本不存在,笔误)、`./scripts`(有 1 个文件被跟踪)。
用户只要求 outputs;要清理得单独确认。
## 13:10 生成器能力盘点(含实测)
用户问「有什么改进建议」,顺手实测了一个之前没验证过的点:**窄屏(390 宽)**。
- 没有 `@media` 规则的主题,在 390×844 下:16:9 桌面背景被 `cover` 裁成一条无意义纹理,
人物被占满屏的卡片完全盖住 —— 等于白配。
- shiroko 因为手写了 `@media (max-width:900px)`(换 `scene-mobile.webp` + deco 全关),
同一视口下背景是协调的竖版、装饰退场。
- 结论:**生成器导出的主题没有窄屏段**,这是真实缺口,不是理论问题。
其它已确认的缺口(grep 过,不是猜):无 localStorage / 无 json 导入导出(关掉全丢)、
无图层上移下移(遮挡关系由顺序决定,只能删了重加)、无导出前校验、无既有主题导入。
⚠️ 测窄屏时必须让 **iframe 宽度**一起变(`verify.html?w=390`)。第一次只改外层 window,
iframe 内部仍是 1512,媒体查询按 1512 判定 → 结论是错的。
## 14:20 生成器补 P0 四项
用户问建议 → 我给了 P0/P1 清单 → 他选「P0 四条一起做」。已实现并推送。
1. **配置留得住**:`localStorage` 自动草稿(renderAll 后 700ms debounce)+ `theme-<slug>.json`
保存/打开(图片 base64 内嵌,单文件带走);zip 里也塞一份 json;`?fresh=1` 跳过恢复。
2. **图层 ↑↓**:顺序即层级(`background-image` 第一层画在最上)。
3. **窄屏段**:竖版背景图 + 隐藏装饰 + 断点(默认 900),导出 `@media` 块。
⚠️ **只写浅色规则**:深色块特异性 0,2,1 > 浅色 0,1,1,本来就压住它;多写一份反而容易出错。
4. **导出前校验**:slug / 图片层缺图 / 引用已删素材 / 超 10 层 /
生成 CSS 每处装饰是否真 10 个值(与 Go `TestDecoSlotCount` 同规则,给工具自身兜底)。
**实测**
- 390 视口首层装饰 `url(char.webp)` → `none`,1512 下不变;两种视口都 12 层对齐。
- 草稿往返:带 `?demo=1` 保存 → 不带 demo 打开,`validate` 仍为空(说明恢复成功)。
- 示例 CSS 过 `TestThemeCSSComplete` + `TestDecoSlotCount`(27 处声明)。
**踩到的小坑**
- `makeDemo()` 里 `S.assets.push` 不检查重复 → 重复点「载入示例」会撞 id(`assetById` 取第一个),
导出时静默用错图。改成开头 `S.assets = []`。
- 拖拽标签写 `top:-17px` 会贴到画板边缘被压线/裁切,改成放在框内左上角;
整块半透明填充会把人物罩成一层米色(看着像"框住空气"),改为纯虚线 + 角标签。
- 面板一多左栏就滚不到底:按使用频率重排(图层 → 背景 → 窄屏 → 图片池 → 配色)。
## 15:20 生成器:图片处理(透明度 / 压白 / 去色 / 裁边 / 去白底)
用户问「图片的透明度或者一些简单的处理能否安排上」。已实现并推送 `c3d34b5`。
**非破坏模型(关键设计)**
- `orig` 存上传时的**原始字节**(`FileReader.readAsDataURL`,不重编码),`fx` 只存参数,
每次处理都从 orig 重算 → 「压白 16% → 20%」不会叠加两次 webp 压缩。
- 打开面板看一眼就关:参数没变就**跳过重编码**,一个像素都不动(`fxSnapshot` 比对)。
这条不加,用户点开看看就会把图越磨越糊。
- 预览按 420px 宽算(滑杆跟手),完成时全分辨率重算;用序号 `fxSeq` 丢弃过期结果,
否则快速拖动时旧渲染覆盖新的。
**去白底两个坑(都实测过)**
- 手内侧 / 两腿之间是**闭合**白区,四边漫水进不去 → 按连通域单独掏,面积 ≥ 24 才掏
(更小的多是眼睛高光,掏了变透明洞)。
- **容差默认 16,不是 22**:背景 253、白袜子 237 只差 16。容差 30 实测把袜子吃穿(alpha 0)。
**验证(跑出来的)**
- `?dump=fx` 13 项断言全过:封闭孔开/关、容差 16 保袜子 / 30 吃穿、收边后轮廓 1px、
alpha 50%→128、压白 20% 时 237→241、去色后 R=G=B、裁边 200×160→80×40。
- `?dump=fxpipe` 链路:弹窗开关、预览 300×960、参数重开后仍在、改参后 dataURL 变、
**无改动时 dataURL 一字节不变**(这条是防回归的关键)。
- `?dump=fxkey` 端到端:把 shiroko 立绘合成到 253 白底再去白底,透明占比 46.3%,
与手写 floodfill 抠出来的 46.6% 差 0.3%。
- 生成的 CSS 仍过 `TestThemeCSSComplete` + `TestDecoSlotCount`。
**顺带**
- 工程文件升 v3:存 `orig` + `fx`,未处理时不重复存 dataURL(否则 json 平白翻倍);
读 v2 旧工程把 dataURL 当 orig 用。
- 新增 `?loadurl=` 把项目里的图直接当素材载入(不必先下载再上传),`?fxjson=` 给初始参数。
- 截图看出两个 UI 问题:拖拽标签 `top:-17px` 贴到画板边缘被压线、半透明填充把人物罩成米色
→ 改框内左上角标签 + 纯虚线(上一轮已修,本轮沿用)。
## 16:10 生成器:图层换不了图 / 背景关不掉 / 草稿清不掉
用户反馈三条,都是**交互设计**问题,核心逻辑其实没坏(实测换图是生效的)。已推送 `937a890`。
- **「每层都只有这张图、换不了」**:`add-layer` 里 `l.asset = S.assets[0].id`,连加三层全是同一张;
而且换图的下拉只在「选中那一层的编辑器」里,用户看不出每层能各自换。
→ 新层不再自动绑图(素材池只剩一张时才绑);**每行带缩略图 + 「图片」下拉**直接换。
- **「背景关不掉」**:`inherit` 时背景下拉 `disabled` —— 用户在箱主页看到背景还在又点不动。
→ 一律可点,一改自动 `ensureOwn(true)`:切成独立配置 + **把全局当前值复制过来当起点**
(否则改背景会顺带把图案弄丢);复制图层要**重发 id**,不然两个作用域共用一批 id,拖拽/高亮会串。
- **「上次的数据一直在」**:`localStorage` 草稿只有隐藏参数 `?fresh=1` 能绕过,UI 无出口。
→ 顶栏加「新建」(清空 + 删草稿 + 二次确认)。
⚠️ **跟随全局时不能给拖拽框**:那时 `cfgOf(S.scope)` 返回的是 global 对象,一拖就改到全局去了。
`syncOverlay` 里已加提前 return。同理 `ensureOwn()` 是「任何写操作前」的必调项。
回归(`?dump=layers`):加层后 `[null,null]`、行内下拉 2 个、换第 2 层后 `[null,blue]` 且 CSS 只出现 blue;
box 页 inherit 时 `addDisabled/bgDisabled` 都 false;在 box 加层后本页 3 层而**全局仍 2 层**;
新建后 assets 0、层 0、草稿已删、scope 回 global。
## 17:5x 生成器:视口自适应 + 导出画质
用户两个问题:插入素材后不同屏幕被裁、导出的 webp 太大。都在 `dev/theme-builder/index.html` 里解决。
**尺寸单位 4 档**(`l.unit`):`fit`(按视口高,人物)/ `fitw`(按视口宽,横幅)/ `px` / `vh`。
输出 `auto clamp(min(calc(100vh - 24px), 231px), 46.67vh, 420px)` 这种。
- 关键点 1:**内层 `min(视口 - 24px, 基准×55%)`** —— 下限让位给视口,自适应单位因此永不溢出。
写死 55% 下限的话,390 竖屏放不下 1067 宽的图,照样裁。
- 关键点 2:**必须有 `fitw`**。`fit` 只按高度缩,宽幅素材宽度按比例涨:1920×1080 素材 h=600 在
390 视口下 fit 出 1001px 宽(裁),fitw 出 366px(完整)。实测见 `?dump=fit` 的 `fitw` 数组。
- 关键点 3:`layerSize()`(出 CSS)与 `unitGeom()`(出预览框/溢出检测)**必须共用一套换算**,
两套各算迟早漂成「框没超、图已裁」。
- ⚠️ 下拉 value 要用 `l.unit`,不能用 `unitOf()`(把 fit 归一成 vh → 选了 fit 却显示「按视口高比例」)。
**视口预设带高度**(`VIEWPORTS` 改成 `{w,h,label}`,加了 390×844)。原来只有宽度、iframe 恒 900 高,
768 的旧笔电和 844 手机根本模拟不出来 —— 而裁切几乎全由高度决定。
**溢出检测 + 一键修好**:`layerMetrics(l,a,VW,VH)` 给 overL/R/T/B,图层编辑区显示
「1512×900 下显示 131×420,完整可见」或橙色「⚠ 左右各 88px 越界」+「按当前视口修好」按钮
(切 px 并算刚好放下的尺寸)。导出前 `overflowReport()` 再扫一遍,只提醒不拦。
**窄屏每层策略** `l.mob`(inherit/scale/hide)+ 全局 `S.narrow.scale`(默认 0.62)。
⚠️ 顺手修了一个既有 bug:`@media` 块原来只写 `html[data-theme-style]`(0,1,1),
被页面覆盖块 (0,2,1) 压住 → **独立配置的页面在手机上从来没生效过**。现在每个独立 scope 各写一条。
⚠️ 竖版图只替换 `cfg.bg` 非空的页面(否则给纯色页塞图 = 改了他的设计)。
**背景焦点** `cfg.bgPos` → `--tb-page-position`;base.css 里 `var(--tb-page-position, center center)` 兜底
(旧主题不写也不坏),StaticVersion → `20261020f`。
**导出画质**:`S.export = {quality: 0.88, maxSide: 1600}`,改完全量 `rebakeAll()`(从 orig 重算,不叠加压缩)。
- ⚠️ **`toDataURL('image/webp', 1)` 是坑**:Skia 在 q=1 切无损,输出反常 —— 实测同一张噪声图
q=1 只有 52KB、q=0.95 是 440KB。`canvasAsset` 与滑块都夹到 0.95。
- ⚠️ 平涂/线稿降 q 可能**变大**(有损引入噪点;实测平涂图各档都是 1KB,噪声图 0.6→0.95 涨 75%)。
所以文案写「先砍长边再降质量」,不承诺「降 q 一定更小」。
- 缩小加 `imageSmoothingQuality='high'`。
**自检**:新增 `?dump=fit`(视口占用/溢出/fitw/旧工程兼容/@media 覆盖各 scope)与
`?dump=quality`(q 单调性、q=1 被夹、长边真压像素),两项 `ok:true`;`?dump=validate` 空、layers/fx/fxpipe 全绿。
生成 CSS 临时存成 `static/css/themes/gentest.css` 跑 `go test ./internal/theme` → 30 处声明全过(`clamp` 里的逗号没被当层分隔符)。
⚠️ 顺带发现 **`//go:embed` 的 `*` 不匹配 `_` 开头文件** —— `_scaffold.css` 与 `_gentest.css` 都不进 embed,
所以那个校验一开始"跑了但数字没变",改名后才真正生效。
UI 踩坑:面板仅 ~240px,`input[type=range]` 有固有宽度 → 必须 `flex:1 1 0;width:0;min-width:0`,
且质量数值要放进 label 里(作第三个 flex 项会被顶出可视区,表现为"滑块没有数字")。
## 18:2x 排查用户部署的 test01:窄屏素材消失 + 主题名显示成 key
用户自己导出 test01 部署到 box-dev-api(admin6),报两个问题。**两个都复现了,都不是"环境坏了"。**
**测试环境体检(都正常)**:`/_/admin6` 200、`test01.css` 200 且与本地 md5 一致、
`base.css?v=20261020f`(新版,含 `--tb-page-position`)、`<html data-theme-style="test01" data-page-scope="box">`。
部署本身是干净的。
**问题 1:窄屏素材消失** = 工程里「窄屏隐藏全部装饰层」勾着 → 导出 CSS 的 `@media` 把 10 槽全写 `none`。
桌面完全不受影响,所以只看桌面发现不了。定位过程绕了两步弯路,记下来:
- 先怀疑 CSS 表达式非法(`clamp(min(calc(...)),…)`)→ 最小复现页证明**表达式没问题**,图能显示。
- 再怀疑层级:其实素材位置 `calc(50% - 0.67vh)` 正好在**卡片正下方**,卡片 `rgba(255,255,255,0.86)`
只剩 14% 可见,本来就非常淡。
- ⚠️ **本地 preview 快照没有 `data-page-scope`**(TestGenPreview 不走 Contexter),所以
`[data-page-scope="question"]` 覆盖块在快照里根本不生效,读到的是 global 的 `--tb-deco: none`。
本地验证主题必须手动给 `<html>` 补 `data-page-scope`,否则会误判成"主题没生效"。
- 修:删掉 test01.css 的 @media 段(装饰是 fit 单位,浏览器自己会缩)。390 截图确认素材回来了。
- 生成器侧:勾选时橙色警告(几层会消失)+ 图层行标「窄屏隐藏」+ 加层/导出提醒 +
「窄屏」下拉被覆盖时标注;新增 `?narrow=hide` 复现。
**问题 2:设置页显示 `theme.name.test01`** = 模板直接 `{{t .LangCode (printf "theme.name.%s" .Slug)}}`,
`i18n.T` 缺词条返回 key。修:`theme.DisplayName(lang, slug)` 缺词条回退 slug,模板函数 `themeName`。
加了 `TestDisplayName`(自建主题必须回退 slug,不能返回 key)。这样自建主题依旧零改代码。
StaticVersion → `20261020g`(主题 css 内容变了,不换缓存键浏览器 1 天内还是旧文件)。