177 lines
16 KiB
Markdown
177 lines
16 KiB
Markdown
# 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 换不了 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 是正式生产环境,**别用「沙箱慢」解释超时**。
|
||
- 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。
|
||
- ⚠️ **同请求渲染只能用 `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=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 修。
|
||
- ⚠️ **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`)而非目测;扫描循环上界不能与物体边界重合
|
||
(曾把边界截断读成"方角")。
|
||
|
||
## 主题(箱主个性化)
|
||
|
||
- 三个注入点,别再漏:`/_/{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 map[string]interface{}, owner)`(与主题同一个函数),
|
||
必须在任何 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 = 旧二进制。
|
||
启动日志有 `主题注册表已加载 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 个颜色令牌。
|
||
- ⚠️ **按页面作用域**:`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`(平铺,`-size` 控尺寸),图案叠在图片之上。
|
||
底色用**纯色**别用整页渐变;图案 ≥24px、透明度 ≤0.06。
|
||
- ⚠️ **多背景层不能塞变量列表**:变量自身是逗号列表会让层数与 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。
|
||
⚠️ 正面立绘直接贴进空场景像贴纸:改**侧后方视角**(不直视访客),并把 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` 能直接推**;
|
||
「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
|
||
比对标记,别先怀疑缓存。
|
||
- 表单字段:提问 `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`;`hide-public [--only-reported] [--since N]`。
|
||
- 本地冒烟:`NEKOBOX_CONFIG_PATH=<ini>`;`conf.Init` 强制 `app.external_url`、`server.salt`、`server.xsrf_key` 非空。
|