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

+251
View File
@@ -0,0 +1,251 @@
//go:build darwin
package tools
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.gopher.ink/mirrors2/AutoZero/diag"
"git.gopher.ink/mirrors2/AutoZero/execx"
)
// darwinBackend:macOS 桌面控制,走系统自带能力(零依赖):
// 截图用 screencapture,鼠标键盘用 System Events 的 osascript;
// 若安装了 cliclick 则用它增强(更可靠的 click/right_click/double_click)。
// 需要终端的 辅助功能(Accessibility)与 屏幕录制 权限。
type darwinBackend struct{}
func newGuiBackend() guiBackend { return darwinBackend{} }
func (darwinBackend) Available() error { return nil } // osascript/screencapture 为系统自带
// PreflightPermissions 探测桌面控制所需的 macOS TCC 权限(辅助功能 / 屏幕录制),
// 缺失时打开系统设置对应面板引导用户授权,并尝试触发一次受保护调用以唤起原生弹窗。
// 返回缺失权限名(空切片=齐全)。仅在首次运行(无标记文件)时自动打开面板,避免每次启动打扰。
func (darwinBackend) PreflightPermissions() []string {
st := checkDesktopPermissions()
var missing []string
if !st.Accessibility {
missing = append(missing, "Accessibility")
}
if !st.ScreenCapture {
missing = append(missing, "Screen Recording")
}
if len(missing) == 0 {
return nil
}
// 首次运行自动打开系统设置面板引导;写标记后不再自动弹。
home, _ := os.UserHomeDir()
marker := filepath.Join(home, ".autogo", ".perms-prompted")
if _, err := os.Stat(marker); err != nil {
openSettingsPane("Privacy_Accessibility")
openSettingsPane("Privacy_ScreenCapture")
_ = os.MkdirAll(filepath.Dir(marker), 0o755)
_ = os.WriteFile(marker, []byte(time.Now().UTC().Format(time.RFC3339)), 0o644)
}
// 触发原生授权弹窗:未授权时调用一次受保护 API 即可让 macOS 弹窗引导。
_, _ = runOSA(context.Background(), `tell application "System Events" to return name of first process`)
// 指引走 diag:TUI 下它被换到服务日志,直写 os.Stderr 会打穿 alt-screen。
diag.Printf("[setup] macOS 桌面权限缺失:%s。请在「系统设置 → 隐私与安全性 → %s」中开启,并完全重启本程序(终端/App)使授权生效。",
strings.Join(missing, "、"), strings.Join(missing, " / "))
return missing
}
// checkDesktopPermissions 探测两项 TCC 权限是否已授予。
func checkDesktopPermissions() permState {
s := permState{}
// 辅助功能:未授权时 System Events 事件请求被拒。
if _, err := runOSA(context.Background(), `tell application "System Events" to return name of first process`); err == nil {
s.Accessibility = true
}
// 屏幕录制:未授权时 screencapture 不产生文件。
if p, err := captureScreen(context.Background(), ""); err == nil {
s.ScreenCapture = true
_ = os.Remove(p)
}
return s
}
type permState struct {
Accessibility bool
ScreenCapture bool
}
// openSettingsPane 打开系统设置的某个隐私子面板。
func openSettingsPane(pane string) {
_ = exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?"+pane).Start()
}
func (darwinBackend) Screenshot(ctx context.Context, region string) (string, error) {
return captureScreen(ctx, region)
}
func (darwinBackend) Click(_ context.Context, x, y int, kind string) error {
if hasCliclick() {
_, err := runShell(context.Background(), clickScript(x, y, true, kind))
return err
}
if _, err := runOSA(context.Background(), clickScript(x, y, false, kind)); err != nil {
return fmt.Errorf("%w (hint: install cliclick for more reliable clicks, or grant Accessibility permission)", err)
}
return nil
}
func (darwinBackend) TypeText(_ context.Context, text string) error {
_, err := runOSA(context.Background(), fmt.Sprintf(`tell application "System Events" to keystroke %s`, osaQuote(text)))
return err
}
func (darwinBackend) KeyChord(_ context.Context, chordStr string) error {
stmt, err := osaKeyChord(chordStr)
if err != nil {
return err
}
_, err = runOSA(context.Background(), `tell application "System Events" to `+stmt)
return err
}
func (darwinBackend) Scroll(_ context.Context, direction string, amount int) error {
code := "125" // down arrow
if direction == "up" {
code = "126"
}
var b strings.Builder
b.WriteString(`tell application "System Events"`)
for i := 0; i < amount; i++ {
fmt.Fprintf(&b, " delay 0.05 key code %s", code)
}
b.WriteString(` end tell`)
_, err := runOSA(context.Background(), b.String())
return err
}
// ---- macOS 底层工具函数(browser.go 也复用 osaQuote/runOSA)----
// osaQuote 把字符串安全嵌入 AppleScript 双引号字符串。
func osaQuote(s string) string {
return "\"" + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s) + "\""
}
// osaKeyChord 把 "cmd+shift+t" 这类键组合编译为 AppleScript 语句。
func osaKeyChord(chord string) (string, error) {
mods := map[string]string{
"cmd": "command down", "command": "command down",
"ctrl": "control down", "control": "control down",
"alt": "option down", "option": "option down",
"shift": "shift down", "fn": "fn down",
}
named := map[string]string{
"enter": "36", "return": "36", "tab": "48", "escape": "53", "esc": "53",
"delete": "51", "backspace": "51", "forwarddelete": "117",
"up": "126", "down": "125", "left": "123", "right": "124",
"space": "49", "home": "115", "end": "119", "pageup": "116", "pagedown": "121",
"f1": "122", "f2": "120", "f3": "99", "f4": "118", "f5": "96", "f6": "97",
"f7": "98", "f8": "100", "f9": "101", "f10": "109", "f11": "103", "f12": "111",
}
var using []string
var main string
for _, part := range strings.Split(strings.ToLower(strings.TrimSpace(chord)), "+") {
part = strings.TrimSpace(part)
if part == "" {
return "", fmt.Errorf("empty key in chord %q", chord)
}
if m, ok := mods[part]; ok && main == "" {
using = append(using, m)
continue
}
if main != "" {
return "", fmt.Errorf("multiple non-modifier keys in chord %q", chord)
}
if code, ok := named[part]; ok {
main = "key code " + code
continue
}
if len([]rune(part)) == 1 {
main = "keystroke " + osaQuote(part)
continue
}
return "", fmt.Errorf("unknown key %q", part)
}
if main == "" {
return "", fmt.Errorf("chord %q has no main key", chord)
}
if len(using) > 0 {
return main + " using {" + strings.Join(using, ", ") + "}", nil
}
return main, nil
}
func runOSA(ctx context.Context, script string) (string, error) {
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
out, err := execx.Run(cctx, execx.Options{}, "osascript", "-e", script)
if err != nil {
return strings.TrimSpace(string(out)), fmt.Errorf("osascript: %w: %s", err, strings.TrimSpace(string(out)))
}
return strings.TrimSpace(string(out)), nil
}
func hasCliclick() bool {
_, err := exec.LookPath("cliclick")
return err == nil
}
// captureScreen 截屏到临时文件(region 非空时截取 "x,y,w,h")。
// screencapture 在等屏幕录制授权弹窗时会一直不返回,所以必须有超时。
func captureScreen(ctx context.Context, region string) (string, error) {
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
path := fmt.Sprintf("/tmp/autogo-shot-%d.png", time.Now().UnixNano())
args := []string{"-x"}
if region != "" {
args = append(args, "-R", region)
}
args = append(args, path)
out, err := execx.Run(cctx, execx.Options{}, "screencapture", args...)
if err != nil {
_ = os.Remove(path)
return "", fmt.Errorf("screencapture: %w: %s", err, strings.TrimSpace(string(out)))
}
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("screenshot was not created (check Screen Recording permission)")
}
return path, nil
}
func clickScript(x, y int, cliclick bool, kind string) string {
if cliclick {
switch kind {
case "right":
return fmt.Sprintf("cliclick rc:%d,%d", x, y)
case "double":
return fmt.Sprintf("cliclick dc:%d,%d", x, y)
default:
return fmt.Sprintf("cliclick c:%d,%d", x, y)
}
}
switch kind {
case "double":
return fmt.Sprintf(`tell application "System Events" to click at {%d, %d} delay 0.05 click at {%d, %d}`, x, y, x, y)
default:
return fmt.Sprintf(`tell application "System Events" to click at {%d, %d}`, x, y)
}
}
func runShell(ctx context.Context, cmdline string) (string, error) {
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
out, err := execx.Shell(cctx, execx.Options{}, cmdline)
if err != nil {
return strings.TrimSpace(string(out)), fmt.Errorf("%s: %w: %s", strings.Fields(cmdline)[0], err, strings.TrimSpace(string(out)))
}
return strings.TrimSpace(string(out)), nil
}