Files
aios/docs/incidents/WEIXIN_TEXT_IMAGE_CACHE_ISOLATION_20260711.md
T

9.8 KiB
Raw Blame History

微信文本-图片缓存隔离事故报告

结论

本次事故已经按最新备份链路回滚并完成第二轮评审修复。3d9782fc346d1e7defde090ab8637e62ab28ab8a 进一步修复了英文诊断文本和未确认引用文本误入图片路径的问题,706c0b0 则是后续最小修复提交。当前文档只记录证据、部署和验收边界,不把“真实 owner inbound 已完成”写成结论。

现象

  • 纯文本消息被误答成旧图片回复,属于 stale-image reply on pure text。
  • 现场证据显示 types=1 且 hasMedia=false,但后续又进入 message_type=image、image_count=0、implicit_image_message,最终落到 cached_image_expired。
  • 这说明“诊断文本”被误判成图片意图后,又继承了 sender 级旧图缓存。

根因

  • fixes/patch_openclaw_weixin_hermes.py:768-839 把文本意图判断和缓存回退放在同一条链上。
  • looksLikeHermesImageFollowup() 会把含图片关键字的诊断文本推进 image intent。
  • resolveHermesFollowupImageContext() 随后会读取 sender 级缓存;一旦缓存命中失败或过期,就会落到 cached_image_expired,从而让纯文本沾上旧图上下文。
  • handleHermesImplicitImage() 继续把消息送入 handleHermes(..., "implicit_image_message"),所以误判会沿着同一条图片处理通道传播。

护栏不变量

  • 图片处理上下文必须来自当前消息图片、已确认的引用图片,或精确匹配的缓存图片 ID;缺少这三类证据时,纯文本不能进入 handleHermesImplicitImage()。
  • 未确认的引用文本不能继承 sender 级旧图缓存,必须返回 referenced_image_not_confirmed。
  • 只有精确缓存图片 ID 命中且缓存仍有效时,才允许走 resolveHermesCachedImageEntry() 的 cached_image_matched 路径。
  • /hermes 这类 slash 路径保持原有分流,slash/channel 面不变。
  • 这次修复的守卫目标,是把中英文诊断文本、未确认引用文本与图片缓存回退拆开,避免再把普通文本提升成图片意图。

TDD 与幂等性

  • 66d637fc0364a617c7b9fd8cd9baa68fb0368675(fix(weixin): isolate text from stale image cache)引入了隔离修复和 tests/test_weixin_image_followup_intent.py。
  • 2de29c59bb946cc3db3e14e8a06270391ede8401(fix(weixin): add process-only runtime patch mode)继续收紧 process-only 运行模式,保持回放幂等。
  • 3d9782fc346d1e7defde090ab8637e62ab28ab8a(fix(weixin): require confirmed image reference context)补齐英文诊断文本和未确认引用文本用例,并把图片引用收紧到已确认上下文。
  • 用户实测的 TDD 路径为 RED 8 PASS / 1 FAIL,随后 GREEN 9 PASS。
  • 最小修复提交为 706c0b0,对应 quoted pure text 的最后一层收敛修复。
  • 第二轮回归结果为 8 PASS;node check 同样通过,重复应用补丁保持幂等。
  • 生产 npm 分发侧本身缺少 tsconfig/tests,baseline 与 patched 共享这一限制,所以这里不把分发目录的缺口当成回归点;验收以 node check 和 8 tests 为准。

用户实测补充

  • 普通文本两项为 PASS。
  • quoted pure text 为 FAIL,日志证据包含 ref_msg.message_item.type=0/NONE、quoted id、implicit_image_message、referenced_image_unresolved。
  • 这组证据说明 quoted pure text 没有被当成真正的图片 follow-up,且没有越过引用确认边界。

回滚与备份

  • 回滚按最新备份执行,基线指向 /data/aios-gitops/runtime/latest-backup-dir.txt 所示的最新备份目录。
  • 备份路径链路按供给证据记录为 T064506Z、T074212Z、T081658Z。
  • 第一轮恢复后的部署过程哈希记录为 e9625d12 和 c00c58f8。
  • v2 部署备份为 /var/backups/aios/weixin-text-cache-isolation-20260711T094908Z。
  • v2 部署文件哈希为 TS 024c6498284d1871b048f6157d5d3dbd4d34e54b53a05afd744b00458ceeb79f、JS d73b15d6752056192a1d1939b04107334419785b41175b39f1cbc1132ac65c47;slash/channel 口径保持不变。
  • 首次部署因为 -- No entries -- 门禁误判而回滚,随后纠正部署在线,PID 为 2496603。
  • 在线修复后的 hashes 为 86dfe... / a9b854...,channel 4 accounts healthy,cache unchanged。

主线合并与生产发布

  • 主线 fast-forward source rev 为 9269e819972c2e709e0c27bed7eab590da6961c9,本次不 tag、不 baseline。
  • 生产发布结论为 PASS,release dir 为 /var/backups/aios/weixin-mainline-release-20260711T181842Z。
  • old PID 为 2655072,new PID 为 2656106。
  • TS hash 为 86dfe4543c4414bc86778a4b218c244a742a139c66e3c9f19cf6f0de871234c3,JS hash 为 a9b854015108020458ce6f31b9d9187eff82e170c4c214259b0afb9be6a7336f。
  • 18789 监听,channel 4 accounts healthy,error_count=0,cache unchanged。
  • 一次参数门禁失败已回滚,且不影响最终发布。
  • owner quoted-text live acceptance 仍为 PENDING。

