feat: 飞书历史图片重构 — 话题首条带标签走多模态, 纯历史走文本路径 - #242
Conversation
历史消息中 interactive 类型卡片被白名单 drop,导致 bot 拉不到话题开头的卡片内容(如 SpaceX/XOVR 分享卡)。 - 新增 formatInteractiveCard():递归解析卡片 header.title + elements (div/markdown/note/column_set/action),未知 tag 走 content 字段兜底 - client.ts fetchRecentMessages 白名单加入 interactive,parsing 分支调用 formatInteractiveCard - message-parser.ts formatMergeForwardSubMessage interactive 分支同步切换 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
覆盖 header/div/fields/note/column_set/action 已知 tag,以及第三方卡片未知 tag 的兜底,共 12 个用例。同步更新 merge-forward.test.ts 的 interactive 用例。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ImageAttachment 加可选 label 字段,buildMultimodalPrompt 在每张带 label 的图片前插入 [图片说明: <label>] 文本块,让 agent 区分图片来源(当前消息/ 话题首条/引用消息),防止混淆为同一组图。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
覆盖 5 个场景:全部带 label / 无 label / 混合 / 与 documents 顺序 / 无图片。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
解决两个生产问题: 1. 话题首条图片(用户提问的核心图)与后续无关图片混在一起,agent 经常分析错对象 2. 纯历史图片占用大量多模态 token 预算,污染上下文 改动: - 新增 fetchTopicRootImages: 用 threadId 单独 getMessageById 取话题根消息, 带 label='话题首条消息的图片' 注入多模态。进程内 LRU 缓存(max 100)避免 resume 时重复 fetch。 - 新增 formatHistoryImageHints: 把纯历史图片落盘路径渲染成 [历史聊天图片] 文本提示,前置到 prompt,让 agent 用 Read 工具按需读取。 - downloadHistoryImages 重构: 不再返回 base64,仅返回 historyImagePaths + lazyHints。话题首条由 fetchTopicRootImages 处理,这里跳过去重。 - HistoryResult: 删除 images/savedImagePaths,新增 topicRootImages + historyImagePaths。 - buildHistoryContext: 并行执行 fetchTopicRootImages + downloadHistoryImages + downloadHistoryFiles。 - 合并点: 当前消息图打默认 label='用户当前消息的图片',话题首条合入多模态, 纯历史图走文本前置 + restartImagePaths 兜底。Resume 时跳过两类注入。 - injectQuotedMessage 引用图带 label='用户引用的消息中的图片'。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- lazy-history-images: 改用 historyImagePaths/lazyHints 字段,新增 topicRoot 排除用例 + saveMessageFileToCache 失败兜底用例。 - inject-quoted-message: 断言引用图片带 label='用户引用的消息中的图片'。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Claude finished @lishuceo's task in 4m 54s —— View job Claude Code Review —
|
| downloadHistoryFiles(messages, parentMsgCount), | ||
| ]); | ||
| const fileTexts = [...historyFiles.fileTexts, ...imagesResult.lazyHints]; | ||
| const historyImagePaths = [...topicRoot.savedPaths, ...imagesResult.historyImagePaths]; |
There was a problem hiding this comment.
🟡 话题首条图片同时走多模态 + 历史文本提示,信号自相矛盾(confidence: 85)
这里把 topicRoot.savedPaths 合并进 historyImagePaths:
const historyImagePaths = [...topicRoot.savedPaths, ...imagesResult.historyImagePaths];下游 executeClaudeTask / executeDirectTask 的非 resume 分支会同时:
- 把
topicRootImages当作多模态注入,带标签[图片说明: 话题首条消息的图片]; - 把
historyImagePaths(包含同一张图的 savedPath)渲染成formatHistoryImageHints的提示 —— 文案是:[历史聊天图片] 历史消息中的图片未自动展开,已落盘到本地,如需查看请使用 Read 工具读取
也就是说,agent 一边能直接看到话题首条的图片,一边又被告知"这张图未自动展开,请用 Read 读取"。这与 line 2461 注释 "纯历史图片转成文本提示" 自相矛盾,且会诱导 agent 浪费一次 Read 调用读重复图片。
建议把"用于 restartImagePaths 兜底"和"用于文本 hint 展示"两条用途分开:
- 让
historyImagePaths(hint 用)只包含纯历史路径(不含话题首条); - 话题首条的 savedPaths 单独放在另一个字段(如
topicRootSavedPaths),只用于 push 到restartImagePaths,不进 hint。
或者在 formatHistoryImageHints 入参上排除掉 topicRoot.savedPaths。
There was a problem hiding this comment.
const historyImagePaths = [...topicRoot.savedPaths, ...imagesResult.historyImagePaths];,话题首条图片的 savedPath 仍会被 formatHistoryImageHints 渲染为 "未自动展开" 文本提示(line 2470 / 3067),与同一张图片以 [图片说明: 话题首条消息的图片] 直接多模态注入形成自相矛盾。
| } else { | ||
| // 非 resume:正常合并历史文件 | ||
| // 合并历史消息中的图片 | ||
| if (history.images && history.images.length > 0) { | ||
| images = [...(history.images), ...(images ?? [])]; | ||
| // 当前消息图片打标签(用户主动发的图片) | ||
| if (images?.length) { | ||
| images = images.map(img => ({ ...img, label: img.label ?? '用户当前消息的图片' })); | ||
| } | ||
| // 话题首条图片(自带 label)合并进多模态 | ||
| if (history.topicRootImages && history.topicRootImages.length > 0) { | ||
| images = [...(images ?? []), ...history.topicRootImages]; | ||
| } | ||
| // 纯历史图片转成文本提示,前置到 effectivePrompt(避免污染多模态) | ||
| if (history.historyImagePaths && history.historyImagePaths.length > 0) { | ||
| const hint = formatHistoryImageHints(history.historyImagePaths); | ||
| if (hint) { | ||
| effectivePrompt = hint + '\n\n---\n\n' + effectivePrompt; | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Resume 分支没有给当前消息的图片打标签,与引用图片混合时出现"半标签"状态(confidence: 80)
标签注入只在 else(非 resume)分支内:
} else {
if (images?.length) {
images = images.map(img => ({ ...img, label: img.label ?? '用户当前消息的图片' }));
}
...
}而 injectQuotedMessage 在 if/else 之外调用(line 2481),始终会给引用图片打 '用户引用的消息中的图片'。结果:
| 模式 | 当前消息图片 | 引用图片 |
|---|---|---|
| 非 resume | ✅ [图片说明: 用户当前消息的图片] |
✅ [图片说明: 用户引用的消息中的图片] |
| resume | ❌ 无 label | ✅ [图片说明: 用户引用的消息中的图片] |
在 resume + 同时带当前图 + 引用图的场景下,agent 收到的 content blocks 是 [image, text(引用 label), image, text(main)] —— 第一张图没有任何说明,与第二张明显不对称。
这恰好是 PR 主旨想解决的场景(长对话区分图片来源),却被 resume 分支绕过了。
建议把标签注入移出 if/else(在 buildHistoryContext 调用后立即执行),或在 resume 分支里也加一份。executeDirectTask line 3050-3052 有同样问题。
There was a problem hiding this comment.
images.map(... label ?? '用户当前消息的图片') 仍只在 else(非 resume)分支内:executeClaudeTask line 2461-2463、executeDirectTask line 3058-3060。而 injectQuotedMessage 始终给引用图片打 label。Resume + 当前图 + 引用图场景下仍是 "半标签" 不对称(当前图无 label,引用图有 label)。
|
Claude finished @lishuceo's task in 2m 9s —— View job Claude Code Review —
|
Deep-review (#242) 高置信度问题修复: 1. fetchTopicRootImages 早返回路径不写缓存,transient 错误每轮 resume 都重新打 API。引入 cacheEmpty() 哨兵,所有失败/空/无图分支都缓存。 2. post-body 解析用 typeof key === 'string' 守卫,避免 image_key=null /数字时把非字符串塞进 downloadMessageImage。Object.values fallback 改为只取 zh_cn/en_us/ja_jp 显式 key,不再吞 title 等无关字段。 3. imageKeys 截断到 MAX_HISTORY_IMAGES (5),防御异常 post 携带巨量 img 把多模态 payload 撑爆。 4. 缓存 hit 时把 '' rootMessageId 哨兵转为 undefined,语义对外清晰。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
覆盖 deep-review 指出的测试缺口: - msg_type=image / msg_type=post 解析 - imageKeys 上限截断 - 非 string image_key 防御 - LRU 缓存命中不重 fetch - 哨兵机制:空结果/transient 错误/无图都不再 hammer API - 部分下载失败仍返回成功部分 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Claude finished @lishuceo's task in 1m 25s —— View job Claude Code Review —
|
Summary
解决飞书机器人长期存在的两个图片处理问题:
实施 方案 1+3:
关键改动
ImageAttachment新增label?: string,buildMultimodalPrompt在带 label 的图片前插 `[图片说明: ]` 文本块fetchTopicRootImages: 用 threadId 单独getMessageById取根消息(进程内 LRU 缓存)formatHistoryImageHints: 渲染 `[历史聊天图片]` 文本提示downloadHistoryImages不再返回 base64,仅返回 pathsTest plan
npx vitest run— 1452 passed (新增 multimodal-image-labels 5 例 + lazy-history-images 拓展)npm run typecheck— green🤖 Generated with Claude Code