Skip to content

feat(core): add full-resolution image zoom tool - #7809

Merged
wenshao merged 7 commits into
QwenLM:mainfrom
qqqys:feat/image-zoom-file-mvp
Jul 27, 2026
Merged

feat(core): add full-resolution image zoom tool#7809
wenshao merged 7 commits into
QwenLM:mainfrom
qqqys:feat/image-zoom-file-mvp

Conversation

@qqqys

@qqqys qqqys commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a deferred, read-only zoom_image capability for image-capable primary models. Given an absolute path to a static PNG, JPEG, or WebP image and a normalized 0–1000 rectangle, it crops from the EXIF-oriented full-resolution original, magnifies the selected region within bounded edge, visual-patch, source-byte, and output-byte budgets, and returns a JPEG view to the model.

The capability follows the existing file-read permission boundary and participates in persistent Read(...) rules, the core-tool allowlist, filesystem-path attribution, AUTO-mode safety classification, and read-loop detection. Text-only models do not register it, the existing image-read behavior is unchanged, and deferred discovery avoids adding its schema to the initial tool list.

Why it's needed

Large images are commonly downscaled before reaching a model, which can erase small text, chart labels, thin lines, and other important details. A targeted crop from the original source lets the model recover those details deterministically without repeatedly sending the entire full-resolution image or changing the existing read contract.

Reviewer Test Plan

How to verify

  1. Configure an image-capable primary model, provide a full-resolution four-quadrant image, discover zoom_image, and select the upper-right quadrant. The returned view should contain the correct source pixels and be visibly magnified.
  2. Repeat with a JPEG carrying EXIF orientation metadata. Normalized coordinates should map to the displayed orientation rather than the raw encoded orientation.
  3. Verify that transparent PNG pixels are flattened onto white and that static PNG, JPEG, and WebP inputs return JPEG views within the documented edge, patch, and byte budgets.
  4. Verify that missing files, directories, non-regular files, unsupported or animated inputs, oversized sources, invalid coordinates, and decode failures return bounded errors instead of crashing the tool loop.
  5. Verify that workspace and managed temporary paths use the existing read permission behavior, external paths request confirmation, persisted Read(...) rules apply, and --core-tools can explicitly enable or disable the capability.
  6. Configure a text-only primary model and confirm that the capability is not registered. Existing image reads should behave exactly as before.

Validation performed locally: the seven affected test suites passed with 1,324 tests; the full workspace build and full workspace typecheck passed after rebasing onto the latest main; the bundled CLI successfully cropped a real screenshot and returned an inline JPEG.

Evidence (Before & After)

Before: when downscaling hid fine image detail, the model had no built-in way to request a bounded crop from the original source.

After: an image-capable model can request a normalized region from the original file and receive a magnified, budget-bounded view while preserving the existing read path.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not locally tested
🐧 Linux ⚠️ not locally tested

Environment (optional)

Node.js v24.14.1, local non-sandboxed unit/build validation.

Risk & Scope

  • Main risk or tradeoff: this makes sharp a direct Core dependency. The module is loaded lazily, input formats and dimensions are bounded, animated images are rejected, and output is constrained before it reaches the model.
  • Not validated / out of scope: live evaluation across every vision provider, remote image URLs, animated images, PDFs, SVG/BMP/GIF inputs, and in-memory images without a file-backed path.
  • Breaking changes / migration notes: none. Text-only configurations and existing image-read behavior are unchanged.

Linked Issues

None.

中文说明

本 PR 做了什么

本 PR 为支持图片输入的主模型增加一个延迟发现、只读的 zoom_image 能力。模型传入静态 PNG、JPEG 或 WebP 的绝对路径以及 0–1000 的归一化矩形后,工具会在应用 EXIF 方向后的全分辨率原图上裁剪,并在长边、视觉 patch、源文件大小和输出大小限制内放大所选区域,最后向模型返回 JPEG 视图。

该能力复用现有文件读取权限边界,并接入持久化 Read(...) 规则、Core 工具白名单、文件路径归因、AUTO 模式安全分类和只读循环检测。纯文本模型不会注册该能力,现有图片读取行为保持不变,延迟发现也避免把 schema 加入初始工具列表。

为什么需要

大图片在发送给模型前通常会被缩小,导致小字、图表标签、细线和其他关键细节丢失。基于原始文件做定向裁剪,可以让模型确定性地恢复这些细节,无需反复发送整张全分辨率图片,也不需要修改现有读取契约。

Reviewer 测试计划

如何验证

  1. 配置支持图片输入的主模型,提供一张全分辨率四色象限图片,发现 zoom_image 后选择右上象限。返回视图应包含正确的原始像素,并且被明显放大。
  2. 使用带 EXIF 方向元数据的 JPEG 重复验证。归一化坐标应对应用户看到的方向,而不是底层编码方向。
  3. 验证透明 PNG 像素会铺到白色背景,并且静态 PNG、JPEG、WebP 返回的 JPEG 视图符合长边、patch 和字节预算。
  4. 验证文件不存在、目录、非普通文件、不支持或动态图片、超大源文件、非法坐标和解码失败会返回有界错误,而不会使工具循环崩溃。
  5. 验证工作区和受管临时路径沿用现有读取权限,外部路径请求确认,持久化 Read(...) 规则生效,且 --core-tools 可以显式启停该能力。
  6. 配置纯文本主模型并确认该能力不会注册,现有图片读取行为保持不变。

本地验证结果:7 个受影响测试套件共 1,324 个测试全部通过;rebase 到最新 main 后,全 workspace build 和全 workspace typecheck 通过;最终 bundle 对真实截图完成裁剪并返回了内联 JPEG。

前后对比证据

Before:图片缩小后隐藏细节时,模型没有内置方式从原始文件请求一个受预算约束的裁剪区域。

After:支持图片输入的模型可以从原始文件请求归一化区域,并在保持现有读取路径不变的前提下获得放大且有预算约束的视图。

测试平台

OS 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未本地测试
🐧 Linux ⚠️ 未本地测试

环境

Node.js v24.14.1,本地非 sandbox 单测和构建验证。

风险与范围

  • 主要风险或权衡:sharp 成为 Core 的直接依赖。模块按需加载,输入格式和尺寸有界,动态图片会被拒绝,输出在发送给模型前受到限制。
  • 未验证或不在范围内:所有视觉 provider 的在线评估、远程图片 URL、动态图片、PDF、SVG/BMP/GIF,以及没有文件路径的纯内存图片。
  • 破坏性变更或迁移说明:无。纯文本配置和现有图片读取行为保持不变。

关联 Issue

无。

@qqqys
qqqys marked this pull request as ready for review July 27, 2026 06:45
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Deferred approval withheld — 1 PR CI workflow run(s) on f451c23 did not finish green; see the updated table in the Stage 2 comment. Re-run @qwen-code /triage after fixes. finalize run

⚠️ 延迟审批已搁置 —— f451c23 有 1 个 PR CI workflow 未以绿色完成,详见 Stage 2 评论中已更新的表格。修复后可重新运行 @qwen-code /triage查看 finalize 运行

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR Review: Add full-resolution image zoom tool

Summary

This PR adds a zoom_image tool allowing image-capable models to request magnified crops from full-resolution images. The implementation is solid and well-integrated with existing permission and scheduling infrastructure.


✅ Strengths

1. Excellent Security Considerations

  • Path validation enforces absolute paths only
  • Permission boundary correctly reuses read_file logic via the new shared file-read-permission.ts module
  • File size limits (100MB source, 9MB output) prevent memory exhaustion
  • limitInputPixels: true prevents decompression bombs
  • Animated images are explicitly rejected
  • Tool is only registered for image-capable models (conditional registration)

2. Comprehensive Test Coverage

  • 480+ lines of tests covering success cases, error handling, edge cases
  • Tests for: missing files, directories, non-regular files, unsupported formats, oversized files, EXIF orientation, transparency flattening, budget constraints, animated images, abort handling, validation errors, and permissions

3. Clean Architecture

  • Good separation with shared file-read-permission.ts extracted from read-file.ts
  • Proper integration with existing systems: permissions, loop detection, tool scheduling, AUTO mode allowlist
  • Well-documented tool schema and description

4. Budget Constraints

  • Visual patch budget (1568 patches of 28px) aligns with model input limits
  • Edge limit (1568px) and output byte limit (9MB) are reasonable

🔍 Issues to Address

1. Copyright Year Typo (Minor)

- * Copyright 2026 Qwen
+ * Copyright 2025 Qwen

The PR was created in 2025-07, but the new files have 2026.

2. Sharp as Direct Dependency
Adding sharp (a native module) increases:

  • Install time and complexity
  • Bundle size
  • Platform-specific considerations

Considerations:

  • Is lazy loading sufficient to mitigate impact on text-only configurations?
  • Should this be documented in a CHANGELOG or migration guide?

3. Platform Testing Gap
Author notes Windows was not locally tested. Given sharp's native dependencies, consider:

  • Adding CI checks for Windows if not present
  • Or documenting this limitation

💡 Suggestions (Non-blocking)

1. Binary Search Edge Case
The magnifiedSize function initializes best = { width: 1, height: 1 }. While this works, consider adding a comment explaining that the minimum valid size will always be found.

2. Error Handling Scope
The catch block in execute() is broad:

} catch {
  signal.throwIfAborted();
  return failureResult(...);
}

This catches all errors. Consider checking if the caught error is an AbortError first to propagate it correctly.

3. Race Condition Documentation
Between fs.stat and image processing, the file could theoretically change. This is acceptable for a read-only tool, but could be documented.


📋 Integration Checklist ✅

  • Added to ToolNames and ToolDisplayNames
  • Registered in config with conditional loading for image-capable models
  • Added to SAFE_TOOL_ALLOWLIST for AUTO mode
  • Added to FS_PATH_TOOL_NAMES for path extraction
  • Added to READ_TOOLS meta-category for permission rules
  • Added to READ_LIKE_TOOLS for loop detection
  • Added to CORE_TOOLS set
  • Added to TOOL_NAME_ALIASES with ZoomImage and ZoomImageTool
  • Proper permission handling via shared file-read-permission.ts

Conclusion

Approve with minor suggestions. The implementation is production-ready with excellent security practices and test coverage. The copyright year and Windows testing are the only actionable items before merge.

Great work on this feature! 🎉

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: this is a feature addition, not a bug fix — and the problem is real. Large images are downscaled before reaching the model, and small text, chart labels, and thin lines are routinely lost. No linked issue, but the gap is well-established and observable any time a high-resolution screenshot or diagram is read.

Direction: aligned. A targeted crop from the original source is a natural extension of the existing image-read path. No direct CHANGELOG reference found, but the area is clearly relevant to a coding assistant that handles images.

Size: 466 production logic lines (additions + deletions, excluding tests), 561 test lines. Core paths are touched (packages/core/src/tools/, config/, permissions/, services/, core/). Under the 500-line threshold; no maintainer escalation needed on size.

Approach: the scope feels right. The new zoom_image tool follows the existing tool patterns closely — deferred discovery, Kind.Read, conditional registration for image-capable models. The extraction of file-read-permission.ts from read-file.ts is a clean, minimal refactor that enables permission reuse without changing behavior. Every edit in the diff serves the stated goal; no drive-by changes. One thing worth noting: this makes sharp a direct packages/core dependency (it was previously only in packages/desktop). The PR acknowledges this tradeoff, and the lazy loading mitigates startup cost, but it does mean every core consumer now pulls a native image-processing library.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个功能新增,而非 bug 修复——问题是真实存在的。大图片在发送给模型前会被缩小,小字、图表标签和细线经常丢失。没有关联 issue,但这个差距是公认的,任何读取高分辨率截图或图表时都会遇到。

方向:对齐。从原始文件做定向裁剪是现有图片读取能力的自然延伸。CHANGELOG 中没有直接参考,但该领域与处理图片的编码助手明显相关。

规模:466 行生产逻辑代码(增删合计,不含测试),561 行测试代码。触及核心路径(packages/core/src/tools/config/permissions/services/core/)。低于 500 行阈值,无需因规模转交维护者。

方案:范围合理。新的 zoom_image 工具紧密遵循现有工具模式——延迟发现、Kind.Read、仅为支持图片的模型条件注册。从 read-file.ts 中提取 file-read-permission.ts 是一个干净、最小的重构,实现了权限复用而不改变行为。diff 中的每一处改动都服务于既定目标,没有顺手改动。值得注意的是:这使得 sharp 成为 packages/core 的直接依赖(此前仅在 packages/desktop 中)。PR 已说明这一权衡,延迟加载缓解了启动开销,但这确实意味着每个 core 消费者都会引入一个原生图片处理库。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at f451c238a802e768f0245cdc4db3ecfed2a67e61 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I would add a zoom_image tool in packages/core/src/tools/, use sharp (already in the project via packages/desktop) for image processing, take normalized 0–1000 coordinates, apply EXIF orientation before cropping, magnify to fit model visual budgets, return inline JPEG, register conditionally for image-capable models via registerLazy, and integrate with the existing permission/config/loop-detection/tool-scheduler systems.

