179 lines
5.2 KiB
Go
179 lines
5.2 KiB
Go
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
|
||
}
|