9.8 KiB
9.8 KiB
微信文本-图片缓存隔离事故报告
结论
本次事故已经按最新备份链路回滚并完成第二轮评审修复。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,随后 GREEN9 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、JSd73b15d6752056192a1d1939b04107334419785b41175b39f1cbc1132ac65c47;slash/channel 口径保持不变。 - 首次部署因为
-- No entries --门禁误判而回滚,随后纠正部署在线,PID 为2496603。 - 在线修复后的 hashes 为
86dfe.../a9b854...,channel 4accounts 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 4accounts 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。