mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 15:41:34 +00:00
* fix(ai): preserve streamed tool-call identity * fix(computer): bind actions to current tool authority * fix(macos): serialize computer control lifecycle * docs(computer): document hardened control contract * chore: follow release-owned changelog policy * test(agents): cover node list cancellation
143 lines
6.3 KiB
Swift
143 lines
6.3 KiB
Swift
import CryptoKit
|
|
import Foundation
|
|
|
|
/// A target display expressed in the global CoreGraphics point space (top-left
|
|
/// origin), which is exactly the space CGEvent mouse coordinates use.
|
|
public struct OpenClawComputerDisplayGeometry: Sendable, Equatable {
|
|
public var originX: Double
|
|
public var originY: Double
|
|
public var widthPoints: Double
|
|
public var heightPoints: Double
|
|
|
|
public init(originX: Double, originY: Double, widthPoints: Double, heightPoints: Double) {
|
|
self.originX = originX
|
|
self.originY = originY
|
|
self.widthPoints = widthPoints
|
|
self.heightPoints = heightPoints
|
|
}
|
|
}
|
|
|
|
/// Maps reference-screenshot pixel coordinates to global display points.
|
|
///
|
|
/// The model reasons over the `screen.snapshot` image whose pixel width equals
|
|
/// the captured width (ScreenSnapshotService downscales the capture source to
|
|
/// `min(refWidth, sourceWidth)`), so model coordinates live in captured-pixel
|
|
/// space. A single uniform factor (display point width / captured pixel width)
|
|
/// recovers global display points; aspect ratio is preserved so it also applies
|
|
/// to the vertical axis. Deriving the factor from the captured pixel width
|
|
/// rather than the display point width keeps clicks aligned on Retina modes
|
|
/// where physical pixels and logical points differ. Retina backing scale never
|
|
/// enters CGEvent coordinates, which are always points.
|
|
public enum OpenClawComputerInputGeometry {
|
|
/// Stable opaque identity for one physical display geometry and reference
|
|
/// scale. Screenshot and input paths independently derive this value so
|
|
/// hot-plug/reindex/geometry/scale changes fail closed before coordinates can
|
|
/// target pixels other than the frame the caller observed.
|
|
public static func displayFrameId(
|
|
displayID: UInt32,
|
|
sourceWidth: Double,
|
|
sourceHeight: Double,
|
|
referenceWidth: Int,
|
|
display: OpenClawComputerDisplayGeometry) -> String
|
|
{
|
|
let descriptor = [
|
|
String(displayID),
|
|
String(sourceWidth.bitPattern),
|
|
String(sourceHeight.bitPattern),
|
|
String(referenceWidth),
|
|
String(display.originX.bitPattern),
|
|
String(display.originY.bitPattern),
|
|
String(display.widthPoints.bitPattern),
|
|
String(display.heightPoints.bitPattern),
|
|
].joined(separator: "\u{0}")
|
|
let digest = SHA256.hash(data: Data(descriptor.utf8))
|
|
.map { String(format: "%02x", $0) }
|
|
.joined()
|
|
return "display-frame:v1:\(digest)"
|
|
}
|
|
|
|
/// Whether capture-source dimensions and their target display form a safe,
|
|
/// finite coordinate mapping. Callers must reject invalid geometry before
|
|
/// dispatching input; otherwise the fallback mapper collapses to an origin.
|
|
public static func isValidMappingGeometry(
|
|
sourceWidth: Double,
|
|
sourceHeight: Double,
|
|
display: OpenClawComputerDisplayGeometry) -> Bool
|
|
{
|
|
guard sourceWidth.isFinite,
|
|
sourceHeight.isFinite,
|
|
display.originX.isFinite,
|
|
display.originY.isFinite,
|
|
display.widthPoints.isFinite,
|
|
display.heightPoints.isFinite
|
|
else {
|
|
return false
|
|
}
|
|
return sourceWidth > 0 &&
|
|
sourceHeight > 0 &&
|
|
display.widthPoints > 0 &&
|
|
display.heightPoints > 0
|
|
}
|
|
|
|
/// The delivered screenshot pixel width for a reference width and the capture
|
|
/// source dimensions. `sourceWidth`/`sourceHeight` are the capture source
|
|
/// dimensions in the same units ScreenSnapshotService reads from the display.
|
|
public static func capturedWidth(
|
|
refWidth: Int?,
|
|
sourceWidth: Double,
|
|
sourceHeight: Double) -> Double
|
|
{
|
|
guard sourceWidth > 0 else { return 0 }
|
|
// Node width cap: ScreenSnapshotService downscales the source to
|
|
// min(refWidth, sourceWidth) and never upscales.
|
|
let widthCap = refWidth.map { min(Double($0), sourceWidth) } ?? sourceWidth
|
|
guard let refWidth, refWidth > 0, sourceHeight > 0 else { return widthCap }
|
|
// The agent additionally caps the delivered screenshot's LONGEST edge to
|
|
// the reference width (this turn and on later replay-sanitization), so a
|
|
// portrait capture whose height exceeds the reference width is scaled down
|
|
// uniformly. Mirror that scaling here so coordinates map against the same
|
|
// pixel width the model actually saw. Landscape frames, whose longest edge
|
|
// is the already-capped width, are unaffected.
|
|
let cappedHeight = widthCap * sourceHeight / sourceWidth
|
|
let longestEdge = max(widthCap, cappedHeight)
|
|
let referenceWidth = Double(refWidth)
|
|
guard longestEdge > referenceWidth else { return widthCap }
|
|
return widthCap * referenceWidth / longestEdge
|
|
}
|
|
|
|
/// Converts a captured-pixel-space point to a global display point.
|
|
/// `capturedWidthPixels` is the actual pixel width of the screenshot the
|
|
/// model saw (see `capturedWidth`).
|
|
public static func mapReferencePointToGlobal(
|
|
x: Double,
|
|
y: Double,
|
|
capturedWidthPixels: Double,
|
|
display: OpenClawComputerDisplayGeometry) -> (x: Double, y: Double)
|
|
{
|
|
guard capturedWidthPixels > 0, display.widthPoints > 0 else {
|
|
return (x: display.originX, y: display.originY)
|
|
}
|
|
let scale = display.widthPoints / capturedWidthPixels
|
|
return (
|
|
x: display.originX + x * scale,
|
|
y: display.originY + y * scale)
|
|
}
|
|
|
|
/// Clamps a global point to strictly inside the display. Coordinate mapping
|
|
/// tolerates a small rounding epsilon at the edges, but the posted event must
|
|
/// stay on the selected display: a far-edge point (e.g. x == captured width,
|
|
/// which maps to originX + widthPoints) would otherwise fall on the adjacent
|
|
/// screen the model never saw.
|
|
public static func clampToDisplay(
|
|
x: Double,
|
|
y: Double,
|
|
display: OpenClawComputerDisplayGeometry) -> (x: Double, y: Double)
|
|
{
|
|
let maxX = display.originX + max(0, display.widthPoints - 1)
|
|
let maxY = display.originY + max(0, display.heightPoints - 1)
|
|
return (
|
|
x: min(max(x, display.originX), maxX),
|
|
y: min(max(y, display.originY), maxY))
|
|
}
|
|
}
|