// Package errs defines the application's error type (AppError) and helpers // to map domain error codes to HTTP status codes. Service layers wrap // underlying errors with errs.Wrap / errs.New; transport layers use errs.As // + errs.HTTPStatus to translate them into Google API style responses // ({code, message, fields, request_id}). package errs import ( "errors" "fmt" "net/http" ) // Code is a stable, machine-readable identifier for a class of error. // Codes are part of the public API contract surfaced by HTTP responses. type Code string // Canonical error codes. Keep this list in sync with HTTPStatus and any // downstream consumers (e.g., HTTP transport, OpenAPI schemas). const ( CodeInvalidInput Code = "invalid_input" CodeUnauthorized Code = "unauthorized" CodeForbidden Code = "forbidden" CodeNotFound Code = "not_found" CodeConflict Code = "conflict" CodeRateLimited Code = "rate_limited" CodeUpstream Code = "upstream_error" CodeUnavailable Code = "unavailable" CodeInternal Code = "internal" // Project 模块专用细粒度 code(HTTP 仍 409,但前端可据此区分提示)。 CodeProjectSlugTaken Code = "project_slug_taken" CodeProjectArchived Code = "project_archived" CodeRequirementClosed Code = "requirement_closed" // Workspace 模块细粒度 code CodeWorkspaceSlugTaken Code = "workspace_slug_taken" CodeWorkspaceCloneFailed Code = "workspace_clone_failed" CodeWorkspaceSyncInProgress Code = "workspace_sync_in_progress" CodeWorkspaceRemoteInvalid Code = "workspace_remote_invalid" CodeWorktreeBranchConflict Code = "worktree_branch_conflict" CodeWorktreeAlreadyActive Code = "worktree_already_active" CodeWorktreeNotHeldByCaller Code = "worktree_not_held_by_caller" CodeWorktreePathInvalid Code = "worktree_path_invalid" CodeGitCredentialInvalid Code = "git_credential_invalid" CodeGitCmdFailed Code = "git_cmd_failed" // Chat 模块 CodeChatConversationNotFound Code = "chat.conversation_not_found" CodeChatMessageNotFound Code = "chat.message_not_found" CodeChatMessageNotPending Code = "chat.message_not_pending" CodeChatConcurrentMessage Code = "chat.concurrent_message" CodeChatAttachmentTooLarge Code = "chat.attachment_too_large" CodeChatAttachmentUnsupportedMime Code = "chat.attachment_unsupported_mime" CodeChatModelCapabilityMismatch Code = "chat.model_capability_mismatch" CodeChatTemplateNotFound Code = "chat.template_not_found" CodeChatEndpointNotFound Code = "chat.endpoint_not_found" CodeChatModelNotFound Code = "chat.model_not_found" // LLM 域 CodeLLMUpstream Code = "llm.upstream_error" CodeLLMTimeout Code = "llm.timeout" CodeLLMRateLimited Code = "llm.rate_limited" CodeLLMContextTooLong Code = "llm.context_too_long" // Storage 域 CodeStoragePutFailed Code = "storage.put_failed" CodeStorageGetFailed Code = "storage.get_failed" // Jobs 域 CodeJobNotFound Code = "job.not_found" CodeJobInvalidType Code = "job.invalid_type" CodeJobPayloadInvalid Code = "job.payload_invalid" CodeJobPayloadTooLarge Code = "job.payload_too_large" CodeWebhookNotConfigured Code = "webhook.not_configured" ) // AppError is the application's structured error type. It carries a stable // Code, a human-readable Message, an optional underlying Cause, and an // optional Fields map for per-field validation details. type AppError struct { Code Code Message string Cause error Fields map[string]any } // Error implements the error interface. When a Cause is present the format // is ": "; otherwise just the message is returned. func (e *AppError) Error() string { if e.Cause != nil { return fmt.Sprintf("%s: %s", e.Message, e.Cause.Error()) } return e.Message } // Unwrap exposes the underlying cause so errors.Is / errors.As can traverse // the chain. func (e *AppError) Unwrap() error { return e.Cause } // WithFields attaches per-field validation details and returns the receiver // for chaining. func (e *AppError) WithFields(f map[string]any) *AppError { e.Fields = f return e } // New constructs a new AppError with the given code and message. func New(code Code, msg string) *AppError { return &AppError{Code: code, Message: msg} } // Wrap wraps an existing error with an AppError carrying the given code and // message. The original error is preserved as Cause and remains discoverable // via errors.Is / errors.As. func Wrap(cause error, code Code, msg string) *AppError { return &AppError{Code: code, Message: msg, Cause: cause} } // As is a convenience wrapper around errors.As that extracts the first // *AppError in err's chain. It returns (nil, false) when err is nil or no // AppError is found. func As(err error) (*AppError, bool) { if err == nil { return nil, false } var ae *AppError if errors.As(err, &ae) { return ae, true } return nil, false } // HTTPStatus maps an error Code to its canonical HTTP status code. Unknown // codes default to 500 Internal Server Error. func HTTPStatus(code Code) int { switch code { case CodeInvalidInput: return http.StatusBadRequest case CodeUnauthorized: return http.StatusUnauthorized case CodeForbidden: return http.StatusForbidden case CodeNotFound: return http.StatusNotFound case CodeConflict, CodeProjectSlugTaken, CodeProjectArchived, CodeRequirementClosed, CodeWorkspaceSlugTaken, CodeWorkspaceSyncInProgress, CodeWorktreeBranchConflict, CodeWorktreeAlreadyActive: return http.StatusConflict case CodeRateLimited: return http.StatusTooManyRequests case CodeUpstream: return http.StatusBadGateway case CodeUnavailable: return http.StatusServiceUnavailable case CodeWorkspaceCloneFailed, CodeGitCmdFailed: return http.StatusBadGateway case CodeWorkspaceRemoteInvalid, CodeWorktreePathInvalid, CodeGitCredentialInvalid: return http.StatusBadRequest case CodeWorktreeNotHeldByCaller: return http.StatusForbidden case CodeChatConversationNotFound, CodeChatMessageNotFound, CodeChatTemplateNotFound, CodeChatEndpointNotFound, CodeChatModelNotFound: return http.StatusNotFound case CodeChatMessageNotPending, CodeChatConcurrentMessage, CodeChatModelCapabilityMismatch: return http.StatusConflict case CodeChatAttachmentTooLarge: return http.StatusRequestEntityTooLarge case CodeChatAttachmentUnsupportedMime: return http.StatusUnsupportedMediaType case CodeLLMRateLimited: return http.StatusTooManyRequests case CodeLLMTimeout: return http.StatusGatewayTimeout case CodeLLMUpstream, CodeStoragePutFailed, CodeStorageGetFailed: return http.StatusBadGateway case CodeLLMContextTooLong: return http.StatusBadRequest case CodeJobNotFound: return http.StatusNotFound case CodeJobInvalidType, CodeJobPayloadInvalid, CodeJobPayloadTooLarge: return http.StatusBadRequest case CodeWebhookNotConfigured: return http.StatusServiceUnavailable default: return http.StatusInternalServerError } }