package flyto import ( "errors" "fmt" "strings" ) // errors.go - 结构化错误类型. // // 消费者可通过 errors.As 提取 *EngineError 获取错误码和恢复建议, // 无需解析错误字符串. // ErrorCode 是引擎错误码枚举. type ErrorCode string const ( ErrAPIAuth ErrorCode = "api_auth_error" ErrAPIRateLimit ErrorCode = "api_rate_limit" ErrAPIOverloaded ErrorCode = "api_overloaded" ErrAPIBadRequest ErrorCode = "api_bad_request" 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" ErrPluginLoad ErrorCode = "plugin_load_error" ErrInternal ErrorCode = "internal_error" ErrStreamTruncated ErrorCode = "stream_truncated" ) // EngineError 是引擎统一错误类型. type EngineError struct { Code ErrorCode Message string Detail string Suggestion string Cause error Retryable bool } func (e *EngineError) Error() string { if e.Message != "" { return e.Message } return string(e.Code) } func (e *EngineError) Unwrap() error { return e.Cause } var defaultSuggestions = map[ErrorCode]string{ ErrAPIAuth: "请检查 API Key 环境变量是否正确设置", ErrAPIRateLimit: "已达到 API 速率限制,将自动重试", ErrAPIOverloaded: "API 服务暂时过载,将自动重试", ErrAPIBadRequest: "请求格式错误,请检查输入参数", ErrToolNotFound: "请检查工具名称是否正确", ErrToolExecution: "工具执行失败,请检查输入参数和工作目录", ErrPermissionDenied: "权限被拒绝,可通过修改权限模式或添加规则来授权", ErrContextOverflow: "对话上下文已满,已自动压缩", ErrBudgetExceeded: "已达到预算上限", ErrMaxTurns: "已达到最大对话轮次限制", ErrSessionNotFound: "指定的会话不存在", ErrSessionClosed: "会话已关闭,请创建新的会话", ErrMCPConnection: "MCP 服务器连接失败,请检查服务器配置", ErrPluginLoad: "插件加载失败,请检查插件配置", ErrInternal: "发生内部错误,请重试", ErrStreamTruncated: "流式响应被中间代理截断,请检查网络代理配置", } var defaultRetryable = map[ErrorCode]bool{ ErrAPIRateLimit: true, ErrAPIOverloaded: true, ErrMCPConnection: true, ErrInternal: true, ErrStreamTruncated: true, } // NewEngineError 创建引擎错误(使用默认 Suggestion 和 Retryable). func NewEngineError(code ErrorCode, message string, cause error) *EngineError { return &EngineError{ Code: code, Message: message, Suggestion: defaultSuggestions[code], Cause: cause, Retryable: defaultRetryable[code], } } // WrapError 将普通错误包装为 EngineError. func WrapError(cause error, code ErrorCode, message string) *EngineError { if cause == nil { return NewEngineError(code, message, nil) } 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 检查错误是否可重试. func IsRetryable(err error) bool { if err == nil { return false } var e *EngineError if errors.As(err, &e) { return e.Retryable } s := err.Error() return strings.Contains(s, "HTTP 429") || strings.Contains(s, "HTTP 529") || strings.Contains(s, "connection reset") || strings.Contains(s, "timeout") } // FormatErrorForDisplay 格式化错误供用户阅读. func FormatErrorForDisplay(err error, verbose bool) string { var e *EngineError if !errors.As(err, &e) { return fmt.Sprintf("错误: %s", err.Error()) } var sb strings.Builder sb.WriteString(fmt.Sprintf("错误: %s", e.Message)) if e.Suggestion != "" { sb.WriteString(fmt.Sprintf("\n建议: %s", e.Suggestion)) } if verbose && e.Detail != "" { sb.WriteString(fmt.Sprintf("\n详情: %s", e.Detail)) } if e.Retryable { sb.WriteString("\n(此错误可自动重试)") } return sb.String() }