Files
tamabox/.workbuddy/memory/MEMORY.md
T
tamakyi 4bd600fb46 主题生成器:素材尺寸按视口自适应 + 导出画质可调
- 尺寸单位 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 同步
2026-10-20 17:51:31 +08:00

18 KiB
Raw Blame History

TamaBox-v4 长期笔记

协作

  • 结论分级 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")。 要检查 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 端到端只能部署后做。 探测端口 200 可能是别的进程,必须看日志确认 Starting web server 且无 panic。
  • ⚠️ 本机 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、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,编译期无提示。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 抖动会让全站突然加严)。处置期内拒绝保存 + 设置页 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 同题唯一索引 → 幂等且提示与首次一致(否则成了内容存在性探测器)。箱主只看「被举报 N 次」,不给 IP。
  • 闭环:删除/设私密/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。 ⚠️ 到期自动解除不发信(读表比较时间)。

GORM

  • 默认值必须带引号;default: 会把零值替换,空串有语义的字段别加。
  • ⚠️ 事务里复用同一 tx 会串条件,每句 tx.WithContext(ctx).<链式>。
  • ⚠️ 软删除 + 唯一索引 = 槽位永久占用:停用(可恢复)= 物理删内容 + 软删用户行,注册侧 Unscoped 给 ErrDeactivatedEmail/Domain;封禁 = 物理删用户行,邮箱释放。级联删除一律 Unscoped()。
  • ⚠️ GORM 链式对象 Count/Find 后带状态,统计/预览/更新必须各自重建 query。

前端样式

  • static/css/base.css(?v= 取 conf.StaticVersion,改完必须 +1)。令牌在 :root(亮)+ html[data-theme="dark"](暗)。新样式一律 var(--tb-*),别写死色值,也别为深色另写分支。
  • 页面级样式内联在各自模板 <style>;尽量用类不用 style=(深色下内联写死色值会露白)。
  • 主题属性在 head 内联脚本里打(cookie theme),不能挪回 body,否则深色用户白闪。
  • 移动端(≤640px)语言切换与主题按钮在固定底栏 #tb-mobile-utils(.tb-nav-utils 被隐藏, body 留 padding-bottom:56px)。用户曾误以为"按钮不见了",先解释位置。 ⚠️ /_/{domain} 箱主页 IsUserPage = true(route/question/page.go:133),语言切换包在 {{ if not .IsUserPage }} 里 → 箱主页没有语言切换入口,有意设计(用户 2026-10-19 确认),别当 bug 修。
  • ⚠️ UIkit 3.3.3 的 .uk-flex::before/::after{display:none}:给带 uk-flex 的元素加伪元素必须写 复合选择器(.tb-cover.uk-flex::after),否则静默失效。
  • 分类/题型标签一律 .tb-chip;彩色 uk-label 只留给语义状态(置顶/私密/被举报/已读、投票四态)。
  • 视觉自检:TEST_PREVIEW=1 go test ./templates -run TestGenPreview → outputs/preview-*.html; 截图必须 chrome --headless=old --force-device-scale-factor=1(新版 headless 本机挂住)。 ⚠️ --window-size ≠ CSS 视口;要精确视口就用 iframe 指定 width/height。验证 CSS 靠像素比对 (PIL 在 venv)而非目测;扫描循环上界不能与物体边界重合。

