You've already forked agentic-coding-workflow
237 lines
8.9 KiB
Go
237 lines
8.9 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"
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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 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
|
|
default:
|
|
return http.StatusInternalServerError
|
|
}
|
|
}
|