10 KiB
10 KiB
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:131govalid.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。