Files
tamabox/.workbuddy/memory/2026-09-07.md
T
2026-09-08 10:10:44 +08:00

11 KiB
Raw Blame History

2026-09-07

notify_domain_change.py 语言列探测 v3(langprobe)

  • 上轮 information_schema 探测在线上误报(探测说 language 存在、真实查询报列不存在,疑似跨 schema 同名表干扰)。
  • 本轮重写为「真实试跑探测」detect_lang_column():依次执行 SELECT ... language / lang / NULL(LIMIT 1),哪个能跑通用哪个;NULL 兜底必然成功,不会再因列缺失 FATAL。
  • fetch_recipients(cfg, args, lang_expr=...) 与 main 调用链对齐;版本号提升为 2026-09-06.langprobe.v3。
  • 6 场景 mock 验证全过:language 报错→降级 lang;全缺失→NULL 回退 default_lang(ja);显式 --lang-column;自动探测拼 SQL 正确。
  • 测试脚本两处自身 bug(dict 键名 language 非 lang;mock 对 NULL 查询仍返回 'en')已修正后 ALL_PASS。

v4(rfc5322.schema):QQ 邮箱 550 退信 + 统计全 0 两个问题

  • 550 退信根因:From: 狼的提问箱 <tamabox@...> 中文显示名未做 RFC2047 编码,QQ(mx3.qq.com)回 550 "From header is missing or invalid"。修复:formataddr((str(Header(显示名, utf-8)), 地址)) 编码 From,Subject 同样用 Header 编码,并补 Message-ID。
  • 统计全 0 根因:psql 未限定 schema,search_path 命中了别的 schema 里的同名空 users 表(也解释了 v2 information_schema 探测误报 language 存在)。修复:新增 resolve_users_table() 枚举所有 schema 的 users 表并逐个 COUNT,选有数据(其次带 language 列)的 schema,全部查询用 "schema".users 限定;main 打印 [DB 连接] 与 [users 表] 诊断行,全空时警告。
  • 5 场景 mock 验证 ALL_PASS(schema 定位/语言探测/counts/全空回退/From+Subject 编码与 Message-ID)。版本 2026-09-07.rfc5322.schema.v4。

新项目 mail-broadcast/(站内信群发工具)

  • 用户确认 v4 后收件正常、数据正确,但邮件里提问箱链接显示成站点首页而非 /_/个性域名;要求新建独立可复用项目、四语言模板拆成独立 HTML。
  • 新建 mail-broadcast/:broadcast.py(通用化:去掉硬编码域名,--site-url/--box-prefix/--var k=v 自定义占位符;缺失占位符置空并上报)+ templates/{zh-CN,zh-TW,en,ja}.{html,subject.txt}(plain 可选,缺省由 HTML 自动抽取纯文本,HTMLParser 实现)+ README.md。
  • 保留 v4 全部 DB/SMTP 能力:schema 定位、语言列试跑探测、断点续发、分群限速、SMTP 拒收捕获、RFC2047 From/Subject、Message-ID。语言回退保持「空→zh-CN、未知→default_lang→zh-CN」。
  • 预览区直接打印每个收件人解析后的 box_link,方便发现链接问题。
  • 7 场景 mock 测试 ALL_PASS(含四语言渲染 box_link、缺失占位符上报、From 编码)。

params.ini 参数文件 + --interactive 交互模式

  • 用户新需求:运行时直接读参数文件,或运行后再交互。
  • 新增 params.ini([site] site_url/box_prefix + [vars] 自定义占位符 + [send] to/delay/group_pause),默认自动读取脚本同目录 params.ini,--params/--no-params 可控;优先级 CLI > 文件 > 默认。
  • 安全边界:参数文件不提供 --send/--yes,正式发送必须命令行显式指定 + 终端确认。
  • --interactive/-i:逐项确认 site_url/box_prefix,自动扫描模板占位符(collect_template_placeholders 排除内置)逐个提问(模板加新占位符会被自动发现),可选测试邮箱,可选保存回 params.ini(save_params)。
  • 踩坑:测试脚本两次自身 bug(回答顺序没按字母序、Args 类属性 vs 实例属性);跑挂的测试曾把 params.ini 写坏,已恢复标准值。7 场景 ALL_PASS。