运行态验证

  • 第一轮 gateway PID 由 4025932 变为 1723576;v2 部署后由 1723576 变为 1876713。
  • v2 现场/供给证据为 active、监听 18789、error_count=0、channel gate pass、cache preserved。
  • 最终 read-only gate 通过,说明部署后运行态未出现新的插件错误,channel 状态和缓存均保持预期。
  • quoted pure text 的第二次 owner live acceptance 仍为 PENDING。

未完成的真实验收

  • owner 纯文本
  • diagnostic text
  • genuine image-followup message
  • quoted pure text 的第二次 owner live acceptance

以上四类消息仍需要由 owner 真实 inbound 到达,才能把这次“文本隔离”结论升格为完整的线上接受;本报告明确保留该项为 pending,不把它们写成已经完成。

引用文字语义修复与发布(2026-07-12)

  • 后续 owner 实测证明图片误路由已消失,但模型在没有被引正文时根据 chat_id/message_id/timestamp 编造解释,因此图片隔离 PASS、语义验收 FAIL。
  • 现场 payload 的 ref_msg.message_item.type=0/NONE 只有 quoted ID 与时间字段,没有 text_item;session transcript 又没有原始 Weixin 数字 message ID,不能精确回表。
  • 出站 clientId 与引用的 19 位数字 msg_id 格式不同,发送 API 也不返回服务端 message ID。本次明确禁止 recent-message、时间窗口、session 顺序和近似 ID 猜测。
  • 1136b4186d7578cd77f255297204b10152c47dab 增加证据优先处理:payload 明确含 text_item.text 时才构造引用上下文;引用存在但正文缺失时,经标准 dispatcher 确定性回复“无法读取被引用文字,请复制原文后再提问。”,并在模型 dispatch 前返回。
  • TDD 先出现两项预期 RED(quoted-text resolver 不存在),最终 GREEN 为 17 passed;py_compile、git diff --check、node --check 均通过。
  • Scratch 双跑验证只改变 src/messaging/process-message.ts 与 dist/src/messaging/process-message.js,第二次应用 diff 为 0;slash/channel 文件保持不变。
  • 主线为 1136b4186d7578cd77f255297204b10152c47dab,生产发布目录为 /var/backups/aios/weixin-quoted-text-failclosed-release-20260712T054451Z。
  • 发布后 PID 从 2656106 变为 3670157;TS hash 为 1a73e735e9c8fb40a1f82d2a66bc022396c8105ed989aac95e11b66326e9fdf1,JS hash 为 1c76bf6ec3b45021e68b932cbf52222ba2130a867b95e41d7a91f261b991a304。
  • 18789、4 个 Weixin accounts、post-start error 0 和图片 cache unchanged 均通过。新的 owner live acceptance 仍为 PENDING。

Owner 引用文字 Fail-Closed 验收(2026-07-12)

  • Owner 引用一条微信未提供正文的文字消息并发送“是什么意思”,收到固定回复“无法读取被引用文字,请复制原文后再提问。”。
  • 日志窗口为 2026-07-12 14:06:23.424-14:06:23.893 +08:00:入站 types=1、bodyLen=6、hasMedia=false,出站 textLen=20 且发送成功。
  • 同一窗口 model-fetch=0、implicit_image_message=0、Hermes route=0,quoted_text_unavailable=1 且 quoted_message_id=present。
  • 该证据确认无正文引用不会进入模型猜测或图片缓存路径。Owner quoted-text no-body live acceptance 为 PASS。
  • 微信 payload 明确携带 text_item.text 时的 grounded explanation 和 genuine image follow-up 仍作为独立兼容性验收项保留。

可见引用摘要解析修复(2026-07-12)

  • Owner 进一步确认微信客户端能够显示被引用正文,因此上一节的“未提供正文”只适用于旧 resolver 的观察结果,不代表协议 payload 没有可用文本。
  • 根因是插件 inbound.ts 已将协议字段 ref_msg.title 定义为引用摘要并拼入 ctx.Body,而 extractHermesReferencedText() 只读取 ref_msg.message_item.text_item.text,导致客户端可见摘要被错误丢弃。
  • 13c2a182860131abcad0f5a70ae8263477578588 在真实 text_item.text 缺失时,仅从 reference envelope 读取 ref_msg.title;不会读取 message_item.title 或任意嵌套通用 title,也不改变图片、slash 或 channel 路由。
  • TDD 先得到 18 passed / 1 failed,失败项为可见 ref_msg.title 仍落到 quoted_text_unavailable;修复后为 19 passed,py_compile、node --check 与 git diff --check 通过。
  • 生产发布目录为 /var/backups/aios/weixin-quoted-text-title-resolution-20260712T155112+0800,PID 从 3859376 变为 3860863,post-start error 为 0,图片 cache unchanged。
  • 部署后 TS hash 为 8e77ce61214688a2507b07f0cf1ee85401b58198b8271bebd69efc2681872726,JS hash 为 663db8d30a5cd5eb80fe46cf941cce6e6f5d04cb7e4b8d9b4d0086d87faba290。
  • 可见正文引用的 owner live acceptance 尚待下一条真实微信消息确认;预期日志必须命中 quoted_text_resolved,且同一窗口不得出现 quoted_text_unavailable 或图片路由。

安全说明

  • 本次仅更新本报告,没有修改代码、runtime、services 或 secrets。
  • 报告里没有任何凭据、密钥或原始敏感 ID。