Files
tamabox/.workbuddy/memory/MEMORY.md
T

19 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"。 但 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 仓库可用(origin = https://git.shiroko.one/tamakyi/tamabox,分支 main)。git push 能直接推, 用户本机的 Git for Windows 已认证,~/.gitconfig 的 credential.helper=store 与系统 manager 并存, store 里的记录会被刷新 —— 所以「Failed to authenticate user」是临时态(用户在别处提交后即恢复), 不要据此断言「无凭据、只能打包」。推之前先 git log --oneline -3 看本地是否已提交,直接试推一次即可。 改代码时仍只做只读 git status/log,别动历史。
  • 打包交付用 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 → 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。
  • ⚠️ [theme] available 是硬白名单,启动时读一次:css 随二进制发布了但没登记,设置页就是看不到, 现象和「部署没生效」一模一样。排查先分两类 —— curl /static/css/themes/<slug>.css 返回 200 说明文件在 (那就是没登记),404 才是旧二进制;StaticVersion 只管缓存、不影响列表。 启动日志会打 主题注册表已加载 enabled=... 与 [theme] available 未登记以下主题(2026-10-19 加)。
  • 带图片素材的主题把图放 static/images/themes/<slug>/(static/fs.go 的 embed 已含 images/themes, 子目录自动覆盖;新增别的顶层目录才要改清单)。整页图用 16:9 + 竖版两张,靠 @media 切换 —— --tb-page-image 是 cover,贴着图片边缘放的插画会被按视口裁掉。
  • 必须同时写浅色块与深色块(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)。
  • ⚠️ 多背景层不能塞变量列表:background-image: var(--a), var(--b) 里若某个变量自身是逗号列表, 层数会变而 size/position 的个数不变 → CSS 循环取值、整层错位(曾出现整页重复同一张插画)。 需要多张装饰时用固定槽位:--tb-deco 恒 6 个,缺的填 none,size/position 同步补齐。
  • ⚠️ 整页 cover 背景图必然裁边(1280×1400 视口只露出 2400 宽的中间 1234px,竖屏更狠) → 贴边的插画不要放进背景图,放 --tb-deco(固定像素 + 四角定位 + fixed)。
  • 主题插画可以不用图片:dev/gen_shiroko_chibi.py 用 SVG 路径手绘 Q 版人物,3.8KB/张、任意尺寸不糊; 生成模型给的"透明背景"线稿抠不干净、无法目检,不如自己画。
  • 主题插画优先"图生图 + 抠白底":image1 给既有立绘、input_fidelity: high、 提示词强调 flat vector illustration, no outlines,四张姿态的角色一致性远好于手绘 SVG。 抠图用四角+四边中点 8 点 floodfill(容差 16)+ alpha MinFilter(3) 收边。
  • ⚠️ deco 的定位百分比是「(视口高-图高)×pct」,center 在矮视口会和大尺寸立绘重叠; 右侧被立绘(bottom 0,500+px)占住时,第二个人物要放左侧、用显式 top N%。
  • 主题装饰摆位:贴边、成组、避开页脚。四角各摆一个会读成贴纸墙纸; 页脚文案右对齐 → 右下角不能放人物。--tb-deco 是 fixed 的视口层, 多个人物时要成组(左右各一个是"图钉",同一角一组才像场景)。
  • ⚠️ background-position 四值语法必须先是边关键字:left calc(...) bottom 0 合法, calc(...) bottom 0 非法 → 整条声明作废、回落到 0% 0%(人物跑到左上、背景层错位出现色带)。