Files
tamabox/.workbuddy/memory/MEMORY.md
T
2026-09-15 18:20:06 +08:00

10 KiB
Raw Blame History

TamaBox-v4 项目长期笔记

协作惯例

  • 结论分级 P0/P1/P2 + 🔴/🟠;给修复建议前先做根因取证与实测复现,不要只静态推断。
  • 改代码前先出「方案/变更清单」获批准,再分批落地。用户常授权自主推进(「你先继续,后面我发现问题再说」),但重大问题仍要显式指出。
  • 工作场景用 @scene#N:"名称" 标记。

构建与测试

  • 本机无 libwebp/GCC,chai2010/webp(cgo) 会挡住 go build ./...;必须用 overlay 把 internal/storage/s3.go 换成 dev/s3stub/s3_stub.go: -overlay="C:/Users/Tamakyi Gekka/AppData/Local/Temp/tb-webpcheck/overlay.json"(dev/overlay.json 里的短名路径可能失效)。
  • 「编译卡死」真因 = proxy.golang.org 不可达(本沙箱 curl 超时;goproxy.cn 正常)。 每条 go 命令前必须:export GOPROXY=https://goproxy.cn,direct GOSUMDB=off GOFLAGS=-mod=mod。 代理正确时 go build ./... EXIT=0、./route/user/ 约 7s。超时 ≠ 慢:先用 go list -deps 拆开「模块解析/编译」两层。 go env -w 在本沙箱失败(无 %AppData%),无法持久化。1C1G 是用户正式生产环境,禁止用「沙箱慢」解释超时。
  • internal/db 测试需真实 DB。本地 PG:DB_TYPE=postgres DB_USER=postgres DB_PASSWORD=15207786347hXPm DB_HOST=127.0.0.1 DB_PORT=5432(DSN 不带库名,NewTestDB 自建 tamabox-test-<rand> 并 DROP)。 2026-09-15 起「带 DB 的 go test ./...」全绿;此前的失败全是测试自身缺陷,已修。
  • ⚠️ internal/db 包级 store 单例只在 db.Init 装配;store 方法内不要引用包级全局,用 NewXStore(db.DB), 否则测试下 nil panic。唯一例外:feature_control.go 的 FeatureStateOf(有 nil 守卫)。
  • 行尾是混合的,不能按规律推断:internal/db/*、internal/mail/mail.go、route/user/profile.go 是 LF; internal/route/route.go、internal/db/users.go、internal/i18n/dict_*.go 是 CRLF。 编辑前用 Python 数 b'\r\n' 确认(grep -c $'\r' 不可靠)。gofmt -l 会把 CRLF 文件误报为未格式化。 格式化统一走 dev/gofmt_keep_crlf.py <files>。
  • internal/security/censor/censor.go、text_test.go 有升级前就存在的空格缩进,别顺手 gofmt。

内容审核

  • 线上生效通道 = 七牛云(ai.qiniuapi.com/v3/text/censor,scene=antispam);阿里云代码在但 key 空。
  • 通道可配:[security] text_censor_source = qiniu | aliyun | auto(留空同 auto)。常量在 conf, 装配在 censor.censorChain(),启动校验 conf.validateTextCensorSource;显式选通道却缺凭据会启动报错。
  • 七牛 IsPass() 只硬拦 politics/terrorism/porn,review 一律放行,且不支持白名单 → 拦截判定收敛到策略层:censor/policy.go(档位裁决)+ moderation/moderation.go(唯一入口 Check)。
  • 新增文本审核调用点必须走 moderation.Check,不要直接调 censor.Text(会绕过审核严格度档位)。 三场景:提问/回答、投票、私聊。
  • LabelScore.Score 统一归一化 0~1(阿里云 2.0 无逐标签置信度,按 riskLevel 折算 high=1/medium=0.6/low=0.3)。
  • 审核服务不可用时 fail-closed,未经批准不要改 fail-open。⚠️ 推论:显式选定的通道一旦坏掉, 整站发布+私信全被拒且现象隐蔽(只在日志)。

阿里云 green(/green/text/scan)

  • 实现与上游/官方示例一致;SetContentType(requests.Json) 仅为接口契约对齐,已证伪与故障无关。
  • 根因(2026-09-10 实证):code=596 "You have not opened Yundun Content Moderation Service", 阿里云请求被拒也回 HTTP 200 + success:false + 无 data。现已把 code/msg/requestId + 响应摘要写进错误。
  • ⚠️ 硬门槛:「内容安全 1.0」需企业实名认证(官方 69806),个人认证账号永远开不了。 替代:迁 green-cip(2.0,标签体系不同、需改代码)或退回 auto/qiniu。RAM 需 AliyunYundunGreenWebFullAccess。
  • data[].code != 200 仍 fail-closed 置 block,但现在会打 Warn(区分「审核失败」与「真违规」)。

表单校验

  • 长度/必填由 valid:"required;maxlen:N" 标签在 Bind() 阶段执行(internal/form/form.go:131 govalid.Check), handler 顶部用 ctx.HasError() 承接,文案经 msg: 指向 i18n 键。
  • ⚠️ 不要再在 handler 手写长度校验:那是永不触发的死代码,且易与真实上限冲突 (profile 实际 Name≤20 / Intro≤100 / link≤30)。2026-09-15 曾据此误判 P2 并已回滚。
  • 判断字段有无校验:先看 internal/form/*.go 的 struct 标签。

GORM 约定

  • 字符串列默认值必须带引号:gorm:"default:'none'"。
  • default: 会让 GORM 在 Create 时把零值替换为默认值(曾致「取消勾选保存后库里仍 true」)。 「空串有语义」的字段不要加 default:。Updates(map) 不受影响。
  • 启动期「历史数据归一」惯例:db.Init 的 AutoMigrate 之后追加幂等函数(backfillFeatureControls 等), 失败仅告警不阻断启动。
  • ⚠️ 事务里复用同一个 tx 句柄会串条件(事务句柄 clone==0)。每条语句都要重新取句柄: tx.WithContext(ctx).<链式>。曾生成 DELETE FROM follow_ups WHERE <follow_ups条件> AND <messages条件> → 42703。
  • ⚠️ 软删除 + 唯一索引 = 槽位被永久占用。User.email/domain、Conversation.token、VoteSubmission 复合唯一索引 都不含 deleted_at,而 gorm 查重默认带 deleted_at IS NULL → 「查重放行、INSERT 被索引拦下」= 500。两种语义:
    • 停用(可恢复):users.Deactivate = purgeUserContent() 物理删内容 + 软删用户行(身份占位)。 邮箱/域名继续占用是有意为之;注册侧必须 Unscoped 识别并给 ErrDeactivatedEmail/Domain (见 validate/fieldTaken),不能当普通重复邮箱。恢复用 TamaBox restore。
    • 封禁(不可恢复):BanUserAndCleanup = purgeUserContent() + 物理删用户行,邮箱释放。
    • 级联内容删除一律 Unscoped();voteSubmissions.DeleteByVoteID 同理。
  • 唯一键冲突兜底 isDuplicateKeyError(err)(MySQL 1062 / PG 23505);注册不需要额外兜底。
  • ⚠️ 新增 CLI 命令记得同时注册到 cmd/tamabox.go 的 app.Commands(ban.go 曾写好却没注册 → 命令不可达)。

文件工具陷阱

  • templates/user/moderation-strictness.html:用 Write/Edit 会被注入 data-page-node-id(shell/sed 不会)。 → 该文件今后只用 shell 改。
  • 往 gofmt 对齐组中间插 map 键/// 注释会打断对齐组 → 污染 diff。新键插在组末尾或独立成组。 验证:tr -d '\r' < f.go > a.go; gofmt a.go > b.go; diff a.go b.go(无输出=不重排)。
  • ⚠️ gofmt 会把 doc comment 里的 '' 规范化为 ”(Go 1.19+),导致反复报未格式化。注释里别用 ''。
  • ⚠️ gofmt 是 Windows 原生程序,看不到 MSYS /tmp;临时比对文件放 Windows 可见路径。

会话失效后的落点

  • 终止性操作(停用/注销)落「独立公开成功页」,不要落登录页:既有 /deactivated (user.DeactivatedProfile → templates/user/deactivated.html)。停用后 Session.Flush() 清空会话, 落 /login 会让用户以为没生效;该路由必须注册在公开区(reqUserSignIn 之外)。
  • 继续可登录的操作(改密码后重登)才用闪信 + /login:顺序先 Session.Flush() 再 SetSuccessFlash()。
  • i18n 键名:deactivate.* = 停用表单页;deactivated.* = 停用成功页。
  • templates/embed.go 整目录 embed,新增模板自动嵌入,无需改指令。

仓库状态

  • git 仓库已损坏,用户明确要求「不要动 git」。备份/回滚用 tar.gz(排除 .git、.workbuddy), 放项目同级:C:/OtherFiles/Github/TamaBox-v4-mainline-<描述>-<日期>.tar.gz。

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

  • 跑的就是本工作区的构建(实测本地新模板/路由/文案能在线访问),但部署有延迟 → 改完代码要提醒用户重新部署,不要假定已生效。
  • 站点默认语言 ja(shared.App.Lang()),匿名断言别写中文,用语言无关标记。
  • 验证码是数学题(CaptchaType == "math"):GET /api/captcha/generate 取 {data:{id,question}}, 算 a op b,提交 g-recaptcha-response = "<id>:<答案>";表单还需页面里的 _csrf 隐藏域。
  • 可复用端到端巡检脚本:dev/devcheck-e2e.py(28 项)——注册/登录/投票全链路(建票发布/公开页/匿名提交/ 导出计数/草稿/表单型)/登录态接口与导出/匿名提问/停用/重注册被拒/公开页/四语言。 单轮会留下一个已停用的身份占位,清理见「管理 CLI」。 ⚠️ 投票单选导出的 CSV 末行是 总参与,N,与选项票数行同形,解析票数时必须单独匹配该行。
  • 表单字段名易错:提问 content;回答 answer。GET /user/votes 故意 302 到 /user/questions#votes; 匿名无邮箱提问成功直接 200 渲染成功页(不重定向)——都不是 bug。

管理 CLI(TamaBox <cmd>)

  • 注册在 cmd/tamabox.go 的 app.Commands。现有:web / censor / uid / ban / restore。
  • ban --email|--domain [--yes] → 物理删除用户+内容(不可恢复,邮箱释放)。
  • restore --email|--domain [--password <新密码>] [--yes] → 恢复已停用账号(清 deleted_at)。 --password 必须在恢复之后执行(UpdatePassword 走普通作用域)。
  • 本地冒烟:仓库只有 conf/app.sample.ini,用 NEKOBOX_CONFIG_PATH=<ini> 指定;conf.Init 强制 app.external_url、server.salt、server.xsrf_key 非空。跑 CLI 会自动 AutoMigrate,可指向临时库验证后 DROP。