import type { APIError } from '@anthropic-ai/sdk'// SSL/TLS error codes from OpenSSL (used by both Node.js and Bun)// See: https://www.openssl.org/docs/man3.1/man3/X509_STORE_CTX_get_error.htmlconst SSL_ERROR_CODES = new Set([// Certificate verification errors'UNABLE_TO_VERIFY_LEAF_SIGNATURE','UNABLE_TO_GET_ISSUER_CERT','UNABLE_TO_GET_ISSUER_CERT_LOCALLY','CERT_SIGNATURE_FAILURE','CERT_NOT_YET_VALID','CERT_HAS_EXPIRED','CERT_REVOKED','CERT_REJECTED','CERT_UNTRUSTED',// Self-signed certificate errors'DEPTH_ZERO_SELF_SIGNED_CERT','SELF_SIGNED_CERT_IN_CHAIN',// Chain errors'CERT_CHAIN_TOO_LONG','PATH_LENGTH_EXCEEDED',// Hostname/altname errors'ERR_TLS_CERT_ALTNAME_INVALID','HOSTNAME_MISMATCH',// TLS handshake errors'ERR_TLS_HANDSHAKE_TIMEOUT','ERR_SSL_WRONG_VERSION_NUMBER','ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',])export type ConnectionErrorDetails = {code: stringmessage: stringisSSLError: boolean}/*** Extracts connection error details from the error cause chain.* The Anthropic SDK wraps underlying errors in the `cause` property.* This function walks the cause chain to find the root error code/message.*/export function extractConnectionErrorDetails(error: unknown,): ConnectionErrorDetails | null {if (!error || typeof error !== 'object') {return null}// Walk the cause chain to find the root error with a codelet current: unknown = errorconst maxDepth = 5 // Prevent infinite loopslet depth = 0while (current && depth < maxDepth) {if (current instanceof Error &&'code' in current &&typeof current.code === 'string') {const code = current.codeconst isSSLError = SSL_ERROR_CODES.has(code)return {code,message: current.message,isSSLError,}}// Move to the next cause in the chainif (current instanceof Error &&'cause' in current &¤t.cause !== current) {current = current.causedepth++} else {break}}return null}/*** Returns an actionable hint for SSL/TLS errors, intended for contexts outside* the main API client (OAuth token exchange, preflight connectivity checks)* where `formatAPIError` doesn't apply.** Motivation: enterprise users behind TLS-intercepting proxies (Zscaler et al.)* see OAuth complete in-browser but the CLI's token exchange silently fails* with a raw SSL code. Surfacing the likely fix saves a support round-trip.*/export function getSSLErrorHint(error: unknown): string | null {const details = extractConnectionErrorDetails(error)if (!details?.isSSLError) {return null}return `SSL certificate error (${details.code}). If you are behind a corporate proxy or TLS-intercepting firewall, set NODE_EXTRA_CA_CERTS to your CA bundle path, or ask IT to allowlist *.anthropic.com. Run /doctor for details.`}/*** Strips HTML content (e.g., CloudFlare error pages) from a message string,* returning a user-friendly title or empty string if HTML is detected.* Returns the original message unchanged if no HTML is found.*/function sanitizeMessageHTML(message: string): string {if (message.includes('<!DOCTYPE html') || message.includes('<html')) {const titleMatch = message.match(/<title>([^<]+)<\/title>/)if (titleMatch && titleMatch[1]) {return titleMatch[1].trim()}return ''}return message}/*** Detects if an error message contains HTML content (e.g., CloudFlare error pages)* and returns a user-friendly message instead*/export function sanitizeAPIError(apiError: APIError): string {const message = apiError.messageif (!message) {// Sometimes message is undefined// TODO: figure out whyreturn ''}return sanitizeMessageHTML(message)}/*** Shapes of deserialized API errors from session JSONL.** After JSON round-tripping, the SDK's APIError loses its `.message` property.* The actual message lives at different nesting levels depending on the provider:** - Bedrock/proxy: `{ error: { message: "..." } }`* - Standard Anthropic API: `{ error: { error: { message: "..." } } }`* (the outer `.error` is the response body, the inner `.error` is the API error)** See also: `getErrorMessage` in `logging.ts` which handles the same shapes.*/type NestedAPIError = {error?: {message?: stringerror?: { message?: string }}}function hasNestedError(value: unknown): value is NestedAPIError {return (typeof value === 'object' &&value !== null &&'error' in value &&typeof value.error === 'object' &&value.error !== null)}/*** Extract a human-readable message from a deserialized API error that lacks* a top-level `.message`.** Checks two nesting levels (deeper first for specificity):* 1. `error.error.error.message` — standard Anthropic API shape* 2. `error.error.message` — Bedrock shape*/function extractNestedErrorMessage(error: APIError): string | null {if (!hasNestedError(error)) {return null}// Access `.error` via the narrowed type so TypeScript sees the nested shape// instead of the SDK's `Object | undefined`.const narrowed: NestedAPIError = errorconst nested = narrowed.error// Standard Anthropic API shape: { error: { error: { message } } }const deepMsg = nested?.error?.messageif (typeof deepMsg === 'string' && deepMsg.length > 0) {const sanitized = sanitizeMessageHTML(deepMsg)if (sanitized.length > 0) {return sanitized}}// Bedrock shape: { error: { message } }const msg = nested?.messageif (typeof msg === 'string' && msg.length > 0) {const sanitized = sanitizeMessageHTML(msg)if (sanitized.length > 0) {return sanitized}}return null}export function formatAPIError(error: APIError): string {// Extract connection error details from the cause chainconst connectionDetails = extractConnectionErrorDetails(error)if (connectionDetails) {const { code, isSSLError } = connectionDetails// Handle timeout errorsif (code === 'ETIMEDOUT') {return 'Request timed out. Check your internet connection and proxy settings'}// Handle SSL/TLS errors with specific messagesif (isSSLError) {switch (code) {case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':case 'UNABLE_TO_GET_ISSUER_CERT':case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':return 'Unable to connect to API: SSL certificate verification failed. Check your proxy or corporate SSL certificates'case 'CERT_HAS_EXPIRED':return 'Unable to connect to API: SSL certificate has expired'case 'CERT_REVOKED':return 'Unable to connect to API: SSL certificate has been revoked'case 'DEPTH_ZERO_SELF_SIGNED_CERT':case 'SELF_SIGNED_CERT_IN_CHAIN':return 'Unable to connect to API: Self-signed certificate detected. Check your proxy or corporate SSL certificates'case 'ERR_TLS_CERT_ALTNAME_INVALID':case 'HOSTNAME_MISMATCH':return 'Unable to connect to API: SSL certificate hostname mismatch'case 'CERT_NOT_YET_VALID':return 'Unable to connect to API: SSL certificate is not yet valid'default:return `Unable to connect to API: SSL error (${code})`}}}if (error.message === 'Connection error.') {// If we have a code but it's not SSL, include it for debuggingif (connectionDetails?.code) {return `Unable to connect to API (${connectionDetails.code})`}return 'Unable to connect to API. Check your internet connection'}// Guard: when deserialized from JSONL (e.g. --resume), the error object may// be a plain object without a `.message` property. Return a safe fallback// instead of undefined, which would crash callers that access `.length`.if (!error.message) {return (extractNestedErrorMessage(error) ??`API error (status ${error.status ?? 'unknown'})`)}const sanitizedMessage = sanitizeAPIError(error)// Use sanitized message if it's different from the original (i.e., HTML was sanitized)return sanitizedMessage !== error.message && sanitizedMessage.length > 0? sanitizedMessage: error.message}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。