package engine // 统一错误处理系统. // // 提供结构化的错误类型,每个错误都包含: // - 错误码:用于程序化处理 // - 人类可读消息:用于展示给用户 // - 技术细节:用于调试和日志 // - 恢复建议:帮助用户解决问题 // - 是否可重试:指导重试逻辑 // // 设计原则: // - 消费层不需要解析错误字符串来判断错误类型 // - 每种错误都有默认的恢复建议 // - 原始错误保留在 Cause 中,方便调试 import ( "errors" "fmt" "regexp" "strconv" "strings" ) // ErrorCode 是引擎错误码枚举. // 消费层可以根据错误码进行程序化处理. type ErrorCode string const ( ErrAPIAuth ErrorCode = "api_auth_error" // API key 无效或过期 ErrAPIRateLimit ErrorCode = "api_rate_limit" // API 速率限制 ErrAPIOverloaded ErrorCode = "api_overloaded" // API 服务过载 ErrAPIBadRequest ErrorCode = "api_bad_request" // API 请求格式错误 ErrContextTooLong ErrorCode = "context_too_long" // 超出模型上下文窗口(触发自适应校准) ErrToolNotFound ErrorCode = "tool_not_found" // 工具不存在 ErrToolExecution ErrorCode = "tool_execution_error" // 工具执行失败 ErrPermissionDenied ErrorCode = "permission_denied" // 权限被拒绝 ErrContextOverflow ErrorCode = "context_overflow" // 上下文溢出 ErrBudgetExceeded ErrorCode = "budget_exceeded" // 预算超限 ErrMaxTurns ErrorCode = "max_turns_reached" // 轮次超限 ErrSessionNotFound ErrorCode = "session_not_found" // 会话不存在 ErrSessionClosed ErrorCode = "session_closed" // 会话已关闭 ErrMCPConnection ErrorCode = "mcp_connection_error" // MCP 连接失败 ErrPluginLoad ErrorCode = "plugin_load_error" // 插件加载失败 ErrInternal ErrorCode = "internal_error" // 内部错误 ErrStreamTruncated ErrorCode = "stream_truncated" // 流式响应被代理截断(partial-stream) ) // EngineError 是引擎统一错误类型. // // 所有引擎内部产生的错误都应包装为此类型, // 消费层可以通过类型断言获取结构化的错误信息. type EngineError struct { // Code 是错误码,用于程序化处理 Code ErrorCode // Message 是人类可读的错误描述 Message string // Detail 是技术细节,用于调试和日志 Detail string // Suggestion 是恢复建议,帮助用户解决问题 Suggestion string // Cause 是导致此错误的原始错误 Cause error // Retryable 指示此错误是否可以通过重试解决 Retryable bool } // Error 实现 error 接口. func (e *EngineError) Error() string { if e.Message != "" { return e.Message } return string(e.Code) } // Unwrap 支持 errors.Is/As 链式解包. func (e *EngineError) Unwrap() error { return e.Cause } // defaultSuggestions 是每种错误码的默认恢复建议. var defaultSuggestions = map[ErrorCode]string{ ErrAPIAuth: "请检查 API Key(如 ANTHROPIC_API_KEY)或 Provider 配置是否正确", ErrAPIRateLimit: "已达到 API 速率限制,将自动重试", ErrAPIOverloaded: "API 服务暂时过载,将自动重试", ErrAPIBadRequest: "请求格式错误,请检查输入参数", ErrToolNotFound: "请检查工具名称是否正确,使用 /tools 查看可用工具列表", ErrToolExecution: "工具执行失败,请检查输入参数和工作目录", ErrPermissionDenied: "权限被拒绝。可以通过修改权限模式或添加规则来授权", ErrContextOverflow: "对话上下文已满,已自动压缩。如果问题持续,请使用 /compact 手动压缩", ErrBudgetExceeded: "已达到本次运行的预算上限。增加 --max-budget 参数或不设限制", ErrMaxTurns: "已达到最大对话轮次限制。增加 --max-turns 参数或不设限制", ErrSessionNotFound: "指定的会话不存在。使用 /sessions 查看可用会话", ErrSessionClosed: "会话已关闭,请创建新的会话", ErrMCPConnection: "MCP 服务器连接失败,请检查服务器配置和网络", ErrPluginLoad: "插件加载失败,请检查插件配置", ErrInternal: "发生内部错误,请重试。如果问题持续,请报告此问题", ErrStreamTruncated: "流式响应被中间代理截断,已重试仍然失败。检查网络代理配置或切换为直连", } // defaultRetryable 是每种错误码的默认可重试性. var defaultRetryable = map[ErrorCode]bool{ ErrAPIAuth: false, ErrAPIRateLimit: true, ErrAPIOverloaded: true, ErrAPIBadRequest: false, ErrToolNotFound: false, ErrToolExecution: false, ErrPermissionDenied: false, ErrContextOverflow: false, ErrBudgetExceeded: false, ErrMaxTurns: false, ErrSessionNotFound: false, ErrSessionClosed: false, ErrMCPConnection: true, ErrPluginLoad: false, ErrInternal: true, ErrStreamTruncated: true, // 瞬态代理故障,消费层可以选择重试整个 Run() } // NewEngineError 创建一个新的引擎错误. // // 使用默认的 Suggestion 和 Retryable 值. // 如果需要自定义,可以在创建后修改字段. func NewEngineError(code ErrorCode, message string, cause error) *EngineError { suggestion := defaultSuggestions[code] retryable := defaultRetryable[code] return &EngineError{ Code: code, Message: message, Suggestion: suggestion, Cause: cause, Retryable: retryable, } } // WrapError 将一个普通错误包装为 EngineError. // // 如果 cause 已经是 EngineError,则保留其信息并用新的 code 和 message 覆盖. // 否则创建新的 EngineError,原始错误保存在 Cause 中. func WrapError(cause error, code ErrorCode, message string) *EngineError { if cause == nil { return NewEngineError(code, message, nil) } // 如果已经是 EngineError,保留 Detail 等信息 var existing *EngineError if errors.As(cause, &existing) { return &EngineError{ Code: code, Message: message, Detail: existing.Detail, Suggestion: defaultSuggestions[code], Cause: cause, Retryable: defaultRetryable[code], } } return &EngineError{ Code: code, Message: message, Detail: cause.Error(), Suggestion: defaultSuggestions[code], Cause: cause, Retryable: defaultRetryable[code], } } // IsRetryable 检查错误是否可重试. // // 支持检查 EngineError 和普通 error. // 对于普通 error,通过错误字符串匹配判断. func IsRetryable(err error) bool { if err == nil { return false } // 检查 EngineError var engineErr *EngineError if errors.As(err, &engineErr) { return engineErr.Retryable } // 对于普通错误,通过字符串模式判断 errStr := err.Error() return strings.Contains(errStr, "HTTP 429") || strings.Contains(errStr, "HTTP 529") || strings.Contains(errStr, "connection reset") || strings.Contains(errStr, "connection refused") || strings.Contains(errStr, "timeout") } // ClassifyAPIError 将 API 错误字符串分类为对应的 ErrorCode. // // 根据 HTTP 状态码和错误消息内容判断错误类型. // 升华改进(ELEVATED): ErrContextTooLong 在 ErrAPIBadRequest 之前检测-- // 大多数 provider 以 HTTP 400 返回 context-too-long, // 若顺序颠倒,HTTP 400 会先命中,engine 会当成格式错误报错而非触发自适应校准. func ClassifyAPIError(errStr string) ErrorCode { lower := strings.ToLower(errStr) switch { case strings.Contains(errStr, "HTTP 401") || strings.Contains(lower, "authentication"): return ErrAPIAuth case strings.Contains(errStr, "HTTP 429"): return ErrAPIRateLimit case strings.Contains(errStr, "HTTP 529") || strings.Contains(lower, "overloaded"): return ErrAPIOverloaded case isContextTooLongMessage(lower): return ErrContextTooLong case strings.Contains(errStr, "HTTP 400") || strings.Contains(lower, "invalid"): return ErrAPIBadRequest default: return ErrInternal } } // isContextTooLongMessage 检测错误消息是否表示上下文超长. // // 精妙之处(CLEVER): 多模式宽泛匹配--不同 provider 和网关代理的错误格式各异: // // Anthropic: "prompt is too long: 210000 tokens > 200000 maximum" // OpenAI/兼容: "This model's maximum context length is 128000 tokens" // MiniMax: "prompt too long" 或 "too many tokens" // HTTP 网关: 413 Payload Too Large(上游截断) func isContextTooLongMessage(lower string) bool { return strings.Contains(lower, "prompt is too long") || strings.Contains(lower, "prompt too long") || strings.Contains(lower, "too many tokens") || strings.Contains(lower, "context length exceeded") || strings.Contains(lower, "maximum context length") || strings.Contains(lower, "context_length_exceeded") || strings.Contains(lower, "http 413") } // 上下文超长错误解析正则--从错误消息中提取 actual / max token 数. var ( // Anthropic 格式: "prompt is too long: 210000 tokens > 200000 maximum" ctxErrAnthropicRe = regexp.MustCompile(`(?i)prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)`) // OpenAI 格式: "maximum context length is 128000 tokens. However, ... resulted in 130000 tokens" ctxErrOpenAIRe = regexp.MustCompile(`(?i)maximum context length is\s+(\d+)\s*tokens.*resulted in\s+(\d+)\s*tokens`) ) // ParseContextError 从 context-too-long 错误消息中提取 (actual, max) token 数. // // actual = 本次请求实际 token 数(> max) // max = provider 申报的上下文窗口上限 // 无法解析时返回 (0, 0)--调用方应退化到使用静态默认值. func ParseContextError(errStr string) (actual, max int) { if m := ctxErrAnthropicRe.FindStringSubmatch(errStr); len(m) == 3 { actual, _ = strconv.Atoi(m[1]) max, _ = strconv.Atoi(m[2]) return actual, max } if m := ctxErrOpenAIRe.FindStringSubmatch(errStr); len(m) == 3 { max, _ = strconv.Atoi(m[1]) actual, _ = strconv.Atoi(m[2]) return actual, max } return 0, 0 } // FormatErrorForDisplay 将 EngineError 格式化为适合展示给用户的文本. // // 输出格式: // // 错误: <人类可读消息> // 建议: <恢复建议> // // 如果有 Detail,在 Verbose 模式下追加技术细节. func FormatErrorForDisplay(err error, verbose bool) string { var engineErr *EngineError if !errors.As(err, &engineErr) { return fmt.Sprintf("错误: %s", err.Error()) } var sb strings.Builder sb.WriteString(fmt.Sprintf("错误: %s", engineErr.Message)) if engineErr.Suggestion != "" { sb.WriteString(fmt.Sprintf("\n建议: %s", engineErr.Suggestion)) } if verbose && engineErr.Detail != "" { sb.WriteString(fmt.Sprintf("\n详情: %s", engineErr.Detail)) } if engineErr.Retryable { sb.WriteString("\n(此错误可自动重试)") } return sb.String() }