// Package hooks 实现 Hook 系统. // // Hook 系统的核心实现. // Hook 系统允许用户在 Agent 的关键生命周期点注入自定义 shell 命令. // // 原项目的问题: // - 12+ 种 hook 类型全塞一个 5000 行文件 // - 每种 hook 的执行逻辑重复但各有特殊分支 // - 同步/异步执行混在一起,错误处理不一致 // // Go 版本的设计: // - Manager 统一管理所有 hook 的注册和触发 // - types.go 定义所有类型和结果结构 // - executor.go 封装命令执行(环境注入,超时,输出捕获) // - integration.go 提供与 Engine 的集成辅助函数 // - 同步/异步执行有明确的 API 分离 package hooks import ( "context" "fmt" "sync" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/execenv" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // L1326 衍生 a (2026-04-16) 重构: hooks 包直接消费 flyto.EventObserver 作为 // Observer 契约, 不再定义本地 HookObserver 接口. // // 历史包袱(LEGACY): 早期注释声称 "直接依赖 flyto.EventObserver 会循环依赖" 是误判 -- // flyto 是零外部依赖的契约层 (见 pkg/flyto/doc.go), 整个项目 29 个包 // import flyto, flyto 零反向 import. hooks → flyto 单向依赖完全安全, // 不构成循环. 方法集"逐字相同"+Go 结构化类型鸭子类型隐式满足的模式 // 造成了隐性 coupling 债 (一方加方法另一方不编译报错). // 现在 Manager.observer 字段直接是 flyto.EventObserver, 契约变化编译期强制同步. // // 替代方案: 保留 HookObserver 作为 flyto.EventObserver 的类型别名 // (type HookObserver = flyto.EventObserver) - 否决, 两个名字指同一 // 类型增加读者认知负担 (与 memory 包 L1326 决策保持一致). // noopHookObserver 是 hooks 包的内部 noop 实现, 在 Observer 未注入时兜底. // 它自然满足 flyto.EventObserver (Event + Error 两个空方法), 不引入对 // engine.NoopObserver 的依赖 (会构成 hooks → engine 循环). type noopHookObserver struct{} func (n *noopHookObserver) Event(name string, data map[string]any) {} func (n *noopHookObserver) Error(err error, ctx map[string]any) {} // Manager 管理所有 hook 的注册和触发. // // 线程安全:内部用 RWMutex 保护 hook 注册表, // 支持并发执行不同类型的 hook. // // # Executor 依赖 (M1 方案 β 严格 DI) // // Manager 持有 execenv.Executor 用于启动 shell hook 子进程. 本地 CLI 场景 // 由 engine.New 把 cfg.Executor (默认 execenv.DefaultExecutor{}) 注入, 云端 // SaaS 场景由 platform 层注入 sandbox.Backend. Manager 自己不知道也不关心 // 后端是哪个, 只负责把 Class/Spec 透传下去. // // 强制约定: executor 字段永远 non-nil. NewManager 会校验, nil 直接 panic -- // 严格 DI 不做 fallback, 避免历史上的 "mock hooks 绕过 executor 路径" 漏网之鱼. type Manager struct { mu sync.RWMutex hooks map[HookType][]HookDef executor execenv.Executor disabled bool // 全局禁用开关(测试用) observer flyto.EventObserver // 可观测性接口,nil 时用 noopHookObserver 兜底 } // getObserver 获取 observer,nil 时返回 noop 兜底. func (m *Manager) getObserver() flyto.EventObserver { if m.observer != nil { return m.observer } return &noopHookObserver{} } // SetObserver 设置可观测性接口. // 升华改进(ELEVATED): Setter 注入而非构造函数参数-- // NewManager 已在多处调用,加参数会破坏所有调用点. // Setter 注入让现有代码零改动即可工作. // 替代方案:修改 NewManager 签名(破坏所有现有调用点). func (m *Manager) SetObserver(obs flyto.EventObserver) { m.observer = obs } // NewManager 创建 hook 管理器. // // 如果 cfg 为 nil,创建一个空的管理器(没有预注册的 hook). // 消费层后续可以用 Register 动态添加 hook. // // executor 参数是**必填**, nil 会 panic. 严格 DI 契约 (M1 方案 β) 不做 // fallback 到 execenv.DefaultExecutor{}: 本地 CLI 场景由 engine.New 注入, // 云端 SaaS 场景由 platform 层注入 sandbox.Backend. 绕过 DI 让测试用 nil // 会让 "shell hook 在沙盒里真的会走后端" 的合同静默失守. // // 测试代码直接传 execenv.DefaultExecutor{} 即可 (零开销包装 os/exec, 和 // 老路径 bit-identical, 详见 core/pkg/execenv bench). func NewManager(cfg *Config, executor execenv.Executor) *Manager { if executor == nil { panic("hooks.NewManager: executor is required (M1 strict DI, no fallback)") } m := &Manager{ hooks: make(map[HookType][]HookDef), executor: executor, } // 从配置加载预定义的 hook if cfg != nil && cfg.Hooks != nil { for hookType, defs := range cfg.Hooks { m.hooks[hookType] = append(m.hooks[hookType], defs...) } } return m } // Register 注册一个新的 hook 定义到指定类型. // // 同一个 hook 类型可以注册多个 handler,执行时按注册顺序依次执行. // 返回错误如果 hookType 无效. func (m *Manager) Register(hookType HookType, def HookDef) error { if def.Command == "" && def.Handler == nil { return fmt.Errorf("hooks: command or handler is required") } // 验证 hook 类型是否合法 if !isValidHookType(hookType) { return fmt.Errorf("hooks: unknown hook type: %s", hookType) } m.mu.Lock() defer m.mu.Unlock() m.hooks[hookType] = append(m.hooks[hookType], def) return nil } // Unregister 移除指定类型的所有 hook. func (m *Manager) Unregister(hookType HookType) { m.mu.Lock() defer m.mu.Unlock() delete(m.hooks, hookType) } // HasHooks 检查指定类型是否有注册的 hook. // 用于 Engine 在关键路径上快速判断是否需要触发 hook(避免不必要的环境变量构建). func (m *Manager) HasHooks(hookType HookType) bool { m.mu.RLock() defer m.mu.RUnlock() return len(m.hooks[hookType]) > 0 } // Execute 同步执行指定类型的所有 hook. // // 按注册顺序依次执行,每个 hook 都会执行(即使前一个失败). // 返回所有 hook 的执行结果,调用方可以检查是否有错误. // // env 是注入到 shell 命令的环境变量(通过 integration.go 的 Build* 函数构建). // // 精妙之处(CLEVER): fail-open 策略--单个 hook 失败不阻断其余 hook 的执行. // 如果 fail-close,一个写错的 hook 脚本就会让整个 Agent 瘫痪. // 结果按执行顺序返回,调用方可以检查每个 hook 的状态. // 如果没有注册的 hook,返回空结果(不是错误). func (m *Manager) Execute(ctx context.Context, hookType HookType, env map[string]string) (*ExecuteResults, error) { if m.disabled { return &ExecuteResults{HookType: hookType}, nil } obs := m.getObserver() m.mu.RLock() defs := make([]HookDef, len(m.hooks[hookType])) copy(defs, m.hooks[hookType]) m.mu.RUnlock() // 埋点说明:Hook 开始执行是性能分析的起点-- // hook 数量和类型决定了主流程被阻塞多久,频繁的 hook 调用需要优化. obs.Event("hook_started", map[string]any{ "type": string(hookType), "count": len(defs), }) results := &ExecuteResults{ HookType: hookType, Results: make([]*HookResult, 0, len(defs)), } for _, def := range defs { // 跳过标记为异步的 hook(应该用 ExecuteAsync) if def.Async { continue } select { case <-ctx.Done(): // context 已取消,停止执行剩余 hook results.Results = append(results.Results, &HookResult{ Command: def.Command, Error: ctx.Err(), ExitCode: -1, }) return results, ctx.Err() default: } start := time.Now() // 升华改进(ELEVATED): 根据 HookDef 选择执行后端-- // Handler(Go 回调)优先于 Command(shell). // 这让 SDK 用户可以注册 Go 函数作为 hook,零 shell 开销. // 替代方案:<原方案只调 executor.execute,只支持 shell> result := m.executeOne(ctx, def, hookType, env) results.Results = append(results.Results, result) if result.Error != nil { if ctx.Err() == context.DeadlineExceeded { // 埋点说明:Hook 超时是常见的性能问题-- // 用户自定义脚本可能挂起,需要告警并建议增加超时或异步执行. obs.Event("hook_timeout", map[string]any{ "type": string(hookType), "command": def.Command, "timeout_ms": def.EffectiveTimeout().Milliseconds(), }) } else { // 埋点说明:Hook 失败需要记录命令和错误-- // 用户脚本出错时,错误上下文是排查的唯一线索. obs.Error(result.Error, map[string]any{ "type": string(hookType), "command": def.Command, }) } } else { // 埋点说明:每个 Hook 的执行结果是审计和性能分析的基础数据. obs.Event("hook_executed", map[string]any{ "type": string(hookType), "command": def.Command, "duration_ms": time.Since(start).Milliseconds(), "exit_code": result.ExitCode, "has_json": result.JSONOutput != nil, }) } } return results, nil } // ExecuteAsync 异步执行指定类型的所有 hook(不阻塞主流程). // // 适用于不需要等待结果的 hook,如 notification,session_end 等. // 返回一个 channel,异步完成后会收到结果. // 调用方可以选择等待结果或忽略. // // 设计决策: // - 使用 goroutine 执行,不阻塞调用方 // - 使用 background context(不受调用方 context 取消影响) // 因为异步 hook 通常在会话结束时触发,此时 context 可能已取消 // - 设置独立的超时(每个 hook 的 EffectiveTimeout) func (m *Manager) ExecuteAsync(hookType HookType, env map[string]string) <-chan *ExecuteResults { ch := make(chan *ExecuteResults, 1) if m.disabled { ch <- &ExecuteResults{HookType: hookType} close(ch) return ch } obs := m.getObserver() m.mu.RLock() defs := make([]HookDef, len(m.hooks[hookType])) copy(defs, m.hooks[hookType]) m.mu.RUnlock() // 埋点说明:异步 Hook 启动是后台任务追踪的起点-- // 异步 hook 不阻塞主流程但可能持续运行,需要知道有多少在后台跑. obs.Event("hook_async_started", map[string]any{ "type": string(hookType), "count": len(defs), }) go func() { defer close(ch) // 使用 background context,不受调用方 context 影响 ctx := context.Background() results := &ExecuteResults{ HookType: hookType, Results: make([]*HookResult, 0, len(defs)), } for _, def := range defs { result := m.executeOne(ctx, def, hookType, env) results.Results = append(results.Results, result) } ch <- results }() return ch } // ExecuteAll 执行指定类型的所有 hook(包括同步和异步的). // // 同步 hook 按顺序执行并等待结果; // 异步 hook 在后台执行,结果通过 asyncResults channel 返回. // // 返回值: // - syncResults: 同步 hook 的执行结果 // - asyncResults: 异步 hook 的结果 channel(可以 select 或忽略) func (m *Manager) ExecuteAll(ctx context.Context, hookType HookType, env map[string]string) (syncResults *ExecuteResults, asyncResults <-chan *ExecuteResults) { if m.disabled { emptyCh := make(chan *ExecuteResults, 1) emptyCh <- &ExecuteResults{HookType: hookType} close(emptyCh) return &ExecuteResults{HookType: hookType}, emptyCh } m.mu.RLock() defs := make([]HookDef, len(m.hooks[hookType])) copy(defs, m.hooks[hookType]) m.mu.RUnlock() // 分离同步和异步 hook var syncDefs, asyncDefs []HookDef for _, def := range defs { if def.Async { asyncDefs = append(asyncDefs, def) } else { syncDefs = append(syncDefs, def) } } // 执行同步 hook syncResults = &ExecuteResults{ HookType: hookType, Results: make([]*HookResult, 0, len(syncDefs)), } for _, def := range syncDefs { select { case <-ctx.Done(): syncResults.Results = append(syncResults.Results, &HookResult{ Command: def.Command, Error: ctx.Err(), ExitCode: -1, }) // 即使 context 取消,也启动异步部分 goto async default: } result := m.executeOne(ctx, def, hookType, env) syncResults.Results = append(syncResults.Results, result) } async: // 启动异步 hook asyncCh := make(chan *ExecuteResults, 1) if len(asyncDefs) == 0 { asyncCh <- &ExecuteResults{HookType: hookType} close(asyncCh) } else { go func() { defer close(asyncCh) bgCtx := context.Background() results := &ExecuteResults{ HookType: hookType, Results: make([]*HookResult, 0, len(asyncDefs)), } for _, def := range asyncDefs { result := m.executeOne(bgCtx, def, hookType, env) results.Results = append(results.Results, result) } asyncCh <- results }() } return syncResults, asyncCh } // UnregisterBySource 移除指定 hook 类型中来自特定来源的所有 hook. // // source 对应 HookDef.Source 字段(空字符串 = 全局,插件名 = 该插件的 hooks). // 若该类型下没有指定来源的 hook,静默返回(幂等操作). // // 用途: // - 禁用插件时,按类型精准移除该插件的 hooks,不影响全局 hooks // - 调试时临时移除某来源的特定类型 hooks // // 精妙之处(CLEVER): 过滤而非删除-- // 保留所有 Source != source 的 hooks,生成新 slice 替换旧 slice. // 若过滤后 slice 为空,用 delete 从 map 中彻底移除(节省内存,HasHooks 返回 false). // 替代方案:<遍历删除 index,原地修改> - 否决:slice 原地删除需要 copy 填坑,代码复杂且有 off-by-one 风险. func (m *Manager) UnregisterBySource(hookType HookType, source string) { m.mu.Lock() defer m.mu.Unlock() defs, ok := m.hooks[hookType] if !ok { return // 该类型没有任何 hook,直接返回 } // 保留所有非目标来源的 hooks kept := defs[:0:0] // 共享底层数组空间但 len=0,避免重新分配 for _, def := range defs { if def.Source != source { kept = append(kept, def) } } if len(kept) == 0 { delete(m.hooks, hookType) // 该类型已无 hooks,清理 map 条目 } else { m.hooks[hookType] = kept } } // UnregisterAllBySource 移除所有 hook 类型中来自特定来源的 hook. // // 典型用途:禁用插件时一次性清理该插件在所有 hook 类型上的注册. // // 升华改进(ELEVATED): 早期实现 的 clearRegisteredPluginHooks() // 清除所有插件 hooks 并保留 callback hooks--这是全量操作,无法精准到单个插件. // 我们按 Source 精准过滤,支持 N 个插件各自独立 disable,互不干扰. // 替代方案:<全量清除所有非全局 hooks 再重建> - 否决:插件 A disable 时不应影响插件 B 的 hooks. func (m *Manager) UnregisterAllBySource(source string) { m.mu.Lock() defer m.mu.Unlock() for hookType, defs := range m.hooks { kept := defs[:0:0] for _, def := range defs { if def.Source != source { kept = append(kept, def) } } if len(kept) == 0 { delete(m.hooks, hookType) } else { m.hooks[hookType] = kept } } } // Disable 全局禁用 hook 执行(测试用). func (m *Manager) Disable() { m.mu.Lock() defer m.mu.Unlock() m.disabled = true } // Enable 重新启用 hook 执行. func (m *Manager) Enable() { m.mu.Lock() defer m.mu.Unlock() m.disabled = false } // Count 返回指定类型已注册的 hook 数量. func (m *Manager) Count(hookType HookType) int { m.mu.RLock() defer m.mu.RUnlock() return len(m.hooks[hookType]) } // executeOne 执行单个 hook,根据 HookDef 选择后端. // 升华改进(ELEVATED): Handler > Command 优先级-- // SDK 用户注册 Go 回调时,即使 HookDef 同时有 Command 也用 Handler. // 替代方案:<原方案只有 m.exec.execute,只能走 shell> func (m *Manager) executeOne(ctx context.Context, def HookDef, hookType HookType, env map[string]string) *HookResult { // Go 回调优先 if def.Handler != nil { return def.Handler.ExecuteHook(ctx, hookType, env) } // Shell 命令 if def.Command != "" { return runShellHook(ctx, m.executor, def, env) } // 什么都没配--不应该到这里(Register 应该校验),兜底返回空结果 return &HookResult{ExitCode: 0} } // isValidHookType 验证 hook 类型是否合法. func isValidHookType(hookType HookType) bool { for _, t := range AllHookTypes() { if t == hookType { return true } } return false }