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。
This commit is contained in:
Sakurasan committed 2026-10-26 00:54:46 +08:00
commit d5dac5a6fc
140 files changed
+26497

No files matched your search

+291
View File
@@ -0,0 +1,291 @@
//go:build linux
package tools
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.gopher.ink/mirrors2/AutoZero/diag"
"git.gopher.ink/mirrors2/AutoZero/execx"
)
// screenshotTimeout 兜住截图工具不返回的情况(授权失败、X server 卡死)。
const screenshotTimeout = 20 * time.Second
// linuxBackend:Linux 桌面控制,自动检测 X11/Wayland 会话。
// X11: 输入用 xdotool,截图用 scrot → import → gnome-screenshot。
// Wayland: 输入用 ydotool,截图用 grim → gnome-screenshot → import。
type linuxBackend struct {
inputFn func(args ...string) (string, error) // xdotool 或 ydotool
isWay bool
}
func newGuiBackend() guiBackend {
b := &linuxBackend{}
if isWayland() {
b.isWay = true
b.inputFn = ydotool
} else {
b.inputFn = xdotool
}
return b
}
// isWayland 检测当前会话是否为 Wayland。
func isWayland() bool {
if v := os.Getenv("XDG_SESSION_TYPE"); strings.EqualFold(v, "wayland") {
return true
}
if os.Getenv("WAYLAND_DISPLAY") != "" {
return true
}
return false
}
func hasBin(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func (b *linuxBackend) Available() error {
if b.isWay {
if _, err := exec.LookPath("ydotool"); err != nil {
return fmt.Errorf("ydotool is required for computer use on Wayland (e.g. apt install ydotool); also ensure /dev/uinput is accessible (user may need input group)")
}
if !hasBin("grim") && !hasBin("gnome-screenshot") && !hasBin("import") {
return fmt.Errorf("a screenshot tool is required on Wayland (install grim, gnome-screenshot, or imagemagick)")
}
return nil
}
// X11
if _, err := exec.LookPath("xdotool"); err != nil {
return fmt.Errorf("xdotool is required for computer use on X11 (e.g. apt install xdotool)")
}
for _, t := range []string{"scrot", "import", "gnome-screenshot"} {
if hasBin(t) {
return nil
}
}
return fmt.Errorf("a screenshot tool is required (install scrot, imagemagick, or gnome-screenshot)")
}
// PreflightPermissions Linux 暂无统一的“首次引导授权”机制,依赖 Available() 的依赖检查。
func (b *linuxBackend) PreflightPermissions() []string { return nil }
func (b *linuxBackend) Screenshot(ctx context.Context, region string) (string, error) {
// 截图工具在 Wayland 授权失败/X server 卡住时会不返回;ctx 由调用方给,
// 这里再加一层兜底上限,保证 computer 工具不会把回合钉死。
cctx, cancel := context.WithTimeout(ctx, screenshotTimeout)
defer cancel()
path := filepath.Join(os.TempDir(), fmt.Sprintf("autogo-shot-%d.png", time.Now().UnixNano()))
var cmd *exec.Cmd
if b.isWay {
switch {
case hasBin("grim") && region != "":
// grim 支持区域: grim -g x,y,w,h
cmd = execx.Command(cctx, "grim", "-g", region, path)
case hasBin("grim"):
cmd = execx.Command(cctx, "grim", path)
case hasBin("gnome-screenshot"):
cmd = execx.Command(cctx, "gnome-screenshot", "-f", path)
case hasBin("import"):
cmd = execx.Command(cctx, "import", "-window", "root", path)
default:
return "", fmt.Errorf("no screenshot tool available on Wayland (install grim)")
}
} else {
// X11
switch {
case region != "" && hasBin("scrot"):
cmd = execx.Command(cctx, "scrot", "-o", "-a", region, path)
case hasBin("scrot"):
if region != "" {
// import/gnome-screenshot 不支持区域,静默截全屏并打印提示
diag.Printf("[computer] warning: region screenshot not supported with current tool; capturing full screen\n")
}
cmd = execx.Command(cctx, "scrot", "-o", path)
case hasBin("import"):
if region != "" {
diag.Printf("[computer] warning: region screenshot not supported with import; capturing full screen\n")
}
cmd = execx.Command(cctx, "import", "-window", "root", path)
case hasBin("gnome-screenshot"):
if region != "" {
diag.Printf("[computer] warning: region screenshot not supported with gnome-screenshot; capturing full screen\n")
}
cmd = execx.Command(cctx, "gnome-screenshot", "-f", path)
default:
return "", fmt.Errorf("no screenshot tool available (install scrot or imagemagick)")
}
}
if out, err := cmd.CombinedOutput(); err != nil {
_ = os.Remove(path)
return "", fmt.Errorf("screenshot failed: %w: %s", err, oneLine(out))
}
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("screenshot was not created (is DISPLAY/WAYLAND_DISPLAY set?)")
}
return path, nil
}
func (b *linuxBackend) Click(_ context.Context, x, y int, kind string) error {
if b.isWay {
return ydotoolClick(x, y, kind)
}
args := []string{"mousemove", itoa(x), itoa(y)}
switch kind {
case "right":
args = append(args, "click", "3")
case "double":
args = append(args, "click", "--repeat", "2", "--delay", "80", "1")
default:
args = append(args, "click", "1")
}
_, err := b.inputFn(args...)
return err
}
func (b *linuxBackend) TypeText(_ context.Context, text string) error {
if b.isWay {
// ydotool type: 逐字符输入
_, err := ydotool("type", "--delay", "15", "--", text)
return err
}
_, err := xdotool("type", "--delay", "15", "--", text)
return err
}
func (b *linuxBackend) KeyChord(_ context.Context, chordStr string) error {
mods, main, err := parseChord(chordStr)
if err != nil {
return err
}
if b.isWay {
combo, err := chordToYdotool(mods, main, chordStr)
if err != nil {
return err
}
_, err = ydotool("key", combo)
return err
}
combo, err := chordToXdotool(mods, main, chordStr)
if err != nil {
return err
}
_, err = xdotool("key", "--clearmodifiers", combo)
return err
}
func (b *linuxBackend) Scroll(_ context.Context, direction string, amount int) error {
if b.isWay {
return ydotoolScroll(direction, amount)
}
button := "5" // 滚轮下
if direction == "up" {
button = "4"
}
_, err := xdotool("click", "--repeat", itoa(amount), "--delay", "90", button)
return err
}
// ---- xdotool (X11) ----
func xdotool(args ...string) (string, error) {
cctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
out, err := execx.Run(cctx, execx.Options{}, "xdotool", args...)
if err != nil {
return string(out), fmt.Errorf("xdotool %s: %w: %s", joinArgs(args), err, oneLine(out))
}
return string(out), nil
}
// ---- ydotool (Wayland) ----
func ydotool(args ...string) (string, error) {
cctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
out, err := execx.Run(cctx, execx.Options{}, "ydotool", args...)
if err != nil {
return string(out), fmt.Errorf("ydotool %s: %w: %s", joinArgs(args), err, oneLine(out))
}
return string(out), nil
}
func ydotoolClick(x, y int, kind string) error {
// ydotool:mousemove absolute x y; click:1=左 3=右
args := []string{"mousemove", "--absolute", itoa(x), itoa(y)}
switch kind {
case "right":
args = append(args, "click", "3")
case "double":
args = append(args, "click", "1", "--next-delay", "80", "click", "1")
default:
args = append(args, "click", "1")
}
_, err := ydotool(args...)
return err
}
func ydotoolScroll(direction string, amount int) error {
// ydotool: wheel: -120=上 120=下
delta := "120"
if direction == "up" {
delta = "-120"
}
for i := 0; i < amount; i++ {
if _, err := ydotool("wheel", "--delta", delta); err != nil {
return err
}
time.Sleep(90 * time.Millisecond)
}
return nil
}
// chordToYdotool 翻译为 ydotool 按键语法。
// ydotool 使用 Linux 内核 keycode 名称(与 xdotool 类似但不完全相同)。
func chordToYdotool(mods []string, main, orig string) (string, error) {
modMap := map[string]string{
"ctrl": "ctrl", "control": "ctrl", "alt": "alt", "option": "alt",
"shift": "shift", "cmd": "meta", "command": "meta",
"win": "meta", "super": "meta", "meta": "meta",
}
mainMap := map[string]string{
"enter": "enter", "return": "enter", "escape": "esc", "esc": "esc",
"tab": "tab", "delete": "delete", "backspace": "backspace", "forwarddelete": "delete",
"up": "up", "down": "down", "left": "left", "right": "right",
"space": "space", "home": "home", "end": "end",
"pageup": "pageup", "pagedown": "pagedown",
}
parts := make([]string, 0, len(mods)+1)
for _, m := range mods {
parts = append(parts, modMap[m])
}
if sym, ok := mainMap[main]; ok {
parts = append(parts, sym)
} else if n := fnKeyNum(main); n > 0 {
parts = append(parts, fmt.Sprintf("f%d", n))
} else {
parts = append(parts, main)
}
return strings.Join(parts, "+"), nil
}
func joinArgs(args []string) string {
s := ""
for i, a := range args {
if i > 0 {
s += " "
}
s += a
}
return s
}