From 4bd600fb46dfd9b64901edd40003169e20478fd4 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Tue, 20 Oct 2026 17:51:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BB=E9=A2=98=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=EF=BC=9A=E7=B4=A0=E6=9D=90=E5=B0=BA=E5=AF=B8=E6=8C=89=E8=A7=86?= =?UTF-8?q?=E5=8F=A3=E8=87=AA=E9=80=82=E5=BA=94=20+=20=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E7=94=BB=E8=B4=A8=E5=8F=AF=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 尺寸单位 4 档:fit(按高)/fitw(按宽)/px/vh,输出 auto clamp(min(calc(100vh-24px),55%),基准,上限),下限让位视口 → 自适应单位永不把图撑出屏幕 - fitw 补上宽幅素材:fit 只按高度缩,16:9 素材在 390 竖屏仍出 1001px 宽(照裁) - 视口预设带高度(含 390×844);layerMetrics 统一出 CSS/预览框/溢出检测,图层编辑区显示实测尺寸并报越界,可一键按当前视口修好 - 窄屏每层策略(原样/缩小/隐藏)+ 缩放系数;修 @media 只覆盖全局、独立配置页面在手机上从不生效的特异性 bug - 导出画质:质量滑块(上限 0.95,Skia 在 q=1 切无损会反常)+ 长边上限,改动后从原图重算 - base.css 新增 --tb-page-position(默认 center center,旧主题不受影响),StaticVersion +1 - 新增 ?dump=fit / ?dump=quality 自检,README 与 _scaffold.css 同步 --- .workbuddy/memory/2026-10-20.md | 45 ++ .workbuddy/memory/MEMORY.md | 222 +++++----- dev/theme-builder/README.md | 91 +++- dev/theme-builder/index.html | 724 ++++++++++++++++++++++++++++---- internal/conf/static.go | 2 +- static/css/base.css | 3 +- static/css/themes/_scaffold.css | 11 +- 7 files changed, 906 insertions(+), 192 deletions(-) diff --git a/.workbuddy/memory/2026-10-20.md b/.workbuddy/memory/2026-10-20.md index cbd3569..26b225a 100644 --- a/.workbuddy/memory/2026-10-20.md +++ b/.workbuddy/memory/2026-10-20.md @@ -217,3 +217,48 @@ iframe 内部仍是 1512,媒体查询按 1512 判定 → 结论是错的。 回归(`?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 项会被顶出可视区,表现为"滑块没有数字")。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 6607b86..d92cc74 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -3,63 +3,65 @@ ## 协作 - 结论分级 P0/P1/P2;给建议前先根因取证 + 实测复现,别只静态推断。 -- 改代码前先出变更清单获批准;用户常授权自主推进,但重大问题仍要显式指出。 -- 用户是运营者也是圈内人:处置功能必须可解释、可留痕、可申诉,不做自动级联封号。 +- 改代码前先出变更清单;重大问题显式指出。用户常授权自主推进。 +- 用户是运营者也是圈内人:处置必须可解释、可留痕、可申诉,不做自动级联封号。 ## 构建与测试 -- **无 libwebp/GCC**,cgo 的 `internal/storage/s3.go` 挡编译 → overlay 换桩 `dev/s3stub/s3_stub.go`: - `-overlay="C:/Users/Tamakyi Gekka/AppData/Local/Temp/tb-webpcheck/overlay.json"`。 - overlay 换不了 GOMODCACHE 里的文件,**要检查 s3.go 本身得 tar 复制项目 + `go mod edit -replace`**。 -- **「编译卡死」= `proxy.golang.org` 不可达**。每条 go 命令前: - `export GOPROXY=https://goproxy.cn,direct GOSUMDB=off GOFLAGS=-mod=mod`(`go env -w` 在本沙箱失败)。 - 1C1G 是正式生产环境,**别用「沙箱慢」解释超时**。 +- **无 libwebp/GCC**:cgo 的 `internal/storage/s3.go` 挡编译 → overlay 换桩 + `dev/s3stub/s3_stub.go`(`-overlay="C:/Users/Tamakyi Gekka/AppData/Local/Temp/tb-webpcheck/overlay.json"`)。 + 要检查 s3.go 本身得 tar 复制项目 + `go mod edit -replace`(overlay 换不了 GOMODCACHE)。 +- **「编译卡死」= `proxy.golang.org` 不可达**。每条 go 命令前 + `export GOPROXY=https://goproxy.cn,direct GOSUMDB=off GOFLAGS=-mod=mod`(`go env -w` 沙箱失败)。 + 1C1G 是正式生产环境,别用「沙箱慢」解释超时。 - DB 测试:`DB_TYPE=postgres DB_USER=postgres DB_PASSWORD=15207786347hXPm DB_HOST=127.0.0.1 DB_PORT=5432` - (DSN 不带库名,`NewTestDB` 自建临时库后 DROP)。带 DB 的 `go test ./...` 目前全绿。 -- store 方法内用 `NewXStore(db.DB)`,别引用包级单例(测试下 nil panic;`FeatureStateOf` 例外,有守卫)。 -- ⚠️ **本沙箱起不了 `TamaBox web`**(session 固定走 redis,6379 未开,启动即 panic)→ HTTP 端到端只能部署后做。 + (DSN 不带库名,`NewTestDB` 自建临时库后 DROP)。带 DB 的 `go test ./...` 全绿。 +- store 方法内用 `NewXStore(db.DB)`,别引用包级单例(测试下 nil panic;`FeatureStateOf` 有守卫)。 +- ⚠️ **本沙箱起不了 `TamaBox web`**(session 固定 redis,6379 未开,启动 panic)→ HTTP 端到端只能部署后做。 探测端口 200 可能是别的进程,必须看日志确认 `Starting web server` 且无 panic。 -- ⚠️ **本机 python urllib 不稳定**(代理下偶发读超时,curl 正常),曾误判「接口卡死」。线上验证优先 curl; - python 别用 `/tmp`(Windows 解析成 `C:\tmp`,与 MSYS `/tmp` 不通)。 +- ⚠️ **本机 python urllib 不稳定**(代理下偶发读超时,curl 正常);python 别用 `/tmp` + (Windows 解析成 `C:\tmp`,与 MSYS 不通)。PIL 在 `~/.workbuddy/binaries/python/envs/default`。 -## 行尾 / 模板 +## 行尾 / 模板 / embed - 行尾混合不可推断(`internal/db/*`、`mail.go`、`route/user/profile.go` 是 LF;`internal/route/route.go`、 - `users.go`、`i18n/dict_*.go`、`route/question/*.go`、`question-list.html` 是 CRLF)。改前用 Python 数 `b'\r\n'`; - `gofmt -l` 会把 CRLF 误报为未格式化。格式化走 `dev/gofmt_keep_crlf.py`。 -- 往 gofmt 对齐组中间插键会污染 diff,新键插组末;注释别用 `''`(gofmt 会改成 `”`)。 + `users.go`、`i18n/dict_*.go`、`route/question/*.go`、`base.css`、`dev/theme-builder/*` 是 CRLF)。 + 改前用 Python 数 `b'\r\n'`;`gofmt -l` 会把 CRLF 误报为未格式化 → 走 `dev/gofmt_keep_crlf.py`。 + Edit 工具会保留原文件行尾,可放心用;新增整份文件用 Write 时要自己补 CRLF。 +- 往 gofmt 对齐组中间插键会污染 diff,新键插组末;注释别用 `''`(gofmt 改成 `”`)。 - ⚠️ **`templates/embed.go` 是显式清单**:新增**顶层**模板必须加进 `//go:embed`,否则运行时 - `html/template: "xxx" is undefined` → 500,编译期无提示(`checktpl` 也只查语法)。子目录已被目录项覆盖。 + `html/template: "xxx" is undefined` → 500,编译期无提示。`static/fs.go` 同理。 +- ⚠️ **`//go:embed` 的 `*` 不匹配 `_` / `.` 开头的文件**:`static/css/themes/_scaffold.css` 与 + 临时建的 `_gentest.css` **都不进 embed**,`TestDecoSlotCount` 扫不到它们(表现是"校验了但数字没变")。 + 验证生成的主题 CSS 要临时改成非下划线文件名。 - `templates/user/moderation-strictness.html` 只用 shell/python 改(Edit/Write 会注入 `data-page-node-id`)。 -## 审核 / 提示 / 处置 +## 审核 / 处置 / 举报 - 线上通道 = 七牛(`ai.qiniuapi.com/v3/text/censor`,antispam);`[security] text_censor_source = qiniu|aliyun|auto`。 七牛只硬拦涉政/暴恐/色情,`review` 放行 → 裁决在 `censor/policy.go` + `moderation.Check`。 -- **新增审核调用点必须走 `moderation.Check`**(直调 `censor.Text` 会绕过档位)。不可用时 **fail-closed**。 -- ⚠️ **圈内黑话(代称+群体标签+避雷叙述)任何通用审核都拦不住**,别指望词库;靠处置 + 举报 + 人工判断。 -- 严格度三档 lax/standard/strict,存 `users.moderation_strictness`。**受处置用户强制 strict**: - `moderation.EffectiveStrictness(ctx, userID, raw)`(7 个入口已全部改用);**查询失败时回退用户设置** - (否则 DB 抖动会让全站突然加严)。处置期内 `UpdateModerationStrictness` 拒绝保存 + 设置页 disabled。 -- ⚠️ **同请求渲染只能用 `ctx.SetError` / `SetInternalError`,不能用 `*Flash`**:flash 写 session,下一请求才读 → - 提示静默丢失(「点了没反应」)。`Set*Flash + Redirect` 是正常模式。 - 扫描法:找 flash 调用 → 向下第一个 `Success(`/`Redirect(` → 前者即缺陷。 -- 处置四级:L1 停止分发(禁分享卡 + 首页/探索/标签/随机位摘除)/L2 收窄公开(新回答强制私密、禁 set-public)/ - L3 冻结发布(仍可收信)/L4 整页停用(`/restricted`)。到期靠 `expires_at` 自动失效,**无 cron**; - `restrict` 是**替换语义**;级别比较用 `RestrictionAtLeast`。 -- ⚠️ L2 强制私密必须在**审核通过之后**,且与写回答**同一条 UPDATE**(`AnswerByIDPrivate`):拆开会留下 - 「公开+已作答」窗口,第二条失败还会变成「私密+无回答」。 +- **新增审核调用点必须走 `moderation.Check`**(直调 `censor.Text` 绕过档位)。不可用时 **fail-closed**。 +- ⚠️ 圈内黑话(代称+群体标签+避雷叙述)通用审核拦不住,别指望词库;靠处置 + 举报 + 人工判断。 +- 严格度 lax/standard/strict 存 `users.moderation_strictness`;**受处置用户强制 strict**: + `moderation.EffectiveStrictness(ctx, userID, raw)`(7 个入口已改)。**查询失败回退用户设置** + (否则 DB 抖动会让全站突然加严)。处置期内拒绝保存 + 设置页 disabled。 +- ⚠️ **同请求渲染只能用 `ctx.SetError` / `SetInternalError`,不能用 `*Flash`**:flash 写 session,下次请求才读 → + 提示静默丢失。`Set*Flash + Redirect` 才是正常模式。扫描法:找 flash 调用 → 向下第一个 `Success(`/`Redirect(`。 +- 处置四级:L1 停止分发/L2 收窄公开(新回答强制私密、禁 set-public)/L3 冻结发布(仍可收信)/ + L4 整页停用(`/restricted`)。到期靠 `expires_at`,**无 cron**;`restrict` 是**替换语义**;比较用 `RestrictionAtLeast`。 +- ⚠️ L2 强制私密必须在**审核通过之后**,且与写回答**同一条 UPDATE**(`AnswerByIDPrivate`): + 拆开会留下「公开+已作答」窗口,第二条失败还会变成「私密+无回答」。 - ⚠️ **`ListAPI`(`/api/v1/user/{domain}/questions`)不经过 Pager 中间件**,L4 与页面密码必须自己校验。 - ⚠️ **整箱分享卡 `ShareCard` 与单题分享卡都要拦**,且拦截必须在缓存查询之前(否则 30 分钟内仍给旧图)。 -- 举报 `POST /_/{domain}/{qid}/report`:公开免登录 + 验证码 + 限流;同 IP 同题唯一索引 → +- 举报 `POST /_/{domain}/{qid}/report`:免登录 + 验证码 + 限流;同 IP 同题唯一索引 → **幂等且提示与首次一致**(否则成了内容存在性探测器)。箱主只看「被举报 N 次」,**不给 IP**。 -- 闭环:箱主删除/设私密或 `hide-public` → `MarkAcceptedByQuestion`;待处理超 30 天视为过期。 - ⚠️ `hide-public` 里必须**先快照目标 ID 再更新**(更新后用带 `is_private=false` 的子查询恒为空集)。 -- 告警:24h≥8 或 7d≥20 → 邮件到 `[security] security_alert_email`(24h 冷却,进程内);单条恰好 3 人举报 → 提醒箱主。 +- 闭环:删除/设私密/`hide-public` → `MarkAcceptedByQuestion`;待处理超 30 天视为过期。 + ⚠️ `hide-public` 里必须**先快照目标 ID 再更新**(更新后带 `is_private=false` 的子查询恒为空集)。 +- 告警:24h≥8 或 7d≥20 → 邮件到 `[security] security_alert_email`(24h 冷却);单条恰好 3 人举报 → 提醒箱主。 ⚠️ 该邮箱为空时静默跳过、无日志;改配置必须重启。 - `/rules` 是处置依据页(四语言);注册页必须勾 `agree_rules`(服务端同样校验),改 e2e 脚本要同步。 -- `restrict`/`unrestrict` 默认给箱主发通知(按 `users.language`,模板 `mail/restriction-notice.html`), - **只含级别名/到期/规则链接,不含 `--reason`**;`--no-notify` 跳过。`--days > 90` 必须加 `--force`。 +- `restrict`/`unrestrict` 默认给箱主发通知(按 `users.language`,`mail/restriction-notice.html`), + **只含级别名/到期/规则链接,不含 `--reason`**;`--no-notify` 跳过。`--days > 90` 要 `--force`。 ⚠️ **到期自动解除不发信**(读表比较时间)。 ## GORM @@ -72,103 +74,113 @@ ## 前端样式 -- `static/css/base.css`(`?v=YYYYMMDDx`,**改完必须 +1**,静态缓存 1 天)。令牌在 `:root`(亮)+ +- `static/css/base.css`(`?v=` 取 `conf.StaticVersion`,**改完必须 +1**)。令牌在 `:root`(亮)+ `html[data-theme="dark"]`(暗)。新样式一律 `var(--tb-*)`,别写死色值,也别为深色另写分支。 -- 页面级样式内联在各自模板 `