//go:build windows package tools import ( "bufio" "context" "fmt" "os" "os/exec" "path/filepath" "strings" "sync" "time" "git.gopher.ink/mirrors2/AutoZero/diag" ) // windowsBackend:Windows 桌面控制,全部走 PowerShell(系统自带,无 CGO): // 键盘用 System.Windows.Forms.SendKeys(Win 键不支持),鼠标与滚轮用 // user32.dll P/Invoke(SetCursorPos + mouse_event),截图用 System.Drawing。 // 使用持久化进程避免每次操作的 200-500ms 启动开销。 type windowsBackend struct { mu sync.Mutex cmd *exec.Cmd stdin *bufio.Writer stdout *bufio.Scanner } func newGuiBackend() guiBackend { return &windowsBackend{} } func (b *windowsBackend) Available() error { if _, err := exec.LookPath("powershell"); err != nil { return fmt.Errorf("powershell is required for computer use on Windows") } return nil } // PreflightPermissions Windows 暂无“首次引导授权”机制,依赖 Available() 的依赖检查。 func (b *windowsBackend) PreflightPermissions() []string { return nil } // ensureStarted 确保持久化 PowerShell 进程已启动。 func (b *windowsBackend) ensureStarted() error { b.mu.Lock() defer b.mu.Unlock() if b.cmd != nil && b.cmd.Process != nil { return nil } cmd := exec.Command("powershell", "-NoProfile", "-NoLogo", "-NonInteractive") cmd.SysProcAttr = nil // 使用默认 stdin, err := cmd.StdinPipe() if err != nil { return err } stdout, err := cmd.StdoutPipe() if err != nil { return err } cmd.Stderr = diag.Indirect() // 不直连终端:TUI 在 alt-screen 下会被打穿 if err := cmd.Start(); err != nil { return err } b.cmd = cmd b.stdin = bufio.NewWriter(stdin) b.stdout = bufio.NewScanner(stdout) // 注入 user32 类定义(只需一次) if _, err := b.exec(psUser32); err != nil { _ = cmd.Process.Kill() b.cmd = nil return fmt.Errorf("failed to inject user32: %w", err) } return nil } // exec 向持久化 PS 进程发送命令并读取输出。 // 使用唯一分隔符区分输出边界。 const psDelim = "__AG_END_" func (b *windowsBackend) exec(script string) (string, error) { delim := fmt.Sprintf("Write-Output '%s'", psDelim) full := script + "; " + delim if _, err := b.stdin.WriteString(full + "\n"); err != nil { return "", err } if err := b.stdin.Flush(); err != nil { return "", err } var lines []string for b.stdout.Scan() { line := b.stdout.Text() if strings.Contains(line, psDelim) { break } lines = append(lines, line) } return strings.Join(lines, "\n"), nil } // restart 重启 PowerShell 进程(进程崩溃时恢复)。 func (b *windowsBackend) restart() { if b.cmd != nil && b.cmd.Process != nil { _ = b.cmd.Process.Kill() _, _ = b.cmd.Process.Wait() } b.cmd = nil _ = b.ensureStarted() } // runScript 在持久化进程失败时自动重启一次。 func (b *windowsBackend) runScript(script string) (string, error) { if err := b.ensureStarted(); err != nil { return "", err } out, err := b.exec(script) if err != nil { // 进程可能已死,重启后重试一次 b.restart() if err2 := b.ensureStarted(); err2 != nil { return "", err } out, err = b.exec(script) } return out, err } // psUser32 是注入 user32 P/Invoke 的 PowerShell 片段(在 ensureStarted 时注入一次)。 const psUser32 = "Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;public class AGUser32{[DllImport(\"user32.dll\")]public static extern bool SetCursorPos(int x,int y);[DllImport(\"user32.dll\")]public static extern void mouse_event(uint f,uint dx,uint dy,uint d,UIntPtr e);}' -ErrorAction SilentlyContinue" // mouse_event flags:2=LEFTDOWN 4=LEFTUP 8=RIGHTDOWN 16=RIGHTUP 2048=WHEEL const ( meLeftDown = "2" meLeftUp = "4" meRightDown = "8" meRightUp = "16" meWheel = "2048" wheelNotch = "120" ) func (b *windowsBackend) Click(_ context.Context, x, y int, kind string) error { var bld strings.Builder fmt.Fprintf(&bld, "[AGUser32]::SetCursorPos(%d,%d); Start-Sleep -Milliseconds 80; ", x, y) switch kind { case "right": fmt.Fprintf(&bld, "[AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40; [AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero);", meRightDown, meRightUp) case "double": fmt.Fprintf(&bld, "[AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40; [AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 120; [AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40; [AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero);", meLeftDown, meLeftUp, meLeftDown, meLeftUp) default: fmt.Fprintf(&bld, "[AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40; [AGUser32]::mouse_event(%s,0,0,0,[UIntPtr]::Zero);", meLeftDown, meLeftUp) } _, err := b.runScript(bld.String()) return err } func (b *windowsBackend) TypeText(_ context.Context, text string) error { seq := strings.ReplaceAll(sendKeysEscape(text), "'", "''") script := "Add-Type -AssemblyName System.Windows.Forms; " + "[System.Windows.Forms.SendKeys]::SendWait('" + seq + "');" _, err := b.runScript(script) return err } func (b *windowsBackend) KeyChord(_ context.Context, chordStr string) error { mods, main, err := parseChord(chordStr) if err != nil { return err } seq, err := chordToSendKeys(mods, main, chordStr) if err != nil { return err } seq = strings.ReplaceAll(seq, "'", "''") script := "Add-Type -AssemblyName System.Windows.Forms; " + "[System.Windows.Forms.SendKeys]::SendWait('" + seq + "');" _, err = b.runScript(script) return err } func (b *windowsBackend) Scroll(_ context.Context, direction string, amount int) error { delta := wheelNotch // 上滚为正 if direction == "down" { delta = "-" + wheelNotch } var bld strings.Builder for i := 0; i < amount; i++ { fmt.Fprintf(&bld, "Start-Sleep -Milliseconds 90; [AGUser32]::mouse_event(%s,0,0,%s,[UIntPtr]::Zero); ", meWheel, delta) } _, err := b.runScript(bld.String()) return err } func (b *windowsBackend) Screenshot(_ context.Context, region string) (string, error) { path := filepath.Join(os.TempDir(), fmt.Sprintf("autogo-shot-%d.png", time.Now().UnixNano())) psPath := strings.ReplaceAll(path, "'", "''") var bld strings.Builder bld.WriteString("Add-Type -AssemblyName System.Windows.Forms,System.Drawing; ") if region != "" { x, y, w, h, err := parseRegion(region) if err != nil { return "", err } fmt.Fprintf(&bld, "$bmp=New-Object System.Drawing.Bitmap(%d,%d); $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen(%d,%d,0,0,$bmp.Size); ", w, h, x, y) } else { bld.WriteString("$b=[System.Windows.Forms.SystemInformation]::VirtualScreen; $bmp=New-Object System.Drawing.Bitmap($b.Width,$b.Height); $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.X,$b.Y,0,0,$bmp.Size); ") } bld.WriteString("$g.Dispose(); $bmp.Save('" + psPath + "',[System.Drawing.Imaging.ImageFormat]::Png); $bmp.Dispose();") if out, err := b.runScript(bld.String()); err != nil { _ = os.Remove(path) return "", fmt.Errorf("screenshot failed: %w: %s", err, out) } return path, nil }