b93c92a3e2
Memind CI / Test, build, and release guards (push) Failing after 14m27s
The read_image poisoning fix had no end-to-end guard because scenarios could only send text. Add an upload_images step that renders and uploads visually distinct images, let chat steps attach them the same way the WeChat channel does, and assert the generated page actually embeds every upload. Co-authored-by: Cursor <cursoragent@cursor.com>
4.8 KiB
4.8 KiB
图片轮次读图工具隔离守卫
已知故障
2026-08-22 服务号用户唐连发 4 张图后要求「把那几张图片做成主题页面」,连续三次没有拿到页面。
链路是这样断的:
attachRecentMediaForFollowup正确把 4 张图挂到了这一轮,Qwen VL 也成功产出了图片描述, 并注入进 Agent 提示。- Agent 拿到描述后,仍然逐张调用
read_image去「确认图片内容」。 read_image的工具结果不是image_url,而是嵌在toolResponse.toolResult.value.content[]里的 base64image块,4 张图累计 1.5MB 以上。Goose 会把它们持久化进会话历史。- 下一次请求把整段历史发给纯文本聊天模型(DeepSeek),上游直接返回
unknown variant image_url, expected text,Agent 还没走到write_file就中断。 - 页面交付是 fail-closed 的:没有新落盘的 HTML,就不会发占位链接,用户只看到 「这次页面没有按服务号页面技能真正生成成功」。
原有的两道防线都没拦住:
conversationHasImageUrlContent只扫image_url,看不见read_image留下的工具图片块, 所以微信侧的会话轮换不会触发,同一个被污染的会话被反复复用。agent-run-gateway的SESSION_VISUAL_CONTEXT_UNSUPPORTED视觉降级是事后补救, 只覆盖 H5 Agent Run,服务号回复路径没有对应保护。
必须保留的行为
- 本轮消息带图且已配置图片模型(
llmProviderService.hasVisionKey())时,prepareSessionReplyBody必须以disableImageReading下发会话策略, 使这一轮的developer扩展不含read_image。视觉理解由图片模型独占, 聊天模型不承担读图。 - 不带图的轮次必须保留
read_image。禁用是按轮次生效的,不能把会话永久降级。 buildVisionPayload注入的提示必须明确写出「本轮不会再提供读图工具 + 禁止调用read_image」,并说明强行读图会让本轮及后续请求全部失败。- 图片模型没能产出描述时,提示必须改口为「不要臆造画面细节」,不能继续声称
「依据上面的描述写文案」。此时仍然禁用读图 —— 纯文本模型看不到图,
放开
read_image只会让整轮崩掉,而图片嵌入路径依然可用。 conversationHasToolImageContent必须能识别toolResponse/tool_response里toolResult.value.content[].type === 'image'的工具图片块, 并覆盖toolResult/tool_result/ 直接数组等结构变体。rotateWechatSessionIfImagePolluted必须同时检查image_url污染和工具图片污染, 任一命中都要在回复前换掉会话。历史遗留的被污染会话靠这条自愈。agent-run-gateway的事后视觉降级(SESSION_VISUAL_CONTEXT_UNSUPPORTED→ 新会话 +disableImageReading: true)保留为兜底,不得因为新增事前预防而删除。
改动前必跑
node --test tkmind-proxy.test.mjs chat-image-turn-scope.test.mjs wechat-mp.test.mjs agent-run-gateway.test.mjs
npm run verify:h5-session-patches
端到端(需本地 Portal 8081 + goosed):
node scripts/run-scenario-test.mjs --scenario image-theme-page
该场景上传 4 张几何图、要求做成主题页面,断言页面生成、公网 200、4 张图全部嵌入,
并禁止回复里出现 unknown variant。2026-08-22 首次通过时会话内 read_image 调用为 0、
持久化工具图片块为 0,工具序列是 load_skill → write_file → read_file。
相关代码与用例
| 位置 | 作用 |
|---|---|
tkmind-proxy.mjs · prepareSessionReplyBody |
事前禁用本轮 read_image |
tkmind-proxy.mjs · buildVisionPayload |
注入禁止读图的硬性提示 |
chat-image-turn-scope.mjs · conversationHasToolImageContent |
识别 read_image 工具图片污染 |
wechat-mp.mjs · rotateWechatSessionIfImagePolluted |
回复前轮换被污染会话 |
capabilities.mjs · withoutSessionImageRead |
从会话策略里摘掉 read_image |
| 用例 | 覆盖 |
|---|---|
tkmind-proxy.test.mjs · image turns drop read_image so vision results cannot poison the text provider |
带图轮次下发的 developer 工具不含 read_image |
tkmind-proxy.test.mjs · text-only turns keep read_image available |
纯文本轮次不降级 |
tkmind-proxy.test.mjs · submitSessionReplyForUser applies the shared Qwen vision preprocessing path |
提示中包含禁止读图约束 |
chat-image-turn-scope.test.mjs · conversationHasToolImageContent detects read_image base64 poison |
工具图片块识别,且 image_url 扫描确实看不见它 |
wechat-mp.test.mjs · wechat mp rotates a session poisoned by read_image tool results |
服务号在工具图片污染时换会话 |