「传上去」之后还得知道「传得对不对」:同一 slug 传过三次、换过 slug 旧图还在、 重新导出后远端仍是旧版,这些在页面上都看不出来。 - serve.py:s3_request 支持 query;新增 ListObjectsV2(含分页)与 DeleteObject, 端点 /api/s3/list、/api/s3/delete。 - index.html:远端素材清单按对象键名跟本地比 —— 一致 / 旧图(同名但大小不同)/ 多余(当前主题已不引用)/ 缺失。「补齐差异」只传缺的与旧图,「删除多余」批量删, 单行可 ↻ 覆盖或 ✕ 删除;删除走 confirm 二次确认。改过 slug 会提示重新检测。 - 修:list 返回完整键而 upload/delete 收相对键,不剥前缀会删到不存在的键 (S3 回 204,看起来删成功其实没删)。加 relKey() 统一。 - check_sigv4.py 补带 query 的用例(ListObjectsV2 三参数、乱序 token)→ 8/8; selftest_upload.py 补列表/分页/幂等删除 → 20 项全过。
6.9 KiB
6.9 KiB
主题生成器 dev/theme-builder/(长版明细)
主题生成器 dev/theme-builder/
serve.py起静态服务(8777),iframe 加载outputs/preview-*.html。file:// 下父页读不到 iframe DOM,必须走 HTTP。 8777 常由用户自己起着,改完直接刷新。- 单文件
index.html(约 2900 行)。图片处理非破坏:orig存原始字节 +fx存参数,每次从 orig 重算; 参数没变则跳过重编码。画质(S.export.quality/maxSide)同理 —— 改完rebakeAll()从 orig 重来,不叠加压缩。 - 尺寸单位 4 档:
fit(按视口高,人物)/fitw(按视口宽,横幅)/px/vh。layerSize()出 CSS、unitGeom()出预览框,两处必须共用同一函数,否则「框没超、图已裁」。 ⚠️ 下拉的 value 用l.unit,不能用unitOf(l)(后者把 fit 归一成 vh → 选了 fit 却显示 vh)。 - 溢出检测
layerMetrics(l, a, VW, VH):算 left/top/overL/R/T/B,编辑器里显示「显示 131×420,完整可见」或 「⚠ 左右各 88px 越界」,并给「按当前视口修好」按钮(切 px + 算刚好放下的尺寸)。导出前overflowReport()再扫一遍。 - 视口预设带高度(1512×900 / 1280×800 / 1024×768 / 820×1180 / 390×844):裁切几乎全由高度决定,
只改宽度模拟不出来。
fitStage()按预设高渲染 iframe。 - 窄屏:全局
hideDeco(急停)> 每层mob(inherit/scale/hide)> 尺寸单位(fit/fitw 自己就缩)。 ⚠️hideDeco会把 @media 里 10 槽全写 none —— 桌面完全正常、手机上素材全没了, 这是「素材在窄屏消失」的头号来源(test01 就是这么中招的)。UI 已在勾选时给橙色警告、 图层行标「窄屏隐藏」、加层/导出再提醒;?narrow=hide可一键复现。 ⚠️@media里每个独立配置的 scope 都要写一条[data-page-scope="x"]:页面覆盖块特异性 (0,2,1), 只写html[data-theme-style](0,1,1) 的话独立配置页在手机上完全不生效。 ⚠️ 竖版图只替换本来有背景图的页面(cfg.bg非空),否则等于给纯色页塞了张图。 - ⚠️ 派生文字色必须走对比度下限,不能按固定比例混:
paletteVars里text-secondary = mixToContrast(text, backdrops, 5.5)、text-muted = 4.5(WCAG AA)。 旧的mix(text,bg,0.55)在浅底只有 2.9:1 —— 每导一次主题就把 base.css 的 AA 修复覆盖回去。- 半透明卡片(
--tb-surface: rgba(255,255,255,0.86))必须flatten()压到底色再算,否则按纯白算偏低; - 参考背景取「页面底色 + 压平后的卡片」两者更不利的那个;
- 手填正文本身不达标时原样返回(宁可不够淡,不能产出看不见的字)。
- 配色面板有读数(正文/次要/淡文字/主色),<4.5 变橙;默认 primary 已压到
#3d6fac(4.63)。 - 还要导出
--tb-on-primary(白字#ffffff/ 深字#0b1016里挑对比度更高的): 深色模式主色偏亮,白字只有 2.2~3.1:1。主色卡中间调时两种字都不够 → 面板橙色提示 +?dump=palette的warnings,不判失败(是主色要调,不是工具挑错)。
- 半透明卡片(
- 自检钩子:
?dump=css|zip|validate|install|fx|fxpipe|fxkey|layers|fit|quality|palette,?demo=1&fresh=1&rows=N&scope=&loadurl=(?demo=1已含 fit 层 + 竖版背景,@media才会生成)。用法:chrome --headless=old --virtual-time-budget=12000 --dump-dom "http://127.0.0.1:8777/dev/theme-builder/index.html?demo=1&fresh=1&dump=fit"抓<pre id="dump">(正则要带属性:<pre id="dump"[^>]*>)。 - ⚠️ canvas
toDataURL('image/webp', 1)不能用:Skia 在 q=1 切无损,输出反常(噪声图 q=1 只 52KB, q=0.95 是 440KB)。canvasAsset夹到 0.95,滑块上限也是 0.95。 - ⚠️ 平涂/线稿降 q 可能变大(有损引入噪点)。压体积先砍长边,再降 q。缩小走
imageSmoothingQuality='high'。 - ⚠️ 面板只有 ~240px:
input[type=range]有固有宽度,必须flex:1 1 0;width:0;min-width:0, 且数值放进 label 里(第三个小 flex 项会被顶出可视区)。 ?dump=css的产物要过 Go 侧校验:临时存成static/css/themes/<非下划线名>.css再go test ./internal/theme, 跑完删掉。- 导出面板「素材前缀」
S.assetBase:空 =/static/images/themes/<slug>/;填了 = CSS 写绝对 URL (R2/OSS)。图片地址只能由urlOf()决定 ——layerCSS曾复制过一份拼接,改一处会出现 「背景走 CDN、装饰还指本地」。文档见docs/theme-assets-cdn.md。 - 素材托管(2026-10-21):面板两选一
S.host.mode = local|s3,字段 endpoint/region/bucket/prefix/pathStyle/publicBase 进工程,accessKey/secretKey 只进 sessionStorage(SEC_SESSION_KEY),绝不进草稿/工程 json/导出 zip(有断言)。publicBaseOf()手填优先,空则按 endpoint 推导(path 风格<ep>/<桶>/<前缀>)—— 对 R2 这个地址不可匿名读,测试的「匿名可读」只给 warn 不拦。 - 上传走
serve.py代理,浏览器不直连对象存储:不用配 CORS,密钥不发给第三方。 端点/api/s3/ping(探测能力,python -m http.server没有 → 按钮禁用)、/api/s3/test、/api/s3/upload。上传集合与导出共用usedAssetIds()。 - ⚠️
usedAssetIds()返回 Set:!ids.length恒真、ids.map不存在 → 上传永远报 「还没有素材」。用[...usedAssetIds()]展开。 - ⚠️ SigV4 手写,必须对拍:
dev/theme-builder/check_sigv4.py调dev/sigv4ref(aws-sdk-go-v2 的signer.SignHTTP)逐字节比。6/6 一致才可信。- 只用 ASCII 用例:裸
SignHTTP默认二次转义 URI,而 S3 client 内部把DisableURIPathEscaping设成 true(单次编码)。我们跟随 S3 client / AWS 文档 RFC3986。 - canonical headers 三件套
host;x-amz-content-sha256;x-amz-date,与 SDK 相同; CanonicalHeaders 自身以\n结尾,故其后与 SignedHeaders 之间有一个空行(规范如此)。
- 只用 ASCII 用例:裸
- ⚠️ HEAD 桶失败拿不到
<Code>(HEAD 响应无 body)→ 只能按状态码归因 (403→AccessDenied / 404→NoSuchBucket / 301|307→PermanentRedirect)。想拿真错误码要靠 PUT。 - ⚠️ Windows 上 8777 可能被上一个进程继续占着(SO_REUSEADDR 让新进程 bind 成功但抢不到
accept)。症状:
/api/s3/ping返回旧版 404,而代码明明改了。用Get-NetTCPConnection -LocalPort 8777查 PID,或换端口验证。 - 自测:
python dev/theme-builder/check_sigv4.py(签名对拍)、python dev/theme-builder/selftest_upload.py(假 S3,16 项)、python dev/theme-builder/mock_s3.py --port 9900(手动点面板)。