

















@@ -3,12 +3,16 @@ require "open3"
33require "json"
44require "fileutils"
55require "tmpdir"
6+require "tempfile"
67require "cgi"
7889default_platform(:ios)
9101011APP_STORE_APP_IDENTIFIER = "ai.openclawfoundation.app"
1112DEFAULT_SNAPSHOT_DEVICES = ["iPhone 16 Pro Max", "iPad Pro 13-inch (M4)"].freeze
13+DEFAULT_WATCH_SNAPSHOT_DEVICE = "Apple Watch Ultra 3 (49mm)"
14+WATCH_SCREENSHOT_MODE_DEFAULTS_KEY = "openclaw.watch.screenshotMode"
15+WATCH_SNAPSHOT_STATUS_BAR_TIME = "09:41"
1216SNAPSHOT_STATUS_BAR_ARGUMENTS = "--time 09:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100".freeze
1317REQUIRED_SCREENSHOT_FAMILIES = {
1418"iPhone" => /iPhone/,
@@ -66,6 +70,270 @@ def snapshot_devices
6670raw.split(",").map(&:strip).reject(&:empty?)
6771end
687273+def watch_snapshot_device
74+raw = ENV["OPENCLAW_WATCH_SNAPSHOT_DEVICE"].to_s.strip
75+raw.empty? ? DEFAULT_WATCH_SNAPSHOT_DEVICE : raw
76+end
77+78+def available_simulator_devices
79+stdout, stderr, status = Open3.capture3("xcrun", "simctl", "list", "devices", "available", "--json")
80+unless status.success?
81+detail = stderr.to_s.strip
82+detail = stdout.to_s.strip if detail.empty?
83+UI.user_error!("Failed to list simulator devices: #{detail}")
84+end
85+86+JSON.parse(stdout).fetch("devices").values.flatten
87+rescue JSON::ParserError => e
88+UI.user_error!("Invalid JSON from simctl device list: #{e.message}")
89+end
90+91+def resolve_simulator_device(name)
92+devices = available_simulator_devices
93+exact = devices.find { |device| device["name"] == name }
94+return exact if exact
95+96+watch_devices = devices.select { |device| device["name"].to_s.include?("Apple Watch") }
97+fallback = watch_devices.find { |device| device["name"].to_s.include?("Ultra") } || watch_devices.first
98+UI.user_error!("No available Apple Watch simulators found.") unless fallback
99+100+UI.important("Apple Watch simulator '#{name}' was not found; using '#{fallback.fetch("name")}'.")
101+fallback
102+end
103+104+def bundle_identifier_for_product(product_path)
105+info_plist_path = File.join(product_path, "Info.plist")
106+UI.user_error!("Expected Info.plist at #{info_plist_path}.") unless File.exist?(info_plist_path)
107+108+stdout, stderr, status = Open3.capture3(
109+"/usr/libexec/PlistBuddy",
110+"-c",
111+"Print:CFBundleIdentifier",
112+info_plist_path
113+)
114+unless status.success?
115+detail = stderr.to_s.strip
116+detail = stdout.to_s.strip if detail.empty?
117+UI.user_error!("Failed to read bundle identifier from #{info_plist_path}: #{detail}")
118+end
119+120+bundle_identifier = stdout.to_s.strip
121+UI.user_error!("Missing bundle identifier in #{info_plist_path}.") if bundle_identifier.empty?
122+bundle_identifier
123+end
124+125+def write_watch_screenshot_mode_defaults(udid, bundle_identifiers)
126+bundle_identifiers.each do |bundle_identifier|
127+sh(
128+shell_join([
129+"xcrun",
130+"simctl",
131+"spawn",
132+udid,
133+"defaults",
134+"write",
135+bundle_identifier,
136+WATCH_SCREENSHOT_MODE_DEFAULTS_KEY,
137+"-bool",
138+"YES",
139+])
140+)
141+end
142+end
143+144+def clear_watch_screenshot_mode_defaults(udid, bundle_identifiers)
145+bundle_identifiers.each do |bundle_identifier|
146+sh(
147+"#{shell_join([
148+ "xcrun",
149+ "simctl",
150+ "spawn",
151+ udid,
152+ "defaults",
153+ "delete",
154+ bundle_identifier,
155+ WATCH_SCREENSHOT_MODE_DEFAULTS_KEY,
156+ ])} >/dev/null 2>&1 || true"
157+)
158+end
159+end
160+161+def status_bar_unsupported?(detail)
162+detail.include?("Status bar overrides not supported on this platform") ||
163+detail.include?("Operation not supported")
164+end
165+166+def set_watch_status_bar_override(udid)
167+stdout, stderr, status = Open3.capture3(
168+"xcrun",
169+"simctl",
170+"status_bar",
171+udid,
172+"override",
173+"--time",
174+WATCH_SNAPSHOT_STATUS_BAR_TIME
175+)
176+return true if status.success?
177+178+detail = stderr.to_s.strip
179+detail = stdout.to_s.strip if detail.empty?
180+if status_bar_unsupported?(detail)
181+UI.important("watchOS simulator status bar overrides are not supported; Watch screenshot clock will use simulator time.")
182+return false
183+end
184+185+UI.user_error!("Failed to override Watch simulator status bar: #{detail}")
186+end
187+188+def clear_watch_status_bar_override(udid)
189+stdout, stderr, status = Open3.capture3("xcrun", "simctl", "status_bar", udid, "clear")
190+return if status.success?
191+192+detail = stderr.to_s.strip
193+detail = stdout.to_s.strip if detail.empty?
194+UI.user_error!("Failed to clear Watch simulator status bar override: #{detail}") unless status_bar_unsupported?(detail)
195+end
196+197+def normalize_watch_screenshot_status_bar(path)
198+script = <<~SWIFT
199+ import AppKit
200+ import Foundation
201+202+ let path = CommandLine.arguments[1]
203+ let timeText = CommandLine.arguments[2]
204+205+ guard let source = NSImage(contentsOfFile: path),
206+ let cgImage = source.cgImage(forProposedRect: nil, context: nil, hints: nil)
207+ else {
208+ fputs("Failed to load screenshot at \\(path)\\n", stderr)
209+ exit(2)
210+ }
211+212+ let width = CGFloat(cgImage.width)
213+ let height = CGFloat(cgImage.height)
214+ guard let bitmap = NSBitmapImageRep(
215+ bitmapDataPlanes: nil,
216+ pixelsWide: Int(width),
217+ pixelsHigh: Int(height),
218+ bitsPerSample: 8,
219+ samplesPerPixel: 4,
220+ hasAlpha: true,
221+ isPlanar: false,
222+ colorSpaceName: .deviceRGB,
223+ bytesPerRow: 0,
224+ bitsPerPixel: 0),
225+ let graphicsContext = NSGraphicsContext(bitmapImageRep: bitmap)
226+ else {
227+ fputs("Failed to create normalized screenshot bitmap at \\(path)\\n", stderr)
228+ exit(3)
229+ }
230+231+ bitmap.size = NSSize(width: width, height: height)
232+ NSGraphicsContext.saveGraphicsState()
233+ NSGraphicsContext.current = graphicsContext
234+ source.draw(
235+ in: NSRect(x: 0, y: 0, width: width, height: height),
236+ from: NSRect(x: 0, y: 0, width: width, height: height),
237+ operation: .copy,
238+ fraction: 1.0)
239+240+ NSColor.black.setFill()
241+ NSBezierPath(rect: NSRect(x: width - 146, y: height - 92, width: 124, height: 70)).fill()
242+243+ let paragraphStyle = NSMutableParagraphStyle()
244+ paragraphStyle.alignment = .right
245+ let attributes: [NSAttributedString.Key: Any] = [
246+ .font: NSFont.monospacedDigitSystemFont(ofSize: 34, weight: .semibold),
247+ .foregroundColor: NSColor.white,
248+ .paragraphStyle: paragraphStyle,
249+ ]
250+ timeText.draw(
251+ in: NSRect(x: width - 134, y: height - 82, width: 102, height: 44),
252+ withAttributes: attributes)
253+ NSGraphicsContext.restoreGraphicsState()
254+255+ guard let png = bitmap.representation(using: .png, properties: [:])
256+ else {
257+ fputs("Failed to encode normalized screenshot at \\(path)\\n", stderr)
258+ exit(4)
259+ }
260+261+ try png.write(to: URL(fileURLWithPath: path))
262+ SWIFT
263+264+Tempfile.create(["openclaw-watch-status-bar", ".swift"]) do |file|
265+file.write(script)
266+file.flush
267+sh(shell_join(["xcrun", "swift", file.path, path, "9:41"]))
268+end
269+end
270+271+def capture_watch_screenshot
272+device = resolve_simulator_device(watch_snapshot_device)
273+device_name = device.fetch("name")
274+udid = device.fetch("udid")
275+output_dir = File.join(ios_root, "fastlane", "screenshots", "en-US")
276+output_path = File.join(output_dir, "#{device_name}-01-quick-reply.png")
277+derived_data_path = File.join(ios_root, "build", "WatchScreenshotDerivedData")
278+app_path = File.join(derived_data_path, "Build", "Products", "Debug-watchsimulator", "OpenClawWatchApp.app")
279+280+FileUtils.mkdir_p(output_dir)
281+Dir[File.join(output_dir, "Apple Watch*-*.png")].each { |path| FileUtils.rm_f(path) }
282+FileUtils.rm_rf(derived_data_path)
283+284+sh(
285+xcodebuild_shell_join([
286+"xcodebuild",
287+"-project",
288+File.join(ios_root, "OpenClaw.xcodeproj"),
289+"-scheme",
290+"OpenClawWatchApp",
291+"-configuration",
292+"Debug",
293+"-destination",
294+"platform=watchOS Simulator,id=#{udid}",
295+"-derivedDataPath",
296+derived_data_path,
297+"build",
298+])
299+)
300+301+UI.user_error!("Watch screenshot build did not produce #{app_path}.") unless File.exist?(app_path)
302+extension_path = File.join(app_path, "PlugIns", "OpenClawWatchExtension.appex")
303+watch_app_identifier = bundle_identifier_for_product(app_path)
304+watch_extension_identifier = bundle_identifier_for_product(extension_path)
305+screenshot_mode_bundle_identifiers = [watch_app_identifier, watch_extension_identifier]
306+307+sh("#{shell_join(["xcrun", "simctl", "boot", udid])} >/dev/null 2>&1 || true")
308+sh(shell_join(["xcrun", "simctl", "bootstatus", udid, "-b"]))
309+sh("#{shell_join(["xcrun", "simctl", "uninstall", udid, watch_app_identifier])} >/dev/null 2>&1 || true")
310+status_bar_overridden = false
311+begin
312+sh(shell_join(["xcrun", "simctl", "install", udid, app_path]))
313+write_watch_screenshot_mode_defaults(udid, screenshot_mode_bundle_identifiers)
314+status_bar_overridden = set_watch_status_bar_override(udid)
315+sh(
316+"SIMCTL_CHILD_OPENCLAW_WATCH_SCREENSHOT_MODE=1 #{shell_join([
317+ "xcrun",
318+ "simctl",
319+ "launch",
320+ udid,
321+ watch_app_identifier,
322+ "--openclaw-watch-screenshot-mode",
323+ ])}"
324+)
325+sleep(3)
326+sh(shell_join(["xcrun", "simctl", "io", udid, "screenshot", output_path]))
327+normalize_watch_screenshot_status_bar(output_path)
328+ensure
329+clear_watch_status_bar_override(udid) if status_bar_overridden
330+clear_watch_screenshot_mode_defaults(udid, screenshot_mode_bundle_identifiers)
331+end
332+333+UI.success("Captured Apple Watch screenshot: #{output_path}")
334+output_path
335+end
336+69337def maybe_decode_hex_keychain_secret(value)
70338return value unless env_present?(value)
71339@@ -567,6 +835,16 @@ platform :ios do
567835skip_open_summary: true,
568836xcargs: "-allowProvisioningUpdates"
569837)
838+839+watch_screenshot
840+end
841+842+desc "Generate deterministic Apple Watch screenshot for App Store metadata"
843+lane :watch_screenshot do
844+sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-configure-signing.sh")]))
845+sh(shell_join(["bash", File.join(repo_root, "scripts", "ios-write-version-xcconfig.sh")]))
846+sh(shell_join(["xcodegen", "generate", "--spec", File.join(ios_root, "project.yml"), "--project", ios_root]))
847+capture_watch_screenshot
570848end
571849572850desc "Validate App Store Connect API auth"
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。