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 @@
package cdp
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
)
// 浏览器 profile 的发现、体检与复制。
//
// 为什么需要这些:Chrome 的**登录态(cookies)与扩展都存在 user-data-dir 里**。
// 以前 profile 为空就用 os.MkdirTemp 建个空目录,于是每次启动都像一台从没登录过的
// 新机器——而且那个临时目录**从不清理**,退出后在 /tmp 留下几百 MB。
//
// 想复用已有登录态,就把 browser.profile 指到一份**已登录的** user-data-dir(通常是
// 系统默认目录的副本)。体检负责把几个常见坑说清楚:指到了 Default 子目录、指到了
// Chrome 默认目录、或者那份 profile 正被一个运行中的 Chrome 占用。
// DefaultUserDataDir 返回当前平台 Chrome 的默认用户数据目录(那份带登录态与扩展的)。
// 找不到时返回候选列表供人肉判断。
func DefaultUserDataDir() (string, error) {
for _, c := range defaultUserDataDirCandidates() {
if st, err := os.Stat(c); err == nil && st.IsDir() {
return c, nil
}
}
return "", fmt.Errorf("找不到默认的 Chrome 用户数据目录(找过:%s)", strings.Join(defaultUserDataDirCandidates(), ", "))
}
// defaultUserDataDirCandidates 列出各平台可能的默认 user-data-dir。
func defaultUserDataDirCandidates() []string {
home, _ := os.UserHomeDir()
switch runtime.GOOS {
case "darwin":
return []string{
filepath.Join(home, "Library", "Application Support", "Google", "Chrome"),
filepath.Join(home, "Library", "Application Support", "Chromium"),
filepath.Join(home, "Library", "Application Support", "Microsoft Edge"),
}
case "windows":
base := os.Getenv("LOCALAPPDATA")
if base == "" {
return nil
}
return []string{
filepath.Join(base, "Google", "Chrome", "User Data"),
filepath.Join(base, "Microsoft", "Edge", "User Data"),
}
default:
return []string{
filepath.Join(home, ".config", "google-chrome"),
filepath.Join(home, ".config", "chromium"),
filepath.Join(home, ".config", "microsoft-edge"),
}
}
}
// PersistentProfileDir 是 AutoGo 自己的持久 profile 目录。
//
// 用它而不是临时目录:临时目录意味着"登录一次也留不住"(每次进程启动都是新的空
// profile),而且没人清理。持久目录下登录一次就长期有效。
func PersistentProfileDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".autogo", "browser-profile"), nil
}
// IsChromeDefaultDir 报告 dir 是不是(或位于)系统默认的 Chrome user-data-dir。
// 是的话要提醒:Chrome 136+ 起出于安全**禁止在默认 profile 上开启远程调试**,必须用
// 非默认的 user-data-dir;而且默认目录通常被日常 Chrome 占着(单实例锁)。
func IsChromeDefaultDir(dir string) bool {
abs, err := filepath.Abs(dir)
if err != nil {
return false
}
for _, c := range defaultUserDataDirCandidates() {
if abs == c {
return true
}
}
return false
}
// looksLikeProfileDir 判断 dir 像是"某个 profile 本体"(Default/ 里的那种),
// 而不是 user-data-dir。这是最常见的配置错误:--user-data-dir 要指向**父目录**。
func looksLikeProfileDir(dir string) bool {
for _, f := range []string{"Cookies", "Login Data", "Preferences"} {
if st, err := os.Stat(filepath.Join(dir, f)); err == nil && !st.IsDir() {
return true
}
}
return false
}
// CheckProfile 对配置里的 profile 目录做体检,返回若干条**人类可读的告警**(可以为空)。
// 不存在之类属于致命问题,由调用方按错误处理——这里只做"能启动但可能不符合预期"的提醒。
func CheckProfile(dir string) []string {
var warns []string
if dir == "" {
return nil
}
st, err := os.Stat(dir)
if err != nil || !st.IsDir() {
return []string{fmt.Sprintf("profile 目录不存在:%s(它不会被自动创建;先确认路径,或用 `a2go browser profile-copy` 复制一份)", dir)}
}
if looksLikeProfileDir(dir) {
warns = append(warns, fmt.Sprintf(
"profile 指向的像是**profile 本体**(里面有 Cookies/Preferences):%s\n"+
" --user-data-dir 要指向它的**父目录**,否则 Chrome 会在里面另建一套结构,你的登录态不会生效。", dir))
}
if IsChromeDefaultDir(dir) {
warns = append(warns, fmt.Sprintf(
"profile 是 Chrome 的**默认目录**:%s\n"+
" Chrome 136+ 起禁止在默认 profile 上开启远程调试,而且日常 Chrome 通常正占着它。\n"+
" 建议改用副本:a2go browser profile-copy", dir))
}
if _, err := os.Lstat(filepath.Join(dir, "SingletonLock")); err == nil {
warns = append(warns, fmt.Sprintf(
"该 profile 里有 SingletonLock:%s\n"+
" 说明可能有一个正在运行的 Chrome 在用这份 profile。同一个 user-data-dir 起第二个实例通常\n"+
" 拿不到调试端口(会被转交给已运行的实例)。请先完全退出 Chrome,或改用副本。", dir))
}
return warns
}
// profileCacheDirs 是复制时跳过的缓存目录:体积大、且与登录态无关(含扩展的缓存)。
// 注意 **不跳过 Extensions** —— 复用方案里扩展正是要带过去的东西之一。
var profileCacheDirs = map[string]bool{
"Cache": true,
"Code Cache": true,
"GPUCache": true,
"ShaderCache": true,
"GrShaderCache": true,
"DawnCache": true,
"DawnGraphiteCache": true,
"GraphiteDawnCache": true,
"Media Cache": true,
"Service Worker": true,
"Crashpad": true,
"BrowserMetrics": true,
"component_crx_cache": true,
"extensions_crx_cache": true,
"Safe Browsing": true,
}
// profileSkipFile 判断某个文件是否属于"复制时跳过"(单实例锁等,带过去反而会误导 Chrome)。
func profileSkipFile(name string) bool {
return strings.HasPrefix(name, "Singleton")
}
// CopyProfile 把 src 里的登录态与扩展复制到 dst。
//
// 复制前会拒绝"源正被占用"的情况:Chrome 运行中复制的 profile 可能不一致,而且它的
// 单实例锁文件会一起被带过去。
func CopyProfile(src, dst string) (int64, error) {
st, err := os.Stat(src)
if err != nil || !st.IsDir() {
return 0, fmt.Errorf("源 profile 不存在或不是目录:%s", src)
}
if looksLikeProfileDir(src) {
return 0, fmt.Errorf("源指向的像是 profile 本体(含 Cookies/Preferences):%s\n要复制的是它的**父目录**(user-data-dir)", src)
}
if _, err := os.Lstat(filepath.Join(src, "SingletonLock")); err == nil {
return 0, fmt.Errorf("源 profile 正被占用(有 SingletonLock):%s\n请先完全退出 Chrome 再复制——运行中复制的 profile 可能不一致", src)
}
if err := os.MkdirAll(dst, 0o755); err != nil {
return 0, err
}
var total int64
err = filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil // 个别文件读不了(权限/正在写)不该中断整次复制
}
rel, err := filepath.Rel(src, path)
if err != nil {
return nil
}
if rel == "." {
return nil
}
// 跳过缓存目录(整个子树)、单实例锁、以及 *-journal 之类的临时文件。
for _, part := range strings.Split(rel, string(filepath.Separator)) {
if profileCacheDirs[part] {
return filepath.SkipDir
}
}
if profileSkipFile(d.Name()) {
return nil
}
target := filepath.Join(dst, rel)
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
if !d.Type().IsRegular() {
return nil // 符号链接等一律跳过
}
n, err := copyFile(path, target)
total += n
return err
})
if err != nil {
return total, err
}
// 目标里也清一次:万一 dst 是之前从运行中的 Chrome 拷来的。
for _, f := range []string{"SingletonLock", "SingletonSocket", "SingletonCookie"} {
_ = os.Remove(filepath.Join(dst, f))
}
return total, nil
}
func copyFile(src, dst string) (int64, error) {
in, err := os.Open(src)
if err != nil {
return 0, nil // 读不了就跳过,不中断
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return 0, err
}
out, err := os.Create(dst)
if err != nil {
return 0, err
}
defer out.Close()
n, err := io.Copy(out, in)
if err != nil {
return n, nil
}
return n, nil
}
// LooksLikeBrowserProfile 判断 dir 是否像一份浏览器 user-data-dir(用于 --force 前的安全校验:
// 只有确认目标确实是 profile 才允许覆盖删除,避免手滑删掉无关目录)。
func LooksLikeBrowserProfile(dir string) bool {
for _, f := range []string{"Local State", "Default", "First Run"} {
if _, err := os.Stat(filepath.Join(dir, f)); err == nil {
return true
}
}
return false
}
// ErrProfileMissing 是 profile 不存在时的哨兵(调用方据此给出配置建议)。
var ErrProfileMissing = errors.New("browser profile not found")