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。
212 lines
7.0 KiB
Go
212 lines
7.0 KiB
Go
//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
|
||
}
|