Skip to content

Commit f02d35f

Browse files
committed
release: bump version to 0.1.48
1 parent 17e6220 commit f02d35f

13 files changed

Lines changed: 220 additions & 12 deletions

File tree

apps/OpenComputerUseLinux/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"time"
2020
)
2121

22-
var version = "0.1.47"
22+
var version = "0.1.48"
2323

2424
//go:embed runtime.py
2525
var linuxRuntimeScript string

apps/OpenComputerUseSmokeSuite/Sources/OpenComputerUseSmokeSuite/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ final class MCPClient {
3535
_ = try request(method: "initialize", params: [
3636
"clientInfo": [
3737
"name": "OpenComputerUseSmokeSuite",
38-
"version": "0.1.47",
38+
"version": "0.1.48",
3939
],
4040
"capabilities": [:],
4141
"protocolVersion": "2025-03-26",

apps/OpenComputerUseWindows/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"time"
1717
)
1818

19-
var version = "0.1.47"
19+
var version = "0.1.48"
2020

2121
//go:embed runtime.ps1
2222
var windowsRuntimeScript string

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
- 开源版当前不复刻官方闭源实现里的 caller signing、私有 IPC、完整 overlay choreography 和 plugin 自安装逻辑。
126126
- 因为官方 `SkyComputerUseClient` 带有宿主侧 launch constraints,普通 stdio MCP client 在本机上可能被系统直接杀掉;如果要探测官方 bundled `computer-use``scripts/computer-use-cli` 的 app-server 模式现在只适合做工具清单和协议面观察。官方 `1.0.755` 的真实 tool call 还会经过 service-side sender authorization / active IPC client 追踪,外部 raw helper 即使走已签名 Codex binary,也可能返回 `Sender process is not authenticated`;需要真实使用官方工具时应走正常 Codex agent/tool 调用链,开源版则继续提供可直连的 `open-computer-use` MCP server。
127127
- 当前权限引导已经具备可运行 app、深链、拖拽辅助,以及一版更接近官方的 accessory panel 入场动画和返回 affordance;点击链路也已经补上独立 visual cursor、官方 asset fallback 和相对目标 window 的排序逻辑,并且在 overlay 可见期间会持续重申“排在目标 window 之上”,避免用户手动激活目标 app 后 cursor 被目标窗口重新盖住;但整体还没有完全复刻官方那套嵌入式 choreography / host 集成 / session approval 体验。
128-
- screenshot 当前通过 `ScreenCaptureKit` 捕获目标窗口,并直接以 MCP `image` content block 的 base64 PNG 返回,不再把普通 app 截图落盘到仓库或临时目录;单次 ScreenCaptureKit capture 会设置超时,超时后省略 image block 而不是卡住整个 `get_app_state`
128+
- screenshot 当前通过 `ScreenCaptureKit` 捕获目标窗口,并以 MCP `image` content block 的 base64 PNG 返回,不再把普通 app 截图落盘到仓库或临时目录;编码前会按最大尺寸和目标字节数自适应缩小,避免复杂页面的大 PNG 触发 host 侧 MCP result 降级,同时 coordinate tools 继续按实际返回的 screenshot pixel 尺寸映射坐标;单次 ScreenCaptureKit capture 会设置超时,超时后省略 image block 而不是卡住整个 `get_app_state`
129129
- 会话状态现在是进程内内存态,保存每个 app 最近一次 snapshot 和 element index 映射。
130130

131131
## 主要验证路径
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Bound screenshot result size
2+
3+
## 用户诉求
4+
5+
用户发现 `open-computer-use` 的部分 tool call 在 Codex 历史记录里变成 `text` 下的 JSON 字符串,怀疑最近几次 commit 引入了额外包装,并提供了历史 session id 辅助定位。
6+
7+
## 本次改动
8+
9+
- **[Root cause]**: 确认 MCP server 原始 `tools/call` 响应是标准 `content: [text, image]`,嵌套只出现在 Codex 为了持久化超大 MCP result 而做的事件日志降级路径里。
10+
- **[Screenshot bound]**: macOS `ScreenCaptureKit` 窗口截图在编码为 PNG 前按最大尺寸和目标字节数自适应缩小,减少复杂页面触发 host 侧大结果降级的概率。
11+
- **[Regression tests]**: 增加截图压缩边界测试,覆盖大图会缩小、小图保持原尺寸。
12+
- **[Docs]**: 更新架构文档,说明截图仍以 MCP image block 返回,但会做尺寸/字节上限控制,coordinate tools 继续按实际返回的 screenshot pixel 尺寸映射。
13+
14+
## 设计动机
15+
16+
问题不是协议层重复包了一层,而是截图 PNG 过大时,Codex 会把整个 `CallToolResult` 序列化成一个 text preview 来保护 rollout storage。直接改 MCP shape 会破坏兼容性;更稳妥的修复是控制截图结果大小,让正常 `get_app_state` / action tool 返回保持扁平的 `text + image` 结构。
17+
18+
## 影响文件
19+
20+
- `packages/OpenComputerUseKit/Sources/OpenComputerUseKit/AccessibilitySnapshot.swift`
21+
- `packages/OpenComputerUseKit/Tests/OpenComputerUseKitTests/OpenComputerUseKitTests.swift`
22+
- `docs/ARCHITECTURE.md`
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Release 0.1.48
2+
3+
## 用户诉求
4+
5+
用户要求提交本轮相关改动并推送发版,同时确认 Codex TOML 配置改回连接线上 `open-computer-use`,不要指向 dev 构建。
6+
7+
## 本次改动
8+
9+
- **[Version bump]**: 将 macOS / Linux / Windows runtime、插件 manifest、smoke suite、测试和 reverse-engineering helper 版本统一 bump 到 `0.1.48`
10+
- **[Release notes]**: 更新用户可见 release notes,说明本版解决复杂窗口截图过大导致 Codex 历史事件降级成 JSON text preview 的问题。
11+
- **[Config check]**: 确认本机 Codex 配置中的 `open-computer-use` MCP server 使用线上命令 `open-computer-use mcp`,没有指向本地 dev binary。
12+
13+
## 设计动机
14+
15+
本轮修复已经影响公开 npm 包里的 macOS runtime 行为,且用户明确要求发版,因此按 release guide 走 patch release。版本源继续以插件 manifest 为准,并同步所有辅助工具与测试里的版本字符串,避免 tag 与 npm staging 包版本不一致。
16+
17+
## 验证
18+
19+
- `swift test`
20+
- `./scripts/run-tool-smoke-tests.sh`
21+
- `node ./scripts/npm/build-packages.mjs --out-dir dist/release/npm-staging-check`
22+
- `./scripts/build-cursor-motion-dmg.sh --configuration release --arch universal --version 0.1.48`
23+
- `node -p "require('./dist/release/npm-staging-check/open-computer-use/package.json').version"` -> `0.1.48`
24+
- `test -x dist/release/npm-staging-check/open-computer-use/dist/linux/arm64/open-computer-use`
25+
- `test -f dist/release/npm-staging-check/open-computer-use/dist/windows/arm64/open-computer-use.exe`
26+
- `node -e "if (require('./dist/release/npm-staging-check/open-computer-use/package.json').optionalDependencies) process.exit(1)"`
27+
- `ls dist/release/cursor-motion/CursorMotion-0.1.48.dmg`
28+
- `git diff --check`
29+
30+
## 影响文件
31+
32+
- `plugins/open-computer-use/.codex-plugin/plugin.json`
33+
- `packages/OpenComputerUseKit/Sources/OpenComputerUseKit/OpenComputerUseVersion.swift`
34+
- `apps/OpenComputerUseLinux/main.go`
35+
- `apps/OpenComputerUseWindows/main.go`
36+
- `apps/OpenComputerUseSmokeSuite/Sources/OpenComputerUseSmokeSuite/main.swift`
37+
- `scripts/computer-use-cli/main.go`
38+
- `scripts/computer-use-cli/README.md`
39+
- `docs/releases/feature-release-notes.md`

docs/releases/feature-release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
| 日期 | 功能域 | 用户价值 | 变更摘要 |
66
| --- | --- | --- | --- |
7+
| 2026-05-08 | MCP 结果大小控制 | Chrome / Electron 等复杂窗口返回截图时更不容易触发 Codex 的大结果降级,历史记录和上层工具消费里会继续保持正常的 `text + image` 结构,而不是把整个 MCP result 塞进 text 字符串。 | 发布 `0.1.48`,macOS `ScreenCaptureKit` 截图在编码为 MCP image block 前会按最大尺寸和目标字节数自适应缩小,并补测试锁住大图压缩、小图保真路径。 |
78
| 2026-05-08 | Smoke 测试静默运行 | 开发者运行 `./scripts/run-tool-smoke-tests.sh` 时不再被内部 fixture 的橙色测试窗口打断,回归验证更安静,也不会让用户误以为桌面上弹出了额外应用。 | 发布 `0.1.47`,smoke suite 默认以 headless 模式启动内部 fixture,并保留 fixture 在 `list_apps` smoke 路径里的可发现性。 |
89
| 2026-05-07 | 窗口不可用错误文案 | Lark / Chrome 等 no-window 场景下,开源版 `get_app_state` 的返回文本与官方 `computer-use` 当前输出一致,减少上层 prompt、测试和用户排查时的差异噪音。 | 发布 `0.1.46`,将 missing AX window 和 missing visible CGWindow 两条路径统一为 `Apple event error -10005: cgWindowNotFound`,并补单元测试锁住该官方风格文本。 |
910
| 2026-05-07 | 可见窗口匹配 | `get_app_state` 在 app 没有可见窗口、窗口已最小化,或 AX 窗口无法匹配到 on-screen CGWindow 时会更接近官方 `computer-use``cgWindowNotFound` 语义,避免返回没有截图坐标基础的半残状态。 | 发布 `0.1.45`,基于官方 `1.0.770` 逆向字符串中的 `cgWindowNotFound``noWindowsAvailable``matchingWindowNotFound``AXWindowMiniaturized` 线索,要求真实 app snapshot 同时满足未最小化 `AXWindow` 与可见 `CGWindow`|

packages/OpenComputerUseKit/Sources/OpenComputerUseKit/AccessibilitySnapshot.swift

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ enum SnapshotMode {
3030
let accessibilityTreeMaxNodeCount = 1200
3131
let accessibilityTreeMaxDepth = 64
3232
let screenshotCaptureTimeout: TimeInterval = 5
33+
let screenshotResultMaxPNGBytes = 900_000
34+
let screenshotResultMaxDimension: CGFloat = 1280
35+
let screenshotResultMinScale: CGFloat = 0.25
3336
private let windowVisibilityRecoveryDelay: TimeInterval = 0.7
3437
private let axWebAreaRole = "AXWebArea"
3538

@@ -412,11 +415,75 @@ private struct WindowCapture {
412415
return nil
413416
}
414417

415-
let bitmap = NSBitmapImageRep(cgImage: image)
416-
return bitmap.representation(using: .png, properties: [:])
418+
return boundedScreenshotPNGData(for: image)
417419
}
418420
}
419421

422+
func boundedScreenshotPNGData(
423+
for image: CGImage,
424+
maxBytes: Int = screenshotResultMaxPNGBytes,
425+
maxDimension: CGFloat = screenshotResultMaxDimension,
426+
minScale: CGFloat = screenshotResultMinScale
427+
) -> Data? {
428+
guard image.width > 0, image.height > 0, maxBytes > 0 else {
429+
return nil
430+
}
431+
432+
let original = pngData(for: image)
433+
let largestDimension = CGFloat(max(image.width, image.height))
434+
var scale = min(1, maxDimension / largestDimension)
435+
436+
if scale >= 1, let original, original.count <= maxBytes {
437+
return original
438+
}
439+
440+
var best = original
441+
while scale >= minScale {
442+
guard let resized = resizedCGImage(image, scale: scale),
443+
let data = pngData(for: resized)
444+
else {
445+
break
446+
}
447+
448+
best = data
449+
if data.count <= maxBytes {
450+
return data
451+
}
452+
453+
scale *= 0.85
454+
}
455+
456+
return best
457+
}
458+
459+
private func pngData(for image: CGImage) -> Data? {
460+
let bitmap = NSBitmapImageRep(cgImage: image)
461+
return bitmap.representation(using: .png, properties: [:])
462+
}
463+
464+
private func resizedCGImage(_ image: CGImage, scale: CGFloat) -> CGImage? {
465+
let width = max(1, Int((CGFloat(image.width) * scale).rounded()))
466+
let height = max(1, Int((CGFloat(image.height) * scale).rounded()))
467+
let colorSpace = CGColorSpaceCreateDeviceRGB()
468+
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
469+
470+
guard let context = CGContext(
471+
data: nil,
472+
width: width,
473+
height: height,
474+
bitsPerComponent: 8,
475+
bytesPerRow: 0,
476+
space: colorSpace,
477+
bitmapInfo: bitmapInfo
478+
) else {
479+
return nil
480+
}
481+
482+
context.interpolationQuality = .medium
483+
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
484+
return context.makeImage()
485+
}
486+
420487
private final class AsyncResultBox<T>: @unchecked Sendable {
421488
var result: Result<T, Error>?
422489
}

packages/OpenComputerUseKit/Sources/OpenComputerUseKit/OpenComputerUseVersion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
public let openComputerUseVersion = "0.1.47"
3+
public let openComputerUseVersion = "0.1.48"
44

55
public func resolvedOpenComputerUseVersion(bundle: Bundle = .main) -> String {
66
if let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String,

packages/OpenComputerUseKit/Tests/OpenComputerUseKitTests/OpenComputerUseKitTests.swift

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AppKit
2+
import ImageIO
23
import XCTest
34
@testable import OpenComputerUseKit
45

@@ -136,6 +137,29 @@ final class OpenComputerUseKitTests: XCTestCase {
136137
XCTAssertEqual(resolvedOpenComputerUseVersion(bundle: Bundle(for: Self.self)), openComputerUseVersion)
137138
}
138139

140+
func testBoundedScreenshotPNGDataShrinksLargeScreenshots() throws {
141+
let image = try makeNoisyTestImage(width: 800, height: 600)
142+
let data = try XCTUnwrap(boundedScreenshotPNGData(
143+
for: image,
144+
maxBytes: 50_000,
145+
maxDimension: 320,
146+
minScale: 0.05
147+
))
148+
let size = try imageSize(in: data)
149+
150+
XCTAssertLessThanOrEqual(data.count, 50_000)
151+
XCTAssertLessThanOrEqual(max(size.width, size.height), 320)
152+
}
153+
154+
func testBoundedScreenshotPNGDataKeepsSmallScreenshotsAtOriginalSize() throws {
155+
let image = try makeSolidTestImage(width: 32, height: 24)
156+
let data = try XCTUnwrap(boundedScreenshotPNGData(for: image, maxBytes: 1_000_000, maxDimension: 320))
157+
let size = try imageSize(in: data)
158+
159+
XCTAssertEqual(size.width, 32)
160+
XCTAssertEqual(size.height, 24)
161+
}
162+
139163
func testToolDefinitionCount() {
140164
XCTAssertEqual(ToolDefinitions.all.count, 9)
141165
}
@@ -436,7 +460,7 @@ final class OpenComputerUseKitTests: XCTestCase {
436460

437461
func testInitializeResponseContainsToolsCapability() throws {
438462
let server = StdioMCPServer(service: ComputerUseService())
439-
let response = server.handle(line: #"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"0.1.47"},"capabilities":{}}}"#)
463+
let response = server.handle(line: #"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"0.1.48"},"capabilities":{}}}"#)
440464
XCTAssertNotNil(response)
441465
XCTAssertTrue(response!.contains(#""name":"open-computer-use""#))
442466
XCTAssertTrue(response!.contains(#""tools":{"listChanged":false}"#))
@@ -445,7 +469,7 @@ final class OpenComputerUseKitTests: XCTestCase {
445469
func testInitializeResponseContainsComputerUseInstructions() throws {
446470
let server = StdioMCPServer(service: ComputerUseService())
447471
let response = try XCTUnwrap(
448-
server.handle(line: #"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"0.1.47"},"capabilities":{}}}"#)
472+
server.handle(line: #"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"0.1.48"},"capabilities":{}}}"#)
449473
)
450474
let data = try XCTUnwrap(response.data(using: .utf8))
451475
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
@@ -1420,4 +1444,59 @@ final class OpenComputerUseKitTests: XCTestCase {
14201444
let length = max(hypot(dx, dy), 0.001)
14211445
return CGVector(dx: dx / length, dy: dy / length)
14221446
}
1447+
1448+
private func makeNoisyTestImage(width: Int, height: Int) throws -> CGImage {
1449+
var pixels = [UInt8](repeating: 0, count: width * height * 4)
1450+
for y in 0..<height {
1451+
for x in 0..<width {
1452+
let offset = (y * width + x) * 4
1453+
pixels[offset] = UInt8((x * 37 + y * 17) & 0xFF)
1454+
pixels[offset + 1] = UInt8((x * 11 + y * 43) & 0xFF)
1455+
pixels[offset + 2] = UInt8((x * 71 + y * 5) & 0xFF)
1456+
pixels[offset + 3] = 255
1457+
}
1458+
}
1459+
1460+
return try makeTestImage(width: width, height: height, pixels: pixels)
1461+
}
1462+
1463+
private func makeSolidTestImage(width: Int, height: Int) throws -> CGImage {
1464+
var pixels = [UInt8](repeating: 0, count: width * height * 4)
1465+
for index in stride(from: 0, to: pixels.count, by: 4) {
1466+
pixels[index] = 80
1467+
pixels[index + 1] = 140
1468+
pixels[index + 2] = 220
1469+
pixels[index + 3] = 255
1470+
}
1471+
1472+
return try makeTestImage(width: width, height: height, pixels: pixels)
1473+
}
1474+
1475+
private func makeTestImage(width: Int, height: Int, pixels: [UInt8]) throws -> CGImage {
1476+
let data = Data(pixels)
1477+
let provider = try XCTUnwrap(CGDataProvider(data: data as CFData))
1478+
let image = CGImage(
1479+
width: width,
1480+
height: height,
1481+
bitsPerComponent: 8,
1482+
bitsPerPixel: 32,
1483+
bytesPerRow: width * 4,
1484+
space: CGColorSpaceCreateDeviceRGB(),
1485+
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
1486+
provider: provider,
1487+
decode: nil,
1488+
shouldInterpolate: false,
1489+
intent: .defaultIntent
1490+
)
1491+
1492+
return try XCTUnwrap(image)
1493+
}
1494+
1495+
private func imageSize(in data: Data) throws -> (width: Int, height: Int) {
1496+
let source = try XCTUnwrap(CGImageSourceCreateWithData(data as CFData, nil))
1497+
let properties = try XCTUnwrap(CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any])
1498+
let width = try XCTUnwrap(properties[kCGImagePropertyPixelWidth] as? Int)
1499+
let height = try XCTUnwrap(properties[kCGImagePropertyPixelHeight] as? Int)
1500+
return (width, height)
1501+
}
14231502
}

0 commit comments

Comments
 (0)