Comparison: the PR matches this proposal closely. The implementation is clean and follows existing tool patterns faithfully.

No critical blockers found. Specific observations:

  • file-read-permission.ts extraction — the permission logic is extracted from read-file.ts verbatim (including the SYNC comment) into a shared function. read-file.ts and zoom-image.ts both call it. The refactor is behavior-preserving; the read-file.ts change from async to Promise.resolve() is a no-op since the interface already returns Promise<PermissionDecision>.

  • sharp as a core dependencysharp@^0.34.5 is added to packages/core/package.json. It was previously only in packages/desktop (pinned at 0.34.5). The ^ range is slightly looser than desktop's pin, but sharp follows semver. The module is loaded lazily via registerLazy → dynamic import(), so native bindings are only loaded when the tool is first discovered, not at startup. This is the main tradeoff of the PR and it's acknowledged in the description.

  • Bounds and safety — source files are capped at 100 MB, output at 9 MB, edge at 1568 px, patches at 1568. Animated images are rejected via metadata.pages > 1. failOn: 'error' and limitInputPixels: true are set on both the metadata and decode passes. Abort signal is checked at three points. All error paths return typed ToolErrorType results rather than throwing.

  • EXIF orientationmetadata.autoOrient.width/height (sharp ≥ 0.33) gives the displayed dimensions. Coordinates are mapped into this space, and the decode pass uses autoOrient: true. The test creates a 60×40 image with orientation 6 and verifies the crop lands in the correct quadrant after rotation.

  • Integration pointstool-names.ts (name + display name), config.ts (conditional registerLazy), coreToolScheduler.ts (FS path extraction), autoMode.ts (safe allowlist), permission-manager.ts (core tools set), rule-parser.ts (aliases, READ_TOOLS, display name, file-targeted tools), loopDetectionService.ts (read-tool allowlist). Each integration is a one-line addition following the existing pattern, with corresponding test coverage.

  • Test coverage — 480 lines covering: basic crop + pixel verification, missing file, directory, non-regular file (socket), unsupported format, oversized source, EXIF orientation, transparency flattening, budget bounds, animated image rejection, abort signal, invalid parameters, permission boundary, and decode failure. The pixel-level assertions (checking RGB values at the crop center) are a good oracle for correctness.

Test Evidence

This is an unattended CI run — PR code is not executed locally. Evidence comes from the PR's own CI checks on commit f451c238a802e768f0245cdc4db3ecfed2a67e61:

Final CI results for f451c23 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
Classify PR ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The main unit test suite (Test (ubuntu-latest, Node 22.x)) is still running. Windows and macOS tests are skipped (fork PR gating). No failures so far. The author reports 1,324 tests passing across 7 affected suites locally — this is the author's claim, not independently verified here.

Not verified: live TUI behavior (CI run — a maintainer can trigger the isolated @qwen-code /tmux job for real-scenario testing, though note this PR is from a fork without write access, so the sandboxed lanes are unavailable; a maintainer would need to check out the PR in a disposable container or reproduce the behavioral claim by hand).

中文说明

代码审查

独立方案: 我会在 packages/core/src/tools/ 中添加 zoom_image 工具,使用 sharp(项目中已通过 packages/desktop 引入)进行图片处理,接受 0–1000 归一化坐标,在裁剪前应用 EXIF 方向,放大以适应模型视觉预算,返回内联 JPEG,通过 registerLazy 仅为支持图片的模型条件注册,并集成现有的权限/配置/循环检测/工具调度系统。

对比: PR 与该方案高度一致。实现干净,忠实遵循现有工具模式。

未发现关键阻塞问题。具体观察:

  • file-read-permission.ts 提取 — 权限逻辑从 read-file.ts 原样提取(包括 SYNC 注释)到共享函数。read-file.tszoom-image.ts 都调用它。重构保持行为不变。
  • sharp 作为核心依赖 — 这是 PR 的主要权衡,已在描述中说明。模块通过 registerLazy → 动态 import() 延迟加载,原生绑定仅在工具首次被发现时加载。
  • 边界和安全 — 源文件上限 100 MB,输出上限 9 MB,长边上限 1568 px,patch 上限 1568。动态图片被拒绝。所有错误路径返回类型化的 ToolErrorType 结果。
  • EXIF 方向 — 使用 metadata.autoOrient 获取显示尺寸,坐标映射到该空间。测试验证了旋转后裁剪的正确性。
  • 集成点 — 每个集成都是遵循现有模式的一行添加,并有对应的测试覆盖。
  • 测试覆盖 — 480 行测试,覆盖所有主要路径,包括像素级断言。

测试证据

这是无人值守的 CI 运行——不在本地执行 PR 代码。证据来自 PR 自身的 CI 检查。主单元测试套件仍在运行中,目前无失败。作者报告本地 1,324 个测试通过——这是作者的声明,非独立验证。

未验证:实时 TUI 行为(CI 运行)。维护者可以在一次性容器中检出 PR 或手动复现行为声明。

Qwen Code · qwen3.8-max-preview

Reviewed at f451c238a802e768f0245cdc4db3ecfed2a67e61 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean implementation that follows existing tool patterns faithfully, with thorough test coverage and correct integration across all touch points. The sharp core dependency is a real tradeoff but acknowledged and mitigated by lazy loading.

Stepping back: this is a well-scoped feature that solves a genuine problem. The code reads like it was written by someone who studied the existing tool architecture and followed it precisely — conditional registration, deferred discovery, shared permission boundary, typed error results, budget-bounded output. The file-read-permission.ts extraction is the kind of minimal refactor that makes the codebase better without changing behavior. The test suite verifies pixel-level correctness, not just "no error thrown."

The only reservation is the sharp dependency landing in packages/core — every consumer of core now pulls a native image-processing library, even if the tool is never used. The lazy loading mitigates runtime cost, but the install-time cost is real. This is a maintainer-level tradeoff call, not a blocker.

Approval deferred until CI lands green on f451c238a802e768f0245cdc4db3ecfed2a67e61 — the main unit test suite is still running.

中文说明

置信度:4/5 — 实现干净,忠实遵循现有工具模式,测试覆盖全面,所有集成点正确。sharp 核心依赖是一个真实的权衡,但已说明并通过延迟加载缓解。

总体来看:这是一个范围合理的功能,解决了真实问题。代码读起来像是作者研究了现有工具架构后精确遵循的——条件注册、延迟发现、共享权限边界、类型化错误结果、有预算约束的输出。file-read-permission.ts 的提取是那种让代码库更好而不改变行为的最小重构。测试套件验证了像素级正确性,而不仅仅是"没有抛出错误"。

唯一的保留意见是 sharp 依赖进入 packages/core——即使工具从未被使用,每个 core 消费者都会引入一个原生图片处理库。延迟加载缓解了运行时开销,但安装时开销是真实的。这是维护者级别的权衡决策,不是阻塞问题。

审批推迟到 CI 在 f451c238a802e768f0245cdc4db3ecfed2a67e61 上全部通过后执行——主单元测试套件仍在运行中。

Qwen Code · qwen3.8-max-preview

Reviewed at f451c238a802e768f0245cdc4db3ecfed2a67e61 · re-run with @qwen-code /triage

@qqqys

qqqys commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

已修复。\n\n验证证据:\n- npm run generate:notices --workspace=qwen-code-vscode-ide-companion:通过,共生成 279 项依赖声明。\n- 连续生成前后 NOTICES.txt SHA-256 一致:3d91d5aac4d3bc4189297d88fe44d36ef3cea28aaefad4f87cff00b751c0b0a5。\n- 提交后再次生成,worktree 保持干净。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/package.json
Comment thread packages/core/src/tools/zoom-image.test.ts
Comment thread packages/core/src/tools/zoom-image.test.ts
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 9cccf54. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:

  • packages/web-shell/client/i18n.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] packages/core/package.json:83 — sharp is a native module with platform-specific binary addons (@img/sharp-{platform}), but it is neither added to esbuild's external list in esbuild.config.js nor to optionalDependencies in scripts/prepare-package.js. The established pattern for native modules externalizes them in esbuild and declares their platform binaries as optional dependencies. Failure scenario: In the published package, zoom_image crashes — the tool is non-functional for every user. (Already reported at packages/core/package.json:83; blocker re-checked — still stands.)

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/components/messages/toolFormatting.ts
@wenshao

wenshao commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Review — feat(core): add full-resolution image zoom tool

Reviewed at head c6b07eb. The tool itself is well built: the file-read-permission.ts extraction is verbatim and behavior-preserving, the integration surface (rule-parser aliases, SAFE_TOOL_ALLOWLIST, FS_PATH_TOOL_NAMES/extractToolFilePaths, loop-detection read set, CORE_TOOLS, web-shell display names) is complete and individually tested, every failure path returns a typed ToolErrorType instead of throwing, and the coordinate clamping is load-bearing (I confirmed extract() throws extract_area: bad extract area on an out-of-range rect, so the Math.min/max guards are doing real work). sharp@0.34.5 typings also check out for the two fields the code leans on — Metadata.format is required (keyof FormatEnum) and Metadata.autoOrient is a required {width, height}.

The problems are not in the tool; they are in how sharp reaches a released binary, and in one test that doesn't pin the behavior it names.


🔴 Blocking — sharp is not wired into any packaging path, so an npm-installed CLI fails to start

sharp was added to packages/core/package.json dependencies and nothing else. It is missing from all four places this repo tracks native modules:

Place Contents today sharp?
esbuild.config.js:145 external node-pty, @teddyzhu/clipboard, audio-capture
scripts/prepare-package.js:305-321 (published package.json) dependencies: {} + explicit optionalDependencies allowlist
scripts/build-standalone-release.js:162 @teddyzhu/clipboard
scripts/create-standalone-package.js:52-56, 401 per-target native package map

