docs(memory): 记录图片处理设计,并压缩 MEMORY.md 去掉过期的 6 槽位说法

This commit is contained in:
tamakyi
2026-10-20 14:53:40 +08:00
parent c3d34b53d2
commit 927f378d9d
2 changed files with 101 additions and 70 deletions
+33
View File
@@ -164,3 +164,36 @@ iframe 内部仍是 1512,媒体查询按 1512 判定 → 结论是错的。
- 拖拽标签写 `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` 贴到画板边缘被压线、半透明填充把人物罩成米色
→ 改框内左上角标签 + 纯虚线(上一轮已修,本轮沿用)。
+68 -70
View File
@@ -10,7 +10,7 @@
- **无 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 把 s3.go 当桩,**要检查 s3.go 本身得 tar 复制项目 + `go mod edit -replace`**(overlay 换不了 GOMODCACHE 文件)。
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 是正式生产环境,**别用「沙箱慢」解释超时**。
@@ -28,11 +28,11 @@
`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 会改成 `”`)。
- ⚠️ **`templates/embed.go` 是显式清单**:新增**顶层**模板必须加进 `//go:embed` 那行,否则运行时
- ⚠️ **`templates/embed.go` 是显式清单**:新增**顶层**模板必须加进 `//go:embed`,否则运行时
`html/template: "xxx" is undefined` → 500,编译期无提示(`checktpl` 也只查语法)。子目录已被目录项覆盖。
- `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`。
@@ -41,34 +41,26 @@
- 严格度三档 lax/standard/strict,存 `users.moderation_strictness`。**受处置用户强制 strict**:
`moderation.EffectiveStrictness(ctx, userID, raw)`(7 个入口已全部改用);**查询失败时回退用户设置**
(否则 DB 抖动会让全站突然加严)。处置期内 `UpdateModerationStrictness` 拒绝保存 + 设置页 disabled。
## 提示(Flash)
- ⚠️ **同请求渲染只能用 `ctx.SetError` / `SetInternalError`,不能用 `*Flash`**:flash 写 session,下一次请求才读 →
提示静默丢失(「点了没反应」)。2026-10-18 清理 9 处;`Set*Flash + Redirect` 是正常模式。
- ⚠️ **同请求渲染只能用 `ctx.SetError` / `SetInternalError`,不能用 `*Flash`**:flash 写 session,下一请求才读 →
提示静默丢失(「点了没反应」)。`Set*Flash + Redirect` 是正常模式。
扫描法:找 flash 调用 → 向下第一个 `Success(`/`Redirect(` → 前者即缺陷。
## 处置与举报
- L1 停止分发(禁分享卡 + 首页/探索/标签/随机位摘除)/L2 收窄公开(新回答强制私密、禁止 set-public)/
- 处置四级:L1 停止分发(禁分享卡 + 首页/探索/标签/随机位摘除)/L2 收窄公开(新回答强制私密、禁 set-public)/
L3 冻结发布(仍可收信)/L4 整页停用(`/restricted`)。到期靠 `expires_at` 自动失效,**无 cron**;
`restrict` 是**替换语义**;级别比较用 `RestrictionAtLeast`。
- ⚠️ L2 强制私密必须在**审核通过之后**,且与写回答**同一条 UPDATE**(`AnswerByIDPrivate`):
拆成「先 SetPrivate 再 Answer」会留下「公开+已作答」窗口,第二条失败还会变成「私密+无回答」。
- ⚠️ **`ListAPI`(`/api/v1/user/{domain}/questions`)不经过 Pager 中间件**,L4 与页面密码必须自己校验
(否则「整页停用」和访问密码都能被 API 绕过)。
- ⚠️ 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` 的子查询恒为空集,曾静默失效)。
⚠️ `hide-public` 里必须**先快照目标 ID 再更新**(更新后用带 `is_private=false` 的子查询恒为空集)。
- 告警:24h≥8 或 7d≥20 → 邮件到 `[security] security_alert_email`(24h 冷却,进程内);单条恰好 3 人举报 → 提醒箱主。
⚠️ 该邮箱为空时静默跳过、无日志;改配置必须重启。
- `/rules` 是处置依据页(四语言);注册页必须勾选 `agree_rules`(服务端同样校验),改 e2e 脚本要同步。
- `restrict` 默认给箱主发处置通知(按 `users.language`,模板 `mail/restriction-notice.html`),
**只含级别名/到期/规则链接,不含 `--reason`**;`--no-notify` 可跳过(L1 静默止血用)。
`--days > 90` 必须加 `--force`,交互模式还要再输一次级别名。`unrestrict` 也发解除通知,双方都有 `--no-notify`。
⚠️ **到期自动解除不发信**(读表比较时间,无调度器)。
- `/rules` 是处置依据页(四语言);注册页必须勾 `agree_rules`(服务端同样校验),改 e2e 脚本要同步。
- `restrict`/`unrestrict` 默认给箱主发通知(按 `users.language`,模板 `mail/restriction-notice.html`),
**只含级别名/到期/规则链接,不含 `--reason`**;`--no-notify` 跳过。`--days > 90` 必须加 `--force`。
⚠️ **到期自动解除不发信**(读表比较时间)。
## GORM
@@ -80,22 +72,23 @@
## 前端样式
- 基础样式 `static/css/base.css`(`/static/css/base.css?v=YYYYMMDDx`,**改完必须 +1**,静态缓存 1 天)。
设计令牌在文件开头 `:root`(亮)+ `html[data-theme="dark"]`(暗)。新样式一律用 `var(--tb-*)`,别写死色值,也别为深色另写分支。
- 页面级样式仍内联在各自模板 `<style>` 里,同样只用令牌;尽量用类不用 `style=`(深色下内联写死色值会露白)。
- 静态文件是**显式 embed 清单**(`static/fs.go`),新增 css/js 必须手动加进 `//go:embed`,否则运行期 404。
- `static/css/base.css`(`?v=YYYYMMDDx`,**改完必须 +1**,静态缓存 1 天)。令牌在 `:root`(亮)+
`html[data-theme="dark"]`(暗)。新样式一律 `var(--tb-*)`,别写死色值,也别为深色另写分支。
- 页面级样式内联在各自模板 `<style>` 里,同样只用令牌;尽量用类不用 `style=`(深色下写死色值会露白)。
- 静态文件是**显式 embed 清单**(`static/fs.go`),新增 css/js 必须加进 `//go:embed`,否则运行期 404。
- 主题属性在 head 内联脚本里打(cookie `theme`),**不能挪回 body**,否则深色用户白闪。
- **移动端(≤640px)语言切换与主题按钮不在顶栏**:`.tb-nav-utils` 被隐藏,改由 `#tb-mobile-utils` 固定底栏承载
(`bottom:0;z-index:1000`,body 留 `padding-bottom:56px`)。用户曾误以为"按钮不见了",先解释位置再排查。
⚠️ `/_/{domain}` 箱主页 `IsUserPage = true`(`route/question/page.go:133`),语言切换包在 `{{ if not .IsUserPage }}` 里,
所以**箱主页没有语言切换入口——有意设计**(用户 2026-10-19 明确:箱主页以箱主配置为准),别当 bug 修。
- **移动端(≤640px)语言切换与主题按钮不在顶栏**:`.tb-nav-utils` 隐藏,由 `#tb-mobile-utils` 固定底栏承载
(`bottom:0;z-index:1000`,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 视口(1080→985)。验证 CSS 靠**像素比对**(PIL 在 `~/.workbuddy/binaries/python/envs/default`)
而非目测;扫描循环上界不能与物体边界重合(曾把边界截断读成"方角")。
⚠️ `--window-size` ≠ CSS 视口(1080→985)。验证 CSS 靠**像素比对**(PIL 在
`~/.workbuddy/binaries/python/envs/default`)而非目测;扫描循环上界不能与物体边界重合
(曾把边界截断读成"方角")。
## 主题(箱主个性化)
@@ -103,68 +96,73 @@
的 Contexter 按 `c.User.Theme`(`isThemeScope()` 只认 `/user` 与 `/user/`);`/vote/{token}` → `route/vote.Page`
按发布者 `owner.Theme`。都看箱主/发布者,不看访客。全站页(`/`、`/explore`、`/login`)不主题化。
- 投票页**语言**也跟发布者:`applyOwnerLocale(data map[string]interface{}, owner)`(与主题同一个函数),
必须在任何 i18n 取词之前调用;箱主未配语言才回退访客/站点默认。签名收 map 是为了能纯内存单测。
- **按页面作用域**:`internal/context.pageScope()` 输出 `<html data-page-scope>`,取值
`box`/`question`/`admin`(/user/questions,/user/votes*)/`profile`/`vote`。主题用
`html[data-theme-style="x"][data-page-scope="y"]` 覆盖整页背景与装饰。⚠️ 深色覆盖必须写
**三重选择器**(加 `[data-theme="dark"]`),否则被浅色作用域块盖掉(同特异性、写在后面)。
没写覆盖块的页面自动回退全局。首页/`/explore` 不加载主题 css。
- ⚠️ **装饰层固定 10 槽**(base.css 12 层 = pattern1 + deco10 + image1),`--tb-deco`/
`-size`/`-position` 三组必须等长。`TestDecoSlotCount` 逐文件逐声明检查;少写一个不报错,
只会让图层与 size/position 错位(整页重复同一张图 / 人物跑到左上角)。
- 位置一律写两值 `calc(0%|50%|100% ± Npx)`(百分比相对「容器 − 图片」)。**别用 left/bottom
关键字混搭**:「水平居中 + 垂直贴底」是三值语法,非法时整条声明作废、图层静默回左上。
- 可视化编辑走 `dev/theme-builder/`(`serve.py` 起服务 + iframe 加载真实页面快照)。
file:// 下父页面读不到 iframe 的 DOM,所以必须走 HTTP;预览快照由
`TEST_PREVIEW=1 go test ./templates -run TestGenPreview` 生成。
必须在任何 i18n 取词之前调用;签名收 map 是为了能纯内存单测。
- 一套主题 = `static/css/themes/<slug>.css`,**只覆盖令牌**,不碰布局。`static/fs.go` 已嵌整个 `themes/` 目录,
新增主题只需放文件 + 登记 `[theme] available`(底稿 `_scaffold.css`)。
- ⚠️ **`[theme] available` 是硬白名单,启动时读一次**:css 发布了但没登记,设置页就是看不到,现象和「部署没生效」一样。
排查分两类:`curl /static/css/themes/<slug>.css` 200 = 文件在(那就是没登记);404 = 旧二进制。
新增只需放文件 + 登记 `[theme] available`(底稿 `_scaffold.css`)。
- ⚠️ **`[theme] available` 是硬白名单,启动时读一次**:css 发布了但没登记,设置页就是看不到,现象和
「部署没生效」一样。排查:`curl /static/css/themes/<slug>.css` 200 = 文件在(没登记);404 = 旧二进制。
启动日志有 `主题注册表已加载 enabled=...` 与 `[theme] available 未登记以下主题`。
- ⚠️ 主题 css **必须成对写**:`html[data-theme-style="x"]`(0,1,1)+ `html[data-theme="dark"][data-theme-style="x"]`(0,2,1)。
只写浅色块会让深色模式被浅色令牌污染。`TestThemeCSSComplete` 查两个选择器 + 10 个颜色令牌。
- ⚠️ 主题 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`。主题用
`html[data-theme-style="x"][data-page-scope="y"]` 覆盖整页背景与装饰。深色覆盖必须写**三重选择器**
(再加 `[data-theme="dark"]`),否则被浅色作用域块盖掉(同特异性、写在后面)。没写覆盖块自动回退全局。
- ⚠️ **装饰层固定 10 槽**(base.css 12 层 = pattern1 + deco10 + image1),`--tb-deco`/`-size`/`-position`
三组必须等长。`TestDecoSlotCount` 逐文件逐声明检查;少写一个不报错,只会让图层与 size/position 错位
(整页重复同一张图 / 人物跑到左上角)。
- 位置一律写两值 `calc(0%|50%|100% ± Npx)`(百分比相对「容器 − 图片」)。**别用 left/bottom 关键字混搭**:
「水平居中 + 垂直贴底」是三值语法,非法时整条声明作废、图层静默回左上。
- ⚠️ **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(danger|warning|success|secondary)`)。
新增会显示主色的组件先确认已映射,否则换主题后「页面变粉、按钮还蓝」。
- 令牌 `--tb-navbar`:顶栏底色,默认 `var(--tb-surface)`。页面底色偏彩时主题必须覆盖,否则顶栏是纯白带压在彩底上割裂。
- 静态资源版本号统一 `conf.StaticVersion`(base.css 与主题 css 共用);**图片不走这个缓存键**(`max-age=86400`),换图要给 URL 带 `?v=`。
- 整页背景 `--tb-page-image`(cover)/ `--tb-page-pattern`(平铺,`--tb-page-pattern-size` 控尺寸),图案叠在图片之上。
uk-button-primary、checkbox/radio、input:focus、uk-tab active、`.uk-progress`、
`.uk-label:not(danger|warning|success|secondary)`)。新增会显示主色的组件先确认已映射。
- 令牌 `--tb-navbar`:顶栏底色,默认 `var(--tb-surface)`。页面底色偏彩时主题必须覆盖,否则纯白横带压在彩底上割裂。
- 静态资源版本号统一 `conf.StaticVersion`(base.css 与主题 css 共用);**图片不走这个缓存键**(`max-age=86400`),
换图要给 URL 带 `?v=`。
- 整页背景 `--tb-page-image`(cover)/ `--tb-page-pattern`(平铺,`-size` 控尺寸),图案叠在图片之上。
底色用**纯色**别用整页渐变;图案 ≥24px、透明度 ≤0.06。
- ⚠️ **多背景层不能塞变量列表**:变量自身是逗号列表会让层数与 size/position 错位(曾整页重复同一张插画)。
多张装饰用固定槽位:`--tb-deco` 恒 6 个,缺的填 `none`,size/position 同步补齐。
- ⚠️ `background-position` 四值语法必须**先是边关键字**:`left calc(...) bottom 0` 合法,`max(...) bottom 0` 非法 →
整条声明作废、人物跑到左上。
- ⚠️ **多背景层不能塞变量列表**:变量自身是逗号列表会让层数与 size/position 错位。多张装饰用固定槽位。
- ⚠️ `background-position` 四值语法必须**先是边关键字**:`left calc(...) bottom 0` 合法,`max(...) bottom 0`
非法 → 整条声明作废、人物跑到左上。
- 装饰摆位:**贴边、成组、避开页脚**;四角各摆一个会读成贴纸墙纸;页脚文案右对齐 → 右下角不能放人物。
`--tb-deco` 是 fixed 视口层,贴边插画别放进 `cover` 背景图(必裁边)。
- 主题视觉的**结构**比摆位重要:二次元站点通行做法是「整屏场景插画 + UI 悬浮其上」(蔚蓝档案 JP)。
白底留白里摆人物,无论多大都会读成贴纸;卡片/顶栏配半透明白 + `backdrop-filter` 才有"浮在画面上"的关系。
- 素材放 `static/images/themes/<slug>/`(embed 已含 `images/themes`,子目录自动覆盖;**新增其它顶层目录才改清单**)。
整页图用 16:9 + 竖版两张,靠 `@media` 切换。
- 人物素材:**图生图 + 抠白底**(`image1` 给立绘、`input_fidelity: high`、提示词强调 `flat vector illustration, no outlines`),
角色一致性远好于手绘 SVG;抠图用四角+四边中点 floodfill(容差 16)+ alpha MinFilter(3) 收边。
⚠️ 正面立绘直接贴进空场景像贴纸:改用**侧后方视角**(不直视访客),并把 RGB **压白 12~16%** 与压白 38% 的场景对齐雾度。
- 人物素材:**图生图 + 抠白底**(`image1` 给立绘、`input_fidelity: high`、提示词强调
`flat vector illustration, no outlines`),角色一致性远好于手绘 SVG。
⚠️ 正面立绘直接贴进空场景像贴纸:改**侧后方视角**(不直视访客),并把 RGB **压白 12~16%**
与压白 38% 的场景对齐雾度。
- ⚠️ **抠白底容差 16**:背景 253、白袜子 237 只差 16,容差 22 以上会把袜子吃穿。手内侧/两腿之间是
**闭合**白区,四边 floodfill 进不去,得按连通域单独掏(面积 ≥24),再 1px 收边。
- ⚠️ `ruleBody` 取令牌前要剥 `/* */`:注释会和后面的令牌粘成一个键,色块预览静默失效。
- 可视化编辑走 `dev/theme-builder/`(`serve.py` 起服务 + iframe 加载真实页面快照;file:// 下父页面读不到
iframe DOM,必须走 HTTP)。图片处理是**非破坏**的:`orig` 存原始字节 + `fx` 存参数,每次从 orig 重算;
参数没变则跳过重编码(反复走有损编码会累积模糊)。
## 其它约定
- 校验靠 `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` 确认已提交。
改代码时只做只读 `git status/log`,别动历史。`outputs/` 未纳入版本控制(untracked),别整目录 add。
- git 仓库 `https://git.shiroko.one/tamakyi/tamabox`(`main`)。**`git push` 能直接推**;
「Failed to authenticate user」是**临时态**。推前 `git log --oneline -3` 确认已提交。
改代码时只做只读 `git status/log`,别动历史。`outputs/` 已在 `.gitignore`(`/outputs/`),别整目录 add。
- 打包交付用 tar.gz(排除 `.git`/`.workbuddy`/`outputs`),放项目同级。
## 测试环境 box-dev-api.337845818.best
- 账号 `[email protected]` / `adminadmin`,公开页 `/_/admin6`(用户 2026-10-19 指定)。
- 登录要过算数验证码:`GET /login` 取 `_csrf`+cookie → `GET /api/captcha/generate` → 算 `a op b` →
`POST /login` 带 `g-recaptcha-response="<id>:<答案>"`。默认语言 **ja**,四语言文案要一起加(`TestDictionaryComplete`)。
- 跑本工作区构建但**部署有延迟**;模板是编译期 embed,**只重启不重编不会变**。用户说「已部署但没变化」→
先抓线上 HTML 比对标记,别先怀疑缓存。
`POST /login` 带 `g-recaptcha-response="<id>:<答案>"`。默认语言 **ja**,四语言文案要一起加
(`TestDictionaryComplete`)。
- **部署有延迟**;模板是编译期 embed,**只重启不重编不会变**。用户说「已部署但没变化」→ 先抓线上 HTML
比对标记,别先怀疑缓存。
- 表单字段:提问 `content`,回答 `answer`。`GET /user/votes` 故意 302;匿名无邮箱提问成功直接 200 渲染。
- 自检:`dev/devcheck-e2e.py`(28 项)、`dev/devcheck-restrict.sh`(**基线非 200 就别往下走**;`RUN=1` 才真跑;`trap` 保证撤销)。
- 自检:`dev/devcheck-e2e.py`(28 项)、`dev/devcheck-restrict.sh`(**基线非 200 就别往下走**;
`RUN=1` 才真跑;`trap` 保证撤销)。
- 图片:编码三档(头像 512/q78、背景 1920/q80、通用 1600/q80),**EXIF 方向必须先归一**(转 WebP 会丢),
比例校验用同一套方向(头像 1:1 测不出,只有背景 16:9 触发)。