conf 路径纳入 params.ini 与交互模式

  • params.ini 新增 [path] config(预填 ./auto/conf/app.ini);-c 默认值改为 None,解析优先级:CLI > params.ini > 环境变量 TAMABOX_CONFIG_PATH > ./conf/app.ini。
  • --interactive 第一步先问配置文件路径(在 load_config 之前),保存回 params.ini 时含 config 路径(save_params 签名加 config 参数)。
  • 5 场景测试 ALL_PASS(params 读取/优先级/save round-trip 含 config/交互完整流程/标准值恢复)。

交互选运行方式 + 模板对齐原程序邮件样式

  • 用户新需求:① send 之类的决策放进交互里而不是命令行;② 模板配色样式参照原程序 templates/mail/。
  • run_interactive 重构:占位符问完后进入「运行方式」选择(1 演练/2 测试/3 群发,默认回车=演练);模式 2 问测试邮箱、模式 3 可问 limit(试水)并可选择是否保留最终确认(答 n → args.yes=True)。args.send/to/yes/limit 就地写入,交互全程无需 --send/--to/--yes。
  • 四语言模板重写为原程序 Google 风格:白底 #FFFFFF、#dadce0 细边框 8px 圆角卡片(max-width 516px)、标题 24px 带下边框、正文 Roboto 14px rgba(0,0,0,0.87)、次要信息 rgba(0,0,0,0.54)、新域名高亮框 #e8f0fe/#c6dafc、按钮 #4184F3 蓝底白字、页脚 11px rgba(0,0,0,0.54)。新增 {{year}} 内置占位符(ctx 用 datetime.now),run_interactive 的 builtin 集合同步排除。
  • 测试踩坑:render() 返回 (结果, missing) 元组需解包;群发路径交互共 8 次输入。24 项检查 ALL_PASS(四语言 box_link/按钮链接/配色/#year/占位符完整 + 4 交互场景 + From 头 ASCII)。

默认交互模式 + 单文件通用模板(--single)

  • 用户新需求:① 默认就是交互模式不用手输;② 新增单文件模式(不按语言,所有人发同一个通用模板,模板单独存放便于以后复用)。
  • 默认交互:--interactive 改 default=None,parse 后判定——未给 --send/--to/--yes/--dry-run 之一即自动进入交互;--no-interactive 强制关闭(脚本化用)。
  • 单文件模式:templates/single.html + single.subject.txt(保留名,load_templates 排除不混入语言列表;load_single_template 单独加载,plain 自动抽取)。--single CLI 或交互「模板模式」选 2。发送时 grouping 固化为 single 组(不 resolve_lang),ctx 仍含每人 box_link/name/site/year;报告加 template_mode。
  • single.html 样式对齐原程序邮件,正文内容区用 HTML 注释标注「编辑这里」,含可复制的强调框示例。
  • 15 项检查 ALL_PASS(single 不混语言/加载/渲染/构建 + 4 交互组合场景)。

交互只问缺失项(不再每次重复确认战役参数)

  • 用户反馈:site_url/box_prefix/old_domain/expire_date 这类一次性参数每次交互都问很烦,直接写死到这次 html 即可。
  • run_interactive 重构:开头打印「[参数确认]」摘要(沿用 params.ini/CLI/app.ini 的值),有值一律不问;只补问 ① 完全没值的 site_url(main 里先做 external_url 兜底)② 模板用到但 extra_vars 里还没有值的自定义占位符。box_prefix 不再询问(默认 /_/,CLI 可覆盖)。
  • 配置文件路径同理:CLI/params.ini 明确给了就打印沿用不问,仅默认猜测时才 ask。
  • 想改值:编辑 params.ini,或把一次性内容写死进模板 html 并删掉对应 {{占位符}}(占位符删了就不会被问)。
  • 测试踩坑:mock 回答数按实际提问数对齐(占位符 2 个非 3 个;摘要走 print 非 input 需 redirect_stdout 捕获)。7 项检查 ALL_PASS。

文案修正:模板问句人称

  • 用户指出邮件模板「我该做什么?」应为「你该做什么?」(站方对收件人的口吻)。
  • 已修 templates/zh-CN.html 与 zh-TW.html 对应问句;渲染验证通过、无残留。en(What should I do?)/ ja(何をすればよいですか?)保持读者视角的惯用表达未改。

--limit 与断点续发顺序 bug 修复

  • 用户反馈:第一次发送部分成功,第二次 --limit 10 时「已跳过 10、剩余 0、一条没发」。
  • 根因:main 中 --limit 截取在断点续发过滤之前执行,limit 永远取名单头部那批已发用户,跳过后剩 0。
  • 修复:调整顺序为「先剔除已发 → 再取前 N」,--limit 提示语带上已剔除数量;补 had_state 标志控制「剩余待发送」打印。
  • 6 项回归测试 ALL_PASS(跳过+limit 组合、无重叠、limit>剩余、全已发、状态读写往返)。

退信核查功能(bouncecheck.v2)

  • 背景:SMTP 250≠送达,阿里云对限流/无效地址先收下再异步退信到发件箱,用户要求发送后核查真实送达。
  • 新增零依赖 IMAP 退信扫描:--check-bounces 登录发件箱(imaplib 993,IMAP 域名由 smtp.→imap. 推导),两段式拉取(先头部命中退信特征再取全文),--since-days 默认 3。
  • 退信解析三层:标准 DSN(message/delivery-status, Action:failed)→ 中文「收信地址/收件人」标签 → 正文地址兜底(剔除发件人与 daemon/postmaster);主题关键词 + multipart/report 双判定。
  • --prune-state 把退信用户从 broadcast_state.json 剔除 → 重跑自动补发;已核查 Message-ID 存 broadcast_bounces_seen.json 防重复报告。交互运行方式新增「4) 退信核查」(并移到模板模式之前,该模式跳过模板提问)。
  • 12 项单元测试 ALL_PASS(含用户真实退信格式、标准 DSN、限流退信全角冒号、prune 往返)。防御修正:正则加 (?<![A-Za-z0-9]) 防乱码前缀吞地址;测试须用 message_from_bytes 贴近 IMAP 真实路径(message_from_string 双编码会污染解析)。

