Files
agentic-coding-workflow/internal/infra/errs/errs.go
T
2026-06-09 22:43:29 +08:00

264 lines
10 KiB
Go

// 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"
// ACP 域
CodeAcpAgentKindNotFound Code = "acp.agent_kind_not_found"
CodeAcpAgentKindDisabled Code = "acp.agent_kind_disabled"
CodeAcpAgentKindInUse Code = "acp.agent_kind_in_use"
CodeAcpSessionNotFound Code = "acp.session_not_found"
CodeAcpSessionQuotaExceeded Code = "acp.session_quota_exceeded"
CodeAcpSessionNotOwned Code = "acp.session_not_owned"
CodeAcpSessionTerminated Code = "acp.session_terminated"
CodeAcpSessionBranchConflict Code = "acp.session_branch_conflict"
CodeAcpSpawnFailed Code = "acp.spawn_failed"
CodeAcpHandshakeTimeout Code = "acp.handshake_timeout"
CodeAcpFsPathOutsideWorktree Code = "acp.fs_path_outside_worktree"
CodeAcpPermissionNotFound Code = "acp.permission_not_found"
CodeAcpPermissionAlreadyDecided Code = "acp.permission_already_decided"
CodeAcpPermissionInvalidOption Code = "acp.permission_invalid_option"
// MCP 域
CodeMcpTokenInvalid Code = "mcp.token_invalid"
CodeMcpTokenRevoked Code = "mcp.token_revoked"
CodeMcpTokenExpired Code = "mcp.token_expired"
CodeMcpTokenNotFound Code = "mcp.token_not_found"
CodeMcpTokenScopeViolation Code = "mcp.token_scope_violation"
CodeMcpTokenNameRequired Code = "mcp.token_name_required"
CodeMcpTokenExpiresRequired Code = "mcp.token_expires_required"
CodeMcpRateLimited Code = "mcp.rate_limited"
// Run 域(workspace run profiles)
CodeRunProfileNotFound Code = "run.profile_not_found"
CodeRunProfileSlugTaken Code = "run.profile_slug_taken"
CodeRunProfileAlreadyRunning Code = "run.profile_already_running"
CodeRunProfileNotRunning Code = "run.profile_not_running"
CodeRunProfileDisabled Code = "run.profile_disabled"
CodeRunSpawnFailed Code = "run.spawn_failed"
CodeRunCommandInvalid Code = "run.command_invalid"
)
// 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 "<message>: <cause>"; 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 CodeAcpAgentKindNotFound, CodeAcpSessionNotFound:
return http.StatusNotFound
case CodeAcpAgentKindDisabled, CodeAcpAgentKindInUse,
CodeAcpSessionTerminated, CodeAcpSessionBranchConflict:
return http.StatusConflict
case CodeAcpSessionQuotaExceeded:
return http.StatusTooManyRequests
case CodeAcpSessionNotOwned:
return http.StatusForbidden
case CodeAcpSpawnFailed, CodeAcpHandshakeTimeout:
return http.StatusBadGateway
case CodeAcpFsPathOutsideWorktree:
return http.StatusInternalServerError
case CodeAcpPermissionNotFound:
return http.StatusNotFound
case CodeAcpPermissionAlreadyDecided:
return http.StatusConflict
case CodeAcpPermissionInvalidOption:
return http.StatusBadRequest
case CodeWebhookNotConfigured:
return http.StatusServiceUnavailable
case CodeMcpTokenInvalid, CodeMcpTokenRevoked, CodeMcpTokenExpired:
return http.StatusUnauthorized
case CodeMcpTokenNotFound:
return http.StatusNotFound
case CodeMcpTokenScopeViolation:
return http.StatusForbidden
case CodeMcpTokenNameRequired, CodeMcpTokenExpiresRequired:
return http.StatusBadRequest
case CodeMcpRateLimited:
return http.StatusTooManyRequests
case CodeRunProfileNotFound:
return http.StatusNotFound
case CodeRunProfileSlugTaken, CodeRunProfileAlreadyRunning,
CodeRunProfileNotRunning, CodeRunProfileDisabled:
return http.StatusConflict
case CodeRunCommandInvalid:
return http.StatusBadRequest
case CodeRunSpawnFailed:
return http.StatusBadGateway
default:
return http.StatusInternalServerError
}
}