Files
AutoZero/tools/computer_windows.go
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

212 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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
}