// Package hooks 的类型定义. // // 定义所有 12 种 hook 类型常量,执行结果结构,环境变量构建器. // Hook 系统的类型定义. package hooks import ( "time" ) // HookType 是 hook 类型枚举. // 原项目有 12+ 种 hook 类型,覆盖了工具调用,会话生命周期,权限等场景. type HookType string const ( // 工具相关的 hook HookPreToolUse HookType = "pre_tool_use" // 工具执行前触发 HookPostToolUse HookType = "post_tool_use" // 工具执行成功后触发 HookPostToolUseFailure HookType = "post_tool_use_failure" // 工具执行失败后触发 // 会话生命周期 hook HookSessionStart HookType = "session_start" // 会话开始时触发 HookSessionEnd HookType = "session_end" // 会话结束时触发 // 权限相关 hook HookPermission HookType = "permission_request" // 请求权限时触发(可自动批准) HookPermissionDenied HookType = "permission_denied" // 权限被拒绝时触发 // 控制流 hook HookStop HookType = "stop" // Agent 停止时触发 // 通知 hook HookNotification HookType = "notification" // 发送通知时触发 // 配置变更 hook HookConfigChange HookType = "config_change" // 配置变更时触发 // 任务相关 hook HookTaskCreated HookType = "task_created" // 子任务创建时触发 HookTaskCompleted HookType = "task_completed" // 子任务完成时触发 // 采样生命周期 hook(模块 9.2) // // 升华改进(ELEVATED): 早期设计只有 post-sampling,且仅为程序化 API(不暴露在 settings.json), // 用于 memory extraction / auto-dream 等内部副作用(post-sampling hook 返回 void). // 我们新增 pre-sampling,并将两者统一进 settings.json 可配置的 hook 体系. // // pre_sampling: API 调用前触发,同步执行,可阻止(exit 2 → 终止本轮,推送 WarningEvent). // 用途:请求级审计,配额控制,合规拦截,动态路由(SDK 模式 CallbackHandler 切换模型). // // post_sampling: API 响应后,工具执行前触发,异步 fire-and-forget,不影响控制流. // 用途:响应记录,内存提取,分析埋点. // // 精妙之处(CLEVER): pre 同步 / post 异步的非对称设计-- // pre 需要拦截能力(同步返回决策),post 只做副作用(不能阻塞用户等待 AI 响应). // 如果 post 也同步,一个慢 hook 脚本会让每轮 API 响应后都多等 N 秒. // 替代方案:两者都同步(统一简单)- 否决:post-sampling 主路径性能不可接受. HookPreSampling HookType = "pre_sampling" // API 调用前触发(同步,可阻止) HookPostSampling HookType = "post_sampling" // API 响应后触发(异步,fire-and-forget) ) // AllHookTypes 返回所有支持的 hook 类型列表. func AllHookTypes() []HookType { return []HookType{ HookPreToolUse, HookPostToolUse, HookPostToolUseFailure, HookSessionStart, HookSessionEnd, HookPermission, HookPermissionDenied, HookStop, HookNotification, HookConfigChange, HookTaskCreated, HookTaskCompleted, HookPreSampling, HookPostSampling, } } // Config 是 hook 系统的配置. // 对应原项目中 settings.json 的 hooks 字段. type Config struct { Hooks map[HookType][]HookDef `json:"hooks"` } // HookDef 是单个 hook 的定义. // 对应原项目中 settings.json 里每个 hook 条目. // // 升华改进(ELEVATED): 支持三种执行后端-- // - Command(shell):CLI 模式,JSON 配置文件使用 // - Handler(Go 回调):SDK 模式,Go 代码注册使用(json:"-" 不序列化) // - WebhookURL(HTTP):API 模式(待实现) // // 优先级:Handler > Command.如果两者都设置,Handler 胜出. // 替代方案:<早期方案只有 Command 一种,SDK 用户也得写 shell 脚本> type HookDef struct { Command string `json:"command,omitempty"` // Shell 命令(CLI 模式) Handler HookHandler `json:"-"` // Go 回调(SDK 模式,不序列化) WebhookURL string `json:"webhook_url,omitempty"` // HTTP 回调 URL(API 模式,待实现) Timeout int `json:"timeout,omitempty"` // 超时秒数,默认 30 Async bool `json:"async,omitempty"` // 是否异步执行(不阻塞主流程) // Source 标识 hook 的来源(模块 9.3). // // 升华改进(ELEVATED): 早期设计在 PluginHookMatcher 里用 pluginRoot/pluginId 区分来源, // 导致"全局 hook 注册表"和"插件 hook 注册表"必须分开维护,执行时三路合并(gh-29767). // 我们把来源信息内嵌进 HookDef,单一注册表即可精准移除特定来源的 hooks, // 不需要维护多个注册表或全量重建. // // 约定: // - ""(空字符串)= 全局 hook(来自 HooksConfig 或 Manager.Register 直接调用) // - 非空字符串 = 插件名(来自 plugin.Host,值为 plugin.Plugin.Name) // // 精妙之处(CLEVER): 空字符串 = 全局,向后兼容-- // 所有现有 HookDef{Command: "..."} 字面量 Source 自动为 ""(全局), // 零迁移成本.UnregisterBySource("") 等价于原来的 Unregister(移除所有全局 hooks). // // 扩展点(预留,未实现): // HTTP API / SaaS 多租户场景可用 "tenantID:pluginName" 前缀, // UnregisterAllBySource 按前缀匹配实现租户级隔离. // C# 移植对应字段:public string Source { get; init; } = "" Source string `json:"source,omitempty"` // 来源标识(空=全局,非空=插件名) // PluginDir 是插件的根目录(绝对路径). // 非空时,executor 将 FLYTO_PLUGIN_ROOT 注入到 hook 子进程环境变量, // 让 hook 脚本通过 $FLYTO_PLUGIN_ROOT/scripts/foo.py 引用插件内文件. // // 升华改进(ELEVATED): 早期设计通过 pluginRoot 参数在调用链中传递, // 需要修改所有 Build*Env 函数签名(破坏性变更). // 我们内嵌在 HookDef 里--注册时知道 pluginDir,执行时自然可用, // 无需修改 Build*Env 或 executor 的公共接口. // 替代方案:os.Setenv 全局设置(无法区分不同插件的路径). PluginDir string `json:"plugin_dir,omitempty"` // 插件根目录(注入 FLYTO_PLUGIN_ROOT) } // DefaultTimeout 是 hook 命令执行的默认超时时间(秒). const DefaultTimeout = 30 // EffectiveTimeout 返回 hook 定义的有效超时时间. // 如果未设置(0),使用默认值 30 秒. func (d HookDef) EffectiveTimeout() time.Duration { if d.Timeout > 0 { return time.Duration(d.Timeout) * time.Second } return time.Duration(DefaultTimeout) * time.Second } // HookResult 是单个 hook 命令的执行结果. type HookResult struct { // Command 是执行的原始命令 Command string `json:"command"` // Stdout 是标准输出内容 Stdout string `json:"stdout"` // Stderr 是标准错误输出内容 Stderr string `json:"stderr"` // ExitCode 是进程退出码(0 表示成功) ExitCode int `json:"exit_code"` // JSONOutput 是从 stdout 解析出的 JSON 对象(如果 stdout 是有效 JSON). // 原项目用这个机制让 hook 影响 Agent 行为, // 例如 permission_request hook 返回 {"decision": "allow"} 可以自动批准. JSONOutput map[string]any `json:"json_output,omitempty"` // Duration 是命令执行耗时 Duration time.Duration `json:"duration"` // Error 是执行过程中的错误(超时,找不到命令等) Error error `json:"error,omitempty"` } // Success 返回 hook 是否执行成功(退出码为 0 且没有错误). func (r *HookResult) Success() bool { return r.ExitCode == 0 && r.Error == nil } // ExecuteResults 是一批 hook 的执行结果. type ExecuteResults struct { HookType HookType `json:"hook_type"` Results []*HookResult `json:"results"` } // HasErrors 检查是否有任何 hook 执行失败. func (er *ExecuteResults) HasErrors() bool { for _, r := range er.Results { if !r.Success() { return true } } return false } // FirstError 返回第一个失败的 hook 结果(如果有). func (er *ExecuteResults) FirstError() *HookResult { for _, r := range er.Results { if !r.Success() { return r } } return nil } // FindJSONOutput 在所有结果中查找包含指定 key 的 JSON 输出. // 返回第一个匹配的值.用于提取 hook 的控制指令. func (er *ExecuteResults) FindJSONOutput(key string) (any, bool) { for _, r := range er.Results { if r.JSONOutput != nil { if v, ok := r.JSONOutput[key]; ok { return v, true } } } return nil, false }