退信核查拆分三档(仅核查/仅剔除/剔除并补发)

  • 用户要求「仅剔除、不一定剔除后直接发送」。交互运行方式 4 下新增子选项:1 仅核查(默认,不改清单)/ 2 仅剔除(更新清单后结束,本次不发送)/ 3 剔除并补发(剔除后继续正常发送流程,发送前仍有统计确认)。
  • CLI 对应:--check-bounces 仅核查;--check-bounces --prune-state 仅剔除;--prune-state --send 剔除并补发(main 分支在 args.send 时不 sys.exit,落回正常发送流程)。
  • 4 项交互 mock 测试 ALL_PASS;README 同步。

退信扫描起点改为群发首条记录

  • 用户反馈:固定近 3 天会把群发无关的退信/邮件扫进来。
  • 新增 resolve_scan_start():auto(默认)取 broadcast_state.json 最早一条 ts - 1 天余量;--since YYYY-MM-DD 显式指定;无记录/无 ts 回退 --since-days(默认3)。IMAP search 用 SINCE 日期,控制台打印 [扫描起点] 说明。
  • 6 项测试 ALL_PASS(最早记录-1天/显式日期/非法日期回退/无清单回退/无 ts 回退)。

移除扫描起点 -1 天余量

  • 用户反馈:提前 1 天余量导致扫描范围仍偏大。改为 auto 模式精确从群发首条记录的 ts 开始(不再 -1 天);--since 显式日期与空清单回退 --since-days 逻辑不变。
  • 3 项测试 ALL_PASS(精确首条 ts/显式日期/空清单回退);README 同步。