diff --git a/.workbuddy/memory/2026-10-29.md b/.workbuddy/memory/2026-10-29.md index 9267d5e..5dc696f 100644 --- a/.workbuddy/memory/2026-10-29.md +++ b/.workbuddy/memory/2026-10-29.md @@ -243,3 +243,57 @@ gzip 实现质量高、后台移动端短标签方案做得细。 真实 HTTP 验证没跑通(靠代码 diff 确认修复正确)。 - **`git show HEAD:file` 输出写到 /tmp 后 Windows Python 读不到**:/tmp 是 Git Bash 的虚拟路径, Windows Python 看不到。要落盘验证行尾,写到仓库内(如 outputs/)再读。 + +## 三项待办落地:AutoMigrate 版本化 + 备份脚本 + Telegram/Webhook 通知 + +用户「先按1-2-3的顺序做」,期间两次纠正 R2 存储结构认知。 + +### 1. AutoMigrate 版本化迁移(internal/db/migrations.go) + +- 新增 `schema_migrations` 表(version 主键 + name + applied_at),`runMigrations(db)` 按版本升序 + 执行未应用的迁移,每个迁移在事务里「跑 up + 记版本」。 +- 把 db.Init 里散落的 5 个幂等补丁收编为版本迁移 v1~v5: + backfillAnsweredAt / backfillFeatureControls / reconcileLegacyChatEnabled / + normalizeChatQuota / normalizeHarassmentSetting。收编后**只跑一次**(原来每次启动都跑)。 +- AutoMigrate 仍负责「加表/加列/加索引」(声明式、幂等、无数据丢失),版本迁移负责 + AutoMigrate 做不了的破坏性变更(改列名/删列)与一次性回填。两者分工,互不替代。 +- 迁移失败阻断启动(半途而废比静默跳过更危险)。 +- 测试:migrations_test.go(幂等跑两遍不重复 / 失败阻断后续 / 清单严格升序)。 +- 实测:真实库首次启动建表 + v1~v5 落账本,二次重启账本仍 5 行(幂等)。 + +### 2. 备份脚本(internal/cmd/backup.go,TamaBox backup) + +- `--output ` 输出;`--db-only` / `--images-only` 分开关。 +- DB:调 pg_dump(PGPASSWORD 环境变量传密码避免 ps 泄漏),输出 db-.sql(标准恢复格式)。 +- 图片:从 R2 下载 tamabox 站全部图片 + 写 images-manifest.json(key→size→md5)。 +- 两个 CLI 入口(根 tamabox.go + cmd/tamabox.go)都注册了 Backup。 +- 实测:db-only 导出 46KB(21 表全含 schema_migrations);images-only 30 秒下 15 个文件、 + 路径保持 tamabox/ 结构、大小与 List 一致。 + +### 3. Telegram/Webhook 通知(internal/notify/notify.go) + +- 站级 Webhook([notify] webhook_url):新提问/新回答/新追问/新私聊/新投票回复统一 POST JSON。 +- 用户级 Telegram([notify] telegram_bot_token + users.telegram_chat_id 字段):新提问/新投票点对点推送。 +- 通知是「尽力而为」旁路:任一通道失败只记日志不抛错,不影响业务主流程;未配置则静默跳过。 +- 6 处业务触发点接入 notify:page.go(新提问+新私聊)、question.go(新回答)、 + follow_up.go(新追问+追问回答)、vote.go(新投票)。私聊消息保留仅邮件(已有节流,避免刷屏)。 +- UI:profile 加 telegram_chat_id 输入框 + 四语言字典(admin.notifyTelegram* 3 条)。 +- 测试:notify_test.go(webhook JSON 结构 / 未配置跳过)。 + +### 踩坑(本轮新) + +- **R2 存储结构(用户两次纠正,重要!)**:图片实际存在 Cloudflare R2,CDN URL 形如 + `https://r2.337845818.best/tamabox/2025/12/1767082504316655685.jpg`(bucket=tamabox, + key=年月/时间戳.扩展名,**无 picture/ 前缀**)。但 R2 API 侧(ListObjectsV2)返回的 key + **带 bucket 名作前缀**(`tamabox/2025/12/...jpg`)——这是 path-style bucket 的 R2 行为。 + - 关键:`storage.GetS3Client` 的 endpoint 配置(image_endpoint 带 `/tamabox` 后缀 + + Bucket 参数 tamabox)对 GetObject/PutObject 恰好可用(aws-sdk 拼出 /tamabox/tamabox/, + R2 容错),但 **ListObjectsV2 会拼成 /tamabox/tamabox?list-type=2 → 404 NoSuchKey**。 + - 备份脚本的正确姿势:剥离 endpoint 的 `/tamabox` 后缀 + UsePathStyle=true + Bucket=tamabox, + List prefix="tamabox/",GetObject 直接用 List 返回的完整 key(含 tamabox/ 前缀)。 + - 同一 R2 account(0c99...)下还有 blog/ 前缀的另一个站点内容(共享 account)。 +- **代码的 PictureKeyPrefix="picture/" 与线上不一致**:mainline 代码上传 key 用 `picture/` 前缀, + 但线上历史图片 key 是 `tamabox/` 前缀(即实际 key 无前缀、靠 bucket 隔离)。这是待澄清的 + 「代码 vs 线上」不一致,本轮未动,需用户确认是否要统一。 +- **Windows Edit 又把 3 个 LF 文件转成 CRLF**(conf.go / users.go / form/user.go): + 老坑复发,用 Python `b.replace(b'\r\n', b'\n')` 统一转回 LF 解决。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index e69e2db..fb8ca7e 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -41,7 +41,13 @@ ## 审核 / 处置 / 举报(topics/review.md) -- 线上通道 = 七牛;裁决在 `censor/policy.go` + `moderation.Check`,新增调用点必须走 `moderation.Check`,不可用时 fail-closed。 +- **图片存储 = Cloudflare R2**(S3 兼容协议,非七牛):`conf.Upload.ImageEndpoint`/`ImageBucket`/ + `ImageBucketCDNHost`,客户端见 `internal/storage/client.go`(R2 的 endpoint resolver 写法, + `HostnameImmutable` + `region=auto`);key = `picture/YYYY/MM/DD/<15hex>`。代码里还留了一套 + 阿里 OSS 客户端(`GetOSSBucket`),是历史遗留,实际在用的是 S3/R2 这条。 +- **文本审核通道 = 七牛/阿里云**(`[security] text_censor_source = qiniu|aliyun|auto`,七牛优先)—— + 这是内容审核 API,与图片存储是两码事,别搞混。线上通道 = 七牛;裁决在 `censor/policy.go` + + `moderation.Check`,新增调用点必须走 `moderation.Check`,不可用时 fail-closed。 - 严格度存 `users.moderation_strictness`;受处置用户强制 strict(`moderation.EffectiveStrictness`),查询失败回退用户设置。 - 同请求渲染只能用 `ctx.SetError`/`SetInternalError`,不能 `*Flash`。 - 写路径查功能开关用 `db.FeatureStateOfStrict`(失败即拒);`FeatureStateOf` 的