Files
tamabox/.workbuddy/memory/MEMORY.md
T
2026-10-19 16:46:42 +08:00

177 lines
16 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.
# 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"`。
但 overlay 会把它当桩,**检查 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 正常),曾误判「接口卡死」。线上验证优先 curl;
python 别用 `/tmp`(Windows 解析成 `C:\tmp`,与 MSYS `/tmp` 不通)。
## 行尾 / 模板
- 行尾混合不可推断(`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 会改成 `”`)。
- ⚠️ **`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`。
- **新增审核调用点必须走 `moderation.Check`**(直调 `censor.Text` 会绕过档位)。不可用时 **fail-closed**。
- ⚠️ **圈内黑话(代称+群体标签+避雷叙述)任何通用审核都拦不住**,别指望词库;靠处置 + 举报 + 人工判断。
- 严格度三档 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` 是正常模式。
扫描法:找 flash 调用 → 向下第一个 `Success(`/`Redirect(` → 前者即缺陷。
## 处置与举报
- 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 与页面密码必须自己校验
(已在 2026-10-19 补;否则「整页停用」和访问密码都能被 API 绕过)。
- ⚠️ **整箱分享卡 `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` 默认给箱主发处置通知邮件(按 `users.language`,模板 `mail/restriction-notice.html`),
**只含级别名/到期/规则链接,不含 `--reason`**;`--no-notify` 可跳过(L1 静默止血用)。
`--days > 90` 必须加 `--force`(防手误),交互模式还要再输一次级别名。
`unrestrict` 也会发解除通知(同模板、无申诉段),双方都有 `--no-notify`。
⚠️ **到期自动解除不发信**(读表比较时间,无调度器)。
## GORM
- 默认值必须带引号;`default:` 会把零值替换,空串有语义的字段别加。
- ⚠️ 事务里复用同一 `tx` 会串条件,每句 `tx.WithContext(ctx).<链式>`。
- ⚠️ 软删除 + 唯一索引 = 槽位永久占用:停用(可恢复)= 物理删内容 + 软删用户行,注册侧 `Unscoped` 给
`ErrDeactivatedEmail/Domain`;封禁 = 物理删用户行,邮箱释放。级联删除一律 `Unscoped()`。
- ⚠️ GORM 链式对象 Count/Find 后带状态,统计/预览/更新必须各自重建 query。
## 前端样式
- 基础样式在 `static/css/base.css`(2026-10-19 从 header 内联块抽出,`/static/css/base.css?v=YYYYMMDDx`,
**改完必须把 header 里的 v 号 +1**,静态缓存 1 天)。设计令牌是文件开头的 `:root`(亮)+ `html[data-theme="dark"]`(暗)。
新样式一律用 `var(--tb-*)`,别再写死色值/圆角/阴影,也别为深色另写一套分支。
- 页面级样式仍内联在各自模板的 `<style>` 里(variance 大、复用少),但同样只能用令牌。
- 静态文件是**显式 embed 清单**(`static/fs.go`),新增 css/js 必须手动加进 `//go:embed` 那行,否则运行期 404。
- 主题属性在 head 内联脚本里打(cookie `theme`),**不能挪回 body**,否则深色用户白闪。
- 页面级样式尽量用类,不用 `style=`(深色下内联写死色值会露白,`.tb-img-uploader` 踩过)。
- **移动端(≤640px)语言切换与主题按钮不在顶栏**:顶栏 `.tb-nav-utils` 被 `display:none !important` 隐藏,
改由 `#tb-mobile-utils` 固定底栏承载(`position:fixed;bottom:0;z-index:1000`,body 预留 `padding-bottom:56px`)。
顶栏图标只在 >640px 出现。用户曾误以为"按钮不见了",先解释位置再排查。
⚠️ `/_/{domain}` 箱主页把 `IsUserPage` 设为 true(`route/question/page.go:133`),语言切换整块包在
`{{ if not .IsUserPage }}` 里(header.html:272 桌面 / :314 移动),所以**箱主页没有语言切换入口**——
**这是有意设计,不是缺陷**(用户 2026-10-19 明确:箱主页属箱主个性化,以箱主配置为准)。
别把它当 bug 去"修",要动先问。
- ⚠️ **UIkit 3.3.3 有 `.uk-flex::before/::after{display:none}`**:给带 `uk-flex` 的元素加伪元素遮罩必须写
`.tb-cover.uk-flex::after` 这类复合选择器,否则静默失效(不报错)。
- 视觉自检:`TEST_PREVIEW=1 go test ./templates -run TestGenPreview` → `outputs/preview-*.html`;
截图必须 `chrome --headless=old`(新版 headless 在本机挂住),外部资源先落到 `outputs/preview-assets/`。
改 CSS 的正确验证法是**像素比对**(PIL 在 `~/.workbuddy/binaries/python/envs/default`),不是目测。
⚠️ 像素扫描的循环上界**不能与物体边界重合**(曾把 `range(110,830)` 当成条宽 →
右端恒为 829 被读成"方角",其实是边界截断)。
- 分类/题型标签一律 `.tb-chip`(浅底 + 次要色);**彩色 `uk-label` 只留给语义状态**
(置顶/私密/被举报/已读、投票未开始/进行中/已结束/已参与)。题型配色曾由 Go 的
`fieldTypeLabelClass` 生成(已删),故 grep `uk-label` 搜不到,改前先搜模板函数名。
## 主题系统
- 一套主题 = `static/css/themes/<slug>.css` 一个文件,**只覆盖令牌**(配色/图案/整页背景),不碰布局。
新增主题:复制 `_scaffold.css` → 改名 → 登记到 `[theme] available`;`static/fs.go` 已嵌整个目录,无需改清单。
- 作用域:仅 `/_/*` 箱主相关页面(`question.Pager` 注入 `ThemeSlug`);`/user` 后台与全站其它页面不主题化。
优先级 `users.theme > [theme] default > 基础外观`;`""`=跟随站点默认,`"default"`=箱主明确选基础外观(不加载 css)。
- ⚠️ 主题 css **必须成对写**:`html[data-theme-style="x"]`(0,1,1,压过 `:root` 的 0,1,0)+
`html[data-theme="dark"][data-theme-style="x"]`(0,2,1,压过 base 的 dark)。只写浅色块会让深色模式被浅色令牌污染。
兜底测试 `TestThemeCSSComplete`(检查两个选择器 + 10 个颜色令牌齐全)。
- ⚠️ **UIkit 3.3.3 的主题色是编译期常量 `#1e87f0`**,令牌注入不进去。base.css 里有一段「UIkit 组件跟随主题色」
按组件映射(a/uk-link、uk-button-primary、checkbox/radio、input:focus、uk-tab active)。新增会显示主色的组件时,
先确认它是否已映射,否则换主题后会出现「页面变粉、按钮还蓝」。
- 静态资源版本号统一 `conf.StaticVersion`(base.css 与主题 css 共用),改样式只改这一处;页面通过 `.StaticVersion` 取。
- 整页背景用 `--tb-page-image`(cover)与 `--tb-page-pattern`(平铺,`--tb-page-pattern-size` 控尺寸),图案叠在图片之上。
## 其它约定
- 校验靠 `valid:"required;maxlen:N"` 标签(`internal/form/*.go`),别在 handler 手写。
- 终止性操作(停用)落独立公开页 `/deactivated`(注册在 `reqUserSignIn` 外);改密码后重登才用 flash + `/login`。
- 只有箱主 `/user` 面板,**无管理员角色、无站方后台,且用户要求不要加**:站方动作只走 CLI / 邮件 / 改配置重启。
要开管理页先问:能不能用 CLI 表达?
- git 仓库已损坏,**不要动 git**;备份用 tar.gz 排除 `.git`/`.workbuddy`,放项目同级。
## 测试环境 box-dev-api.337845818.best
- 专属测试账号:`[email protected]` / `adminadmin`,公开页 `/_/admin6`(用户 2026-10-19 指定)。
登录要过算数验证码:`GET /login` 取 `_csrf`+cookie → `GET /api/captcha/generate` → `POST /api/captcha/verify`
(头 `X-CSRF-Token`)→ `POST /login` 带 `g-recaptcha-response="<id>:<答案>"`。
- 跑本工作区构建,但**部署有延迟**;模板是编译期 embed,**只重启不重编不会变**。用户说「已部署但没变化」→
先抓线上 HTML 比对标记,别先怀疑缓存。
- 默认语言 **ja**;四语言文案要一起加(`TestDictionaryComplete` 查齐全)。验证码是数学题:
`GET /api/captcha/generate` → 算 `a op b` → `g-recaptcha-response="<id>:<答案>"`,并带页面 `_csrf`。
- 表单字段:提问 `content`,回答 `answer`。`GET /user/votes` 故意 302;匿名无邮箱提问成功直接 200 渲染。
- 自检:`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 触发)。
## 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`(Top 被举报箱主 + Top 举报 IP);
`hide-public [--only-reported] [--since N]`。
- 本地冒烟:`NEKOBOX_CONFIG_PATH=<ini>`;`conf.Init` 强制 `app.external_url`、`server.salt`、`server.xsrf_key` 非空。
## 主题(箱主个性化)
- 三个注入点,别再漏:`/_/{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(ctx.Data, owner)`,与主题同一个函数):
必须在任何 i18n 取词之前调用,否则先渲染出的提示会按访客语言。箱主未配语言时才回退访客/站点默认。
- 主题 = 一个 css 文件只覆盖令牌;新增主题不用改 Go/模板,放 `static/css/themes/<slug>.css` +
`[theme] available` 登记即可(`static/fs.go` 已嵌整个目录)。底稿 `_scaffold.css`。
- **必须同时写浅色块与深色块**(`html[data-theme-style=x]` / `html[data-theme="dark"][data-theme-style=x]`),
`TestThemeCSSComplete` 会查;色块预览由浅色块的 `--tb-bg` + `--tb-primary` 自动生成(色值过正则防注入)。
- 令牌 `--tb-navbar`:顶栏底色,默认 `var(--tb-surface)`。页面底色偏彩时主题应覆盖它,
否则顶栏是纯白带、压在彩底上割裂(用户实测反馈过)。
- UIkit 主色是编译期常量,主题要走通必须逐个组件映射(见 base.css 末尾那段):a/uk-link、
uk-button-primary、checkbox/radio、input:focus、uk-tab active、`.uk-progress`,
以及 **`.uk-label:not(danger|warning|success|secondary)`**——`uk-label` 默认色就是主色,
但必须用 :not() 排除语义色(同特异性 + base.css 后加载,不排除会连带刷掉 danger/success)。
- 主题底色用**纯色**,别用整页渐变——会被读成「底色分布不均匀」;图案建议 ≥24px、透明度 ≤0.06。
- 改了 base.css / 主题 css 必须 +1 `conf.StaticVersion`(两者共用同一个 v)。