package main import ( "bytes" "encoding/json" "fmt" "math" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) func maybeScrollBeforeObserve(config Config, summaries []RegionSummary, screenshotPath string) error { if config.Agent.ObserveMode != "scroll_first" { return nil } if runtime.GOOS != "darwin" { return fmt.Errorf("observe_mode=scroll_first currently supports macOS only") } chat, ok := regionByType(summaries)["chat_content"] if !ok { return fmt.Errorf("cannot scroll before observe: chat_content region not found") } maxScrolls := config.Agent.MaxScrolls if maxScrolls <= 0 { maxScrolls = 1 } for index := 0; index < maxScrolls; index++ { window, err := findWeChatWindow() if err != nil { return err } pointX := window.X + chat.CenterSource[0] pointY := window.Y + chat.CenterSource[1] logTask(fmt.Sprintf("观察前滚动聊天区域:round=%d point=[%.1f, %.1f] delta_y=%d window=[%.0f, %.0f, %.0f, %.0f]", index+1, pointX, pointY, config.Agent.ScrollDeltaY, window.X, window.Y, window.Width, window.Height)) if err := scrollAt(pointX, pointY, config.Agent.ScrollDeltaY); err != nil { return err } time.Sleep(700 * time.Millisecond) if err := captureWindow(window, screenshotPath); err != nil { return err } logInfo("已滚动并覆盖更新截图 WeChat.jpg") } return nil } func findWeChatWindow() (WindowBounds, error) { script := ` import CoreGraphics import Foundation let options = CGWindowListOption(arrayLiteral: [.optionOnScreenOnly, .excludeDesktopElements]) guard let infos = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { exit(1) } for info in infos { let owner = info[kCGWindowOwnerName as String] as? String ?? "" let title = info[kCGWindowName as String] as? String ?? "" let layer = info[kCGWindowLayer as String] as? Int ?? -1 if layer == 0 && (owner.contains("微信") || owner.lowercased().contains("wechat") || title == "微信") { if let bounds = info[kCGWindowBounds as String] as? [String: Any], let x = bounds["X"] as? Double, let y = bounds["Y"] as? Double, let w = bounds["Width"] as? Double, let h = bounds["Height"] as? Double { let id = info[kCGWindowNumber as String] as? Int ?? 0 let result: [String: Any] = ["x": x, "y": y, "width": w, "height": h, "owner": owner, "title": title, "id": id] let data = try! JSONSerialization.data(withJSONObject: result) print(String(data: data, encoding: .utf8)!) exit(0) } } } exit(2) ` output, err := runSwift(script) if err != nil { return WindowBounds{}, fmt.Errorf("find WeChat window failed: %w", err) } var raw struct { X float64 `json:"x"` Y float64 `json:"y"` Width float64 `json:"width"` Height float64 `json:"height"` Owner string `json:"owner"` Title string `json:"title"` ID int `json:"id"` } if err := json.Unmarshal([]byte(output), &raw); err != nil { return WindowBounds{}, err } return WindowBounds(raw), nil } func scrollAt(x float64, y float64, deltaY int) error { if deltaY == 0 { deltaY = 6 } script := fmt.Sprintf(` import CoreGraphics import Foundation import AppKit NSWorkspace.shared.runningApplications .first { $0.localizedName == "微信" || $0.localizedName == "WeChat" }? .activate(options: [.activateIgnoringOtherApps]) usleep(300000) let point = CGPoint(x: %.3f, y: %.3f) let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left) move?.post(tap: .cghidEventTap) usleep(80000) let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left) let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left) down?.post(tap: .cghidEventTap) usleep(60000) up?.post(tap: .cghidEventTap) usleep(120000) let delta = Int32(%d) for _ in 0..<8 { let lineScroll = CGEvent(scrollWheelEvent2Source: nil, units: .line, wheelCount: 1, wheel1: delta, wheel2: 0, wheel3: 0) lineScroll?.post(tap: .cghidEventTap) usleep(45000) } for _ in 0..<4 { let pixelScroll = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 1, wheel1: delta * 80, wheel2: 0, wheel3: 0) pixelScroll?.post(tap: .cghidEventTap) usleep(45000) } `, x, y, deltaY) _, err := runSwift(script) return err } func captureWindow(window WindowBounds, screenshotPath string) error { if err := os.MkdirAll(filepath.Dir(screenshotPath), 0755); err != nil { return err } rect := fmt.Sprintf("%d,%d,%d,%d", int(math.Round(window.X)), int(math.Round(window.Y)), int(math.Round(window.Width)), int(math.Round(window.Height))) command := exec.Command("screencapture", "-x", "-R", rect, screenshotPath) if output, err := command.CombinedOutput(); err != nil { return fmt.Errorf("screencapture failed: %w output=%s", err, string(output)) } return nil } func runSwift(script string) (string, error) { command := exec.Command("swift", "-") command.Stdin = strings.NewReader(script) var stderr bytes.Buffer command.Stderr = &stderr output, err := command.Output() if err != nil { return "", fmt.Errorf("%w stderr=%s", err, stderr.String()) } return strings.TrimSpace(string(output)), nil }