Skip to content

fix(codex): 在回合结束前发送生成图片 - #190

Open
zpdg wants to merge 1 commit into
zarazhangrui:mainfrom
zpdg:agent/fast-image-delivery
Open

fix(codex): 在回合结束前发送生成图片#190
zpdg wants to merge 1 commit into
zarazhangrui:mainfrom
zpdg:agent/fast-image-delivery

Conversation

@zpdg

@zpdg zpdg commented Jul 15, 2026

Copy link
Copy Markdown

背景

Codex 通过 imagegen 生成图片后,文件会先写入
$CODEX_HOME/generated_images/<thread-id>/。当前 Bridge 在 Codex 回合仍在运行时没有把这些文件发送到飞书的路径,因此图片即使已经生成完毕,也可能继续等待模型后续响应、网络重试或整个回合结束。

在网络不稳定或图片工具结果较大时,这段额外等待可能从数秒扩大到数分钟。一次实际排查中,图片已经落盘,但直到约 47 分钟后才被发送;瓶颈发生在图片生成之后,而不是图片生成本身。

根因

现有 Codex 适配器会处理文本和命令执行事件,但不会在运行过程中把 generated_images 中的新文件交给 channel 层。channel 层因此无法在图片准备好时直接调用已有的图片发送接口,只能依赖 Codex 后续行为或最终输出。

修改内容

  • 由 Codex 适配器暴露实际使用的 generated_images 目录,兼容显式 codexHome、profile 隔离目录、CODEX_HOME 和默认用户目录。
  • 在 Codex IM 回合运行期间,仅监控当前 Codex thread 对应的图片目录。
  • 图片大小和修改时间连续稳定后,通过现有 channel SDK 回复到触发该回合的消息。
  • 在回合结束时补做稳定性检查,避免刚生成的最后一张图片被遗漏。
  • 告知 Codex 图片由 Bridge 负责发送,避免同时调用 lark-cli 导致重复。

安全与去重

  • 校验 thread id,并使用 realpath 做根目录和 thread 目录的路径包含检查。
  • 拒绝通过符号链接逃逸 generated_images 的目录或文件。
  • 仅允许 PNG、JPEG、GIF 和 WebP,并沿用配置中的图片大小上限。
  • 仅处理当前回合开始后修改的文件,每个回合最多发送 6 张。
  • 同一路径最多发送一次。
  • 对结果不确定的上传超时不自动重试,因为飞书可能已收到请求,盲目重试会产生重复图片;此时最多发送一次失败提示。

#113 的区别

#113 从最终 Markdown 回复中提取并发送工作区产物,执行时机在 Agent 产生最终输出之后。本 PR 针对 Codex 的 generated_images/<thread-id>,在回合结束前即可发送稳定的图片文件,因此能覆盖“图片已生成,但后续 Codex 响应或网络重试仍然很慢”的场景。

如果未来合并 #113,两个发送路径应统一去重语义,避免同一图片同时被快速路径和最终 Markdown 路径发送。

验证

  • pnpm test:93 个测试文件、560 项测试全部通过。
  • pnpm typecheck:通过。
  • pnpm build:通过。
  • git diff --check:通过。
  • 在真实 Git checkout 和部署构建中均完成了上述验证。
  • 最终代码已部署到真实飞书 Bot,并确认部署的 dist/cli.js 与本分支源码构建产物逐字节一致。
  • 第一次最终版实测:图片发送时间为 12:03:57.767,回合结束时间为 12:04:00.878,提前约 3.1 秒,仅发送一次。
  • 第二次最终版实测(同一会话继续编辑图片):图片发送时间为 12:07:11.921,回合结束时间为 12:08:44.389,提前约 92.5 秒,仅发送一次。

影响范围

该快速发送路径仅对能提供 generated_images 目录的 Codex 适配器启用;Claude 和其他未实现该可选能力的 Agent 行为不变。

@zpdg
zpdg marked this pull request as ready for review July 15, 2026 06:22
Copilot AI review requested due to automatic review settings July 15, 2026 06:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Codex agent experience by delivering imagegen outputs to Feishu as soon as the generated image files become stable on disk, instead of waiting for the Codex turn to fully complete.

Changes:

  • Add a GeneratedImageDelivery watcher that polls the active Codex thread’s generated_images/<thread-id>/ directory and sends stable images during the run (with safety checks, dedupe, and a max-per-run cap).
  • Expose getGeneratedImagesDir() as an optional AgentAdapter capability and implement it for the Codex adapter (supporting configured home, profile-local home, inherited CODEX_HOME, and default home).
  • Wire the watcher into the IM run loop and add unit tests for directory resolution and delivery behavior (including symlink escape rejection and ambiguous failure handling).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/bot/generated-image-delivery.test.ts Adds unit coverage for polling/stability, per-thread isolation, symlink escape protection, and non-retry failure notice behavior.
tests/unit/agent/codex-generated-images-dir.test.ts Verifies Codex generated images directory resolution across config/env/profile scenarios.
src/bot/generated-image-delivery.ts Introduces the polling-based delivery implementation with containment checks, file type/size gating, stability detection, and dedupe.
src/bot/channel.ts Integrates fast-path image delivery into Codex runs and adds a Codex instruction to avoid double-sending via tools/CLI.
src/agent/types.ts Extends the adapter interface with an optional generated-images directory hint.
src/agent/codex/adapter.ts Implements getGeneratedImagesDir() consistent with how CODEX_HOME is configured for the Codex process.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +153 to +156
for (const entry of entries) {
if (this.delivered.size >= this.maxImages) return;
if (!entry.isFile() || !IMAGE_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue;
const candidatePath = resolve(threadDir, entry.name);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants