feat(core): bound image reads for reliable zoom - #7911
Conversation
|
Thanks for the PR! Re-run after five autofix rounds and maintainer verification. Template looks good ✓ Problem: this is a feature follow-up to #7809 (zoom tool), not a bug fix. The motivation is observed: large source images exceed the legacy inline-media limit or get downscaled inconsistently by different providers, hiding small details before the model sees them. Returning one deterministic bounded overview with zoom guidance is a natural next step. Direction: aligned — reliable image ingestion is core to the coding agent experience, and this builds directly on the already-merged zoom tool. No CHANGELOG reference needed for an in-progress feature arc. Size: 642 production lines (image-view.ts 314 new, zoom-image.ts 216 refactored, fileUtils.ts 112) + 550 test lines across 5 test files. Above the 500-line awareness threshold for core paths — flagged here for the record. Maintainer @wenshao has reviewed, verified locally, and approved. Approach: the scope feels right. Extracting the shared rendering path into Risk: no elevated risk signals — none of the changed files match the high-risk path patterns. Moving on to code review. 🔍 中文说明感谢贡献!这是经过五轮 autofix 和维护者验证后的重新审查。 模板完整 ✓ 问题:这是 #7809(放大工具)的功能后续,不是 bug 修复。动机来自实际观察:大尺寸源图超过旧内联媒体限制,或被不同 provider 以不同方式缩小,导致小细节丢失。返回确定且有界的概览是自然的下一步。 方向:对齐——可靠的图片处理是编码代理体验的核心,且直接基于已合并的放大工具。 规模:642 行生产代码 + 550 行测试代码。超过核心路径 500 行关注阈值——在此记录。维护者 @wenshao 已审查、本地验证并批准。 方案:范围合理。提取共享渲染路径到 风险:无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewIndependent proposal: I would extract the shared rendering constants and pipeline from Comparison with the diff: the PR matches this approach closely. The extraction into No critical blockers found. A few observations:
CI test evidence
Ubuntu test suite passed. macOS and Windows tests were skipped (infra configuration, not failures). No red checks. Sandboxed verification is unavailable for this fork PR (the author lacks write access). Maintainer @wenshao has verified locally with real builds and image reads (see comments below) and triggered 中文说明代码审查独立方案: 我会将共享渲染常量和管线从 与 diff 对比: PR 与此方案高度一致。 未发现关键阻塞问题。测试现在使用真实 sharp 生成的图片而非伪造字节缓冲区,测试保真度显著提升。 CI 测试证据Ubuntu 测试套件通过。macOS 和 Windows 测试被跳过(基础设施配置,非失败)。无红色检查。 沙箱验证对此 fork PR 不可用(作者无写权限)。维护者 @wenshao 已使用真实构建和图片读取进行本地验证,并触发了 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — clean extraction, comprehensive tests, green CI, maintainer-verified. This is what a good follow-up PR looks like. The zoom tool landed in #7809 with its own rendering pipeline; this PR extracts that pipeline into a shared module and hooks it into the file-read path so every static PNG/JPEG/WebP read returns a bounded, oriented JPEG overview with zoom guidance — one deterministic coordinate frame for every provider. The fallback for non-renderable inputs (corrupt, animated, sharp missing) preserves the legacy forward-verbatim behavior, so nothing regresses. The tests are the strongest part: real sharp-generated images, EXIF rotation, transparency flattening, budget assertions, and format-passthrough checks. Five autofix rounds addressed earlier review feedback, and the maintainer verified the real product locally with before/after evidence. No reservations. Shipping this. 中文说明置信度:5/5 —— 干净的提取、全面的测试、绿色 CI、维护者已验证。 这是一个优秀的后续 PR。放大工具在 #7809 中落地时自带渲染管线;本 PR 将该管线提取为共享模块并接入文件读取路径,使每个静态 PNG/JPEG/WebP 读取都返回有界、按方向处理的 JPEG 概览和放大引导——为所有 provider 提供统一的坐标框架。不可渲染输入(损坏、动画、sharp 缺失)的回退保留了旧的转发原始字节行为,无回归。测试是最强的部分:真实 sharp 生成图片、EXIF 旋转、透明铺白、预算断言和格式透传检查。五轮 autofix 处理了早期评审反馈,维护者已使用真实产品进行本地 before/after 验证。 无保留意见。准备合并。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
审查完成,但 GitHub 发布操作被取消,评论尚未发出。 结论: 现有机器人所称“未处理异常”不准确:外层 catch 会转换为 ✅ completed |
|
@qwen-code /takeover |
1 similar comment
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Review Round SummaryFeedback triage
What changedBoth findings identified that the
The fix adopts @doudouOUC's simpler suggestion: catch all A regression test was added: a corrupt Conflict notesNo conflicts ( Verification
中文说明审查轮次总结反馈分类
变更内容两个发现都指出
修复采纳了 @doudouOUC 更简洁的建议:捕获所有 新增了回归测试:损坏的 冲突说明无冲突( 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
中文说明
已审查——无阻断问题。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Review Feedback Summary — PR #7911Feedback points1. [rc:3666858578] Misleading test name in
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: invariant check (state, timers, collections) on packages/core/src/tools/zoom-image.ts, invariant check (counters, return values, error taxonomies) on packages/core/src/tools/zoom-image.ts, invariant check (config fields, early returns) on packages/core/src/tools/zoom-image.ts — the agent returned no evidence of its walk twice. Not reviewed: the invariant check (state, timers, collections) on packages/core/src/tools/zoom-image.ts, the invariant check (counters, return values, error taxonomies) on packages/core/src/tools/zoom-image.ts, the invariant check (config fields, early returns) on packages/core/src/tools/zoom-image.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.
中文说明
已审查。 建议见行内评论。 未审查:invariant check (state, timers, collections) on packages/core/src/tools/zoom-image.ts、invariant check (counters, return values, error taxonomies) on packages/core/src/tools/zoom-image.ts、invariant check (config fields, early returns) on packages/core/src/tools/zoom-image.ts——该 agent 连续两次未返回任何检查过程的证据。 未审查:不变量检查(状态、定时器、集合)(packages/core/src/tools/zoom-image.ts)、不变量检查(计数器、返回值、错误分类)(packages/core/src/tools/zoom-image.ts)、不变量检查(配置字段、提前返回)(packages/core/src/tools/zoom-image.ts)——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。 未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。
— qwen3.7-max via Qwen Code /review
Local verification — real build & real image reads, head
|
| arm | @img/* present |
renderImageOverview("small.png") |
|---|---|---|
published layout (matches npm i -g) |
no | renderer_unavailable |
identical layout + @img/* copied in |
yes | OK → 640×400, 3,268 B |
On main this gap is bounded — zoom_image is an optional deferred tool, so it just reports itself unavailable (I flagged the same packaging gap during the #7809 review). With this PR the gap lands on read_file / @-attachments / read_many_files, and images stop being readable at all rather than merely un-zoomable.
🔴 B2 — Animated WebP reads now hard-fail
.webp is in CANONICAL_IMAGE_MIME_TYPES, so an animated WebP reaches prepareImage, trips (metadata.pages ?? 1) > 1, and returns READ_CONTENT_FAILURE — "Only static images are supported". On main the bytes were forwarded verbatim. The PR body lists GIF/BMP/audio/video/SVG/PDF as unchanged; animated WebP isn't in that list but is equally affected.
Rejecting animated input in zoom_image is correct — it's opt-in. Rejecting it in read_file removes a capability.
🔴 B3 — Non-canonical content behind a canonical extension now hard-fails
Measured precisely (real renderImageOverview):
| file | content | before | after |
|---|---|---|---|
x.png |
JPEG bytes | forwarded | ✅ renders |
x.png |
WebP bytes | forwarded | ✅ renders |
x.jpg |
PNG bytes | forwarded | ✅ renders |
x.png |
GIF bytes | forwarded | ❌ READ_CONTENT_FAILURE |
x.png |
TIFF bytes | forwarded | ❌ READ_CONTENT_FAILURE |
x.png |
AVIF bytes | forwarded | ❌ READ_CONTENT_FAILURE |
Cross-canonical mismatches are fine; GIF/TIFF/AVIF/BMP behind a .png/.jpg/.webp name are not. A truncated/corrupt PNG flips the same way (that one is arguably an improvement — your call).
One fix covers all three
In the catch around renderImageOverview in fileUtils.ts, keep the FILE_TOO_LARGE mapping for source_too_large / output_too_large, and for every other ImageViewError code fall through to the legacy inline-bytes branch that already sits directly below (it still enforces the 9.9 MB / base64 limits, which is exactly right for a source we could not render). That turns renderer_unavailable, animated_image, unsupported_image and decode_failed back into main's behaviour instead of an error, and leaves the happy path untouched.
Independently of this PR, sharp + the needed @img/* should be declared in scripts/prepare-package.js and staged by build-standalone-release.js / create-standalone-package.js, or the feature never actually reaches users.
What I verified, and what it showed
16 real fixtures through the real read path, same harness on both arms, only fileUtils.ts / zoom-image.ts swapped. The wins are real: a 17.6 MB and an 83.6 MB screenshot that main rejected outright now return bounded overviews. The legacy paths are genuinely untouched: GIF, TIFF, AVIF/HEIF, SVG and audio come back byte-identical to main.
The motivating case end to end. main: file_too_large, the model sees no pixels. PR: overview text + a 1389×868 / 220 KB JPEG at 1,550 of 1,568 patches. I also ran the honest counterfactual for the zoom round-trip — cropping the overview the model already holds and upscaling it, versus a real zoom_image call. The overview alone already resolves the layout, the KPI values and the 8.5 px table; only the 6.5 px axis labels genuinely need the round-trip. That is the split your design assumes, and it holds.
Mutation matrix, 13/20 killed by your own tests. The budget, orientation, flatten, upscale-cap and canonical-set semantics are all tightly pinned — IMAGE_MAX_UPSCALE 8→9 still dies, and boundedSize(…, 1) → IMAGE_MAX_UPSCALE on the overview dies in 6 places. Notes on the survivors:
- The three 100 MB guards mutually mask each other. Removing the
fileUtilspre-check alone, or theimage-viewstat guard alone, or the post-readFilebytes.lengthcheck alone, is invisible; removing all three fails 2 tests. Thebytes.lengthre-check added in the autofix round is the one nothing reaches — and since it runs afterfs.readFilehas already allocated, it can report the overrun but not prevent it. - The 9 MB output guard is unreachable. Worst case at the patch budget, measured on pure noise at 1568×784 q92 4:4:4, is 2.05 MB. Dead branch in both the overview and zoom paths.
- Quality 92, chroma 4:4:4 and
lanczos3have no assertion — alanczos3 → nearestswap would visibly alias every overview and no test notices.
Cost, measured out-of-process with /usr/bin/time -l:
IMAGE_MAX_SOURCE_BYTESis not a memory bound. The most expensive read in my whole fixture set is a 0.74 MB file — a 16000×16000 PNG, 357–362 MB peak RSS, ~300 ms — whichmainnever decoded at all. Memory tracks pixels; sharp'slimitInputPixels: true(268 Mpx) is the real ceiling. Worth stating explicitly in the risk section, and a pixel budget would bound it far better than the byte cap.sharp(filePath)→sharp(Buffer)costs the source size in RSS. Same pipeline, same crop, identical output bytes: 106.7 → 194.7 MB on an 83.6 MB source, i.e. +87 MB, in exchange for ~25% less wall time (one read instead of two). Fine for the overview, which must decode everything anyway; forzoom_imageit gives up libvips' demand-driven read for a crop that may touch 1% of the image. Non-blocking, but it is a real trade and it isn't in the PR description.
Other behaviour I checked and would not change:
readManyFilesdegrades gracefully — a failing image becomes error text inline and its siblings are unaffected; one bad file does not poison a glob.convertToFunctionResponsehandles the new two-part shape correctly (text →response.output, image →parts); it's the same pathzoom_imagehas been using since feat(core): add full-resolution image zoom tool #7809. No base64 leaks as model-visible text.read_fileis already inCOMPACTABLE_TOOLS/FILE_PATH_TOOLS, so microcompaction behaviour is unchanged in class.- Extreme aspect ratios collapse:
panorama.png20000×40 → overview 1568×3, using 56 of 1568 patches. The patch budget alone would allow it natively; the 1568 max-edge is what destroys it. Consistent with the existing zoom budget and with provider conventions, so not a regression — just worth knowing that onlyzoom_imagerecovers anything from a very wide image. - Small flat assets get larger:
alpha.webp270 B → 1,349 B,exif6.jpg2,382 B → 3,082 B. Immaterial for tokens (patch count is unchanged), but@-attaching a folder of UI icons now sends more bytes.
Gates
| gate | result |
|---|---|
| the 6 spec files this PR touches | 326 / 326 pass |
| same specs with base sources overlaid | 11 fail — the discriminating set |
prettier --check on all 8 changed files |
pass |
eslint on all 8 changed files |
pass |
tsc --noEmit (packages/core) |
0 errors in any changed file; the 186 reported errors are all in ide-client.ts / client.test.ts / sessionService*.test.ts and are local toolchain drift (my worktree resolves tsc 6.0.3 vs the lockfile's 5.8.3) |
full packages/core suite |
17,431 pass / 19 fail — all pre-existing. Run in isolation, the same three files fail more on the merge base than on this PR (16 vs 11), so it's local flake, not the PR |
Method
Isolated worktree detached at 974b1c35a5; merge-base b3873571aa. Before/after produced by overlaying only fileUtils.ts / zoom-image.ts from the base while keeping everything else, so both arms run the identical harness. Fixtures generated with sharp + a Playwright-rendered dashboard (6.5 px labels at deviceScaleFactor 3, photographic grain so the PNG is genuinely incompressible — 4800×3000, 17.6 MB). Peak RSS via /usr/bin/time -l, 2–3 runs each; sharp 0.34.5 / libvips 8.17.3, macOS arm64, Node 22.23.1. Every mutant was checked to produce a real diff before its run and reverted after.
The head moved from 6d1e431f2a to 974b1c35a5 while I was working (the decode_failed rename); everything above was re-run at the final head.
中文说明
本地验证 — 真实构建与真实图片读取,head 974b1c35a5
我在隔离 worktree 中本地构建并运行(merge-base b3873571aa),用真实的 processSingleFileContent / readManyFiles / renderImageOverview 读取磁盘上的真实图片,而不是内存里的假数据。功能本身可用且测试扎实。我阻塞在三点上,而它们共用同一个小修复。
结论:修完再合。 设计方向正确,共享预算模块是实打实的改进,11 个相对 main 有区分度的测试确实钉住了行为。但本 PR 把图片解码搬到了通用读取路径上,三类原本可用的输入在这条路径上变成硬失败——其中在已发布构建里,是所有 PNG/JPEG/WebP。
🔴 B1 — 已发布的 npm 包从不安装 sharp,因此所有标准图片读取都会失败
sharp 并未由 packages/core 或 packages/cli 声明,它进入 node_modules 只是 workspace 传递依赖(@qwen-code/mobile-mcp → mobilewright → @mobilewright/core → sharp@0.34.5);而 scripts/prepare-package.js:305-322 发布的是 dependencies: {} 加一份不含它的固定 optionalDependencies。我对着真实发布的包核对过(npm pack @qwen-code/qwen-code@0.21.0):没有 sharp,没有 @img/*。
对照实验:用仓库自己的 esbuild 选项(含 inject: scripts/esbuild-shims.js)构建同一个 bundle,放进发布包的目录结构,唯一变量是 @img/* 是否可解析——不可解析时 renderer_unavailable,拷进去后正常输出 640×400 / 3,268 B。
在 main 上这个缺口是有界的:zoom_image 是可选的延迟工具,npm 安装的 CLI 只会报该工具不可用(#7809 复查时我提过同一个打包缺口)。到了本 PR,缺口落在 read_file / @ 附件 / read_many_files 上,图片从「无法放大」变成「完全读不了」。
🔴 B2 — 动图 WebP 读取变成硬失败
.webp 在 CANONICAL_IMAGE_MIME_TYPES 中,动图 WebP 会进入 prepareImage,命中 (metadata.pages ?? 1) > 1,返回 READ_CONTENT_FAILURE — "Only static images are supported"。main 上是原样透传。PR 描述把 GIF/BMP/音频/视频/SVG/PDF 列为不变,动图 WebP 不在列表里,却同样受影响。zoom_image 拒绝动图是对的(那是主动调用);read_file 拒绝则是丢掉了一项能力。
🔴 B3 — 扩展名是标准格式、内容不是的文件变成硬失败
实测:.png 里装 JPEG / WebP 字节、.jpg 里装 PNG 字节都正常;.png 里装 GIF / TIFF / AVIF 字节则全部 READ_CONTENT_FAILURE,而 main 会透传。截断损坏的 PNG 同样翻转(这一条算改进还是回退,由你们定)。
一个修复覆盖三点
在 fileUtils.ts 中 renderImageOverview 的 catch 里,保留 source_too_large / output_too_large → FILE_TOO_LARGE 的映射,其余 ImageViewError 一律落回紧挨其下的原有 inline-bytes 分支(它仍然执行 9.9 MB / base64 限制,对渲染不出来的源正合适)。这样 renderer_unavailable、animated_image、unsupported_image、decode_failed 都恢复成 main 的行为,正常路径完全不受影响。
与本 PR 相对独立地,sharp 与所需 @img/* 应在 scripts/prepare-package.js 中声明,并由 build-standalone-release.js / create-standalone-package.js 打包,否则这个特性根本到不了用户手里。
验证到了什么
- Fig 1:16 个真实 fixture 走真实读取路径,两臂同一套 harness,只替换
fileUtils.ts/zoom-image.ts。收益是真的:main直接拒绝的 17.6 MB 与 83.6 MB 截图现在返回有界概览。旧路径确实没动:GIF、TIFF、AVIF/HEIF、SVG、音频返回结果与main逐字节一致。 - Fig 2:端到端的目标场景。
main返回file_too_large,模型看不到任何像素;PR 返回概览文字 + 1389×868 / 220 KB JPEG(1,568 patch 用了 1,550)。我还做了放大往返的诚实对照——把模型已持有的概览裁同一区域再放大,对比真实zoom_image。概览本身已能读出版式、KPI 数值和 8.5 px 表格,只有 6.5 px 的坐标轴标签确实需要往返。这正是你们设计所假设的分工,结论成立。 - Fig 3:变异矩阵 13/20 被 PR 自带测试杀死。预算、方向、白底、放大上限、标准格式集合都钉得很紧(
IMAGE_MAX_UPSCALE 8→9仍会被杀)。存活项说明:三处 100 MB 守卫互相遮蔽(单独去掉任意一处都无人察觉,三处全去才失败 2 个测试),其中 autofix 轮次新增的bytes.length复查无人触达,且它在fs.readFile已分配之后才运行,只能报告不能预防;9 MB 输出守卫不可达(patch 预算下纯噪声实测最大输出仅 2.05 MB);quality 92 / 4:4:4 /lanczos3无任何断言,把lanczos3换成nearest会让每张概览肉眼可见地锯齿化而无人发现。 - Fig 4:
/usr/bin/time -l实测成本。IMAGE_MAX_SOURCE_BYTES不是内存上界——整组 fixture 里最贵的一次读取来自一个 0.74 MB 的文件(16000×16000 PNG,峰值 357–362 MB RSS,约 300 ms),而main根本不解码它;内存跟随像素数,真正的天花板是 sharp 自己的limitInputPixels: true(268 Mpx)。建议在风险段落写明,并考虑加像素预算。sharp(filePath)→sharp(Buffer)的代价是源文件大小的常驻内存:同一条流水线、同一裁剪、输出字节完全一致,83.6 MB 源上从 106.7 MB 涨到 194.7 MB(+87 MB),换来约 25% 的耗时下降(少读一次盘)。概览路径无所谓(本来就要全量解码),但zoom_image因此放弃了 libvips 的按需读取,而裁剪可能只碰到 1% 的图像。不阻塞,但这是实打实的取舍,PR 描述里没写。
其他已核对、我不建议改的行为:readManyFiles 降级得体(失败图片就地变成错误文本,不影响同批其他文件);convertToFunctionResponse 正确处理新的两 Part 形态(文本进 response.output,图片进 parts),与 #7809 起 zoom_image 走的是同一条路,没有 base64 以文本形式泄漏;read_file 本就在 COMPACTABLE_TOOLS / FILE_PATH_TOOLS 中,压缩行为类别不变;极端长宽比会塌陷(panorama.png 20000×40 → 概览 1568×3,1568 个 patch 只用了 56 个)——这由 1568 最长边而非 patch 预算造成,与既有 zoom 预算和 provider 惯例一致,不算回退,但很宽的图只有 zoom_image 能救;小的扁平素材反而变大(alpha.webp 270 B → 1,349 B,exif6.jpg 2,382 B → 3,082 B),对 token 无影响(patch 数不变),但 @ 附加一整个图标目录时字节数会上升。
关卡
| 关卡 | 结果 |
|---|---|
| 本 PR 改动的 6 个 spec | 326 / 326 通过 |
| 同样的 spec 覆盖 base 源码 | 11 个失败 —— 有区分度的集合 |
prettier --check(8 个改动文件) |
通过 |
eslint(8 个改动文件) |
通过 |
tsc --noEmit(packages/core) |
改动文件 0 错误;报出的 186 条全在 ide-client.ts / client.test.ts / sessionService*.test.ts,属本地工具链漂移(我的 worktree 解析到 tsc 6.0.3,锁文件是 5.8.3) |
packages/core 全量 |
17,431 通过 / 19 失败,全部既有问题。单独重跑时这三个文件在 merge-base 上失败得更多(16 vs 11),属本地 flake,与本 PR 无关 |
方法:worktree detach 在 974b1c35a5,merge-base b3873571aa。前后对比只从 base 覆盖 fileUtils.ts / zoom-image.ts,其余保持不变,因此两臂跑的是同一套 harness。fixture 由 sharp 与 Playwright 渲染的 dashboard 生成(deviceScaleFactor 3 下 6.5 px 标签,叠加照片级颗粒使 PNG 真正不可压缩——4800×3000,17.6 MB)。峰值 RSS 用 /usr/bin/time -l,每项 2–3 次;sharp 0.34.5 / libvips 8.17.3,macOS arm64,Node 22.23.1。每个变异体都先确认产生了真实 diff 再运行,运行后还原。验证期间 head 从 6d1e431f2a 变为 974b1c35a5(decode_failed 重命名),以上全部在最终 head 上重跑过。
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Autofix review round summary (PR #7911)Both inline suggestions from the automated reviewer were addressed in code. No Feedback points
Conflict notesNone — Verification
中文说明Autofix 审查轮次总结(PR #7911)自动化审查器提出的两条行内建议均已在代码中处理。无需冲突合并( 反馈点
冲突说明无 —— 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
[Critical] fileUtils.ts catch block — B1: In the shipped npm package, sharp is never installed (not declared in packages/core or packages/cli, omitted from scripts/prepare-package.js optionalDependencies). Every canonical image read (PNG/JPEG/WebP) would return renderer_unavailable → READ_CONTENT_FAILURE. On main this only affected the optional zoom_image tool; this PR moves it to read_file / @-attachments / read_many_files.
[Critical] fileUtils.ts catch block — B2: Animated WebP reads now hard-fail. .webp is in CANONICAL_IMAGE_MIME_TYPES, so animated WebP reaches prepareImage, trips (metadata.pages ?? 1) > 1, and returns animated_image → READ_CONTENT_FAILURE. On main the bytes were forwarded verbatim. Rejecting animated input in zoom_image is correct (opt-in); rejecting it in read_file removes a capability.
[Critical] fileUtils.ts catch block — B3: Non-canonical content behind a canonical extension (e.g. GIF bytes in a .png file) now hard-fails with decode_failed/unsupported_image → READ_CONTENT_FAILURE. On main the bytes were forwarded verbatim. The suggested fix for all three: fall through to the legacy inline-bytes branch (already at lines 1497-1515) for every non-size ImageViewError code, keeping FILE_TOO_LARGE for source_too_large/output_too_large.
— qwen3.7-max via Qwen Code /review
| } catch { | ||
| signal.throwIfAborted(); | ||
| throw new ImageViewError( | ||
| 'decode_failed', | ||
| `Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP): ${filePath}`, |
There was a problem hiding this comment.
[Suggestion] The bare catch {} discards the underlying sharp error and always throws 'decode_failed'. However, sharp(bytes, { limitInputPixels: true }).metadata() can also reject when pixel dimensions exceed the default ~268 Mpx limit — a valid, non-corrupt JPEG that simply has too many pixels. The error code and message then claim the file "may be corrupt or not a static PNG, JPEG, or WebP", which is misleading.
Failure scenario: a 50 MB JPEG at 20000×20000 pixels (400M px) passes the 100 MB byte-size check but sharp rejects on limitInputPixels. The caller receives decode_failed and cannot distinguish this from actual corruption.
Concrete cost: the operator concludes the file is corrupt when it is a valid image exceeding the pixel-dimension ceiling, wasting debugging time.
| } catch { | |
| signal.throwIfAborted(); | |
| throw new ImageViewError( | |
| 'decode_failed', | |
| `Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP): ${filePath}`, | |
| } catch (error) { | |
| signal.throwIfAborted(); | |
| const msg = error instanceof Error ? error.message : ''; | |
| if (msg.includes('pixels')) { | |
| throw new ImageViewError( | |
| 'source_too_large', | |
| `Image pixel dimensions exceed the processing limit: ${filePath}`, | |
| ); | |
| } | |
| throw new ImageViewError( | |
| 'decode_failed', | |
| `Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP): ${filePath}`, | |
| ); | |
| } |
中文说明
[Suggestion] 裸 catch {} 丢弃了底层 sharp 错误,始终抛出 'decode_failed'。但 sharp(bytes, { limitInputPixels: true }).metadata() 也可能因像素尺寸超过默认 ~268 Mpx 限制而拒绝——一个有效、未损坏的 JPEG 只是像素过多。错误码和消息会声称文件"可能已损坏或不是静态 PNG、JPEG 或 WebP",这是误导性的。
失败场景:一个 50 MB 的 20000×20000 像素(400M px)JPEG 通过了 100 MB 字节大小检查,但 sharp 在 limitInputPixels 上拒绝。调用方收到 decode_failed,无法将其与实际损坏区分开来。
具体代价:运维人员会认为文件已损坏,而实际上它只是超过了像素尺寸上限,浪费调试时间。
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Declining this one. Since commit f5d1b2772, the read_file / @-attachment / read_many_files paths fall through to the legacy inline-bytes branch on decode_failed, so the "may be corrupt" message no longer reaches users there — a pixel-over-limit image instead surfaces as the base64-size FILE_TOO_LARGE from that branch. The only remaining surface for this message is zoom_image's opt-in path. The proposed msg.includes('pixels') matcher also depends on sharp/libvips' English error string, which is an unverified, fragile heuristic: it becomes dead branching if it does not match the real message and silently regresses if libvips rewords. Given the reduced impact and the fragile matcher, this is not worth the diff growth; leaving the thread open in case a robust detector (e.g. a structured sharp error) is preferred later.
中文说明
拒绝此项。自提交 f5d1b2772 起,read_file / @ 附件 / read_many_files 路径在 decode_failed 时回退到旧的内联字节分支,因此 "may be corrupt" 消息不再在这些路径中到达用户 — 超过像素上限的图像会改为以该分支的 base64 尺寸 FILE_TOO_LARGE 呈现。此消息唯一剩余的暴露面是 zoom_image 的可选路径。建议的 msg.includes('pixels') 匹配器还依赖 sharp/libvips 的英文错误字符串,这是一个未经验证、脆弱的启发式:若不匹配真实消息则成为死分支,若 libvips 改写措辞则会静默回归。鉴于影响已减小且匹配器脆弱,这不值得增加 diff;保持线程开启,以便后续倾向于更稳健的检测器(例如 sharp 的结构化错误)。
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressedThanks for the thorough local verification. The three blocking regressions (B1–B3) all shared one root cause and are fixed by the single change you recommended: in the Blocking findings
Non-blocking observations (considered, not changed)These were framed as non-blocking and are left as-is to keep the change minimal:
Conflict notes
Verification
中文说明已处理的复查反馈感谢你扎实的本地验证。三个阻塞性回退(B1–B3)同属一个根因,已用你推荐的单一改动修复:在 阻塞项
非阻塞观察项(已评估,未改动)这些被明确标为非阻塞,为保持改动最小而未处理:
冲突说明
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
中文说明
未发现问题。LGTM!✅
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下: Autofix review round summary — PR #7911Feedback triage[Critical] CHANGES_REQUESTED review — B1 / B2 / B3 (fileUtils.ts catch block). Already resolved in the code; no new change required this round. These three points (sharp absent in the shipped package → [Suggestion] rc:3668422192 (image-view.ts:235) — implemented. The [Suggestion] rc:3668422177 (image-view.ts:184) — declined. See the threaded reply for the full reason. In short: (1) since commit Conflict notes
Verification
中文说明Autofix 审查轮次总结 — PR #7911反馈分类[Critical] CHANGES_REQUESTED 审查 — B1 / B2 / B3(fileUtils.ts catch 块)。 代码中已解决;本轮无需新改动。这三点(发布的 npm 包中缺少 sharp → [Suggestion] rc:3668422192(image-view.ts:235)— 已实现。 [Suggestion] rc:3668422177(image-view.ts:184)— 拒绝。 完整原因见线程回复。简言之:(1) 自提交 冲突说明
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
中文说明
— qwen3.7-max via Qwen Code /review
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action takenNo changes were made this round. There was no actionable feedback to address:
The only listed item is in the Deferred non-Critical feedback section. The PR The branch 中文说明Autofix 审查轮次 — 未采取任何操作本轮未做任何更改。 没有需要处理的可操作反馈:
唯一列出的条目位于 延迟的非 Critical 反馈(Deferred non-Critical feedback) 部分。该 PR 分支 Deferred non-Critical feedbackCritical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf. 中文说明完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Re-verification — head
|
| gate | result |
|---|---|
| 6 touched spec files at head | 328 / 328 pass |
trial-merge onto current main 0c0ca5fed0 (main's drift since merge-base is comment-only), same suites |
clean merge · 328 / 328 pass |
prettier --check on all 8 changed files |
pass |
eslint on all 8 changed files |
pass (a planted unused-var control confirmed the linter actually fires) |
One nit, non-blocking
9351160d4d renamed the decode_failed message to Failed to render image overview: …, but that message lives in the shared renderImageView, which the zoom path also calls — a zoom decode failure will now describe itself as an "overview" failure. On the read path the message is unused (decode failures fall through silently). Fine to fix in a follow-up or leave.
R1's non-blocking observations (memory tracks pixels, not bytes — a 0.74 MB / 16000×16000 PNG peaks at ~360 MB RSS; quality/lanczos3 unasserted) are unchanged by these commits and stand as documented risk notes.
Method
Isolated worktree detached at 9351160d4d, node_modules APFS-cloned from a lockfile-matching checkout (all @qwen-code/* entries verified to resolve in-tree). Matrix driven by tsx over the real processSingleFileContent with a real FileSystemService; main arm produced by overlaying only fileUtils.ts + zoom-image.ts from origin/main in the same tree (main's drift since merge-base b3873571aa is comment-only, verified by diff). Bundle arms: esbuild with the repo's external list (sharp externalized), packages: 'bundle', inject: scripts/esbuild-shims.js, run from a fake published layout in a directory with no node_modules ancestor; sharp state probed from inside the bundle via await import('sharp'). Arm C stages sharp plus its real dependency closure (@img/colour, detect-libc, semver) and @img/sharp-{darwin-arm64,libvips-darwin-arm64}. Mutants applied one at a time and restored; each verified to produce a real source diff before its run. sharp 0.34.5 / libvips 8.17.3, macOS arm64, Node 22.23.1. New figures pushed as pr7911-v2-* on the existing pr-assets/pr-7911-verify branch so round-1 images keep resolving.
中文说明
复验 — head 9351160d4d,第 2 轮
承接第 1 轮报告(#issuecomment-5107723627,head 974b1c35a5)。此后新增三个提交,均为响应该轮反馈:1fb281c69e(错误展示中去掉绝对路径)、f5d1b2772a(回退到 inline 字节)、9351160d4d(错误文案改名)。以下所有内容都在全新隔离 worktree 中针对新 head 重新实测,没有任何一条沿用旧结论。
结论:我这边认为可以合并。 三个阻塞项全部修复,每个修复都被自己的测试钉住,且"只还原修复"的对照实验证明站岗的正是这些测试。
B2 / B3 — 真实读取重测,与当前 main 对照(见 v2-fig2):同一套 harness 跑两臂(仅替换 fileUtils.ts / zoom-image.ts 为 origin/main 版本),13 个磁盘 fixture 走真实 processSingleFileContent。所有回退用例——动图 WebP、.png 扩展名下的 GIF/TIFF/AVIF、截断 PNG——返回字节与 main 完全一致;仅剩的差异就是特性本身:静态标准图片的有界概览,包括 main 直接拒绝的 12.3 MB 不可压缩 PNG。旧路径(.gif、SVG)两臂逐字节一致;101 MB 源文件两臂都保持硬性 FILE_TOO_LARGE,且用户可见文本已不含绝对路径(error 字段保留完整路径供日志用)。
B1 — 发布包环境重测(见 v2-fig3):用仓库自己的 esbuild 选项(含 inject: scripts/esbuild-shims.js)构建同一个 bundle,放在 workspace 之外的 node_modules/@qwen-code/qwen-code/dist/ 运行,唯一变量是 sharp 安装状态。R1 时无 sharp 臂对所有图片返回 READ_CONTENT_FAILURE;新 head 上,两个损坏臂(完全没有 sharp;有 sharp 但缺 @img/* 二进制)都优雅降级为逐字节 inline 读取、零错误,完整安装臂正常渲染概览。
对我 R1 报告的一处修正:我当时写 scripts/prepare-package.js 漏掉了 sharp。这对已发布的 0.21.0 tarball(我当时解包核对的对象)成立,但脚本本身其实已在 optionalDependencies 中声明 sharp: '0.34.5'——随 #7809(bd2c0b47f1)落地,下个版本即包含。打包与回退双保险:正常安装获得概览管线,可选依赖未装上的环境静默保持 main 行为。
修复是否承重、是否被钉住(见 v2-fig4):只还原 fallthrough(恢复修复前的 catch 块原文)后重跑套件,恰好三个新增 forwards … verbatim 测试失败——去掉修复,R1 阻塞项立即复现,且守卫正是 f5d1b2772a 新增的测试。两个较小变异存活(去掉 catch 内硬性 FILE_TOO_LARGE 映射;去掉其路径剥离):都被 fileUtils.ts:1248 的 stat 预检遮蔽——与 R1 fig 3 测得的守卫互遮同族,非阻塞。
关卡:6 个相关 spec 328/328 通过;trial-merge 到当前 main 0c0ca5fed0(main 自 merge-base 以来仅注释漂移)合并干净、328/328 通过;prettier / eslint 8 个改动文件全过(植入错误对照确认 linter 真实生效)。
一个非阻塞小瑕疵:9351160d4d 把 decode_failed 文案改为 Failed to render image overview: …,但该文案位于共享的 renderImageView 中,zoom 路径同样会走到——zoom 解码失败会自称"overview"失败。读取路径上该文案已不可见(解码失败静默回退)。可随手修或保留。
R1 的非阻塞观察(内存跟随像素而非字节——0.74 MB / 16000×16000 PNG 峰值约 360 MB RSS;quality/lanczos3 无断言)不受这三个提交影响,作为风险记录继续成立。
方法:worktree detach 在 9351160d4d,node_modules 从 lockfile 匹配的检出 APFS 克隆(已验证所有 @qwen-code/* 解析到树内)。矩阵用 tsx 驱动真实 processSingleFileContent 与真实 FileSystemService;main 臂仅覆盖 origin/main 的 fileUtils.ts + zoom-image.ts(main 自 merge-base b3873571aa 起仅注释漂移,已 diff 验证)。bundle 臂:esbuild 使用仓库 external 列表(sharp 外部化)、packages: 'bundle'、inject: scripts/esbuild-shims.js,在无 node_modules 祖先目录的伪发布布局中运行,并从 bundle 内部用 await import('sharp') 探针确认状态;C 臂补齐 sharp 的真实依赖闭包(@img/colour、detect-libc、semver)及 @img/sharp-{darwin-arm64,libvips-darwin-arm64}。变异逐个应用、确认产生真实 diff 后运行、随后还原。sharp 0.34.5 / libvips 8.17.3,macOS arm64,Node 22.23.1。新图以 pr7911-v2-* 命名推到既有 pr-assets/pr-7911-verify 分支,第 1 轮图片链接继续有效。
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action takenThis round had no actionable feedback for PR #7911:
The PR is in critical-only mode after five change-producing rounds. The one No commits were made and the branch is unchanged. 中文说明Autofix 审查轮次 —— 未采取任何操作本轮针对 PR #7911 没有可处理的反馈:
在经历 5 个产生改动的轮次后,该 PR 已进入仅处理 Critical 的模式。剩余的唯一一项 —— @wenshao 的一条 PR 评论(https://github.com/QwenLM/qwen-code/pull/7911#issuecomment-5111110398)—— 被记录在“延迟的非 Critical 反馈(Deferred non-Critical feedback)”下。根据审查政策,它属于供人工跟进的审计记录,而非本机器人需要处理的工作:未修改任何代码,未解决任何线程,也未代其发表任何回复。 未创建任何提交,分支保持不变。 Deferred non-Critical feedbackCritical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf. 中文说明完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /verify |
|
Sandboxed verification: merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. 沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 Scripted assertions: 97 passed · 0 failed · 97 total 脚本断言:97 通过 · 0 失败 · 97 总计 Verification report (report.md)Harness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action takenNo new actionable feedback was found for PR #7911 in this round.
Critical-only mode is active (5 change-producing rounds completed). The one deferred non-Critical item (a CI bot PR comment) is an audit record and requires no code change, thread resolution, or reply. The PR branch ( 中文说明未采取任何操作本轮在 PR #7911 中未发现新的可处理反馈。
当前处于仅处理 Critical 的模式(已完成 5 个产生改动的轮次)。唯一一条被延迟的非 Critical 条目(CI 机器人的 PR 评论)属于审计记录,无需修改代码、解决线程或回复。 PR 分支( Deferred non-Critical feedbackCritical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf. 中文说明完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
Released in v0.21.2. |









What this PR does
Static PNG, JPEG, and WebP reads now return a canonical JPEG overview together with its oriented source dimensions and a short hint describing how to request a normalized zoom. The overview is auto-oriented, flattened onto white, and constrained by the same edge, visual-patch, source-byte, and output-byte budgets as image zooms. Small overviews are not upscaled, while zoomed crops may still be magnified within the shared budget.
The shared rendering path keeps direct single-file reads and zoomed crops aligned. Batch and directory reads preserve the overview text immediately before the image. GIF, BMP, audio, video, SVG, and PDF handling retain their existing byte and size-limit behavior.
The downstream paths reviewed for the new two-part image result include direct reads, batch reads, workspace path reads, interactive
@attachments, pasted temporary images, vision-bridge preservation, request token estimation, Gemini/Anthropic/OpenAI-compatible conversion, file-read telemetry, and compaction.Why it's needed
Large source images can exceed the existing inline-media limit or be downscaled differently by providers, which can hide labels and other small details before the model sees them. Returning one deterministic bounded overview reduces first-request payload size, gives every provider the same coordinate frame, and tells the model how to reopen a full-resolution region through the zoom tool introduced in #7809.
Reviewer Test Plan
How to verify
@attachments, batch reads, a text-only vision bridge, and image-capable provider conversion. Expect overview text to remain text, image bytes to remain image parts, and no raw base64 to appear as model-visible text.Evidence (Before & After)
Automated evidence after rebasing onto the latest upstream main: 1,084 focused Core tests passed across image rendering, zoom, direct and batch reads, workspace paths, registration, and scheduling. Full repository build, typecheck, and bundle also passed.
Tested on
Environment (optional)
macOS, Node.js 22-compatible workspace, native Sharp renderer, sandbox disabled for local build and tests.
Risk & Scope
Linked Issues
Follow-up to #7809.
中文说明
本 PR 做了什么
静态 PNG、JPEG 和 WebP 读取现在会返回统一的 JPEG 概览、按 EXIF 方向处理后的原图尺寸,以及如何使用归一化坐标请求局部放大的简短提示。概览会自动旋转、将透明背景铺成白色,并与图片放大共用最长边、视觉 patch、源文件字节和输出字节预算。小图不会被放大,局部裁剪仍可在统一预算内放大。
统一渲染路径保证直接读取与局部放大使用相同坐标空间。批量和目录读取会保持概览文字紧邻图片之前。GIF、BMP、音频、视频、SVG 和 PDF 继续沿用现有字节与大小限制行为。
本次复查的新双 Part 图片结果下游包括直接读取、批量读取、工作区路径读取、交互式
@附件、粘贴到临时目录的图片、视觉桥接保留、请求 token 估算、Gemini/Anthropic/OpenAI-compatible 转换、文件读取遥测和上下文压缩。为什么需要
大尺寸源图可能超过现有内联媒体限制,也可能被不同 provider 以不同方式缩小,导致标签等小细节在模型看到之前丢失。返回确定且有界的概览可以降低首次请求负载、为所有 provider 提供一致坐标框架,并引导模型通过 #7809 引入的放大工具重新读取全分辨率局部。
Reviewer Test Plan
如何验证
@附件、批量读取、文本模型视觉桥接以及支持图片的 provider 转换。预期概览仍为文本、图片字节仍为图片 Part,原始 base64 不会作为模型可见文本泄漏。证据(前后对比)
重放到最新 upstream main 后的自动化证据:图片渲染、放大、直接与批量读取、工作区路径、注册和调度共 1,084 个 Core 测试通过;全仓 build、typecheck 和 bundle 也全部通过。
测试平台
环境(可选)
macOS、兼容 Node.js 22 的工作区、原生 Sharp 渲染器;本地构建和测试未启用 sandbox。
风险与范围
关联项
#7809 的后续阶段。