Because it is not external, esbuild inlines sharp's JS into the bundle (I measured 233 KB, no warnings) while the native addon stays a runtime require('@img/sharp-<platform>/sharp.node') resolved relative to the emitted bundle. Since dist/'s package.json declares neither sharp nor @img/*, that require has nothing to find. Reproduced with this repo's exact bundle settings (format: esm, packages: 'bundle', inject: scripts/esbuild-shims.js) emitted into a directory with no @img present:

Error: Could not load the "sharp" module using the darwin-arm64 runtime
Possible solutions:
- Ensure optional dependencies can be installed: npm install --include=optional sharp
  ...

The blast radius is larger than "the tool is unavailable", because the sharp import is static at zoom-image.ts:9 and the factory is warmed at startup:

Config.initialize()warmAll({ strict: options?.lenientToolWarmup !== true }) (config.ts:2765) → ensureTool (tool-registry.ts:352, rethrows) → Promise.allSettled rejected + strictthrow (tool-registry.ts:393-405) → initialize() throws → the CLI does not start for any image-capable model. The factory in config.ts:7157 has no try/catch either.

Why local validation passed: sharp is already hoisted into the root node_modules as a transitive dep of packages/mobile-mcpmobilewright@mobilewright/core, so an in-repo node dist/cli.js resolves it fine. packages/desktop is not evidence either way — it is excluded from the root workspace ("!packages/desktop"), is a separate private Electron app, and only lists sharp in trustedDependencies. And scripts/verify-installation-release.js only checks artifact names + SHA256, it never executes the binary, so CI will not catch this.

Suggested fix, matching the existing native-module pattern (clipboardUtils.ts is the model):

  1. sharpoptionalDependencies in packages/core/package.json, plus esbuild.config.js external and all three release allowlists (with the @img/sharp-* + @img/sharp-libvips-* platform packages, as node-pty/clipboard do).
  2. const sharp = (await import('sharp')).default inside execute(), wrapped so a missing binding returns a bounded ToolErrorType result instead of propagating.

Worth a maintainer call regardless: @qwen-code/qwen-code-core is a published package, and sharp in dependencies (not optional) adds ~16 MB per platform (@img/sharp-libvips-darwin-arm64 alone is 15 MB) to every core/SDK consumer, image tool or not.

🟠 "The module is loaded lazily" isn't true today

Even with packaging fixed, registerLazy only defers registration; Config.initialize() calls warmAll() unconditionally, which runs every factory and therefore import('../tools/zoom-image.js') → top-level import sharp → libvips dlopen on every startup for image-capable models. Moving the import into execute() (fix #2 above) is what actually makes it lazy.

🟠 The registration gate reads a value that isn't always populated yet

getEffectiveInputModalities() (config.ts:3665) returns getContentGeneratorConfig()?.modalities ?? {}, and contentGeneratorConfig is assigned only in refreshAuth (config.ts:3399). The common path is fine — gemini.tsx:934 initializeApp()performInitialAuthrefreshAuth runs before config.initialize() at gemini.tsx:1084 — but only when an authType is already persisted. Probed on main with a real Config and model: 'qwen3-vl-plus':

>>> modalities AT createToolRegistry: {}
>>> modalities AFTER initialize:     {}

So with no auth yet configured the gate is false. Since createToolRegistry has exactly one caller (config.ts:2736) and nothing rebuilds the registry, the consequences are:

  • first run where auth is selected in the dialog → zoom_image absent for that entire session;
  • /model switch into an image-capable model → absent until restart;
  • switch away from one → the tool lingers and will hand an image to a text-only model.

Both new config.test.ts cases mock getEffectiveInputModalities, so they can't see any of this. Consider registering unconditionally and gating at execute time — image-gen.ts:131 already does exactly that check at invocation — or re-gating from onModelChange.

🟠 The EXIF test does not pin auto-orientation (mutation-verified)

interprets coordinates in the EXIF-oriented image space is the test covering verify-item #2 of your test plan, and it survives deletion of the behavior. I reimplemented the exact pipeline (magnifiedSize + extract → resize → flatten → jpeg) and ran it with and without autoOrient: true on the crop's sharp() options:

autoOrient=true : out=1328x896 rgb=254,0,0 text="Oriented source: 40x60" => assertions PASS
autoOrient=false: out=1328x896 rgb=251,3,1 text="Oriented source: 40x60" => assertions PASS

Two reasons: the fixture is a single vertical red/green split, so the requested top strip's center pixel is red in raw space too; and Oriented source: 40x60 is built from metadata.autoOrient, which the mutant never touches. A fixture that does discriminate — 4-color quadrants, orientation 6, select the displayed top-left quadrant — verified:

autoOrient=true : displayed-top-left-quadrant center RGB = 0,2,251   (blue)
autoOrient=false: displayed-top-left-quadrant center RGB = 254,0,2   (red)

🟡 Smaller points

  • Dead budget branch. IMAGE_MAX_OUTPUT_BYTES (9 MB) is unreachable in practice: output is already capped at ≤1568px edge and ≤1568 28px patches (≈1092² for a square), so a q92 4:4:4 JPEG can't get close. bounds the returned view by edge, patch, and byte budgets only asserts the output is under the cap, never exercises the failure path. Fine as a backstop — just say so in a comment, or drop it.
  • limitInputPixels: true is sharp's 268 MP default. Paired with a 100 MB source cap, a decompression-bomb PNG can still force a very large decode buffer for what is only ever a crop. An explicit tighter cap (~100 MP) would bound it.
  • No telemetry. read_file emits FileOperationEvent via logFileOperation; zoom_image emits nothing, so usage of the new capability is invisible in metrics.
  • i18n gap. toolDisplayName.ZoomImage landed in en/zh/zh-TW, but ca.js also carries toolDisplayName.ReadFile ('Llegeix fitxer') and was missed.
  • webui not updated. packages/web-shell/.../toolFormatting.ts got zoom_image, but packages/webui/src/components/toolcalls/labelUtils.ts has the same per-name map and didn't. (routing.ts is fine — it keys on Kind.)
  • Read-only family sets left out — all fail safe, but worth a deliberate decision: speculationToolGate.ts:31 SAFE_READ_ONLY_TOOLS (unknown tool → boundary, so a zoom aborts speculation) and subagent-plan-tool-policy.ts:21 PLAN_REQUIRED_TEAMMATE_PRE_APPROVAL_TOOLS (a read-only zoom will prompt for plan-required teammates). Microcompaction is genuinely fine — zoom results land in the nested-media branch, so the JPEG is evicted while the text survives; no COMPACTABLE_TOOLS entry needed.
  • Discoverability. The tool is deferred and nothing in prompts.ts or the image read_file path mentions it, so the model only finds it if it happens to search "zoom". The image-read path is the natural place for a one-line hint.
  • Description nit. "You may call it repeatedly" invites iterative zooming, but coordinates always address the original file, never the previously returned view. Worth stating explicitly so the model doesn't treat the last crop as the new coordinate space.

中文说明

c6b07eb 上审阅。工具本体质量很好:file-read-permission.ts 抽取是逐行等价的重构;权限规则、SAFE_TOOL_ALLOWLISTextractToolFilePaths、循环检测只读集合、CORE_TOOLS、web-shell 展示名等接入点齐全且各有测试;所有失败路径都返回带类型的 ToolErrorType 而不是抛异常;坐标裁剪是有效的(我确认越界 extract() 会抛 extract_area: bad extract area)。sharp@0.34.5 的类型也符合代码假设(formatautoOrient 均为必填)。

问题不在工具本身,而在 sharp 如何进入发布产物,以及一个没有真正锁住行为的测试。

🔴 阻塞:sharp 没有接入任何打包路径,npm 安装版 CLI 会“无法启动”。 本仓库在四处维护原生模块清单,sharp 四处全缺:esbuild.config.js:145externalscripts/prepare-package.js:305-321 生成的发布版 package.jsonscripts/build-standalone-release.js:162scripts/create-standalone-package.js:52-56,401。由于不是 external,esbuild 会把 sharp 的 JS 内联进 bundle(233 KB,无告警),而原生扩展仍靠运行时 require('@img/sharp-<平台>/sharp.node') 解析——发布包里既没有 sharp 也没有 @img/*,必然找不到。我用本仓库完全相同的打包配置(esm + packages:'bundle' + scripts/esbuild-shims.js)在没有 @img 的目录复现出:Error: Could not load the "sharp" module using the darwin-arm64 runtime

影响范围不止“工具不可用”:zoom-image.ts:9静态 import,而 Config.initialize()warmAll({strict})config.ts:2765)→ ensureTool 重新抛出 → strict 直接 throw,即任何图片能力模型下 CLI 直接启动失败,工厂函数也没有 try/catch。本地验证之所以通过:sharp 已经通过 packages/mobile-mcpmobilewright@mobilewright/core 被提升到根 node_modules;而 packages/desktop 不能作为依据(它被 "!packages/desktop" 排除在 workspace 外,且只在 trustedDependencies 里提到 sharp)。scripts/verify-installation-release.js 只校验文件名和 SHA256,不会执行产物,因此 CI 也发现不了。建议按现有原生模块模式修复:sharp 放入 optionalDependencies,补齐 esbuild external 与三个发布清单(含 @img/sharp-* 平台包),并在 execute() 内改为 await import('sharp') 且失败时返回有界的工具错误。另外 @qwen-code/qwen-code-core 是对外发布的包,把 sharp 放进非可选 dependencies 会给每个 core/SDK 使用者增加约 16 MB(仅 @img/sharp-libvips-darwin-arm64 就 15 MB)。

🟠“按需加载”目前并不成立。 registerLazy 只延迟注册;Config.initialize() 无条件调用 warmAll(),会执行所有工厂,也就是每次启动都会动态导入 zoom-image.jsdlopen libvips。把 import 移入 execute() 才是真正的懒加载。

🟠 注册开关读取的值并非总已就绪。 getEffectiveInputModalities()config.ts:3665)取自 contentGeneratorConfig.modalities,而该字段只在 refreshAuthconfig.ts:3399)赋值。常规路径没问题(gemini.tsx:934initializeApp 早于 1084 行的 config.initialize()),但前提是已持久化 authType。我在 main 上用真实 Configmodel: 'qwen3-vl-plus' 实测:createToolRegistry 时刻与 initialize 之后都是 {}。由于 createToolRegistry 只有一个调用点(config.ts:2736)且没有重建机制,后果是:首次在对话框中选择认证的那个 session 拿不到该工具;/model 切换到图片模型需重启才生效;切离图片模型后工具仍残留。两个新增 config 测试都 mock 了 getEffectiveInputModalities,覆盖不到这一点。建议无条件注册、在执行时判定(image-gen.ts:131 已是此模式),或在 onModelChange 时重新判定。

🟠 EXIF 测试没有锁住方向纠正(已做变异验证)。 我复现了完整流水线,仅删除裁剪时的 autoOrient: true:两个断言依旧全绿(中心像素 251,3,1 vs 254,0,0;Oriented source: 40x60 来自 metadata.autoOrient,变异不影响)。原因是 fixture 只有左右红绿两半,所选顶部条带的中心像素在原始方向下也是红色。改用四象限 + orientation 6 + 取“显示后的左上象限”即可区分:开启为蓝 (0,2,251)、关闭为红 (254,0,2),已验证。

🟡 其他小点: 9 MB 输出上限在现有 1568 边长 / 1568 patch 约束下不可达,测试也只断言未超限,属死分支(保留可加注释说明);limitInputPixels: true 是 268 MP 默认值,配合 100 MB 源限制仍可能让解压炸弹撑出很大解码缓冲,建议显式收紧;缺少 read_file 那样的 FileOperationEvent 遥测;ca.jstoolDisplayName.ReadFile 但漏了 ZoomImagepackages/webui/.../labelUtils.ts 与 web-shell 同类映射未同步;speculationToolGate.ts:31subagent-plan-tool-policy.ts:21 的只读集合未加入(行为安全但值得明确决策),微压缩无需改动(走 nested-media 分支,图片被清理、文本保留);工具为 deferred 且 prompts.ts 与图片读取路径都没有提示,模型可能永远不会去搜索它;描述可明确说明坐标永远相对原图而非上一次返回的视图。

@qqqys

qqqys commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 27, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

中文说明

未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/package.json
Comment thread packages/web-shell/client/components/messages/toolFormatting.ts
Comment thread packages/core/src/tools/zoom-image.ts Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Unresolved, please confirm: [Critical] zoom-image.ts:9 — top-level import sharp from 'sharp' makes loading eager at warmAll(), not lazy (raised by @wenshao; mechanism traced through config.ts:2765 warmAll → tool-registry.ts:352 ensureTool → import → dlopen; code unchanged since report) [Critical] config.ts:7157 — registration gate reads getEffectiveInputModalities() which may be empty when no auth persisted; tool absent for first-run session (raised by @wenshao; code unchanged since report) [Critical] zoom-image.test.ts — EXIF orientation test fixture is a red/green split that passes with autoOrient=true or false; does not pin auto-orientation behavior (raised by @wenshao; mutation-verified) 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. Not reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries. [Critical] sharp is a native module not wired into esbuild external or optionalDependencies — CLI fails to start for image-capable models (existing blocker at packages/core/package.json:83, still stands after re-check of esbuild.config.js, scripts/prepare-package.js, scripts/build-standalone-release.js, scripts/create-standalone-package.js)

中文说明

已审查。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。 未决,请确认:[Critical] zoom-image.ts:9 — top-level import sharp from 'sharp' makes loading eager at warmAll(), not lazy (raised by @wenshao; mechanism traced through config.ts:2765 warmAll → tool-registry.ts:352 ensureTool → import → dlopen; code unchanged since report) [Critical] config.ts:7157 — registration gate reads getEffectiveInputModalities() which may be empty when no auth persisted; tool absent for first-run session (raised by @wenshao; code unchanged since report) [Critical] zoom-image.test.ts — EXIF orientation test fixture is a red/green split that passes with autoOrient=true or false; does not pin auto-orientation behavior (raised by @wenshao; mutation-verified) 未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。 未审查:验证——本次评审发布了发现,但没有验证 agent 是用本 skill 构建的 prompt 启动的——这些发现即便被裁定过,也缺失了 brief 承载的裁定标准。 [Critical] sharp is a native module not wired into esbuild external or optionalDependencies — CLI fails to start for image-capable models (existing blocker at packages/core/package.json:83, still stands after re-check of esbuild.config.js, scripts/prepare-package.js, scripts/build-standalone-release.js, scripts/create-standalone-package.js)

— qwen3.7-max via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 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/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 4 finishedview run. See this round's report below.

中文说明

AutoFix 第 4 轮已完成 —— 查看运行。本轮报告见下方。

Externalize sharp in esbuild and declare it (plus the @img platform
binaries) in the published package's optionalDependencies so an
npm-installed CLI resolves the native binding. Import sharp dynamically
inside execute() so a missing binding returns a bounded tool error
instead of crashing startup during strict tool warmup, and so the module
is only loaded when zoom_image actually runs. Also pin the EXIF
auto-orientation test with a discriminating fixture and cover the
.qwenignore and y1>=y2 validation paths.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — summary

Commit c9e098805 (4 files, +87/−9). This round fixes the blocking sharp
packaging defect and the verified test defect, adds the two requested
validation tests, and triages the remaining feedback.

Resolved (implemented in this commit)

  • sharp not wired into packaging — Critical (rc:3655391707, rc:3655997676; @wenshao blocking). Fixed along the established native-module pattern:
    • esbuild.config.js: added 'sharp' to external, so esbuild no longer inlines sharp's JS (~233 KB) and the native binding resolves from node_modules at runtime. Verified: the bundled dist/chunks/zoom-image-*.js is ~10 KB and contains an external import("sharp") with no inlined sharp/libvips.
    • scripts/prepare-package.js: added sharp plus the @img/sharp-* / @img/sharp-libvips-* platform binaries (darwin-arm64/x64, linux-arm64/x64, win32-x64) to the published package's optionalDependencies, so npm installs the matching native binary. Versions match the lockfile (0.34.5 / 1.2.4).
  • Eager load / startup crash (@wenshao). zoom-image.ts now imports sharp dynamically inside execute() (type-only import at the top), so the module is loaded only when zoom_image actually runs, and a missing binding returns a bounded READ_CONTENT_FAILURE result instead of throwing during strict tool warmup. The CJS export = interop unwrap mirrors utils/iconvHelper.ts.
  • EXIF test did not pin auto-orientation — mutation-verified (@wenshao). Replaced the red/green split fixture with four distinct quadrants and a crop of the displayed top-left quadrant; the center pixel is blue with autoOrient and red without. Confirmed by mutation: flipping autoOrient: true → false makes the test fail (1 failed); reverted afterward.
  • .qwenignore rejection path untested (rc:3655391727, rc:3655997691). Added a test that writes a .qwenignore pattern matching the image and asserts build() throws the ignore-pattern message.
  • Symmetric y1 >= y2 branch untested (rc:3655391733). Added the assertion alongside the existing x1 >= x2 case.
  • Missing web-shell zh-CN translation (rc:3655786203, rc:3655997688). Already fixed in d3227c5a3; re-verified — toolFormatting.test.ts + toolFormatting.drift.test.ts pass (38/38).
  • Description nit (@wenshao). Clarified that coordinates always refer to the original full-resolution image, never a previously returned view.

Escalated to a maintainer (open questions, not settled here)

  • Registration gate reads getEffectiveInputModalities(), which is empty before auth is persisted (@wenshao). Real behavior gap (tool absent on first-run session / /model switch). Recommendation: register zoom_image unconditionally and gate at execute time (the image-gen.ts pattern). Left open because changing tool visibility for text-only models is a product/scope call.
  • Standalone release scripts (scripts/build-standalone-release.js, scripts/create-standalone-package.js) do not stage sharp/libvips (@wenshao). The lazy import already prevents the startup crash there (the tool degrades to a bounded error), so this is a feature-availability follow-up. Staging sharp's two package families per target is release-engineering work that cannot be verified in this environment (no networked npm installs).
  • Published-core dependency type (@wenshao): moving sharp from dependencies to optionalDependencies in packages/core/package.json (SDK-consumer size) requires regenerating package-lock.json (networked npm install), which is out of reach here; flagged as a maintainer decision.
  • Read-only family sets (speculationToolGate.ts, subagent-plan-tool-policy.ts) (@wenshao): both fail safe today; whether to add zoom_image is a deliberate decision requested by the reviewer.

Declined (with reasons)

  • Copyright year 2026 → 2025 (@yiliang114): cosmetic and ambiguous (current date is 2026); left as-is.
  • Windows CI coverage (@yiliang114): CI/documentation concern, out of scope for a code fix.
  • Binary-search / race-condition / dead-budget comments (@yiliang114, @wenshao): per the house "comments default to none" rule; the code reads clearly and the 9 MB cap is an intentional backstop.
  • Broad catch in execute() (@yiliang114): already correct — signal.throwIfAborted() runs first in the catch, so aborts propagate.
  • Tighter limitInputPixels cap (@wenshao): tuning judgment; the current sharp default is paired with the 100 MB source cap.
  • FileOperationEvent telemetry (@wenshao): feature addition, out of scope for this fix.
  • ca.js ZoomImage translation (@wenshao): no completeness test enforces toolDisplayName parity for ca.js; partial-locale translation is best left to translators.
  • packages/webui labelUtils.ts (@wenshao): separate package, no failing test, consistency-only — suitable follow-up.
  • Discoverability hint in prompts.ts (@wenshao): product decision on prompt content.

Process notes (not code defects)

The "Integration Tests skipped", "reverse audit", and "verification" review
observations describe how the review/CI was conducted, not a defect in this
PR's code; no code change applies. The integration-test skip is a CI/workflow
matter for the maintainer.

Verification

  • npm run typecheck — passed
  • npm run build — passed (after fixing the CJS dynamic-import typing surfaced by tsc --build)
  • npm run bundle — passed; dist/chunks/zoom-image-*.js ≈ 10 KB, contains external import("sharp"), no inlined sharp/libvips
  • npm run lint — passed (no errors/warnings)
  • npx prettier --write (changed files) — applied
  • node --check esbuild.config.js scripts/prepare-package.js — passed
  • vitest packages/core src/tools/zoom-image.test.ts — 15 passed
  • vitest packages/core src/config/config.test.ts — 439 passed
  • vitest packages/web-shell toolFormatting.test.ts + toolFormatting.drift.test.ts — 38 passed
  • Mutation check: autoOrient: true → false makes the EXIF test fail (1 failed), confirming the fixture pins the behavior; reverted
中文说明

Autofix 评审轮次 — 总结

提交 c9e098805(4 个文件,+87/−9)。本轮修复了阻塞性的 sharp 打包缺陷和已验证的测试缺陷,补充了两个所需的校验测试,并对其余反馈做了分类处理。

已解决(本次提交中实现)

  • sharp 未接入打包路径 — Critical(rc:3655391707、rc:3655997676;@wenshao 阻塞项)。按现有原生模块模式修复:
    • esbuild.config.js:将 'sharp' 加入 external,使 esbuild 不再内联 sharp 的 JS(约 233 KB),原生绑定在运行时从 node_modules 解析。已验证:打包后的 dist/chunks/zoom-image-*.js 约 10 KB,包含外部 import("sharp"),且没有内联 sharp/libvips。
    • scripts/prepare-package.js:在发布包的 optionalDependencies 中加入 sharp 以及 @img/sharp-* / @img/sharp-libvips-* 平台二进制(darwin-arm64/x64、linux-arm64/x64、win32-x64),使 npm 安装匹配的原生二进制。版本与 lockfile 一致(0.34.5 / 1.2.4)。
  • 按需加载 / 启动崩溃@wenshao)。zoom-image.ts 现在在 execute() 内部动态导入 sharp(顶部仅保留 type-only 导入),因此该模块只在 zoom_image 实际运行时加载;绑定缺失时返回有界的 READ_CONTENT_FAILURE 结果,而不是在严格工具预热时抛异常。CJS export = 的互操作解包参照 utils/iconvHelper.ts
  • EXIF 测试未锁住自动方向纠正 — 已变异验证@wenshao)。将红绿两半的 fixture 替换为四个不同象限,并裁剪“显示后的左上象限”;中心像素在开启 autoOrient 时为蓝、关闭时为红。已通过变异确认:将 autoOrient: true → false 会使该测试失败(1 failed),随后已还原。
  • .qwenignore 拒绝路径无测试(rc:3655391727、rc:3655997691)。新增测试:写入匹配该图片的 .qwenignore 模式,断言 build() 抛出 ignore-pattern 消息。
  • 对称的 y1 >= y2 分支无测试(rc:3655391733)。在已有 x1 >= x2 用例旁补充了该断言。
  • web-shell 缺少 zh-CN 翻译(rc:3655786203、rc:3655997688)。已在 d3227c5a3 修复;重新验证 — toolFormatting.test.ts + toolFormatting.drift.test.ts 通过(38/38)。
  • 描述 nit@wenshao)。明确说明坐标始终相对原始全分辨率图像,而非上一次返回的视图。

升级给维护者(开放问题,本轮不裁定)

  • 注册开关读取 getEffectiveInputModalities(),该值在持久化认证前为空@wenshao)。真实的行为缺口(首次运行 session / /model 切换时工具缺失)。建议:无条件注册 zoom_image,在执行时判定(image-gen.ts 模式)。保持开放,因为改变文本模型下的工具可见性属于产品/范围决策。
  • 独立发布脚本scripts/build-standalone-release.jsscripts/create-standalone-package.js)未暂存 sharp/libvips(@wenshao)。按需导入已经避免了该路径下的启动崩溃(工具会降级为有界错误),因此这是一个“功能可用性”的后续项。按目标平台暂存 sharp 的两个包族属于发布工程工作,在本环境无法验证(不允许联网 npm 安装)。
  • 发布版 core 的依赖类型@wenshao):把 packages/core/package.json 中的 sharpdependencies 移到 optionalDependencies(SDK 使用者体积)需要重新生成 package-lock.json(联网 npm install),本环境无法完成;已标记为维护者决策。
  • 只读工具集合speculationToolGate.tssubagent-plan-tool-policy.ts)(@wenshao):目前两者都安全失败;是否加入 zoom_image 是评审者明确要求的“ deliberate decision”。

已拒绝(附原因)

  • 版权年份 2026 → 2025@yiliang114):外观性问题且有歧义(当前日期为 2026);保持原样。
  • Windows CI 覆盖@yiliang114):CI/文档问题,超出代码修复范围。
  • 二分查找 / 竞态 / 死预算分支的注释@yiliang114@wenshao):按仓库“默认不写注释”规则;代码本身清晰,9 MB 上限是有意的兜底。
  • execute() 中的宽泛 catch@yiliang114):已经正确 — catch 中先执行 signal.throwIfAborted(),abort 会正确传播。
  • 收紧 limitInputPixels 上限@wenshao):调参判断;当前 sharp 默认值已与 100 MB 源限制配合。
  • FileOperationEvent 遥测@wenshao):功能新增,超出本次修复范围。
  • ca.jsZoomImage 翻译@wenshao):没有完整性测试强制 ca.jstoolDisplayName 全量对齐;部分语言翻译最好留给译者。
  • packages/webuilabelUtils.ts@wenshao):独立包,无失败测试,仅一致性 — 适合作为后续项。
  • prompts.ts 中的可发现性提示@wenshao):关于 prompt 内容的产品决策。

流程说明(非代码缺陷)

“Integration Tests skipped”“reverse audit”“verification”等评审观察描述的是评审/CI 的执行方式,而非本 PR 代码的缺陷;不适用代码改动。Integration test 被跳过属于 CI/工作流事项,由维护者处理。

验证

  • npm run typecheck — 通过
  • npm run build — 通过(在修复 tsc --build 暴露的 CJS 动态导入类型问题之后)
  • npm run bundle — 通过;dist/chunks/zoom-image-*.js 约 10 KB,包含外部 import("sharp"),无内联 sharp/libvips
  • npm run lint — 通过(无错误/告警)
  • npx prettier --write(改动文件)— 已应用
  • node --check esbuild.config.js scripts/prepare-package.js — 通过
  • vitest packages/core src/tools/zoom-image.test.ts — 15 通过
  • vitest packages/core src/config/config.test.ts — 439 通过
  • vitest packages/web-shell toolFormatting.test.ts + toolFormatting.drift.test.ts — 38 通过
  • 变异检查:autoOrient: true → false 使 EXIF 测试失败(1 failed),确认 fixture 锁住了该行为;已还原

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/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Re-review — feat(core): add full-resolution image zoom tool

Re-reviewed at head c9e0988 (autofix round 1). The blocking packaging defect is fixed — I re-tested it rather than taking the report at face value. One carried finding is still open, one new finding, and I'm retracting one of my own requests from the last round because I measured it and the bot was right to decline.


✅ Resolved — sharp packaging (was blocking)

The fix is correct along all three axes: esbuild.config.js external, scripts/prepare-package.js optionalDependencies, and the type-only top-level import + await import('sharp') inside execute(). The startup-crash path is genuinely closed — zoom-image.ts no longer touches sharp at module scope, so a strict warmAll can't throw during Config.initialize().

I verified the CJS interop unwrap under real Node ESM (not just vitest), since that's where a .default collapse would actually bite:

unwrap .default typeof = function | kernel.lanczos3 = lanczos3

Two follow-ups on the fix itself, neither blocking:

  • The 10 explicit @img/* pins are redundant, and drift-prone. sharp@0.34.5 declares all 24 platform packages in its own optionalDependencies, so sharp: '0.34.5' alone is sufficient for npm to install the right binary — including the 14 the new list omits (@img/sharp-linuxmusl-x64 / -arm64 for Alpine, win32-arm64, win32-ia32, linux-s390x / -ppc64 / -riscv64 / -arm). So coverage is fine today. The cost is that the duplicated 0.34.5 / 1.2.4 versions are hand-maintained: the next sharp bump leaves them stale, and npm then installs a mismatched top-level binary alongside the correct nested one. Suggest keeping only sharp: '0.34.5' and dropping the @img/* block.
  • Standalone releases still don't ship sharp — confirmed, scripts/build-standalone-release.js and scripts/create-standalone-package.js stage only @teddyzhu/clipboard. With the lazy import this is now a bounded READ_CONTENT_FAILURE, not a crash, so I agree it's a follow-up. Worth stating in the PR description that zoom_image is npm-install-only for now, so it isn't discovered as a "bug" later.

✅ Resolved — EXIF orientation test

The 4-quadrant + orientation: 6 + displayed-top-left fixture discriminates properly now, and the reported autoOrient: true → false mutation failure matches what I got locally last round. Good fix.


❌ Still open — the registration gate (the one substantive item left)

This is unchanged, and I can now state it as a structural fact rather than a suspicion:

  • this.toolRegistry is assigned at exactly one place, config.ts:2736, inside initialize(). Nothing else in the file rebuilds it.
  • setModel() (config.ts:3984) explicitly hot-updates contentGeneratorConfig.modalities, with its own comment saying it must — "a hot switch between oauth models with different image support must update them, or the vision-bridge gate and image-stripping read the previous model's modalities." But it never rebuilds the registry. So /model from a text-only model to an image model does not add zoom_image, and the reverse does not remove it. The comment at 4056 documents precisely the invariant the new gate breaks.
  • getEffectiveInputModalities() is this.getContentGeneratorConfig()?.modalities ?? {}, and contentGeneratorConfig is populated only by refreshAuth. gemini.tsx:476 guards that call with if (authType) and its own comment: "Fresh users may not have selected/persisted an authType yet." So on a first run — and whenever security.auth.useExternal is set, which skips the block entirely — initialize() reads {} and the tool is absent for that whole session even on an image-capable model.

The two new config.test.ts cases can't catch any of this, because they vi.spyOn(config, 'getEffectiveInputModalities').mockReturnValue(...). They pin the if, not whether the value is populated at the moment it is read.

Recommendation is the same as last round and I think it's now cheaper than the alternative: register unconditionally, gate at execute time (precedent: image-gen.ts:131). That also collapses to the failure branch you already added for a missing sharp, so it's one error path instead of two behaviors.

🆕 New — tiny crops burn a full image budget

magnifiedSize() always expands to the largest size fitting the visual budget, with no floor on the source crop, and validateToolParamValues only checks x1 < x2 — nothing bounds x2 - x1:

crop 1×1     → view 1092×1092   (1092× upscale)
crop 10×10   → view 1092×1092   (109×)
crop 100×100 → view 1092×1092   (10.9×)
crop 2000×500→ view 1568×392    (0.8× — downscale, fine)

Measured: a 10×10 crop returns a 126 KB JPEG at 1092×1092 = 1521 patches — essentially the entire per-image budget spent on lanczos interpolation of 100 source pixels. A model that miscomputes normalized coordinates (easy: 1 normalized unit ≈ 1 px on a 1000px-wide image) pays full image-token price for noise.

Two cheap mitigations, either is fine:

  • cap the upscale factor (e.g. ≤ 8×) so the view degrades to the crop's own resolution instead of inflating; and/or
  • report the actual pixel rect in the text part. Right now the text echoes the requested normalized coords, so the Math.min/Math.max clamping is invisible — the model can't tell it over-zoomed or that its rect was clipped.

🔄 Retracting my own request: limitInputPixels

Last round I asked for a tighter limitInputPixels, reasoning that a 268 Mpx source under the 100 MB byte cap would force a ~1 GB decode. The bot declined it as tuning judgment. I measured, and the decline was correct — libvips is demand-driven here, so cost tracks the crop, not the source:

source file crop wall peak RSS
16000×16000 (256 Mpx — the max the current limit allows) 0.74 MB bottom-edge 160×160 96 ms 61 MB
5750×5750 full-noise (33 Mpx — worst case still under the 100 MB byte cap) 2.4 MB bottom-edge 58×58 39 ms 104 MB

Bottom-edge crops specifically, since a top-left crop would let a sequential decoder stop early. No unbounded decode in either case. Please disregard that request; the current pairing of the sharp default with the 100 MB cap is fine.


Minor

  • validateToolParamValues calls this.config.getFileService() three times on the ignore path; read-file.ts:663 binds it to a local first. Cosmetic, but each call constructs work the sibling avoids.
  • Sibling read-only sets still omit zoom_image: followup/speculationToolGate.ts:31 and agents/runtime/subagent-plan-tool-policy.ts:21 (both list READ_FILE/GREP/GLOB), plus packages/webui/.../labelUtils.ts:86 and routing.ts:40. All fail safe — the tool simply isn't speculated / isn't offered to plan-mode subagents / renders with a default label — so I'm flagging these as a deliberate decision, not a defect.
  • Defensive nit: the try around the dynamic import wraps only the import, not the unwrap. If .default were ever undefined, sharp stays undefined and the resulting TypeError is swallowed by the metadata catch, surfacing as "Unsupported image. zoom_image accepts static PNG, JPEG, or WebP files" — a misleading message for a load failure. I confirmed .default is correct for sharp 0.34.5 (CJS, no exports map), so this is theoretical; a typeof sharp !== 'function' check in the load branch would make the diagnostic honest.

Verdict

The blocker is cleared and the tool itself remains well built. The registration gate is the one thing I'd want settled before merge — it's a real user-visible gap (first-run sessions and every /model switch), and the fix is smaller than the conditional it replaces.

中文说明

复审 —— feat(core): add full-resolution image zoom tool

在 head c9e0988(autofix 第 1 轮)复审。阻塞性的打包缺陷已修复 —— 我重新做了验证,而不是直接采信报告。还有 1 项遗留问题未解决、1 项新发现,另外我撤回上一轮自己提的一个要求,因为实测下来 bot 拒绝得没错。

✅ 已解决 —— sharp 打包(原阻塞项)

三个环节都改对了:esbuild.config.jsexternalscripts/prepare-package.jsoptionalDependencies,以及顶部 type-only 导入 + execute()await import('sharp')。启动崩溃路径确实关闭了 —— zoom-image.ts 不再在模块作用域接触 sharp,因此严格 warmAll 不会在 Config.initialize() 中抛错。

我在真实 Node ESM(而非仅 vitest)下验证了 CJS 互操作解包,因为 .default 塌陷只会在那里出问题:typeof sharp = functionkernel.lanczos3 = lanczos3

两点后续,均不阻塞:

  • 那 10 条 @img/* 显式 pin 是冗余的,且容易漂移。 sharp@0.34.5 自身的 optionalDependencies 已声明全部 24 个平台包,所以只写 sharp: '0.34.5' 就足以让 npm 安装正确的二进制 —— 包括列表遗漏的 14 个(Alpine 的 @img/sharp-linuxmusl-x64/-arm64win32-arm64win32-ia32linux-s390x/-ppc64/-riscv64/-arm)。因此当前覆盖没问题。代价是重复的 0.34.5 / 1.2.4 版本号需要手工维护:下次升级 sharp 时它们会静默过期,npm 就会在正确的嵌套二进制之外再装一个不匹配的顶层二进制。建议只保留 sharp: '0.34.5',删掉 @img/* 那一段。
  • standalone 发布仍未打包 sharp —— 已确认,scripts/build-standalone-release.jscreate-standalone-package.js 只暂存 @teddyzhu/clipboard。有了懒加载,这现在是有界的 READ_CONTENT_FAILURE 而非崩溃,所以我同意作为后续项。建议在 PR 描述中写明 zoom_image 目前仅在 npm 安装下可用,免得后面被当成 bug 报上来。

✅ 已解决 —— EXIF 方向测试

四象限 + orientation: 6 + 选取显示后左上象限的 fixture 现在具备区分能力,报告中 autoOrient: true → false 的变异失败结果与我上一轮本地所得一致。修得很好。

❌ 仍未解决 —— 注册开关(唯一实质性遗留项)

这一项没有变化,而且我现在可以把它陈述为结构性事实而非猜测:

  • this.toolRegistry 只在一处赋值:config.ts:2736,位于 initialize() 内。文件中没有任何其他地方重建它。
  • setModel()config.ts:3984明确热更新 contentGeneratorConfig.modalities,其注释还说明必须如此 —— "不同图片支持能力的 oauth 模型之间热切换必须更新它,否则 vision-bridge 判定和图片剥离会读到上一个模型的 modalities。" 但它从不重建工具注册表。因此 /model 从纯文本模型切到图片模型不会新增 zoom_image,反向切换也不会移除它。 4056 行的注释恰好描述了新开关所破坏的那条不变式。
  • getEffectiveInputModalities()this.getContentGeneratorConfig()?.modalities ?? {},而 contentGeneratorConfig 只由 refreshAuth 填充。gemini.tsx:476if (authType) 守卫该调用,其注释写道:"新用户可能尚未选择/持久化 authType。" 因此在首次运行时 —— 以及只要设置了 security.auth.useExternal(会整体跳过该代码块)—— initialize() 读到的是 {},即便主模型支持图片,该工具在整个 session 中都不存在。

新增的两个 config.test.ts 用例无法覆盖上述任何一种情况,因为它们 vi.spyOn(config, 'getEffectiveInputModalities').mockReturnValue(...)。它们锁住的是那个 if,而不是"在真正读取的时刻该值是否已被填充"。

建议与上一轮相同,而且现在看成本比维持现状更低:无条件注册,在执行时判定(先例:image-gen.ts:131)。这样还能与你已经加好的 "sharp 缺失" 失败分支合并为同一条错误路径,而不是两种行为。

🆕 新发现 —— 极小裁剪区域会耗尽整张图片的预算

magnifiedSize() 始终放大到符合视觉预算的最大尺寸,且对源裁剪尺寸没有下限,而 validateToolParamValues 只检查 x1 < x2 —— 没有任何地方约束 x2 - x1

裁剪 1×1     → 视图 1092×1092   (放大 1092 倍)
裁剪 10×10   → 视图 1092×1092   (109 倍)
裁剪 100×100 → 视图 1092×1092   (10.9 倍)
裁剪 2000×500→ 视图 1568×392    (0.8 倍 —— 缩小,正常)

实测:10×10 的裁剪返回 1092×1092、126 KB 的 JPEG = 1521 个 patch —— 几乎把单张图片的全部预算,花在对 100 个源像素做 lanczos 插值上。模型只要算错归一化坐标(很容易:在 1000 像素宽的图上,1 个归一化单位 ≈ 1 px),就要为噪声付出整张图片的 token 代价。

两个低成本缓解方案,任选其一:

  • 限制放大倍数(例如 ≤ 8 倍),使视图退化到裁剪自身分辨率而不是被撑大;以及/或者
  • 在 text part 中报告实际像素矩形。目前 text 回显的是请求的归一化坐标,因此 Math.min/Math.max 的钳制对模型不可见 —— 模型无法得知自己放大过头或矩形被裁掉了。

🔄 撤回我自己的要求:limitInputPixels

上一轮我要求收紧 limitInputPixels,理由是 100 MB 字节上限内的 268 Mpx 源图会导致约 1 GB 的解码。bot 以"调参判断"为由拒绝。我做了实测,拒绝是对的 —— 此处 libvips 是按需驱动的,开销跟随裁剪区域而非源图:

源图 文件 裁剪 耗时 峰值 RSS
16000×16000(256 Mpx —— 当前上限允许的最大值) 0.74 MB 底边 160×160 96 ms 61 MB
5750×5750 全噪声(33 Mpx —— 仍在 100 MB 字节上限内的最坏情况) 2.4 MB 底边 58×58 39 ms 104 MB

特意选底边裁剪,因为左上角裁剪会让顺序解码器提前结束。两种情况都没有无界解码。请忽略该要求;当前 sharp 默认值与 100 MB 上限的搭配是合适的。

次要项

  • validateToolParamValues 在 ignore 分支上调用了三次 this.config.getFileService()read-file.ts:663 先绑定到局部变量。属于外观问题,但每次调用都做了同级工具已避免的工作。
  • 同类只读工具集合仍未包含 zoom_imagefollowup/speculationToolGate.ts:31agents/runtime/subagent-plan-tool-policy.ts:21(两者都列了 READ_FILE/GREP/GLOB),以及 packages/webui/.../labelUtils.ts:86routing.ts:40。它们都安全失败 —— 工具只是不被推测执行 / 不提供给 plan 模式子代理 / 用默认标签渲染 —— 所以我把这些标记为需要明确决策的事项,而非缺陷。
  • 防御性细节:动态导入外的 try 只包住了 import,没包住解包。若 .defaultundefinedsharp 会保持 undefined,随后的 TypeError 会被 metadatacatch 吞掉,表现为 "Unsupported image. zoom_image accepts static PNG, JPEG, or WebP files" —— 对加载失败而言是误导性信息。我已确认 sharp 0.34.5(CJS,无 exports 映射)的 .default 是正确的,所以这属于理论情形;在加载分支加一个 typeof sharp !== 'function' 检查可以让诊断信息如实反映问题。

结论

阻塞项已清除,工具本身依然做得很扎实。注册开关是我希望在合并前敲定的唯一一点 —— 它是真实的用户可见缺口(首次运行的 session 以及每一次 /model 切换),而且修复方案比它所替换的条件判断更小。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline. Unresolved, please confirm: [Critical] config.ts:7157 — registration gate reads getEffectiveInputModalities() which may be empty when no auth is persisted; tool absent for first-run session / model switch (raised by @wenshao; escalated to maintainer as product decision; code unchanged since report — cannot determine if defect or by-design)

中文说明

已审查。 建议见行内评论。 未决,请确认:[Critical] config.ts:7157 — registration gate reads getEffectiveInputModalities() which may be empty when no auth is persisted; tool absent for first-run session / model switch (raised by @wenshao; escalated to maintainer as product decision; code unchanged since report — cannot determine if defect or by-design)

— qwen3.7-max via Qwen Code /review

Comment thread scripts/prepare-package.js Outdated
…wenLM#7809)

Register zoom_image unconditionally and move the image-modality check to
execute time so first-run sessions and hot /model switches resolve the
tool without re-running initialize(). Cap magnification at 8x so a tiny
crop no longer inflates to the full image-token budget. Drop the redundant
@img/* platform pins; sharp's own optionalDependencies install the matching
binary for each OS/arch.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round 2 — PR #7809

Addressed the feedback newer than the last evaluation. One commit
(fix(core): gate zoom_image at execute time and cap tiny-crop upscale).
No conflict merge was needed (--conflict false).

Feedback points

  1. [Critical] Registration gate (config.ts) — implemented. This was
    previously escalated as a product decision; @wenshao settled it in the
    re-review with an explicit recommendation, so it is now a directed fix.
    zoom_image is registered unconditionally (right after read_file),
    and the image-modality check moved to execute time in
    zoom-image.ts, mirroring the image-gen.ts precedent. This closes both
    gaps the reviewer proved structurally: first-run / security.auth.useExternal
    sessions (where getEffectiveInputModalities() reads {} at
    initialize()) and hot /model switches (which update modalities but never
    rebuild the registry). When the current model does not accept images,
    execute() now returns the same bounded READ_CONTENT_FAILURE path as the
    missing-sharp branch — one error path instead of two behaviors.

    • Tests: replaced the two registration tests in config.test.ts with one
      that pins unconditional registration even when modalities are {}, and
      added an execute-time gate test in zoom-image.test.ts.
  2. [Suggestion] Packaging pins (inline rc:3656856889) — resolved, via
    the maintainer-endorsed approach rather than the literal suggestion. The bot
    asked to add the missing @img/sharp-linuxmusl-* / @img/sharp-win32-arm64
    pins; @wenshao's re-review recommended the opposite — drop the whole @img/*
    block
    — because sharp@0.34.5 already declares all 24 platform packages in
    its own optionalDependencies (verified against the installed
    node_modules/sharp/package.json), so sharp: '0.34.5' alone gives npm every
    OS/arch binary transitively, including the musl and win32-arm64 ones the bot
    flagged. Dropping them also removes the hand-maintained 0.34.5 / 1.2.4
    versions that drift on the next sharp bump. The platform-coverage concern is
    therefore resolved, not dismissed.

  3. 🆕 Tiny crops burn a full image budgetimplemented. Added an
    IMAGE_MAX_UPSCALE = 8 cap to magnifiedSize(): the long-edge search bound
    is now min(IMAGE_VIEW_MAX_EDGE, max(w,h) * 8), so a 10×10 crop yields an
    80×80 view instead of inflating to ~1092×1092 (≈ the whole per-image budget
    spent on lanczos interpolation of 100 source pixels). Downscale cases are
    unaffected. The text part already reports the actual source crop and
    returned view pixel sizes, so with the cap in place the model sees the
    degraded resolution directly. Added a regression test asserting a 10×10 crop
    stays ≤ 80×80 (fails without the cap).

  4. Minor: getFileService() called three timesimplemented. Bound it
    to a local in validateToolParamValues, matching read-file.ts.

  5. Minor: sibling read-only sets omit zoom_image
    (speculationToolGate.ts, subagent-plan-tool-policy.ts, webui
    labelUtils.ts / routing.ts) — no action. The reviewer flagged these as
    a deliberate decision, not a defect: every one fails safe (the tool simply is
    not speculated / not offered to plan-mode subagents / renders with a default
    label). Left as-is.

  6. Minor: defensive typeof sharp !== 'function' checkdeclined. The
    reviewer confirmed .default is correct for sharp 0.34.5 (CJS, no exports
    map) and called the branch theoretical. Adding a guard for an unreachable case
    is exactly the "error handling for impossible scenarios" AGENTS.md's Simplicity
    First rules out, so it is not worth the diff growth.

  7. limitInputPixels retraction — acknowledged, no action. The reviewer
    measured that libvips is demand-driven here and the current pairing with the
    100 MB cap is correct; the earlier decline stands.

  8. Standalone releases still don't ship sharp — follow-up, no code change.
    With the lazy import this is a bounded READ_CONTENT_FAILURE, not a crash,
    and the reviewer agreed it is a follow-up. This flow cannot edit the PR
    description, so flagging here: the description may want to note that
    zoom_image is npm-install-only for now.

Conflict notes

None — --conflict false, no merge performed.

Verification

  • npm run typecheck — passed
  • npm run build — passed (exit 0)
  • npx eslint on the 5 changed files — passed, no findings
  • npx prettier --write on the 5 changed files — already formatted (unchanged)
  • vitest src/tools/zoom-image.test.ts (touched) — 17 passed
  • vitest src/config/config.test.ts (touched) — 438 passed
  • vitest coreToolScheduler / autoMode / permission-manager / loopDetectionService
    (PR-touched) — 784 passed
  • vitest agent-override / tool-registry / config.workflow-registration /
    config.safe-mode (real-Config registry tests) — 87 passed
  • Full packages/core suite — 17840 passed; the 81 failures (skill/subagent
    managers, editor sandbox detection, installation ID, memory/rules discovery,
    token storage) reproduce identically on the clean tree with this change stashed
    (14 failed across 3 of those files), so they are pre-existing/environmental
    (HOME-dir, sandbox, encryption env) and unrelated to this change.
  • No settings source changed → generate:settings-schema not required. Behavior
    is covered by unit tests, not bundle-only → no integration run required.
中文说明

Autofix 审查第 2 轮 — PR #7809

已处理上一轮评估之后的新反馈。一个提交(fix(core): gate zoom_image at execute time and cap tiny-crop upscale)。无需合并冲突(--conflict false)。

反馈点

  1. [Critical] 注册开关config.ts)— 已实现。此项此前作为产品决策
    上报;@wenshao 在复审中给出了明确建议,因此现在是一项有明确方向的修复。
    zoom_image 改为无条件注册(紧跟在 read_file 之后),图片模态判定
    移到 zoom-image.ts执行时,与 image-gen.ts 的先例一致。这关闭了
    审查者从结构上证实的两个缺口:首次运行 / 设置了 security.auth.useExternal
    的 session(此时 initialize() 读到 getEffectiveInputModalities()
    {}),以及热切换 /model(会更新 modalities 但从不重建注册表)。当当前
    模型不接受图片时,execute() 现在返回与 sharp 缺失分支相同的有界
    READ_CONTENT_FAILURE 路径 —— 一条错误路径,而非两种行为。

    • 测试:将 config.test.ts 中两个注册测试替换为一个锁定"即使 modalities 为
      {} 也无条件注册"的测试,并在 zoom-image.test.ts 新增执行时开关测试。
  2. [Suggestion] 打包 pin(行内 rc:3656856889)— 已解决,但采用的是
    维护者认可的方案,而非字面建议。bot 要求新增缺失的
    @img/sharp-linuxmusl-* / @img/sharp-win32-arm64 pin;@wenshao 的复审给出
    相反建议 —— 删掉整个 @img/* —— 因为 sharp@0.34.5 自身的
    optionalDependencies 已声明全部 24 个平台包(已对照安装好的
    node_modules/sharp/package.json 核实),所以仅写 sharp: '0.34.5' 就能让
    npm 传递性地装上每个 OS/arch 的二进制,包括 bot 指出的 musl 与 win32-arm64。
    删除它们同时也去掉了手工维护的 0.34.5 / 1.2.4 版本号,避免下次升级 sharp
    时漂移。因此平台覆盖问题是被解决了,而非被忽略。

  3. 🆕 极小裁剪耗尽整张图片预算已实现。在 magnifiedSize() 中加入
    IMAGE_MAX_UPSCALE = 8 上限:长边搜索上界改为
    min(IMAGE_VIEW_MAX_EDGE, max(w,h) * 8),因此 10×10 的裁剪产出 80×80 视图,
    而不是被撑大到约 1092×1092(几乎把整张单图预算花在对 100 个源像素做 lanczos
    插值上)。缩小情形不受影响。text part 本就已报告实际的 source crop
    returned view 像素尺寸,加上上限后模型能直接看到退化后的分辨率。新增了一个
    回归测试,断言 10×10 裁剪保持 ≤ 80×80(无上限时会失败)。

  4. 次要:getFileService() 被调用三次已实现。在
    validateToolParamValues 中绑定到局部变量,与 read-file.ts 一致。

  5. 次要:同类只读集合未含 zoom_imagespeculationToolGate.ts
    subagent-plan-tool-policy.ts、webui 的 labelUtils.ts / routing.ts)—
    不处理。审查者把这些标记为刻意决策而非缺陷:每一处都安全失败(工具只是
    不被推测执行 / 不提供给 plan 模式子代理 / 用默认标签渲染)。保持现状。

  6. 次要:防御性 typeof sharp !== 'function' 检查拒绝。审查者已确认
    sharp 0.34.5(CJS,无 exports 映射)的 .default 是正确的,并称该分支属于
    理论情形。为不可达情形添加守卫,正是 AGENTS.md 的 Simplicity First 所排除的
    "为不可能场景做错误处理",不值得增加 diff。

  7. 撤回 limitInputPixels — 已知悉,不处理。审查者实测此处 libvips 是按需
    驱动的,当前与 100 MB 上限的搭配是正确的;此前的拒绝成立。

  8. standalone 发布仍未打包 sharp — 后续项,不改代码。有了懒加载,这是有界的
    READ_CONTENT_FAILURE 而非崩溃,审查者也同意作为后续项。本流程无法编辑 PR
    描述,故在此提示:描述中或许应写明 zoom_image 目前仅在 npm 安装下可用。

冲突说明

无 —— --conflict false,未执行合并。

验证

  • npm run typecheck — 通过
  • npm run build — 通过(exit 0)
  • npx eslint(5 个改动文件)— 通过,无发现
  • npx prettier --write(5 个改动文件)— 已格式化(无变化)
  • vitest src/tools/zoom-image.test.ts(改动)— 17 通过
  • vitest src/config/config.test.ts(改动)— 438 通过
  • vitest coreToolScheduler / autoMode / permission-manager /
    loopDetectionService(PR 改动)— 784 通过
  • vitest agent-override / tool-registry / config.workflow-registration /
    config.safe-mode(真实 Config 注册表测试)— 87 通过
  • 完整 packages/core 套件 — 17840 通过;81 个失败(skill/subagent 管理器、
    editor 沙箱检测、installation ID、memory/rules 发现、token 存储)在本次改动
    被 stash 的干净树上同样复现(其中 3 个文件即有 14 个失败),因此属于既有/
    环境性失败(HOME 目录、沙箱、加密环境),与本次改动无关。
  • 未改动任何 settings 源 → 无需 generate:settings-schema。行为由单测覆盖,
    非仅 bundle 验证 → 无需集成测试。

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/模型 qwen3.8-max-preview

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/tools/zoom-image.ts
Comment thread packages/core/src/tools/zoom-image.ts
Comment thread packages/core/src/tools/zoom-image.ts
…wenLM#7809)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #7809

Addressed all three inline suggestions from the automated reviewer. No conflict
merge was needed (--conflict false); the change sits cleanly on the current
head.

Feedback points

  • [rc:3658224652] sharp dynamic-import failure path untested — Implemented.
    Added packages/core/src/tools/zoom-image.sharp-failure.test.ts, which mocks
    sharp so the dynamic import('sharp') in execute() rejects (simulating a
    missing/incompatible native binary, e.g. musl Linux without
    @img/sharp-linuxmusl-x64) and asserts a recoverable READ_CONTENT_FAILURE
    mentioning sharp. It lives in its own file because vi.mock('sharp') is
    file-scoped and hoisted — placing it in zoom-image.test.ts would break the
    real-sharp fixtures used there.

  • [rc:3658224665] format-guard branch for sharp-decodable unsupported formats
    untested — Implemented.
    Added a test in zoom-image.test.ts that creates a
    real single-frame GIF. Verified via a sharp probe that such a file yields
    metadata() format=gif, pages=1, so it passes the decode and multi-page
    guards and is rejected specifically by the SUPPORTED_IMAGE_FORMATS guard
    (the existing .txt test fails earlier, at metadata()). Asserts
    READ_CONTENT_FAILURE with the "PNG, JPEG, or WebP" message.

  • [rc:3658224672] zoom_image reads a file but emits no logFileOperation
    telemetry — Implemented.
    zoom_image now emits a FileOperationEvent
    (FileOperation.READ) on the success path, mirroring read_file and the
    other sibling file tools. The event carries the tool name, the file's
    mimetype, and its extension; lines/programming_language are omitted as
    not applicable to images. Added the matching vi.mock('../telemetry/loggers.js')
    to the test file (same convention as read-file.test.ts et al.) and a focused
    test asserting exactly one file_operation event with tool_name: 'zoom_image'
    and operation: 'read' is emitted on a successful zoom.

Conflict notes

None. --conflict false; no merge performed.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx eslint packages/core/src/tools/zoom-image.ts zoom-image.test.ts zoom-image.sharp-failure.test.ts — passed (no findings)
  • vitest run src/tools/zoom-image.test.ts src/tools/zoom-image.sharp-failure.test.ts (packages/core) — 20 passed (19 + 1)
  • node sharp probe of a generated single-frame GIF — format=gif, pages=1 (confirms the new GIF test exercises the format guard, not the metadata catch)

No settings source changed, so npm run generate:settings-schema was not required. The touched behavior is unit-covered, not bundle/integration-only, so no integration run was needed.

中文说明

Autofix 审查轮次 — PR #7809

已处理自动审查器提出的全部三条行内建议。无需冲突合并(--conflict false);改动干净地落在当前 head 之上。

反馈点

  • [rc:3658224652] sharp 动态导入失败路径缺少测试 — 已实现。
    新增 packages/core/src/tools/zoom-image.sharp-failure.test.ts,对 sharp 进行 mock,使 execute() 中的动态 import('sharp') 拒绝(模拟原生二进制缺失或不兼容,例如 musl Linux 缺少 @img/sharp-linuxmusl-x64),并断言返回可恢复的 READ_CONTENT_FAILURE 且提及 sharp。该测试单独成文件,因为 vi.mock('sharp') 是文件级且会被提升——若放入 zoom-image.test.ts,会破坏其中使用真实 sharp 生成的测试夹具。

  • [rc:3658224665] sharp 可解码但不受支持格式的格式守卫分支缺少测试 — 已实现。
    zoom-image.test.ts 中新增测试,创建一个真实的单帧 GIF。通过 sharp 探针验证:此类文件的 metadata() 返回 format=gifpages=1,因此它能通过解码守卫和多帧守卫,最终被 SUPPORTED_IMAGE_FORMATS 守卫专门拒绝(现有的 .txt 测试更早在 metadata() 处失败)。断言 READ_CONTENT_FAILURE 且消息包含 "PNG, JPEG, or WebP"。

  • [rc:3658224672] zoom_image 读取文件却不发出 logFileOperation 遥测 — 已实现。
    zoom_image 现在在成功路径上发出 FileOperationEventFileOperation.READ),与 read_file 及其他同级文件工具保持一致。事件携带工具名、文件的 mimetype 与扩展名;lines/programming_language 因不适用于图片而省略。在测试文件中添加了对应的 vi.mock('../telemetry/loggers.js')(与 read-file.test.ts 等相同的约定),并新增一个聚焦测试,断言成功 zoom 时恰好发出一个 file_operation 事件,其 tool_name: 'zoom_image'operation: 'read'

冲突说明

无。--conflict false;未执行合并。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx eslint packages/core/src/tools/zoom-image.ts zoom-image.test.ts zoom-image.sharp-failure.test.ts — 通过(无问题)
  • vitest run src/tools/zoom-image.test.ts src/tools/zoom-image.sharp-failure.test.ts(packages/core)— 20 通过(19 + 1)
  • node sharp 探针检测生成的单帧 GIF — format=gifpages=1(确认新增的 GIF 测试触发的是格式守卫,而非 metadata catch)

未改动任何 settings 源文件,因此无需运行 npm run generate:settings-schema。改动的行为已由单元测试覆盖,并非仅能通过 bundle/集成验证,因此无需集成测试。

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/模型 qwen3.8-max-preview

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅

中文说明

未发现问题。LGTM!✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Local verification (round 3) — feat(core): add full-resolution image zoom tool

Re-verified at head 9cccf54 in an isolated worktree, following round 1 (c6b07eb) and round 2 (c9e0988).

Both of my remaining findings are fixed, and I re-tested each one rather than reading the commit messages. Every claim below is backed by a run against the actual head; the two carried findings were re-tested as hypotheses about the new head, not carried forward as facts.


Prior findings → status at 9cccf54

# Finding Raised Status at this head
1 sharp not externalized/packaged → startup crash R1 blocking ✅ Fixed in c9e0988, re-confirmed here
2 EXIF-orientation test could not discriminate R1 ✅ Fixed in c9e0988
3 Registration gate — tool absent on first-run sessions and never added/removed by /model R1, R2 Fixed in 573b0d9 — proven by controlled experiment below
4 Tiny crops burn a full image budget (1×1 → 1092×1092) R2 Fixed in 573b0d9 — 8× cap, measured below
5 10 redundant @img/* pins, drift-prone R2 minor ✅ Removed in 573b0d9; sharp: '0.34.5' alone, with a comment explaining why
6 getFileService() constructed 3× on the ignore path R2 minor ✅ Fixed in 573b0d9
7 Tighter limitInputPixels R1 🔄 I retracted this in R2 after measuring; it stays retracted
8 Standalone releases don't ship sharp R2 follow-up ⚠️ Still true — re-tested, both scripts stage only @teddyzhu/clipboard. Now a bounded error, not a crash. Follow-up.
9 Sibling read-only sets omit zoom_image R2 informational ⚠️ Still true — re-checked all four, all fail safe

One correction to my own R2 text: I suggested the tool "report the actual pixel rect". It already did at c9e0988 — the text part carries Oriented source: …; source crop: …; returned view: …. What is still unreported is the crop's origin, which is only reachable through the degenerate-crop guard, so it isn't worth changing.


1 · The registration gate — controlled experiment

The fix registers zoom_image unconditionally and moves the modality check into execute(), which is what I asked for (precedent: image-gen.ts:131). To confirm it is load-bearing rather than incidental, I wrote a spec that uses a real Config, a real ToolRegistry, and a real image on disk — the PR's own suite drives a stub config object literal, so it pins the if inside execute() but never the registration path — and ran the identical 4-test spec twice, changing only config.ts + zoom-image.ts:

  • pre-fix (c9e0988) overlaid → 4 failed. registry.getTool('zoom_image') returns undefined; the tool simply does not exist for a first-run or text-only session, so a later /model switch has nothing to call.
  • head (9cccf54) → 4 passed. The tool resolves with empty modalities; a text-only model gets a bounded read_content_failure; and a hot /model switch works on the same registry object with no re-initialize(), returning the correct pixels.

Worth recording why the hot switch now works: handleModelChange() mutates contentGeneratorConfig.modalities in place, and getEffectiveInputModalities() reads it live at execute time — so the fix composes with the existing hot-update path rather than depending on a registry rebuild that never happens (this.toolRegistry is still assigned at exactly one place, inside initialize()).

I also mutation-tested the PR's own tests to check they pin the new behaviour rather than merely passing: 9 / 9 mutants killed, no survivors, each by a test whose name matches the mutated behaviour.

verification

The one I'd point at is M2 — changing IMAGE_MAX_UPSCALE from 8 to 9 also dies. The cap assertion is tight, not just "smaller than before".


2 · The 8× cap, and pixel-level correctness

Measured by driving the real tool over a 1000×1000 noise source (where 1 normalized unit = exactly 1 source pixel, which is the coordinate mistake a model actually makes), once per arm:

requested crop before after
1×1 1092×1092 · 1521 patches · 14,345 B 8×8 · 1 patch · 267 B
10×10 1092×1092 · 1521 patches · 105,110 B 80×80 · 9 patches · 2,705 B
100×100 1092×1092 · 1521 patches · 350,482 B 800×800 · 841 patches · 237,790 B
≥196×196 byte-for-byte unchanged

The crossover is exactly 1568 / 8 = 196 px, so every legitimate zoom is untouched and only the pathological end is bounded. That is the right shape for this fix.

cap and correctness

Correctness spot-checks all came back exact: each of the four quadrants returns the precise colour encoded in it; an orientation:6 JPEG (stored rotated 90°) returns green for the displayed top-right, so coordinates follow what a user sees rather than the stored bytes; and a fully transparent PNG flattens to rgb(255,255,255).


3 · End-to-end on a real screenshot

Finally I drove the real tool against a 3200×628 dashboard screenshot with ~10–11 px table text — the situation the PR is actually for. Both panels below are the tool's own returned JPEGs written straight to disk.

end to end

The whole frame reaches a model as 1568×308 (0.49×), leaving that region as 251×73 pixels where 0.0126 is indistinguishable from 0.0128 and 11,904 from 11,804. zoom_image returns 512×148 real source pixels magnified to 1568×453, and every value matches the source data exactly. This is a genuine capability gain, not a re-render of what the model already had.

Also verified: a persisted Read(...) rule covers zoom_image through the meta-category, and the rule the tool offers is identical to read_file's (parent directory) — so the permission story in the description holds.


New findings at this head — both minor, neither blocking

1 · A text-only model still reaches the approval prompt before the gate fires. build() runs validateToolParamValues at coreToolScheduler.ts:1826, well before evaluatePermissionFlow at :2362, and validation does not consult modalities. So for an image outside the workspace, getFileReadDefaultPermission returns 'ask', the user is prompted to approve a read, and then execute() returns "requires a model that accepts image inputs". I have this reproducing in the spec above. It is a direct consequence of the placement I recommended, so I'm flagging the cost rather than complaining about it. If you want it gone, moving the check into validateToolParamValues() keeps the entire hot-switch benefit — build() runs per call — while failing before any prompt.

2 · The new telemetry call is never exercised against a real Config. zoom-image.test.ts does vi.mock('../telemetry/loggers.js') for the whole module, so logFileOperation() is a spy in every test. In reality it reaches QwenLogger.getInstance(config)config.getUsageStatisticsEnabled(); my harness hit that instantly because the stub configs in that file don't define it. I ran it with a real Config and it works — no throw, correct event — so this is not a defect, just a note that a future signature change to logFileOperation/FileOperationEvent would not be caught by these tests. Mutants M6/M7 do confirm the call site itself is pinned.

Minor bookkeeping: the description says the affected suites total 1,324 tests; at this head I measure 1,242 across the seven files, so that line predates the last two commits.


Environment and gates

Isolated worktree at 9cccf54; node_modules APFS-cloned from a lockfile-matching tree; all 20 @qwen-code/* entries verified to resolve inside the worktree (no cross-tree contamination); vite 7.0.0 / vitest 3.2.4 / sharp 0.34.5, all matching the lockfile.

gate result
7 affected suites ✅ 1,242 passed
read-file regression ✅ 87 passed
tsc --noEmit (packages/core) ✅ exit 0, 0 errors
eslint (24 changed files) ✅ exit 0 — positive control: planted unused import → exit 1, restored → exit 0
prettier --check ✅ all match
i18n keys en / zh / zh-TW ✅ present in all three; the self-referential en value matches the file's convention (1555/1556 entries)
mutation matrix ✅ 9/9 killed

Verdict

Merge-ready from my side. The blocker cleared two rounds ago, and the two substantive findings I was holding out on are now genuinely fixed — I verified both by controlled experiment rather than inspection, and the tests that cover them survive mutation. The two new items are minor, and the first one is a tradeoff I asked for.

The one thing worth stating in the description before merge is that zoom_image is npm-install-only for now — standalone builds still don't stage sharp, so it degrades to a bounded error there. Better to document it than to have it reported as a bug later.

中文说明

本地验证(第 3 轮)—— feat(core): add full-resolution image zoom tool

在隔离 worktree 中基于 head 9cccf54 复验,承接第 1 轮(c6b07eb第 2 轮(c9e0988

我遗留的两项问题都已修复,并且每一项都重新做了实测,而不是采信 commit message。 下面每条结论都有针对当前 head 的实际运行支撑;两项遗留问题被当作"关于新 head 的假设"重新验证,而非直接沿用。

历史问题 → 在 9cccf54 的状态

# 问题 提出轮次 当前状态
1 sharp 未 external/打包 → 启动崩溃 R1 阻塞 c9e0988 已修,本轮再次确认
2 EXIF 方向测试不具区分能力 R1 c9e0988 已修
3 注册开关 —— 首次运行时工具缺失,/model 切换不增不减 R1、R2 573b0d9 已修,见下方对照实验
4 极小裁剪耗尽整张图片预算(1×1 → 1092×1092) R2 573b0d9 已修,8× 上限,实测见下
5 10 条冗余 @img/* pin,易漂移 R2 次要 573b0d9 已删除,仅保留 sharp: '0.34.5' 并附注释说明
6 ignore 路径上 getFileService() 构造 3 次 R2 次要 573b0d9 已修
7 收紧 limitInputPixels R1 🔄 我在 R2 实测后已撤回,继续维持撤回
8 standalone 发布未打包 sharp R2 后续项 ⚠️ 仍然存在 —— 已重测,两个脚本仍只暂存 @teddyzhu/clipboard。现在是有界错误而非崩溃,可作后续项
9 同类只读工具集合遗漏 zoom_image R2 提示 ⚠️ 仍然存在 —— 四处均已重查,全部 fail-safe

对我自己 R2 措辞的一处更正:我曾建议"在文本部分报告实际像素矩形",其实 c9e0988 就已经在报告 Oriented source: …; source crop: …; returned view: …。真正未报告的是裁剪区域的起点坐标,而它只能通过退化裁剪保护分支触达,因此不值得改动。

1 · 注册开关 —— 对照实验

修复方案是无条件注册 zoom_image,把 modality 判定移到 execute(),与我的建议一致(先例:image-gen.ts:131)。为确认该修复是真正起作用而非碰巧通过,我写了一份使用真实 Config、真实 ToolRegistry 和磁盘上真实图片的测试 —— PR 自带的测试驱动的是一个 stub config 字面量,因此只锁住了 execute() 里的 if,从未覆盖注册路径 —— 并用完全相同的 4 条测试跑了两遍,仅替换 config.tszoom-image.ts

  • 覆盖回 pre-fix(c9e0988)→ 4 条全部失败。 registry.getTool('zoom_image') 返回 undefined,该工具在首次运行/纯文本会话中根本不存在,后续 /model 切换也就无从调用。
  • 当前 head(9cccf54)→ 4 条全部通过。 modalities 为空时工具可解析;纯文本模型得到有界的 read_content_failure;热切 /model同一个 registry 对象上、无需重新 initialize() 即可工作,并返回正确像素。

值得记录热切换现在为何有效:handleModelChange()就地修改 contentGeneratorConfig.modalities,而 getEffectiveInputModalities() 在执行时实时读取 —— 因此该修复与既有热更新路径天然契合,不再依赖那个实际从未发生的 registry 重建(this.toolRegistry 仍然只在 initialize() 内部的唯一一处赋值)。

我还对 PR 自带测试做了变异测试,确认它们确实锁住新行为而非仅仅通过:9 / 9 变异体全部被杀,无存活,且每个都由名称与被变异行为对应的测试杀死。其中最值得一提的是 M2 —— 把 IMAGE_MAX_UPSCALE8 改成 9 同样被杀,说明该断言是紧的,而不只是"比原来小"。

2 · 8× 上限与像素级正确性

用真实工具在 1000×1000 噪声图上实测(此时 1 归一化单位恰好等于 1 源像素,这正是模型真实会犯的坐标错误),两个分支各跑一次:

请求裁剪 修复前 修复后
1×1 1092×1092 · 1521 patch · 14,345 B 8×8 · 1 patch · 267 B
10×10 1092×1092 · 1521 patch · 105,110 B 80×80 · 9 patch · 2,705 B
100×100 1092×1092 · 1521 patch · 350,482 B 800×800 · 841 patch · 237,790 B
≥196×196 逐字节不变

分界点恰好是 1568 / 8 = 196 px,因此所有正常的放大请求完全不受影响,只有病态区间被收敛 —— 这正是该修复应有的形状。

正确性抽查全部精确:四个象限各自返回其编码的确切颜色;orientation:6 的 JPEG(字节中旋转了 90°)在请求显示后的右上象限时返回绿色,说明坐标遵循用户所见而非存储字节;全透明 PNG 铺白为 rgb(255,255,255)

3 · 真实截图上的端到端验证

最后我用一张 3200×628、表格文字约 10–11 px 的 dashboard 截图驱动真实工具 —— 这正是本 PR 的目标场景。图中两块面板都是工具自己返回、直接落盘的 JPEG。

整帧到达模型时是 1568×308(0.49×),该区域只剩 251×73 像素,此时 0.01260.012811,90411,804 无法区分。zoom_image 返回 512×148 个真实源像素并放大到 1568×453,每个数值都与源数据完全一致。这是真实的能力增益,而不是把模型已有的信息重新渲染一遍。

另外验证:持久化的 Read(...) 规则通过 meta-category 覆盖 zoom_image,且该工具提议的规则与 read_file 完全一致(父目录),说明描述中的权限说法成立。

本轮新发现 —— 两项均为次要,都不阻塞

1 · 纯文本模型仍会先走到授权确认再触发拦截。 build()coreToolScheduler.ts:1826 执行 validateToolParamValues,远早于 :2362evaluatePermissionFlow,而校验并不检查 modalities。因此对工作区之外的图片,getFileReadDefaultPermission 返回 'ask',用户被要求批准一次读取,之后 execute() 才返回"requires a model that accepts image inputs"。上述测试中已复现。这是我所建议的放置位置带来的直接代价,所以我是在标注成本而非指责。若希望消除,把该检查移入 validateToolParamValues() 可完整保留热切换收益(build() 每次调用都会执行),同时在任何确认弹窗之前失败。

2 · 新增的 telemetry 调用从未在真实 Config 下被执行。 zoom-image.test.ts../telemetry/loggers.js 整个模块做了 vi.mock,因此 logFileOperation() 在所有测试中都是 spy。实际运行时它会走到 QwenLogger.getInstance(config)config.getUsageStatisticsEnabled();我的验证脚手架立刻撞上了这一点,因为该文件中的 stub config 并未定义此方法。我用真实 Config 跑通了这条路径 —— 不抛错、事件正确 —— 所以这不是缺陷,只是提示:未来 logFileOperation/FileOperationEvent 的签名变更不会被这些测试捕获。变异体 M6/M7 确认调用点本身是被锁住的。

小的记录性问题:描述中写受影响套件共 1,324 个测试;我在当前 head 实测这七个文件为 1,242,说明该行早于最后两个 commit。

环境与门禁

隔离 worktree 位于 9cccf54node_modules 由与 lockfile 一致的树 APFS 克隆而来;已验证全部 20 个 @qwen-code/* 条目解析到 worktree 内部(无跨树污染);vite 7.0.0 / vitest 3.2.4 / sharp 0.34.5 均与 lockfile 一致。

门禁 结果
7 个受影响套件 ✅ 1,242 通过
read-file 回归 ✅ 87 通过
tsc --noEmit(packages/core) ✅ exit 0,0 错误
eslint(24 个改动文件) ✅ exit 0 —— 阳性对照:植入未使用 import → exit 1,还原后 → exit 0
prettier --check ✅ 全部符合
i18n 键 en / zh / zh-TW ✅ 三者齐备;en 的自引用值符合该文件既有约定(1555/1556 条)
变异矩阵 ✅ 9/9 被杀

结论

从我这边看可以合并。 阻塞项两轮前已清除,我一直保留意见的两项实质问题现已真正修复 —— 两项我都用对照实验而非阅读代码来验证,且覆盖它们的测试能经受变异测试。本轮两项新发现均为次要,其中第一项还是我自己要求的取舍。

合并前唯一值得在描述中写明的是:zoom_image 目前仅在 npm 安装下可用 —— standalone 构建仍未暂存 sharp,在那里会退化为有界错误。与其日后被当作 bug 报上来,不如现在写清楚。

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅

中文说明

未发现问题。LGTM!✅

— qwen3.8-max-preview via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review triage (round 3) — no code change

Thanks for the thorough round-3 verification and the merge-ready verdict. This round's feedback contains no CHANGES_REQUESTED review and no inline comments — only an issue-level verification report with two explicitly non-blocking minor findings plus follow-up notes. After triage, none warrant a code change in this PR, so the branch is left as-is at 9cccf54.

Finding 1 — text-only model reaches the approval prompt before the modality gate · Declined

This is the optional suggestion to move the modality check from execute() into validateToolParamValues() so a text-only model fails before the read-approval prompt. Declined for three reasons:

  • You explicitly framed it as "a tradeoff I asked for … flagging the cost rather than complaining about it," i.e. not a requested change.
  • The current placement (modality gate inside execute()) matches the established precedent in image-gen.ts:131. Moving it would make the two image tools inconsistent.
  • It would change the failure semantics: today the gate returns a graceful READ_CONTENT_FAILURE from execute(); in validateToolParamValues() it becomes a thrown error surfaced as INVALID_TOOL_PARAMS, which is a less accurate type because the parameters themselves are valid — only the model lacks image input support.

The cost (an occasional spurious approval prompt for an out-of-workspace image under a text-only model) is bounded and is the direct consequence of the execute-time placement recommended earlier. If a maintainer later wants it gone, the validateToolParamValues() move is the right lever and preserves the hot-switch benefit; recording that here so it isn't lost.

Finding 2 — telemetry call never exercised against a real Config · No action (informational)

You stated this "is not a defect, just a note," and confirmed the path works against a real Config (no throw, correct event). The call site is already pinned by mutants M6/M7. Adding a real-Config test would be diff growth guarding against a hypothetical future signature change to logFileOperation/FileOperationEvent, so no change is made.

Carried follow-ups (items 8 & 9) · Out of scope

Both remain true and both were already classified as follow-up / informational, failing safe:

  • Standalone release scripts stage only @teddyzhu/clipboard, not sharpzoom_image degrades to a bounded error there rather than crashing.
  • Sibling read-only tool sets omit zoom_image — all four fail safe.

These are best tracked as separate follow-up work rather than expanding this PR.

Bookkeeping · Noted

The PR description's affected-suite total (1,324) predates the last two commits; the measured total at this head is 1,242 across the seven files. This is a description-text observation, not a code change.

Verification

No code was modified this round, so no build/test commands were run. The branch remains at the reviewer-verified head 9cccf54, where round-3 verification already confirmed: 7 affected suites passing (1,242 tests), read-file regression passing (87), tsc --noEmit clean, eslint clean, prettier --check clean, and 9/9 mutants killed.

中文说明

Autofix 评审分诊(第 3 轮)—— 无代码改动

感谢第 3 轮的详尽验证以及"可以合并"的结论。本轮反馈中没有 CHANGES_REQUESTED 评审,也没有行内评论 —— 只有一条 issue 级验证报告,其中包含两项明确标注为不阻塞的次要发现,以及若干后续项说明。经分诊,本 PR 没有任何需要改动代码的项,因此分支保持在 9cccf54 不变。

发现 1 —— 纯文本模型先走到授权确认再触发 modality 拦截 · 拒绝(Declined)

这是那条可选建议:把 modality 检查从 execute() 移入 validateToolParamValues(),让纯文本模型在读取授权弹窗之前就失败。基于以下三点拒绝:

  • 你明确把它表述为"这是我自己要求的取舍……我是在标注成本而非指责",即并非一项改动请求。
  • 当前的放置位置(modality 拦截放在 execute() 内)与 image-gen.ts:131 的既有先例一致。移动它会使两个图片工具变得不一致。
  • 它会改变失败语义:如今该拦截从 execute() 返回一个优雅的 READ_CONTENT_FAILURE;放进 validateToolParamValues() 后会变成一个抛出的错误,并以 INVALID_TOOL_PARAMS 呈现 —— 而这个类型并不准确,因为参数本身是合法的,只是模型不支持图片输入。

其代价(在纯文本模型下,对工作区外的图片偶尔弹出一次多余的授权确认)是有界的,且正是早先所建议的"执行时放置"带来的直接结果。如果维护者日后希望消除它,移到 validateToolParamValues() 是正确的抓手,并且能保留热切换收益;在此记录以免遗失。

发现 2 —— telemetry 调用从未在真实 Config 下被执行 · 不处理(信息性)

你已说明这"不是缺陷,只是一条提示",并确认该路径在真实 Config 下可以跑通(不抛错、事件正确)。调用点本身已由变异体 M6/M7 锁住。新增一个真实 Config 测试只会带来 diff 增长,去防范一个假设性的未来 logFileOperation/FileOperationEvent 签名变更,因此不做改动。

遗留后续项(第 8、9 项)· 超出范围

两者仍然成立,且都已被归类为后续项 / 信息性,并均 fail-safe:

  • standalone 发布脚本只暂存 @teddyzhu/clipboard,未包含 sharp —— zoom_image 在那里会退化为有界错误,而非崩溃。
  • 同类只读工具集合遗漏了 zoom_image —— 四处全部 fail-safe。

这些更适合作为单独的后续工作来跟踪,而不是扩大本 PR。

记录性事项 · 已知悉

PR 描述中受影响套件总数(1,324)早于最后两个 commit;在当前 head 实测这七个文件为 1,242。这是描述文字层面的观察,并非代码改动。

验证

本轮未改动任何代码,因此未运行构建 / 测试命令。分支保持在经评审者验证的 head 9cccf54,第 3 轮验证已确认:7 个受影响套件通过(1,242 个测试)、read-file 回归通过(87 个)、tsc --noEmit 干净、eslint 干净、prettier --check 干净,且 9/9 变异体被杀。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

— qwen3.7-max via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no action needed

The newest feedback on PR #7809 (items newer than the last evaluation at
2026-07-27T18:38:21Z) contains nothing actionable:

  • Review (@doudouOUC, COMMENTED): "Reviewed — no blockers." The single
    Suggestion-level finding could not be anchored to a changed line and was
    dropped by the reviewer, so there is nothing to respond to.
  • Inline comments: none.
  • Issue-level comments: none newer than the last evaluation. (Earlier
    comments — the sharp packaging block, execute-time gating, and telemetry
    findings — were already addressed by prior commits on this branch.)
  • Failed / still-red checks: none.

No code change is warranted this round, so no commit was made and no
verification commands were run (none are required when nothing changes).

中文说明

Autofix 审查轮次 — 无需处理

PR #7809 的最新反馈(即上次评估 2026-07-27T18:38:21Z 之后的新增项)没有任何需要处理的内容:

  • 审查(@doudouOUC,COMMENTED): “已审查 — 无阻塞问题。” 唯一一条 Suggestion 级别的发现无法锚定到任何被修改的代码行,已被审查者丢弃,因此没有需要回应的内容。
  • 行内评论: 无。
  • Issue 级评论: 上次评估之后没有新增。(更早的评论 —— sharp 打包阻塞问题、执行期门控以及遥测相关发现 —— 已由本分支此前的提交处理。)
  • 失败 / 持续飘红的检查: 无。

本轮无需任何代码改动,因此没有产生提交,也没有运行验证命令(在没有任何改动时无需运行)。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao
wenshao added this pull request to the merge queue Jul 27, 2026
Merged via the queue into QwenLM:main with commit bd2c0b4 Jul 27, 2026
69 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants