chore: baseline snapshot before moderation-strictness feature

Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
# 2026-09-06
## 移除 vercount 第三方页脚统计(用户确认"不用就去掉")
- 【诊断】cn.vercount.one 实际 = Vercel 托管 + CF 前置(104.21.1.96),cf-cache-status DYNAMIC 每次回源跨洋;南宁实测 js ttfb ~1.9s(DNS 0.72s 高);脚本内部 DOMContentLoaded 后才 fetch events.vercount.one(ttfb ~3s,才是页脚 spinner 真瓶颈);且慢 defer 会推迟 DOMContentLoaded ~1.4s。页脚仅展示 site_uv/page_pv 两徽标(busuanzi 兼容命名)。
- 【决策】第三方装饰统计非必须 → 用户选彻底移除(非动态注入方案)。备份 .workbuddy/backup/remove-vercount-20260906/。
- 【改动 7 文件】
1. templates/base/header.html:删 dns-prefetch cn.vercount.one;删 busuanzi 注释行与 <script defer vercount>;删专用 .tb-spinner CSS 块(含 dark 覆盖)
2. templates/base/footer.html:删整行 visitor/pageview 统计(busuanzi_value_site_uv/page_pv)
3-6. internal/i18n/dict_{zh_cn,zh_tw,en,ja}.go:删 site.footer.visitor/pageview 两个 key
7. internal/i18n/i18n_test.go:visitor 参数填充断言改指 auth.recover.greeting(保 %s 覆盖)
- 【验证】残留 grep 全空;gofmt clean;go test ./internal/i18n/ + ./templates/ 全绿。
- 【未动】static/tamabox.changelogs.json 为 2024 停更归档;根 CHANGELOG.md 是否追加条目待用户确认。
## umami 数据展示放弃 + tamami.js 加载优化(方案A load 注入)
- 【umami 取数可行性】mami.tama.guru = Vercel 托管 Next.js(CNAME vercel-dns-017.com);umami v2 API 全需登录(/api/websites 401、login 空体 zod 400);新版 umami 无 Share 功能 → 用户放弃 umami 数据恢复页脚统计,footer 维持无统计徽标(vercount 移除后现状)。
- 【mami 慢根因】Vercel 无国内节点,白天/冷访问 1-2s+;tamami.js 4.6KB 缓存头好(public max-age=86400, Age=303024 靠 CDN 兜底)凌晨命中 0.3s;用户体感慢主因=umami 后台(Next.js 动态回源)+ 白天边缘波动。defer 在 head 会推迟 DOMContentLoaded(同 vercount 问题)。
- 【R2 方案排除】实测 R2 直链 0.80-1.56s 比 Vercel 缓存命中 0.28-0.35s 更慢(凌晨南宁),印证 UIkit 当初迁 cdnjs.snrat.com 的原因。
- 【脚本能力确认】tamami.js 内部:host = data-host-url 优先否则脚本同源,拼 /api/send 上报 → 静态搬迁可行但无快托管点故弃。
- 【改动】templates/base/header.html(备份 .workbuddy/backup/tamami-load-inject-20260906/):638 行 <script defer tamami.js> → window load 后 createElement 注入 + setAttribute data-website-id。效果:mami 再慢不阻塞页面关键事件;PV 统计损失≈0(load 后立即上报当前页)。
- 【验证】go test ./templates/ 通过。
- 【未做】mami 接入 EdgeOne(方案C)未实施——用户暂选 A;上报 /api/send 跨洋延迟/丢失为 Vercel 托管固有限制。
## 全站外部资源加载体检(南宁凌晨实测 2 次取快值)
- 【健康】snrat 全家(uikit css/js/icons/dayjs/alpine/waline) 0.14-0.32s;www.recaptcha.net 0.17s;cf-turnstile 0.61s;bytecdntp 0.065s(注释未启用);cdn.github.red 0.15s;sentry-cdn 0.32s(仅 SentryDSN 非空才加载)。
- 【🔴唯一慢域=R2】time.box.js 0.7-1.7s(header 全站 defer 1.6KB,纯装饰 runtime 倒计时,硬编码 2023-06-01 起算,无外部依赖);neko-pixel umd 0.87s(pixel页);waline R2 源/home.html bytecdntp 均注释残留未启用(实际 waline 走 snrat)。图片类(头像/social icons)同 R2 慢但异步非阻塞,缓存头已 immutable。
- 【发现】header 的 vConsole 有 .VConsole 开关;sentry script 无 defer/async 且 tracesSampleRate=1.0——若生产 SentryDSN 启用则同步阻塞+高采样开销(需用户确认是否启用)。
- 【未决】time.box.js 优化方案待用户选(内联 footer / load 注入 / 不动);SentryDSN 生产是否配置待确认。
## time.box.js 内联到 footer(用户选"内联",备份 .workbuddy/backup/timebox-inline-20260906/)
- 【背景】加载体检确认 R2 是全站唯一慢域(0.71-1.68s),公共路径只剩 time.box.js(header 全站 defer 1.6KB 存活倒计时,无外部依赖)拖慢 DOMContentLoaded。
- 【改动】① header.html 删 <script defer r2 cdn/time.box.js>;② footer.html box_runtime 后内联紧凑版 IIFE:功能等价 + 修 2 个原版 bug(先赋值后判空会抛错;DOMContentLoaded 后首个 1s 才显示)。
- 【验证】time.box.js 引用清零(仅注释提及);go test ./templates/ 通过。
- 【待确认】SentryDSN 生产是否配置(若启用 sentry bundle 250KB 同步阻塞 + tracesSampleRate=1.0);R2 旧对象 time.box.js 可留作兼容不动。
## 测试环境部署验证通过(box-dev-api,01:19-01:21)
- 【tamami load 注入】主页+admin6:旧 defer src=mami.tama.guru 标签残留 0;addEventListener("load") 注入监听器 1;内联代码含 createElement+data-website-id 与源码一致。
- 【time.box.js 内联】主页+admin6:time.box.js 外链 0;id="box_runtime" 元素 1;footer 内联脚本(Date.UTC(2023 + setInterval(tick))就位。
- 【说明】未做浏览器 JS 行为验证(box-dev 境外站 agent-browser 有卡死史);SSR HTML 结构与源码逐字一致 + 标准 API 逻辑,判定部署生效。文案「存活」未出现因 curl 无语言 cookie 回退 en。
## EO(生产 box.tama.guru) vs CF(测试 box-dev) 速度实测对比(南宁,凌晨 01:36,http1.1)
- 【归属】prod: Server TencentEdgeOne IP 43.174.246/247.43(EO 新加坡);dev: Server cloudflare 104.21.47.52/172.67.144.141,但 CF-RAY 后缀 AMS(阿姆斯特丹)与实测 conn ~0.24s 矛盾(疑 Ray 字段不准/边缘非 AMS)。
- 【数据】①动态首页 8 轮 TTFB:prod 0.86-1.69s 均~1.22 vs dev 1.06-1.81s 均~1.49 → EO 快 ~0.27s;②连接阶段 8 轮:prod conn 0.20/TLS 中位 0.43(2次>0.9s 尖峰)vs dev conn 0.25/TLS 0.53(平稳无尖峰)→ EO 网络层快 50-100ms 但稳定性差;③favicon 静态 6 轮:prod 0.60-1.41 均0.84(有1.4s尖峰)vs dev 0.70-0.85 均0.75 → CF 反而更稳更快。
- 【结论】EO vs CF 对南宁同一量级互有胜负,无碾压差异;动态页 EO 略快,静态/稳定性 CF 优。差异更多源于回源链路+边缘抖动而非 CDN 标签本身。局限:凌晨单点、http1.1、生产/测试后端实例不同(变量未隔离);白天高峰需复测。
## EO vs CF 正式 A/B(01:40-01:47,南宁,http1.1,后端不同服务器-残余变量已注明)
- 【方法】robots.txt(静态文本近零后端耗时)30 轮交替 + 随机 query 强制回源 15 轮 + 此前 TCP/TLS/首页/favicon 数据汇总。
- 【robots 30轮】EO med 0.61s/mean 0.74/p90 1.16/>1s×7;CF med 1.16/mean 1.34/p90 2.23/全部>1s(min 1.03)。
- 【强制回源 15轮】EO med 0.83/mean 0.97;CF med 1.56/mean 1.68(CF min 1.39)→ EO 快 ~0.7s 近 2 倍。
- 【综合】TCP 0.20vs0.25、TLS 0.43vs0.53、首页 1.22vs1.49:大样本下 EO 全面领先;favicon 6轮小样本 CF 略快(缓存状态差异)不改变结论。
- 【坑】本机 Windows curl(Schannel) 不支持 --http2,勿用于 A/B;上轮 --http2 全失败 999。Bash 同名 /tmp 文件曾出现残留污染→用 $$ 文件名。
- 【说明】CF 回源走 tunnel(cloudflared)、EO 回源直连;源站不同为残余变量,strict A/B 需同源(把 dev 挂 EO 或 prod 挂 CF)。
## autoban 敏感特征补强:sensitiveFragments 片段匹配(用户选"补强 WP 特征表",备份 .workbuddy/backup/wp-fragments-20260906/)
- 【背景】生产日志见 wlwmanifest 扫描://wp/wp-includes/wlwmanifest.xml、//news/...、//2018/...(枚举前缀变体)全部 302 放行。根因:① 所有未注册路径走 f.NotFound→ctx.Redirect("/") 得 302(非 404,注释"先写404"与实际不符,flamego Redirect 覆盖状态);② TrackNotFound=false 后 404 不计数;③ IsSensitivePath 前缀匹配抓不到中部敏感段 + 特征表无 wp-includes/xmlrpc/wlwmanifest。
- 【改动 autoban.go】新增 sensitiveFragments 数组(子串匹配强指纹,仅 WP 特征业务必无):/wp-includes/、/wp-json/、/wp-cron.php、/wp-trackback.php、/wp-admin、/wp-content、/wp-login.php、/wlwmanifest.xml、/xmlrpc.php、/wordpress;IsSensitivePath 增加 Contains 第二循环(大小写不敏感)。前缀表 sensitivePaths 保留不动(防 /administratorx 类误伤语义不变)。
- 【测试】TestIsSensitivePath 补 9 正例(含日志真实形态 //wp/wp-includes/wlwmanifest.xml 等)+ 6 负例(/_/tamakyi、/_/admin6、/api/v1/question/xxx、/explore、/i18n 不误伤)。
- 【验证】gofmt clean;autoban 全测 PASS;route 带 overlay 编译+测试通过(IsSensitivePath 由 internal/route/autoban.go:35 中间件调用,改匹配器全链生效)。
## 域名迁移方案:box.tama.guru → box.shiroko.one(方案阶段,未动代码)
- 【用户决策】tama.guru 不续费(2026-12-24 到期,RDAP 实证,注册商 DNSPod)→ 硬过渡;新域 box.shiroko.one;Bing+GSC 全套;umami 随迁 mami.shiroko.one。方案文档 outputs/domain-migration-box-shiroko-2026-09-06.md。
- 【取证要点】shiroko.one 未配置(NS=spaceship,http 200 为注册商 Parking Page);tama.guru apex=导航页 Hugo/CF Pages,box 走 DNSPod 三线路(境内 EO/境外+默认 CF SaaS);mami=Vercel。
- 【SEO 现状】box sitemap 仅 8 条(/ /explore /register /login /_/tamakyi /change-logs),explore 是唯一内容页;robots Disallow /_/ 与 sitemap 提交 /_/tamakyi 矛盾;页面无 canonical;og:url/邮件链接均依赖必填配置 external_url;static/robots.txt+sitemap.xml go:embed 硬编码旧域;register.go:64 Bloglink 硬编码;censor TrustedHosts 含 tama.guru/337845818.best;R2 CORS 白名单需加新域。
- 【核心机制】301 逐 URL 是唯一可靠保排名路径;Bing Site Move 工具可用性不稳定(2021 传闻下线)仅加速器;Site Move 后新域 6 个月禁再迁;通行 301 保持 6-12 月 vs 本方案仅 3.5 月窗口=R1 主风险,靠索引面小+早切换(≤10/01)+IndexNow 缓解。
- 【301 载体推荐】旧域 DNS 切换日统一 CNAME → CF Pages 跳转项目(_redirects: /* https://box.shiroko.one/:splat 301!),与源站/旧 CDN 解耦;弃"源站 nginx 按 Host 301"(回源 Host=旧域会成环)。
- 【开放项待用户】站群 blog/status/share/chat 是否迁(影响 footer/home 内链);/_/tamakyi 是否保(决定 robots/sitemap 取舍);cloudflared ingress 是否按 hostname。
## 域名迁移批1落地:全项目 tama.guru → shiroko.one(本地代码,备份 .workbuddy/backup/domain-shiroko-20260906/)
- 【用户决策】其他 blog 之类子域一并调整到 shiroko.one;/_/tamakyi 不保(sitemap 移除,robots 维持 Disallow /_/);先做本地操作。
- 【替换规则】*.tama.guru → *.shiroko.one(box/blog/chat/share/status/mami),apex tama.guru → shiroko.one。
- 【改 11 文件】static/robots.txt(Sitemap行);static/sitemap.xml(5条换域+删/_/tamakyi+去重旧域无斜杠重复项+lastmod全刷2026-09-06);static/llms.txt(6处);static/llms-full.txt(7处);templates/base/footer.html(6条链接);templates/base/header.html(umami注释同步+验证token加迁移提醒注释);templates/home.html(站长箱/投票/Markdown/4条blog链/explore);route/auth/register.go:64 Bloglink默认值;internal/security/censor/text.go(TrustedHosts 新增 shiroko.one,**保留** tama.guru 过渡期);conf/app.sample.ini(补齐缺失的必填项 external_url,conf.go:36 强制校验,原模板缺失→照抄即启动失败);dev/eo-*.py(3脚本 --host 默认值)。
- 【关键发现】① header 的 mami src 此前已先行改为 mami.shiroko.one(仅注释待同步);② google-site-verification(636行) 与 msvalidate.01(7行+637行重复) 是域名绑定 token,新域验证后必须替换 content,否则 GSC/Bing 无法验证新站→Site Move/Change of Address 无法提交;③ app.sample.ini 缺 external_url 必填键(模板缺陷)。
- 【验证】sitemap XML 解析通过(5条无重复);go test ./templates/ + ./internal/security/censor/ 通过;route/auth、static、internal/conf 带 overlay 编译通过。
- 【方案文档同步】outputs/domain-migration-box-shiroko-2026-09-06.md:§0 补两条决策;§11 开放项标 1/2 已答并新增 token 项;新增 §12 批1实施记录;附批次标批1完成。
- 【待用户操作】blog/chat/share/status 各子域实际服务搬迁(否则新域链接成死链);censor 旧域条目待旧域到期后删。
## IndexNow key 文件 302 修复(static/index-now/<key>.txt 无法访问)
- 【现象】用户新建 static/index-now/66e5e0911b0941f2ba7dbfbbd961e9be.txt 并改 fs.go 的 //go:embed,但访问 302 跳首页。
- 【根因】internal/route/route.go:138 的 flamego.Static 中间件 **Prefix="/static"** 只接管 /static/... 前缀;而 robots.txt/sitemap.xml/llms*.txt/favicon.ico 等根目录静态文件**每个都是单独注册的 f.Get handler**。新文件无对应路由 → 落 f.NotFound → ctx.Redirect("/") 得 302。embed 本身没问题(实测 FS.Open 可读,内容正确)。
- 【修复】route.go:① import 加 regexp;② 包级 var indexNowKeyPattern = `^[0-9a-f]{32}\.txt$`(防路径穿越);③ 在 llms-full.txt 路由后加 f.Get("/index-now/{key}"),校验白名单后 static.FS.Open("index-now/"+key),Content-Type text/plain + max-age=86400,失败 404。备份 .workbuddy/backup/domain-shiroko-20260906/route.go.bak。
- 【新增回归测试】internal/route/indexnow_route_test.go TestIndexNowKeyFileRoute:正确key→200+内容;路径穿越(..%2f 与 ../../)、合法格式但不存在、后缀不符→全 404。用 flamego.NewWithLogger(io.Discard) 构造最小实例(不依赖 New())。
- 【flamego 关键行为】internal/route/tree.go placeholderTree.match 把整个路径段赋给参数,**不排除点号** → /index-now/{key} 能匹配 <key>.txt。
- 【验证】gofmt clean;带 overlay 编译 ./internal/route/ 通过;route/static/templates 三包测试 ok。
- 【部署提醒】需重新编译部署(embed+路由均在二进制内);IndexNow 提交时 keyLocation 填完整 URL https://box.shiroko.one/index-now/66e5e0911b0941f2ba7dbfbbd961e9be.txt;旧域 301 后该路径会被重定向,搜索引擎校验需 200 直出,故切换后应用新域 keyLocation 重新提交。
## 域名变更全员邮件通知脚本(dev/notify_domain_change.py,不碰项目源码)
- 【需求】用户改了站点域名,想向全体未注销用户发邮件通知;要求复用生产 conf/app.ini 的 DB+SMTP,不动项目源码。
- 【依据】① users 表软删除用 gorm.Model,未注销=deleted_at IS NULL;收件=email 列;可顺带取 name/domain 做个性化。② 发信对齐 internal/mail/mail.go:发件人"TamaBox <mail.account>",465=隐式SSL 否则 STARTTLS,支持 mail.skip_tls_verify。③ DSN 拼装见 internal/conf/db.go(postgres sslmode=disable+search_path=schema)。
- 【实现】独立 Python 脚本:读 NEKOBOX_CONFIG_PATH 或 -c,configparser 设 strict=False(容忍示例 [upload] 重复键 image_endpoint)、去引号、去行内注释;fetch_recipients 查未注销有邮箱用户;smtp_login 复用生产配置;模板用 {{占位符}} 避免与 HTML 大括号冲突。
- 【安全开关】默认 --dry-run;--send 才真发;--to 单测地址;--limit N 小批量;单条失败隔离+汇总;每封 --delay 默认 0.3s 防限流。
- 【验证】py_compile OK;对 app.sample.ini 实测 load_config 解析正确(external_url 去引号、重复键容忍);render 模板无残留占位符。依赖 psycopg2-binary(Postgres)/pymysql(MySQL)。
- 【增强·四语言】TamaBox 为四语言站点,给脚本加 LANG_TEMPLATES(zh-CN/zh-TW/en/ja),fetch_recipients 多取 language 列;发送时按 users.language 选模板,回退 用户语言→[app] default_lang→zh-CN;--subject/--html/--plain 为全局覆盖。effective_templates() 选模板,build_message() 内统一 render 后才拼装(曾一度漏 render 已在自检发现并修正)。去掉默认模板 emoji。复测:四语言 text/plain+text/html 双部、box_link/site 就位、无残留占位符(正文 base64 传输编码属正常,解码核验通过)。
## 用户问"前端还有什么要改造的" + "box.tama.guru 接 CF 自定义主机名如何配 301"
- 【诊断残留】grep 全项目:tama.guru 残留仅在 sitemap.xml 注释、header.html 验证 token 提醒注释、censor/text.go(过渡期有意保留)、outputs/(文档)——即迁移批1已完成,无业务代码残留旧域。
- 【R2 硬编码仍散落】`r2.337845818.best` 约 14 处:header.html(preconnect/waline css/favicon/OG)、pixel.html(neko-pixel css+js)、question-not-found.html & list.html(social icons + easy.qrcode.min.js)、user/profile.html(qrcode js + pickr css/js)、route/question/share.go(social icons);waline serverURL `waline.337845818.best` 在 home.html/item.html。建议抽 `CdnBase`/`WalineURL` 模板变量集中管理(为同源化/换域铺路)。
- 【301 指引(CF 自定义主机名架构)】规则建在承载该自定义主机名的 zone 上;迁移未完成前**不要**整站 301 到 shiroko.one(会断线上)。①现在做:SSL/TLS→Always Use HTTPS(http→https 自动301)+ www 规范化 Redirect Rule;②切换时做:Bulk Redirects(box.tama.guru→https://box.shiroko.one, 301, include subpaths+query)或 Single Redirect Dynamic `concat("https://box.shiroko.one", http.request.uri.path, http.request.uri.query!=""?concat("?",http.request.uri.query):"")`;③SEO 配套:sitemap 已换域重提交 GSC/Bing、旧域 DNS 保 6-12 月、header 638-639 行验证 token + 第7行 msvalidate.01 新域重新验证替换、canonical/OG 指向新域。
- 【架构提醒】A/B 实测 EO 国内比 CF 快约 2 倍(EO 0.61 vs CF 1.16,CF 走 tunnel 回源);box.tama.guru 接 CF 后国内(南宁电信)失 EO 提速优势,规划 box.shiroko.one CDN 时建议继续用 EO 或 CF China 保国内速。
## 通知脚本三要求落地(dev/notify_domain_change.py,不碰项目源码)
- 【需求】①发送前打印 DB 总用户/已注销/未注销/未注销有邮箱数,须人工 yes 确认才发;②不一口气发完留间隔;③正文明确换域原因+用户动作:新域 box.shiroko.one、旧域 2026-12-24 到期、期间 301 转发、到期后旧域停用;Why=.guru 连年涨价一年三百出头遭不住;Do=记新域+替换此前分享的二维码。
- 【改动】抽 connect_db() 供 fetch_recipients/fetch_counts 复用;新增 fetch_counts() 打统计;main 发送前打印五类统计;--send 路径加 input("yes") 人工确认(EOFError→中止,--yes 跳过);--delay 默认 1.0s,新增 --pause-every/--pause-for 分批长暂停;四语言模板全重写,加 {{old_domain}}/{{expire_date}} 占位符与「为什么/该做什么」问答段;--old-domain 默认 box.tama.guru、--expire-date 默认 2026-12-24(均可覆盖)。
- 【验证】py_compile OK;四语言 build_message 解码核验:新域名/旧域名/2026-12-24/301 均就位、无残留 {{占位符}}、text/plain+text/html 双部。
## 通知脚本二次微调(硬编码域名 + 价格模糊 + 无语言默认简中)
- 【改动】① 新旧域名+到期日改为模块级硬编码常量 NEW_DOMAIN=box.shiroko.one / OLD_DOMAIN=box.tama.guru / EXPIRE_DATE=2026-12-24,删除 --old-domain/--expire-date 参数;site 与 box_link 改用硬编码新域(不再依赖 app.external_url,故移除其空值中止判断)。② 四语言「为什么换」文案去掉具体金额与「遭不住」,改为「.guru 连续涨价/续费成本越来越高」把原因写清。③ 语言回退:空语言直接→zh-CN(不再先落 default_lang);未知语言→default_lang→zh-CN。
- 【验证】py_compile OK;源码 grep 无「三百/over 300/年間300/遭不住」;四语言渲染含 box.shiroko.one/box.tama.guru/2026-12-24/301、无占位符;回退逻辑 空/未知→zh-CN、en→en。
## 通知脚本加「断点续发」+「按语言分群」
- 【改动】① 断点续发:新增 load_sent_state/record_sent/reset_state(JSONL 逐行追加,崩溃安全);默认状态文件 DEFAULT_STATE=脚本同目录 notify_state.json(--state 可改);发送前 load 已发邮箱集合并过滤,打印「已跳过 N 封 / 剩余待发送 N 封」;每成功一封 record 一行;--reset-state(仅 --send 时)清空后全量重发;测试 --to 模式不写状态避免污染。② 按语言分群:resolve_lang() 统一语言解析(空→zh-CN、未知→default_lang→zh-CN);发送前打印「按语言分布」统计;main 发送循环改为按 LANG_ORDER=[zh-CN,zh-TW,en,ja]+其他 分组,每组打印「[语言分组 gi/gn] 语言(标签) : 本组 N 封」,组间 --group-pause 默认 3.0s(0 关);组内仍走 --delay/--pause-every。
- 【验证】py_compile OK;monkeypatch DB+SMTP 实测:RUN1(--limit 3)→发3、state 3 行且分组正确;RUN2(同 state 续发)→跳过3、发剩余3(zh-CN 组2+ja 组1)、state 6 行;RUN3(再续发)→跳过6、发0;RUN4(--reset-state)→清空并全量重发2。空语言/未知语言均归 zh-CN。
- 【增强】捕获 send_message 返回字典(RCPT TO 阶段被拒收的 {addr:(code,err)}),非空时计为「[SMTP 拒收]」失败(不计入 sent、不写断点状态、可续发重试),避免误把"已被拒收"算成成功。验证:2 收件人第 2 封返回 550 拒收→成功1/失败1、state 仅1行。
- 【用户现象】实测运行时显示"成功 N 封"但收不到信;定位:SMTP 250 OK 仅代表 RCPT TO 接收,不等于投递成功;截图收件人全是 *@admin.com(含 admin3x/admin4 等大概率不存在账号),最可能是收件不存在→退信回到 From(mail_account) 而未被监控,或发件域 SPF/DKIM 未对齐被静默丢信。建议用 `--to 真实外部邮箱 --send` 隔离验证。
- 【增强】notify_domain_change.py 加「监控报告」+「测试模式命中数据库」:① 每次运行结束写 notify_report.json(JSON,含 db_stats/lang_distribution/sent/failed/smtp_rejected/skipped_already_sent/failed_list/config(无密码)/test_mode),`--report PATH` 改路径、`--no-report` 关闭,dry-run 与 send 都会写;② `--to 邮箱` 先查库,命中则套用该用户姓名/域名/语言,未命中按 zh-CN。验证:en 命中→英文主题、未命中→中文主题、报告字段与 SMTP 拒收单独计数均正确。
## 通知脚本模板修正(box_link 路径 + 头部致谢/群发 + 底部版权)
- 【用户反馈】实测邮件内容两点问题:① 提问箱个性域名实际为 `box.shiroko.one/_/xxx`(项目 templates/profile.html 与 base/footer.html 均为 `/_/{domain}` 路径),脚本写成 `box.shiroko.one/xxx` 不对;② 邮件头部应先感谢用户使用提问箱并说明是群发站内信;③ 底部加 `© 2026 狼的提问箱`。
- 【改动】发送循环 box_link 由 `https://{NEW_DOMAIN}/{domain}` 改为 `https://{NEW_DOMAIN}/_/{domain}`;四语言 HTML+纯文本模板(zh-CN/zh-TW/en/ja)头部问候后新增「首先感谢使用(狼的提问箱)」+「这是群发的站内通知邮件,请放心查阅」两段;底部系统通知行追加 `© 2026 狼的提问箱`(英 Wolf's Question Box / 日 狼の質問箱 / 繁 狼的提問箱)。
- 【验证】py_compile OK;四语言解码核验:box_link 含 `/_/`、致谢+群发+版权关键词均就位、无残留 {{占位符}}。
## 通知脚本去外部依赖(零 pip 安装)
- 【用户痛点】当前脚本依赖 psycopg2-binary(Postgres)/ pymysql(MySQL),但环境 pip install 基本不可用,装不了第三方包。
- 【改动】DB 层改为「零依赖优先」:默认 auto 驱动优先调用系统自带命令行客户端 psql(Postgres)/mysql(MySQL),纯标准库 subprocess,无需 pip;仅当客户端都不存在时才回退 psycopg2/pymysql(可选 pip 安装)。新增 _build_pg_uri(密码 URL 编码)、_choose_driver、_psql_query(-X -t -A -F '\x1f' -P null=\N -P footer=off)、_mysql_query(临时 cnf 传密码避免进程参数泄露)、_lib_query、run_query 统一入口;fetch_recipients/fetch_counts 改为接收 args 并调 run_query;新增 --db-driver {auto,psql,mysql,psycopg2,pymysql};load_config 增加 db_sslmode(默认 disable,与旧 psycopg2 行为一致);PG 统计用 COUNT FILTER(WHERE...) ,MySQL 用 SUM(CASE...);main 打印 [DB 驱动]。
- 【验证】py_compile OK;monkeypatch 实测:驱动选择(pg有psql→psql / mysql有mysql→mysql / 无客户端但可import psycopg2→lib / 全无→psql报错路径);psql 路径解析(含空语言/空名/语言NULL→空);mysql 路径解析;E2E 经 psql 路径 dry-run 写报告、send 6 封(按 zh-CN3/zh-TW1/en1/ja1 分组、空语言+未知fr回退zh-CN)、RESUME 跳过6发0、--to 命中库用户发往其邮箱。
- 【结论】现在只需 Python 标准库 + 服务器上的 psql/mysql 客户端即可运行,彻底不依赖 pip。
## 通知脚本 bug 修复:language 列不存在导致 FATAL
- 【现象】用户用 `python3 notify_domain_change.py -c ./auto/conf/app.ini --to [email protected] --send` 报 `ERROR: column "language" does not exist`。
- 【根因】GORM 模型 `User.Language` 默认 column=language,但线上 `nekobox` 库 schema 较旧,users 表无 language 列(可能叫 lang 或根本没有该列)。脚本此前在 fetch_recipients 里硬编码 `SELECT ..., language FROM users`。
- 【修复】新增 resolve_lang_column(cfg, args):默认 auto 探测 information_schema.columns 的 candidates(language/lang),找到即用、都不存在返回 None(调用处用 `NULL AS language` 兜底,全员回退默认语言);新增 `--lang-column` 参数可手动指定(如 --lang-column=lang)。fetch_recipients 改用探测到的列名拼 SQL;main 打印 `[语言列]` 行。mock 验证 4 场景(lang 命中/lang 缺失→NULL/显式指定/language 命中)均正确。
- 【给用户建议】直接重跑即可;若库里列叫 lang 会自动识别,若真无该列则全员按 [app] default_lang(ja) 模板发送,不影响发信。
## 通知脚本「依旧报错」二次排查:版本标记 + 改名验证法
- 【现象】首轮修完用户报「依旧不行,报错一致」。初判为用户在 Linux 服务器跑的是仓库根 `/home/tamakyi/Github/Tamabox_v4/notify_domain_change.py`(state_file 路径证伪),而工作区仅 `dev/notify_domain_change.py` 一份;用户坚称没用错文件。
- 【加版本标记】SCRIPT_VERSION = "2026-09-06.langdetect.v2",main 配置头打印 `[版本] SCRIPT_VERSION = ...`。作用:运行输出若无此行即证明跑的不是最新副本,把"是否用错文件"变成客观判定(不再靠争论)。
- 【改名验证】建议用户在服务器 `mv notify_domain_change.py notify_mail_v2.py` + `find . -name __pycache__ -exec rm -rf` 后跑新名,强制重编译以排除陈旧 .pyc / 同名模块作祟(用户自己提出的怀疑点)。
- 【结论】修复后版本不可能再出现 `column "language" does not exist`(查询已无 language 字面量);若改名+清缓存后仍报该错且带 [版本] 行,则需查 information_schema 探测是否异常。