主题(箱主个性化)

  • 三个注入点:/_/{domain}/* → question.Pager 按 pageUser.Theme;/user/* → internal/context Contexter 按 c.User.Theme(isThemeScope() 只认 /user 与 /user/);/vote/{token} → route/vote.Page 按发布者 owner.Theme。都看箱主/发布者,不看访客。全站页(/、/explore、/login)不主题化。
  • 投票页语言也跟发布者:applyOwnerLocale(data, owner)(与主题同一函数),必须在任何 i18n 取词之前调用。
  • 一套主题 = static/css/themes/<slug>.css,只覆盖令牌,不碰布局。static/fs.go 已嵌整个 themes/ 目录。
  • ⚠️ [theme] available 是硬白名单,启动时读一次:css 发布了但没登记,设置页看不到,现象同「部署没生效」。 排查:curl /static/css/themes/<slug>.css 200 = 文件在(没登记);404 = 旧二进制。
  • ⚠️ 主题 css 必须成对写:html[data-theme-style="x"](0,1,1) + html[data-theme="dark"][data-theme-style="x"](0,2,1)。 TestThemeCSSComplete 查两个选择器 + 10 个颜色令牌。
  • ⚠️ 按页面作用域:internal/context.pageScope() → <html data-page-scope>,取值 box/question/admin(/user/questions,/user/votes*)/profile/vote。深色作用域覆盖必须三重选择器, 否则被浅色作用域块(同特异性、写在后面)盖掉。
  • ⚠️ 装饰层固定 10 槽(base.css 12 层 = pattern1 + deco10 + image1),三组 --tb-deco/-size/-position 必须等长。TestDecoSlotCount 逐文件逐声明检查(只扫 embed 到的文件)。
  • 位置一律两值 calc(0%|50%|100% ± N)(百分比相对「容器 − 图片」)。别用 left/bottom 关键字混搭: 三值语法非法时整条声明作废 → 图层静默回左上。
  • 尺寸想随视口缩用 clamp,不要写死 px:auto clamp(min(calc(100vh - 24px), 231px), 46.67vh, 420px)。 内层 min() 是兜底 —— 屏幕小到连下限都放不下时让位给视口。宽幅素材只能按宽度缩(vw), 按高度缩的话宽度按比例涨,窄屏照样裁。
  • ⚠️ UIkit 3.3.3 主色是编译期常量 #1e87f0,令牌注入不进去。base.css 末尾按组件映射(a/uk-link、 uk-button-primary、checkbox/radio、input:focus、uk-tab active、.uk-progress、.uk-label:not(...))。
  • 令牌 --tb-navbar(顶栏底色,默认 var(--tb-surface)):页面底色偏彩时主题必须覆盖。
  • 静态资源版本号统一 conf.StaticVersion;图片不走这个缓存键(max-age=86400),换图要给 URL 带 ?v=。
  • 整页背景 --tb-page-image(cover)/ --tb-page-pattern(平铺,-size 控尺寸)/ --tb-page-position (焦点,默认 var(--tb-page-position, center center),旧主题不写也不坏)。底色用纯色别用渐变; 图案 ≥24px、透明度 ≤0.06。
  • ⚠️ 多背景层不能塞变量列表:变量自身是逗号列表会让层数与 size/position 错位。多张装饰用固定槽位。
  • 装饰摆位:贴边、成组、避开页脚;四角各摆一个会读成贴纸墙纸;页脚文案右对齐 → 右下角不能放人物。 --tb-deco 是 fixed 视口层,贴边插画别放进 cover 背景图(必裁边)。
  • 主题视觉的结构比摆位重要:通行做法是「整屏场景插画 + UI 悬浮其上」(蔚蓝档案 JP)。 白底留白里摆人物,无论多大都读成贴纸;卡片/顶栏配半透明白 + backdrop-filter 才有"浮在画面上"的关系。
  • 素材放 static/images/themes/<slug>/(embed 已含 images/themes,子目录自动覆盖)。整页图 16:9 + 竖版两张。
  • 人物素材:图生图 + 抠白底(image1 给立绘、input_fidelity: high、flat vector illustration, no outlines)。 ⚠️ 正面立绘直接贴进空场景像贴纸:改侧后方视角(不直视访客),RGB 压白 12~16% 与压白 38% 的场景对齐雾度。
  • ⚠️ 抠白底容差 16:背景 253、白袜子 237 只差 16,容差 22 以上会把袜子吃穿。手内侧/两腿之间是 闭合白区,四边 floodfill 进不去,得按连通域单独掏(面积 ≥24),再 1px 收边。
  • ⚠️ ruleBody 取令牌前要剥 /* */:注释会和后面的令牌粘成一个键,色块预览静默失效。

主题生成器 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 自己就缩)。 ⚠️ @media 里每个独立配置的 scope 都要写一条 [data-page-scope="x"]:页面覆盖块特异性 (0,2,1), 只写 html[data-theme-style](0,1,1) 的话独立配置页在手机上完全不生效。 ⚠️ 竖版图只替换本来有背景图的页面(cfg.bg 非空),否则等于给纯色页塞了张图。
  • 自检钩子:?dump=css|zip|validate|install|fx|fxpipe|fxkey|layers|fit|quality,?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, 跑完删掉。

其它约定

  • 校验靠 valid:"required;maxlen:N" 标签(internal/form/*.go),别在 handler 手写。
  • 终止性操作(停用)落独立公开页 /deactivated(注册在 reqUserSignIn 外);改密码后重登才用 flash + /login。
  • 只有箱主 /user 面板,无管理员角色、无站方后台,用户要求不要加:站方动作只走 CLI / 邮件 / 改配置重启。
  • git https://git.shiroko.one/tamakyi/tamabox(main)。git push 能直接推(本机 Git for Windows 已认证); 「Failed to authenticate user」是临时态。推前 git log --oneline -3。outputs/ 已 gitignore,别整目录 add。
  • 打包交付用 tar.gz(排除 .git/.workbuddy/outputs),放项目同级。

测试环境 box-dev-api.337845818.best

  • 账号 [email protected] / adminadmin,公开页 /_/admin6。
  • 登录要过算数验证码:GET /login 取 _csrf+cookie → GET /api/captcha/generate → 算 a op b → POST /login 带 g-recaptcha-response="<id>:<答案>"。默认语言 ja,四语言文案要一起加。
  • 部署有延迟;模板是编译期 embed,只重启不重编不会变。用户说「已部署但没变化」→ 先抓线上 HTML 比对标记。
  • 表单字段:提问 content,回答 answer。GET /user/votes 故意 302;匿名无邮箱提问成功直接 200 渲染。
  • 自检:dev/devcheck-e2e.py(28 项)、dev/devcheck-restrict.sh(基线非 200 就别往下走;RUN=1 才真跑)。
  • 图片编码三档(头像 512/q78、背景 1920/q80、通用 1600/q80),EXIF 方向必须先归一(转 WebP 会丢)。

CLI(TamaBox <cmd>)

  • ⚠️ 两个构建入口必须同步:根 go build .(tamabox.go)与 Dockerfile 的 ./cmd/(cmd/tamabox.go), 否则 No help topic for '<cmd>'。
  • 现有:web censor uid ban restore reports restrict unrestrict hide-public。
  • restrict --domain|--email --level L1..L4 --days N --reason / unrestrict; reports --domain|--email [--days](证据包)、--pending、--stats --days N;hide-public [--only-reported] [--since N]。
  • 本地冒烟:NEKOBOX_CONFIG_PATH=<ini>;conf.Init 强制 app.external_url、server.salt、server.xsrf_key 非空。