Просмотр исходного кода

feat(error-rules): add error override feature

Add ability to override error responses and status codes when error rules match:
- New DB fields: override_response (JSONB) and override_status_code
- Runtime override logic in error-handler with validation (400-599 range)
- New validator for Claude API error format with 10KB size limit
- Frontend: override section component with real-time JSON validation
- Frontend: error rule tester to preview override behavior
- Support empty message fallback to original error
- Backward compatible: no behavior change without override config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
sususu98 2 месяцев назад
Родитель
Сommit
3fb8a81f88

+ 2 - 0
drizzle/0021_broad_black_panther.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "error_rules" ADD COLUMN "override_response" jsonb;--> statement-breakpoint
+ALTER TABLE "error_rules" ADD COLUMN "override_status_code" integer;

+ 1602 - 0
drizzle/meta/0021_snapshot.json

@@ -0,0 +1,1602 @@
+{
+  "id": "c1a2ce3e-8996-44e3-87ca-e64437d86004",
+  "prevId": "e423d87a-7e70-4a76-b7ad-4011efd95f2a",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.error_rules": {
+      "name": "error_rules",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "pattern": {
+          "name": "pattern",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'regex'"
+        },
+        "category": {
+          "name": "category",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_response": {
+          "name": "override_response",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "override_status_code": {
+          "name": "override_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_error_rules_enabled": {
+          "name": "idx_error_rules_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "unique_pattern": {
+          "name": "unique_pattern",
+          "columns": [
+            {
+              "expression": "pattern",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_category": {
+          "name": "idx_category",
+          "columns": [
+            {
+              "expression": "category",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_match_type": {
+          "name": "idx_match_type",
+          "columns": [
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "can_login_web_ui": {
+          "name": "can_login_web_ui",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_keys_user_id": {
+          "name": "idx_keys_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_created_at": {
+          "name": "idx_keys_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_deleted_at": {
+          "name": "idx_keys_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(21, 15)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_chain": {
+          "name": "provider_chain",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "api_type": {
+          "name": "api_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "endpoint": {
+          "name": "endpoint",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "original_model": {
+          "name": "original_model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_reason": {
+          "name": "blocked_reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "messages_count": {
+          "name": "messages_count",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_message_request_user_date_cost": {
+          "name": "idx_message_request_user_date_cost",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_query": {
+          "name": "idx_message_request_user_query",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_id": {
+          "name": "idx_message_request_session_id",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_endpoint": {
+          "name": "idx_message_request_endpoint",
+          "columns": [
+            {
+              "expression": "endpoint",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_id": {
+          "name": "idx_message_request_provider_id",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_id": {
+          "name": "idx_message_request_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key": {
+          "name": "idx_message_request_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at": {
+          "name": "idx_message_request_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_deleted_at": {
+          "name": "idx_message_request_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_model_prices_latest": {
+          "name": "idx_model_prices_latest",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_model_name": {
+          "name": "idx_model_prices_model_name",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_created_at": {
+          "name": "idx_model_prices_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_settings": {
+      "name": "notification_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_enabled": {
+          "name": "circuit_breaker_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_webhook": {
+          "name": "circuit_breaker_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_enabled": {
+          "name": "daily_leaderboard_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "daily_leaderboard_webhook": {
+          "name": "daily_leaderboard_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_time": {
+          "name": "daily_leaderboard_time",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'09:00'"
+        },
+        "daily_leaderboard_top_n": {
+          "name": "daily_leaderboard_top_n",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "cost_alert_enabled": {
+          "name": "cost_alert_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "cost_alert_webhook": {
+          "name": "cost_alert_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_alert_threshold": {
+          "name": "cost_alert_threshold",
+          "type": "numeric(5, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.80'"
+        },
+        "cost_alert_check_interval": {
+          "name": "cost_alert_check_interval",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'1.0'"
+        },
+        "group_tag": {
+          "name": "group_tag",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "model_redirects": {
+          "name": "model_redirects",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "codex_instructions_strategy": {
+          "name": "codex_instructions_strategy",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'auto'"
+        },
+        "mcp_passthrough_type": {
+          "name": "mcp_passthrough_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'none'"
+        },
+        "mcp_passthrough_url": {
+          "name": "mcp_passthrough_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "circuit_breaker_failure_threshold": {
+          "name": "circuit_breaker_failure_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "circuit_breaker_open_duration": {
+          "name": "circuit_breaker_open_duration",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1800000
+        },
+        "circuit_breaker_half_open_success_threshold": {
+          "name": "circuit_breaker_half_open_success_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 2
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "first_byte_timeout_streaming_ms": {
+          "name": "first_byte_timeout_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "streaming_idle_timeout_ms": {
+          "name": "streaming_idle_timeout_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "request_timeout_non_streaming_ms": {
+          "name": "request_timeout_non_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_providers_enabled_priority": {
+          "name": "idx_providers_enabled_priority",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "weight",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_group": {
+          "name": "idx_providers_group",
+          "columns": [
+            {
+              "expression": "group_tag",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_created_at": {
+          "name": "idx_providers_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_deleted_at": {
+          "name": "idx_providers_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sensitive_words": {
+      "name": "sensitive_words",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "word": {
+          "name": "word",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'contains'"
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_sensitive_words_enabled": {
+          "name": "idx_sensitive_words_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_sensitive_words_created_at": {
+          "name": "idx_sensitive_words_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.system_settings": {
+      "name": "system_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "site_title": {
+          "name": "site_title",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'Claude Code Hub'"
+        },
+        "allow_global_usage_view": {
+          "name": "allow_global_usage_view",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "currency_display": {
+          "name": "currency_display",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'USD'"
+        },
+        "billing_model_source": {
+          "name": "billing_model_source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'original'"
+        },
+        "enable_auto_cleanup": {
+          "name": "enable_auto_cleanup",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "cleanup_retention_days": {
+          "name": "cleanup_retention_days",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 30
+        },
+        "cleanup_schedule": {
+          "name": "cleanup_schedule",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0 2 * * *'"
+        },
+        "cleanup_batch_size": {
+          "name": "cleanup_batch_size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10000
+        },
+        "enable_client_version_check": {
+          "name": "enable_client_version_check",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_users_active_role_sort": {
+          "name": "idx_users_active_role_sort",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "role",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_created_at": {
+          "name": "idx_users_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_deleted_at": {
+          "name": "idx_users_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {
+    "public.daily_reset_mode": {
+      "name": "daily_reset_mode",
+      "schema": "public",
+      "values": [
+        "fixed",
+        "rolling"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -148,6 +148,13 @@
       "when": 1764210000000,
       "tag": "0020_glossy_grandmaster",
       "breakpoints": true
+    },
+    {
+      "idx": 21,
+      "version": "7",
+      "when": 1764223312347,
+      "tag": "0021_broad_black_panther",
+      "breakpoints": true
     }
   ]
 }

+ 34 - 0
messages/en/settings.json

@@ -1510,6 +1510,27 @@
     "section": {
       "title": "Error Rules List"
     },
+    "tester": {
+      "title": "Error Rule Tester",
+      "description": "Input an error message to check if it matches configured rules and see the final response.",
+      "inputLabel": "Test Error Message",
+      "inputPlaceholder": "Enter an error message to test...",
+      "testButton": "Run Test",
+      "testing": "Testing...",
+      "matched": "Matched an error rule",
+      "notMatched": "No rule matched",
+      "finalResponse": "Override response to return",
+      "ruleInfo": "Matched rule",
+      "noRule": "No rule matched",
+      "category": "Category",
+      "pattern": "Pattern",
+      "matchType": "Match type",
+      "overrideStatusCode": "Override status code",
+      "testFailed": "Test failed, please try again",
+      "messageRequired": "Please enter an error message to test",
+      "warnings": "Configuration Warnings",
+      "statusCodeOnlyOverride": "Only status code will be overridden, response body will use upstream error"
+    },
     "add": "Add Error Rule",
     "addSuccess": "Error rule created successfully",
     "addFailed": "Failed to create error rule",
@@ -1553,6 +1574,19 @@
       "invalidPattern": "Invalid Regex",
       "matchedText": "Matched Text",
       "defaultRuleHint": "Default rule pattern cannot be modified",
+      "enableOverride": "Enable Error Override",
+      "enableOverrideHint": "When enabled, you can customize the error response and status code returned to clients. Original errors are still logged to the database. Currently only supports Claude API error format.",
+      "overrideResponseLabel": "Override Response (JSON)",
+      "overrideResponsePlaceholder": "{\n  \"type\": \"error\",\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"Your custom message\"\n  }\n}",
+      "overrideResponseHint": "Leave empty to only override status code.",
+      "overrideStatusCodeLabel": "Override Status Code (Optional)",
+      "overrideStatusCodePlaceholder": "e.g. 400",
+      "overrideStatusCodeHint": "Leave empty to use upstream status code. Range: 400-599.",
+      "useTemplate": "Claude Error Template",
+      "useTemplateConfirm": "Existing content will be replaced by the template. Continue?",
+      "validJson": "JSON format is valid",
+      "invalidJson": "Invalid JSON format",
+      "invalidStatusCode": "Status code must be between 400-599",
       "creating": "Creating...",
       "saving": "Saving..."
     },

+ 34 - 0
messages/ja/settings.json

@@ -1462,6 +1462,27 @@
     "section": {
       "title": "エラールールリスト"
     },
+    "tester": {
+      "title": "エラールールテスト",
+      "description": "エラーメッセージを入力して、設定済みルールに一致するかと最終的な返却内容を確認します。",
+      "inputLabel": "テストするエラーメッセージ",
+      "inputPlaceholder": "検証したいエラーメッセージを入力...",
+      "testButton": "テストを実行",
+      "testing": "テスト中...",
+      "matched": "エラールールに一致しました",
+      "notMatched": "一致するルールなし",
+      "finalResponse": "オーバーライドレスポンス",
+      "ruleInfo": "一致したルール",
+      "noRule": "一致したルールはありません",
+      "category": "カテゴリ",
+      "pattern": "パターン",
+      "matchType": "マッチタイプ",
+      "overrideStatusCode": "オーバーライドステータスコード",
+      "testFailed": "テストに失敗しました。再度お試しください",
+      "messageRequired": "テストするエラーメッセージを入力してください",
+      "warnings": "設定の警告",
+      "statusCodeOnlyOverride": "ステータスコードのみオーバーライドされ、レスポンスボディはアップストリームのエラーが使用されます"
+    },
     "add": "エラールールを追加",
     "addSuccess": "エラールールが正常に作成されました",
     "addFailed": "エラールールの作成に失敗しました",
@@ -1505,6 +1526,19 @@
       "invalidPattern": "無効な正規表現",
       "matchedText": "マッチしたテキスト",
       "defaultRuleHint": "デフォルトルールのパターンは変更できません",
+      "enableOverride": "エラーオーバーライドを有効にする",
+      "enableOverrideHint": "有効にすると、クライアントに返すエラーレスポンスとステータスコードをカスタマイズできます。元のエラーはデータベースに記録されます。現在、Claude APIエラー形式のみサポートしています。",
+      "overrideResponseLabel": "オーバーライドレスポンス(JSON形式)",
+      "overrideResponsePlaceholder": "{\n  \"type\": \"error\",\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"カスタムメッセージ\"\n  }\n}",
+      "overrideResponseHint": "空白のままにするとステータスコードのみオーバーライドします。",
+      "overrideStatusCodeLabel": "オーバーライドステータスコード(オプション)",
+      "overrideStatusCodePlaceholder": "例: 400",
+      "overrideStatusCodeHint": "空白のままにするとアップストリームのステータスコードを使用します。範囲: 400-599。",
+      "useTemplate": "Claude Error テンプレート",
+      "useTemplateConfirm": "入力済みの内容をテンプレートで上書きしますか?",
+      "validJson": "JSON 形式は有効です",
+      "invalidJson": "JSON形式が無効です",
+      "invalidStatusCode": "ステータスコードは400-599の範囲内でなければなりません",
       "creating": "作成中...",
       "saving": "保存中..."
     },

+ 34 - 0
messages/ru/settings.json

@@ -1462,6 +1462,27 @@
     "section": {
       "title": "Список правил ошибок"
     },
+    "tester": {
+      "title": "Тестирование правил ошибок",
+      "description": "Введите сообщение об ошибке, чтобы проверить совпадение с настроенными правилами и увидеть итоговый ответ.",
+      "inputLabel": "Тестовое сообщение об ошибке",
+      "inputPlaceholder": "Введите сообщение об ошибке для проверки...",
+      "testButton": "Запустить тест",
+      "testing": "Тестирование...",
+      "matched": "Совпало с правилом ошибки",
+      "notMatched": "Правила не совпали",
+      "finalResponse": "Ответ замены",
+      "ruleInfo": "Совпавшее правило",
+      "noRule": "Совпавших правил нет",
+      "category": "Категория",
+      "pattern": "Шаблон",
+      "matchType": "Тип совпадения",
+      "overrideStatusCode": "Код статуса замены",
+      "testFailed": "Тест не удался, попробуйте позже",
+      "messageRequired": "Введите сообщение об ошибке для проверки",
+      "warnings": "Предупреждения конфигурации",
+      "statusCodeOnlyOverride": "Заменяется только код статуса, тело ответа будет использовать исходную ошибку"
+    },
     "add": "Добавить правило ошибки",
     "addSuccess": "Правило ошибки успешно создано",
     "addFailed": "Не удалось создать правило ошибки",
@@ -1505,6 +1526,19 @@
       "invalidPattern": "Недействительное регулярное выражение",
       "matchedText": "Совпавший текст",
       "defaultRuleHint": "Шаблон правила по умолчанию не может быть изменен",
+      "enableOverride": "Включить переопределение ошибки",
+      "enableOverrideHint": "При включении вы можете настроить ответ об ошибке и код статуса, возвращаемые клиентам. Исходные ошибки по-прежнему записываются в базу данных. В настоящее время поддерживается только формат ошибок Claude API.",
+      "overrideResponseLabel": "Ответ замены (JSON)",
+      "overrideResponsePlaceholder": "{\n  \"type\": \"error\",\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"Ваше пользовательское сообщение\"\n  }\n}",
+      "overrideResponseHint": "Оставьте пустым для переопределения только кода статуса.",
+      "overrideStatusCodeLabel": "Код статуса замены (Необязательно)",
+      "overrideStatusCodePlaceholder": "например, 400",
+      "overrideStatusCodeHint": "Оставьте пустым для использования кода статуса upstream. Диапазон: 400-599.",
+      "useTemplate": "Шаблон Claude Error",
+      "useTemplateConfirm": "Существующее содержимое будет заменено шаблоном. Продолжить?",
+      "validJson": "Формат JSON корректен",
+      "invalidJson": "Неверный формат JSON",
+      "invalidStatusCode": "Код статуса должен быть между 400-599",
       "creating": "Создание...",
       "saving": "Сохранение..."
     },

+ 34 - 0
messages/zh-CN/settings.json

@@ -1529,6 +1529,27 @@
     "section": {
       "title": "错误规则列表"
     },
+    "tester": {
+      "title": "错误规则测试",
+      "description": "输入错误消息,检查是否命中已配置的规则以及最终返回给用户的内容。",
+      "inputLabel": "测试错误消息",
+      "inputPlaceholder": "输入要检测的错误消息...",
+      "testButton": "开始测试",
+      "testing": "测试中...",
+      "matched": "已命中错误规则",
+      "notMatched": "未命中任何规则",
+      "finalResponse": "覆写响应",
+      "ruleInfo": "匹配的规则",
+      "noRule": "未匹配到任何规则",
+      "category": "规则类别",
+      "pattern": "匹配模式",
+      "matchType": "匹配类型",
+      "overrideStatusCode": "覆写状态码",
+      "testFailed": "测试失败,请稍后重试",
+      "messageRequired": "请输入要测试的错误消息",
+      "warnings": "配置警告",
+      "statusCodeOnlyOverride": "仅覆写状态码,响应体将使用上游错误消息"
+    },
     "add": "添加错误规则",
     "addSuccess": "错误规则创建成功",
     "addFailed": "创建错误规则失败",
@@ -1572,6 +1593,19 @@
       "invalidPattern": "正则无效",
       "matchedText": "匹配内容",
       "defaultRuleHint": "默认规则的模式不可修改",
+      "enableOverride": "启用错误覆写",
+      "enableOverrideHint": "启用后可自定义返回给客户端的错误响应和状态码,原始错误仍会记录到数据库。当前仅支持 Claude API 错误格式。",
+      "overrideResponseLabel": "覆写响应(JSON 格式)",
+      "overrideResponsePlaceholder": "{\n  \"type\": \"error\",\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"您的自定义消息\"\n  }\n}",
+      "overrideResponseHint": "留空则仅覆写状态码。",
+      "overrideStatusCodeLabel": "覆写状态码(可选)",
+      "overrideStatusCodePlaceholder": "例如 400",
+      "overrideStatusCodeHint": "留空则使用上游状态码。范围:400-599。",
+      "useTemplate": "Claude Error 模板",
+      "useTemplateConfirm": "输入框已有内容,确定覆盖为模板示例?",
+      "validJson": "JSON 格式正确",
+      "invalidJson": "JSON 格式无效",
+      "invalidStatusCode": "状态码必须在 400-599 范围内",
       "creating": "创建中...",
       "saving": "保存中..."
     },

+ 34 - 0
messages/zh-TW/settings.json

@@ -1468,6 +1468,27 @@
     "section": {
       "title": "錯誤規則列表"
     },
+    "tester": {
+      "title": "錯誤規則測試",
+      "description": "輸入錯誤訊息,檢查是否命中已設定的規則以及最終返回的內容。",
+      "inputLabel": "測試錯誤訊息",
+      "inputPlaceholder": "輸入要檢測的錯誤訊息...",
+      "testButton": "開始測試",
+      "testing": "測試中...",
+      "matched": "已命中錯誤規則",
+      "notMatched": "未命中任何規則",
+      "finalResponse": "覆寫回應",
+      "ruleInfo": "匹配的規則",
+      "noRule": "沒有匹配到任何規則",
+      "category": "規則類別",
+      "pattern": "匹配模式",
+      "matchType": "匹配類型",
+      "overrideStatusCode": "覆寫狀態碼",
+      "testFailed": "測試失敗,請稍後重試",
+      "messageRequired": "請輸入要測試的錯誤訊息",
+      "warnings": "設定警告",
+      "statusCodeOnlyOverride": "僅覆寫狀態碼,回應內容將使用上游錯誤訊息"
+    },
     "add": "新增錯誤規則",
     "addSuccess": "錯誤規則建立成功",
     "addFailed": "建立錯誤規則失敗",
@@ -1511,6 +1532,19 @@
       "invalidPattern": "無效的正則表達式",
       "matchedText": "符合內容",
       "defaultRuleHint": "預設規則的模式無法修改",
+      "enableOverride": "啟用錯誤覆寫",
+      "enableOverrideHint": "啟用後可自訂返回給客戶端的錯誤回應和狀態碼,原始錯誤仍會記錄到資料庫。當前僅支援 Claude API 錯誤格式。",
+      "overrideResponseLabel": "覆寫回應(JSON 格式)",
+      "overrideResponsePlaceholder": "{\n  \"type\": \"error\",\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"您的自訂訊息\"\n  }\n}",
+      "overrideResponseHint": "留空則僅覆寫狀態碼。",
+      "overrideStatusCodeLabel": "覆寫狀態碼(選填)",
+      "overrideStatusCodePlaceholder": "例如 400",
+      "overrideStatusCodeHint": "留空則使用上游狀態碼。範圍:400-599。",
+      "useTemplate": "Claude Error 範本",
+      "useTemplateConfirm": "輸入框已有內容,確定以範本覆蓋?",
+      "validJson": "JSON 格式正確",
+      "invalidJson": "JSON 格式無效",
+      "invalidStatusCode": "狀態碼必須在 400-599 範圍內",
       "creating": "建立中...",
       "saving": "儲存中..."
     },

+ 234 - 22
src/actions/error-rules.ts

@@ -3,12 +3,39 @@
 import { revalidatePath } from "next/cache";
 import * as repo from "@/repository/error-rules";
 import { errorRuleDetector } from "@/lib/error-rule-detector";
-import { eventEmitter } from "@/lib/event-emitter";
 import { logger } from "@/lib/logger";
 import { getSession } from "@/lib/auth";
+import { validateErrorOverrideResponse } from "@/lib/error-override-validator";
 import safeRegex from "safe-regex";
 import type { ActionResult } from "./types";
 
+/** 覆写状态码最小值 */
+const OVERRIDE_STATUS_CODE_MIN = 400;
+/** 覆写状态码最大值 */
+const OVERRIDE_STATUS_CODE_MAX = 599;
+
+/**
+ * 验证覆写状态码范围
+ *
+ * @param statusCode - 要验证的状态码
+ * @returns 错误消息(如果验证失败)或 null(验证通过)
+ */
+function validateOverrideStatusCodeRange(statusCode: number | null | undefined): string | null {
+  if (statusCode === null || statusCode === undefined) {
+    return null;
+  }
+
+  if (
+    !Number.isInteger(statusCode) ||
+    statusCode < OVERRIDE_STATUS_CODE_MIN ||
+    statusCode > OVERRIDE_STATUS_CODE_MAX
+  ) {
+    return `覆写状态码必须是 ${OVERRIDE_STATUS_CODE_MIN}-${OVERRIDE_STATUS_CODE_MAX} 范围内的整数`;
+  }
+
+  return null;
+}
+
 /**
  * 获取所有错误规则列表
  */
@@ -33,15 +60,19 @@ export async function listErrorRules(): Promise<repo.ErrorRule[]> {
 export async function createErrorRuleAction(data: {
   pattern: string;
   category:
-    | "prompt_limit"
-    | "content_filter"
-    | "pdf_limit"
-    | "thinking_error"
-    | "parameter_error"
-    | "invalid_request"
-    | "cache_limit";
+  | "prompt_limit"
+  | "content_filter"
+  | "pdf_limit"
+  | "thinking_error"
+  | "parameter_error"
+  | "invalid_request"
+  | "cache_limit";
   matchType?: "contains" | "exact" | "regex";
   description?: string;
+  /** 覆写响应体(JSON 格式,符合 Claude API 错误格式) */
+  overrideResponse?: repo.ErrorOverrideResponse | null;
+  /** 覆写状态码:null 表示透传上游状态码 */
+  overrideStatusCode?: number | null;
 }): Promise<ActionResult<repo.ErrorRule>> {
   try {
     const session = await getSession();
@@ -115,19 +146,38 @@ export async function createErrorRuleAction(data: {
       }
     }
 
+    // 验证覆写响应体格式
+    if (data.overrideResponse) {
+      const validationError = validateErrorOverrideResponse(data.overrideResponse);
+      if (validationError) {
+        return {
+          ok: false,
+          error: validationError,
+        };
+      }
+    }
+
+    // 验证覆写状态码范围
+    const statusCodeError = validateOverrideStatusCodeRange(data.overrideStatusCode);
+    if (statusCodeError) {
+      return {
+        ok: false,
+        error: statusCodeError,
+      };
+    }
+
     const result = await repo.createErrorRule({
       pattern: data.pattern,
       category: data.category,
       matchType,
       description: data.description,
+      overrideResponse: data.overrideResponse ?? null,
+      overrideStatusCode: data.overrideStatusCode ?? null,
     });
 
-    // 刷新缓存
+    // 刷新缓存(直接调用 reload,不再触发事件避免重复刷新)
     await errorRuleDetector.reload();
 
-    // 触发事件
-    eventEmitter.emit("errorRulesUpdated");
-
     revalidatePath("/settings/error-rules");
 
     logger.info("[ErrorRulesAction] Created error rule", {
@@ -160,6 +210,10 @@ export async function updateErrorRuleAction(
     category: string;
     matchType: "regex" | "contains" | "exact";
     description: string;
+    /** 覆写响应体(JSON 格式),设为 null 可清除 */
+    overrideResponse: repo.ErrorOverrideResponse | null;
+    /** 覆写状态码:null 表示透传上游状态码 */
+    overrideStatusCode: number | null;
     isEnabled: boolean;
     priority: number;
   }>
@@ -210,7 +264,30 @@ export async function updateErrorRuleAction(
       }
     }
 
-    const result = await repo.updateErrorRule(id, updates);
+    // 验证覆写响应体格式
+    if (updates.overrideResponse !== undefined && updates.overrideResponse !== null) {
+      const validationError = validateErrorOverrideResponse(updates.overrideResponse);
+      if (validationError) {
+        return {
+          ok: false,
+          error: validationError,
+        };
+      }
+    }
+
+    // 验证覆写状态码范围
+    const statusCodeError = validateOverrideStatusCodeRange(updates.overrideStatusCode);
+    if (statusCodeError) {
+      return {
+        ok: false,
+        error: statusCodeError,
+      };
+    }
+
+    // 直接使用 updates,不做额外处理
+    const processedUpdates = updates;
+
+    const result = await repo.updateErrorRule(id, processedUpdates);
 
     // 注意:result 为 null 的情况已在上方 getErrorRuleById 检查时处理
     // 这里保留检查作为防御性编程,应对并发删除场景
@@ -221,12 +298,9 @@ export async function updateErrorRuleAction(
       };
     }
 
-    // 刷新缓存
+    // 刷新缓存(直接调用 reload,不再触发事件避免重复刷新)
     await errorRuleDetector.reload();
 
-    // 触发事件
-    eventEmitter.emit("errorRulesUpdated");
-
     revalidatePath("/settings/error-rules");
 
     logger.info("[ErrorRulesAction] Updated error rule", {
@@ -270,12 +344,9 @@ export async function deleteErrorRuleAction(id: number): Promise<ActionResult> {
       };
     }
 
-    // 刷新缓存
+    // 刷新缓存(直接调用 reload,不再触发事件避免重复刷新)
     await errorRuleDetector.reload();
 
-    // 触发事件
-    eventEmitter.emit("errorRulesUpdated");
-
     revalidatePath("/settings/error-rules");
 
     logger.info("[ErrorRulesAction] Deleted error rule", {
@@ -318,7 +389,7 @@ export async function refreshCacheAction(): Promise<
     // 1. 同步默认规则到数据库
     const syncedCount = await repo.syncDefaultErrorRules();
 
-    // 2. 重新加载缓存(syncDefaultErrorRules 已经触发了 eventEmitter,但显式调用确保同步)
+    // 2. 重新加载缓存
     await errorRuleDetector.reload();
 
     const stats = errorRuleDetector.getStats();
@@ -345,6 +416,147 @@ export async function refreshCacheAction(): Promise<
   }
 }
 
+/**
+ * 测试错误规则匹配
+ *
+ * 用于前端测试功能,模拟错误消息被系统处理后的结果:
+ * - 是否命中错误规则
+ * - 命中的规则详情
+ * - 最终返回给用户的响应(考虑覆写,与运行时逻辑一致)
+ *
+ * 运行时处理逻辑(与 error-handler.ts 保持一致):
+ * 1. 验证覆写响应格式是否合法(isValidErrorOverrideResponse)
+ * 2. 移除覆写中的 request_id(运行时会从上游注入)
+ * 3. 验证状态码范围(400-599)
+ * 4. message 为空时运行时会回退到原始错误消息
+ */
+export async function testErrorRuleAction(input: {
+  message: string;
+}): Promise<
+  ActionResult<{
+    matched: boolean;
+    rule?: {
+      category: string;
+      pattern: string;
+      matchType: "regex" | "contains" | "exact";
+      overrideResponse: repo.ErrorOverrideResponse | null;
+      overrideStatusCode: number | null;
+    };
+    /** 最终返回给用户的响应体(经过运行时验证处理) */
+    finalResponse: repo.ErrorOverrideResponse | null;
+    /** 最终返回的状态码(经过范围校验) */
+    finalStatusCode: number | null;
+    /** 警告信息(如果有配置问题) */
+    warnings?: string[];
+  }>
+> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return {
+        ok: false,
+        error: "权限不足",
+      };
+    }
+
+    const rawMessage = input.message ?? "";
+
+    // 仅用 trim 做空值校验,检测时使用原始消息以保持与实际运行时一致
+    if (!rawMessage.trim()) {
+      return {
+        ok: false,
+        error: "测试消息不能为空",
+      };
+    }
+
+    // 使用异步检测确保规则已加载
+    // 注意:使用原始消息检测,与实际运行时逻辑保持一致
+    const detection = await errorRuleDetector.detectAsync(rawMessage);
+
+    // 验证 matchType 是有效值
+    const validMatchTypes = ["regex", "contains", "exact"] as const;
+    const matchType = validMatchTypes.includes(detection.matchType as typeof validMatchTypes[number])
+      ? (detection.matchType as "regex" | "contains" | "exact")
+      : "regex";
+
+    // 模拟运行时处理逻辑,确保测试结果与实际行为一致
+    const warnings: string[] = [];
+    let finalResponse: repo.ErrorOverrideResponse | null = null;
+    let finalStatusCode: number | null = null;
+
+    if (detection.matched) {
+      // 1. 验证覆写响应格式(与 error-handler.ts 运行时逻辑一致)
+      if (detection.overrideResponse) {
+        const validationError = validateErrorOverrideResponse(detection.overrideResponse);
+        if (validationError) {
+          warnings.push(`${validationError},运行时将跳过响应体覆写`);
+        } else {
+          // 2. 移除 request_id(运行时会从上游注入)
+          // eslint-disable-next-line @typescript-eslint/no-unused-vars
+          const { request_id: _ignoredRequestId, ...responseWithoutRequestId } =
+            detection.overrideResponse as Record<string, unknown>;
+
+          // 3. 处理 message 为空的情况(运行时会回退到原始错误消息)
+          const overrideErrorObj = detection.overrideResponse.error as Record<string, unknown>;
+          const overrideMessage =
+            typeof overrideErrorObj?.message === "string" &&
+              overrideErrorObj.message.trim().length > 0
+              ? overrideErrorObj.message
+              : rawMessage;
+
+          if (overrideMessage === rawMessage) {
+            warnings.push("覆写响应的 message 为空,运行时将回退到原始错误消息");
+          }
+
+          // 构建最终响应(与 error-handler.ts 构建逻辑一致)
+          finalResponse = {
+            ...responseWithoutRequestId,
+            error: {
+              ...overrideErrorObj,
+              message: overrideMessage,
+            },
+          } as repo.ErrorOverrideResponse;
+        }
+      }
+
+      // 4. 验证状态码范围(与 error-handler.ts 运行时逻辑一致)
+      const statusCodeError = validateOverrideStatusCodeRange(detection.overrideStatusCode);
+      if (!statusCodeError && detection.overrideStatusCode !== undefined && detection.overrideStatusCode !== null) {
+        finalStatusCode = detection.overrideStatusCode;
+      } else if (statusCodeError) {
+        warnings.push(
+          `覆写状态码 ${detection.overrideStatusCode} 非整数或超出有效范围(${OVERRIDE_STATUS_CODE_MIN}-${OVERRIDE_STATUS_CODE_MAX}),运行时将使用上游状态码`
+        );
+      }
+    }
+
+    return {
+      ok: true,
+      data: {
+        matched: detection.matched,
+        rule: detection.matched
+          ? {
+            category: detection.category ?? "unknown",
+            pattern: detection.pattern ?? "",
+            matchType,
+            overrideResponse: detection.overrideResponse ?? null,
+            overrideStatusCode: detection.overrideStatusCode ?? null,
+          }
+          : undefined,
+        finalResponse,
+        finalStatusCode,
+        warnings: warnings.length > 0 ? warnings : undefined,
+      },
+    };
+  } catch (error) {
+    logger.error("[ErrorRulesAction] Failed to test error rule:", error);
+    return {
+      ok: false,
+      error: "测试错误规则失败",
+    };
+  }
+}
+
 /**
  * 获取缓存统计信息
  */

+ 15 - 13
src/app/[locale]/settings/data/_components/database-import.tsx

@@ -258,19 +258,21 @@ export function DatabaseImport() {
               <AlertCircle className="h-5 w-5 text-orange-500" />
               {t('confirmTitle')}
             </AlertDialogTitle>
-            <AlertDialogDescription className="space-y-2">
-              <p>
-                {cleanFirst ? t('confirmOverwrite') : t('confirmMerge')}
-              </p>
-              <p className="font-semibold text-foreground">
-                {cleanFirst ? t('warningOverwrite') : t('warningMerge')}
-              </p>
-              <p>
-                {t('backupFile')} <span className="font-mono text-xs">{selectedFile?.name}</span>
-              </p>
-              <p className="text-xs text-muted-foreground">
-                {t('backupRecommendation')}
-              </p>
+            <AlertDialogDescription asChild>
+              <div className="text-muted-foreground text-sm space-y-2">
+                <p>
+                  {cleanFirst ? t('confirmOverwrite') : t('confirmMerge')}
+                </p>
+                <p className="font-semibold text-foreground">
+                  {cleanFirst ? t('warningOverwrite') : t('warningMerge')}
+                </p>
+                <p>
+                  {t('backupFile')} <span className="font-mono text-xs">{selectedFile?.name}</span>
+                </p>
+                <p className="text-xs text-muted-foreground">
+                  {t('backupRecommendation')}
+                </p>
+              </div>
             </AlertDialogDescription>
           </AlertDialogHeader>
           <AlertDialogFooter>

+ 45 - 0
src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx

@@ -26,6 +26,8 @@ import { Plus } from "lucide-react";
 import { createErrorRuleAction } from "@/actions/error-rules";
 import { toast } from "sonner";
 import { RegexTester } from "./regex-tester";
+import { OverrideSection } from "./override-section";
+import type { ErrorOverrideResponse } from "@/repository/error-rules";
 
 export function AddRuleDialog() {
   const t = useTranslations("settings");
@@ -34,6 +36,9 @@ export function AddRuleDialog() {
   const [pattern, setPattern] = useState("");
   const [category, setCategory] = useState("");
   const [description, setDescription] = useState("");
+  const [enableOverride, setEnableOverride] = useState(false);
+  const [overrideResponse, setOverrideResponse] = useState("");
+  const [overrideStatusCode, setOverrideStatusCode] = useState<string>("");
 
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
@@ -56,6 +61,31 @@ export function AddRuleDialog() {
       return;
     }
 
+    // Parse and validate override response JSON (only when override is enabled)
+    let parsedOverrideResponse: ErrorOverrideResponse | undefined = undefined;
+    let parsedStatusCode: number | undefined = undefined;
+
+    if (enableOverride) {
+      if (overrideResponse.trim()) {
+        try {
+          parsedOverrideResponse = JSON.parse(overrideResponse.trim());
+        } catch {
+          toast.error(t("errorRules.dialog.invalidJson"));
+          return;
+        }
+      }
+
+      // Parse override status code
+      if (overrideStatusCode.trim()) {
+        const code = parseInt(overrideStatusCode.trim(), 10);
+        if (isNaN(code) || code < 400 || code > 599) {
+          toast.error(t("errorRules.dialog.invalidStatusCode"));
+          return;
+        }
+        parsedStatusCode = code;
+      }
+    }
+
     setIsSubmitting(true);
 
     try {
@@ -70,6 +100,8 @@ export function AddRuleDialog() {
           | "invalid_request"
           | "cache_limit",
         description: description.trim() || undefined,
+        overrideResponse: parsedOverrideResponse ?? null,
+        overrideStatusCode: parsedStatusCode ?? null,
       });
 
       if (result.ok) {
@@ -79,6 +111,9 @@ export function AddRuleDialog() {
         setPattern("");
         setCategory("");
         setDescription("");
+        setEnableOverride(false);
+        setOverrideResponse("");
+        setOverrideStatusCode("");
       } else {
         toast.error(result.error);
       }
@@ -159,6 +194,16 @@ export function AddRuleDialog() {
               />
             </div>
 
+            <OverrideSection
+              idPrefix="add"
+              enableOverride={enableOverride}
+              onEnableOverrideChange={setEnableOverride}
+              overrideResponse={overrideResponse}
+              onOverrideResponseChange={setOverrideResponse}
+              overrideStatusCode={overrideStatusCode}
+              onOverrideStatusCodeChange={setOverrideStatusCode}
+            />
+
             {pattern && (
               <div className="grid gap-2">
                 <Label>{t("errorRules.dialog.regexTester")}</Label>

+ 57 - 7
src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx

@@ -23,8 +23,9 @@ import {
 } from "@/components/ui/select";
 import { updateErrorRuleAction } from "@/actions/error-rules";
 import { toast } from "sonner";
-import type { ErrorRule } from "@/repository/error-rules";
+import type { ErrorRule, ErrorOverrideResponse } from "@/repository/error-rules";
 import { RegexTester } from "./regex-tester";
+import { OverrideSection } from "./override-section";
 
 interface EditRuleDialogProps {
   rule: ErrorRule;
@@ -38,6 +39,9 @@ export function EditRuleDialog({ rule, open, onOpenChange }: EditRuleDialogProps
   const [pattern, setPattern] = useState("");
   const [category, setCategory] = useState("");
   const [description, setDescription] = useState("");
+  const [enableOverride, setEnableOverride] = useState(false);
+  const [overrideResponse, setOverrideResponse] = useState("");
+  const [overrideStatusCode, setOverrideStatusCode] = useState<string>("");
 
   // Update form when rule changes
   useEffect(() => {
@@ -45,6 +49,13 @@ export function EditRuleDialog({ rule, open, onOpenChange }: EditRuleDialogProps
       setPattern(rule.pattern);
       setCategory(rule.category || "");
       setDescription(rule.description || "");
+      // Enable override if rule has override response or status code
+      const hasOverride = !!rule.overrideResponse || !!rule.overrideStatusCode;
+      setEnableOverride(hasOverride);
+      setOverrideResponse(
+        rule.overrideResponse ? JSON.stringify(rule.overrideResponse, null, 2) : ""
+      );
+      setOverrideStatusCode(rule.overrideStatusCode?.toString() || "");
     }
   }, [rule]);
 
@@ -61,12 +72,39 @@ export function EditRuleDialog({ rule, open, onOpenChange }: EditRuleDialogProps
       return;
     }
 
-    // Validate regex pattern
-    try {
-      new RegExp(pattern.trim());
-    } catch {
-      toast.error(t("errorRules.dialog.invalidRegex"));
-      return;
+    // Validate regex pattern (only for regex match type)
+    if (rule.matchType === "regex") {
+      try {
+        new RegExp(pattern.trim());
+      } catch {
+        toast.error(t("errorRules.dialog.invalidRegex"));
+        return;
+      }
+    }
+
+    // Parse and validate override response JSON (only when override is enabled)
+    let parsedOverrideResponse: ErrorOverrideResponse | null = null;
+    let parsedStatusCode: number | null = null;
+
+    if (enableOverride) {
+      if (overrideResponse.trim()) {
+        try {
+          parsedOverrideResponse = JSON.parse(overrideResponse.trim());
+        } catch {
+          toast.error(t("errorRules.dialog.invalidJson"));
+          return;
+        }
+      }
+
+      // Parse override status code
+      if (overrideStatusCode.trim()) {
+        const code = parseInt(overrideStatusCode.trim(), 10);
+        if (isNaN(code) || code < 400 || code > 599) {
+          toast.error(t("errorRules.dialog.invalidStatusCode"));
+          return;
+        }
+        parsedStatusCode = code;
+      }
     }
 
     setIsSubmitting(true);
@@ -83,6 +121,8 @@ export function EditRuleDialog({ rule, open, onOpenChange }: EditRuleDialogProps
           | "invalid_request"
           | "cache_limit",
         description: description.trim() || undefined,
+        overrideResponse: parsedOverrideResponse,
+        overrideStatusCode: parsedStatusCode,
       });
 
       if (result.ok) {
@@ -172,6 +212,16 @@ export function EditRuleDialog({ rule, open, onOpenChange }: EditRuleDialogProps
               />
             </div>
 
+            <OverrideSection
+              idPrefix="edit"
+              enableOverride={enableOverride}
+              onEnableOverrideChange={setEnableOverride}
+              overrideResponse={overrideResponse}
+              onOverrideResponseChange={setOverrideResponse}
+              overrideStatusCode={overrideStatusCode}
+              onOverrideStatusCodeChange={setOverrideStatusCode}
+            />
+
             {pattern && (
               <div className="grid gap-2">
                 <Label>{t("errorRules.dialog.regexTester")}</Label>

+ 176 - 0
src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx

@@ -0,0 +1,176 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { testErrorRuleAction } from "@/actions/error-rules";
+import { toast } from "sonner";
+import { AlertTriangle, CheckCircle2, Loader2, XCircle } from "lucide-react";
+import type { ErrorOverrideResponse } from "@/repository/error-rules";
+
+interface TestResult {
+  matched: boolean;
+  rule?: {
+    category: string;
+    pattern: string;
+    matchType: "regex" | "contains" | "exact";
+    overrideResponse: ErrorOverrideResponse | null;
+    overrideStatusCode: number | null;
+  };
+  finalResponse: ErrorOverrideResponse | null;
+  finalStatusCode: number | null;
+  warnings?: string[];
+}
+
+export function ErrorRuleTester() {
+  const t = useTranslations("settings");
+  const [message, setMessage] = useState("");
+  const [isTesting, setIsTesting] = useState(false);
+  const [result, setResult] = useState<TestResult | null>(null);
+
+  const handleTest = async () => {
+    const trimmedMessage = message.trim();
+    if (!trimmedMessage) {
+      toast.error(t("errorRules.tester.messageRequired"));
+      return;
+    }
+
+    setIsTesting(true);
+    setResult(null);
+
+    try {
+      const response = await testErrorRuleAction({ message });
+
+      if (response.ok) {
+        setResult(response.data);
+      } else {
+        toast.error(response.error);
+      }
+    } catch {
+      toast.error(t("errorRules.tester.testFailed"));
+    } finally {
+      setIsTesting(false);
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      <div className="grid gap-2">
+        <Label htmlFor="error-rule-test-message">{t("errorRules.tester.inputLabel")}</Label>
+        <Textarea
+          id="error-rule-test-message"
+          value={message}
+          onChange={(e) => setMessage(e.target.value)}
+          placeholder={t("errorRules.tester.inputPlaceholder")}
+          rows={3}
+        />
+      </div>
+
+      <Button onClick={handleTest} disabled={isTesting}>
+        {isTesting ? (
+          <>
+            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+            {t("errorRules.tester.testing")}
+          </>
+        ) : (
+          t("errorRules.tester.testButton")
+        )}
+      </Button>
+
+      {result && (
+        <div className="space-y-4 rounded-lg border border-muted bg-muted/30 p-4">
+          {/* 匹配状态 */}
+          <div className="flex flex-wrap items-center gap-2">
+            {result.matched ? (
+              <>
+                <CheckCircle2 className="h-4 w-4 text-green-600" />
+                <span className="text-sm font-medium text-green-700">{t("errorRules.tester.matched")}</span>
+              </>
+            ) : (
+              <>
+                <XCircle className="h-4 w-4 text-muted-foreground" />
+                <span className="text-sm font-medium text-muted-foreground">
+                  {t("errorRules.tester.notMatched")}
+                </span>
+              </>
+            )}
+          </div>
+
+          <div className="grid gap-4">
+            {/* 规则信息 */}
+            <div className="space-y-2">
+              <p className="text-xs font-medium text-muted-foreground">{t("errorRules.tester.ruleInfo")}</p>
+              {result.rule ? (
+                <div className="space-y-1 rounded border border-border bg-card/60 px-3 py-2 text-sm">
+                  <div className="flex items-center justify-between gap-4">
+                    <span className="text-muted-foreground">{t("errorRules.tester.category")}</span>
+                    <Badge variant="secondary">{result.rule.category}</Badge>
+                  </div>
+                  <div className="flex items-center justify-between gap-4">
+                    <span className="text-muted-foreground">{t("errorRules.tester.matchType")}</span>
+                    <Badge variant="outline">{result.rule.matchType}</Badge>
+                  </div>
+                  <div className="flex items-start justify-between gap-4">
+                    <span className="text-muted-foreground shrink-0">{t("errorRules.tester.pattern")}</span>
+                    <code className="max-w-[260px] break-all text-right font-mono text-xs">
+                      {result.rule.pattern}
+                    </code>
+                  </div>
+                  {result.rule.overrideStatusCode !== null && (
+                    <div className="flex items-center justify-between gap-4 pt-1 border-t border-border/50">
+                      <span className="text-muted-foreground">{t("errorRules.tester.overrideStatusCode")}</span>
+                      <Badge variant="outline">{result.rule.overrideStatusCode}</Badge>
+                    </div>
+                  )}
+                </div>
+              ) : (
+                <p className="text-sm text-muted-foreground">{t("errorRules.tester.noRule")}</p>
+              )}
+            </div>
+
+            {/* 警告信息 */}
+            {result.warnings && result.warnings.length > 0 && (
+              <div className="space-y-2">
+                <p className="text-xs font-medium text-muted-foreground">{t("errorRules.tester.warnings")}</p>
+                <div className="space-y-1">
+                  {result.warnings.map((warning, index) => (
+                    <div key={index} className="flex items-start gap-2 rounded border border-yellow-200 bg-yellow-50 px-3 py-2 text-sm dark:border-yellow-900 dark:bg-yellow-950">
+                      <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-yellow-600 dark:text-yellow-500" />
+                      <span className="text-yellow-800 dark:text-yellow-200">{warning}</span>
+                    </div>
+                  ))}
+                </div>
+              </div>
+            )}
+
+            {/* 最终返回响应(响应体覆写或仅状态码覆写) */}
+            {(result.finalResponse || result.finalStatusCode !== null) && (
+              <div className="space-y-2">
+                <div className="flex items-center justify-between">
+                  <p className="text-xs font-medium text-muted-foreground">{t("errorRules.tester.finalResponse")}</p>
+                  {result.finalStatusCode !== null && (
+                    <Badge variant="outline" className="text-xs">
+                      HTTP {result.finalStatusCode}
+                    </Badge>
+                  )}
+                </div>
+                {result.finalResponse ? (
+                  <pre className="rounded bg-background px-3 py-2 text-xs font-mono overflow-x-auto max-h-48">
+                    {JSON.stringify(result.finalResponse, null, 2)}
+                  </pre>
+                ) : (
+                  <p className="text-sm text-muted-foreground rounded bg-background px-3 py-2">
+                    {t("errorRules.tester.statusCodeOnlyOverride")}
+                  </p>
+                )}
+              </div>
+            )}
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}

+ 156 - 0
src/app/[locale]/settings/error-rules/_components/override-section.tsx

@@ -0,0 +1,156 @@
+"use client";
+
+import { useCallback, useMemo } from "react";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { CheckCircle2, XCircle } from "lucide-react";
+
+/** JSON 验证状态类型 */
+type JsonValidationState =
+  | { state: "empty" }
+  | { state: "valid" }
+  | { state: "invalid"; message: string };
+
+/** 默认的覆写响应模板 */
+const DEFAULT_OVERRIDE_RESPONSE = `{
+  "type": "error",
+  "error": {
+    "type": "invalid_request_error",
+    "message": "Your custom error message here"
+  }
+}`;
+
+interface OverrideSectionProps {
+  /** 输入框 ID 前缀,用于区分 add/edit 对话框 */
+  idPrefix: string;
+  enableOverride: boolean;
+  onEnableOverrideChange: (enabled: boolean) => void;
+  overrideResponse: string;
+  onOverrideResponseChange: (value: string) => void;
+  overrideStatusCode: string;
+  onOverrideStatusCodeChange: (value: string) => void;
+}
+
+export function OverrideSection({
+  idPrefix,
+  enableOverride,
+  onEnableOverrideChange,
+  overrideResponse,
+  onOverrideResponseChange,
+  overrideStatusCode,
+  onOverrideStatusCodeChange,
+}: OverrideSectionProps) {
+  const t = useTranslations("settings");
+
+  /** 实时 JSON 格式验证 */
+  const jsonStatus = useMemo((): JsonValidationState => {
+    const trimmed = overrideResponse.trim();
+    if (!trimmed) {
+      return { state: "empty" };
+    }
+    try {
+      JSON.parse(trimmed);
+      return { state: "valid" };
+    } catch (error) {
+      return { state: "invalid", message: (error as Error).message };
+    }
+  }, [overrideResponse]);
+
+  /** 处理使用模板按钮点击 */
+  const handleUseTemplate = useCallback(() => {
+    // 如果输入框已有内容,弹出确认对话框
+    if (overrideResponse.trim().length > 0) {
+      const confirmed = window.confirm(t("errorRules.dialog.useTemplateConfirm"));
+      if (!confirmed) return;
+    }
+    onOverrideResponseChange(DEFAULT_OVERRIDE_RESPONSE);
+  }, [overrideResponse, onOverrideResponseChange, t]);
+
+  return (
+    <div className="rounded-lg border p-4 space-y-4">
+      <div className="flex items-center space-x-2">
+        <Checkbox
+          id={`${idPrefix}-enableOverride`}
+          checked={enableOverride}
+          onCheckedChange={(checked) => onEnableOverrideChange(checked === true)}
+        />
+        <Label htmlFor={`${idPrefix}-enableOverride`} className="font-medium cursor-pointer">
+          {t("errorRules.dialog.enableOverride")}
+        </Label>
+      </div>
+      <p className="text-xs text-muted-foreground">
+        {t("errorRules.dialog.enableOverrideHint")}
+      </p>
+
+      {enableOverride && (
+        <div className="space-y-4 pt-2">
+          <div className="grid gap-2">
+            <div className="flex items-center justify-between">
+              <Label htmlFor={`${idPrefix}-overrideResponse`}>
+                {t("errorRules.dialog.overrideResponseLabel")}
+              </Label>
+              <div className="flex items-center gap-3">
+                {/* JSON 验证状态指示器 */}
+                {jsonStatus.state === "valid" && (
+                  <span className="flex items-center gap-1 text-xs text-emerald-600">
+                    <CheckCircle2 className="h-3 w-3" />
+                    {t("errorRules.dialog.validJson")}
+                  </span>
+                )}
+                {jsonStatus.state === "invalid" && (
+                  <span className="flex items-center gap-1 text-xs text-destructive">
+                    <XCircle className="h-3 w-3" />
+                    {t("errorRules.dialog.invalidJson")}
+                  </span>
+                )}
+                <Button
+                  type="button"
+                  variant="ghost"
+                  size="sm"
+                  className="h-6 text-xs"
+                  onClick={handleUseTemplate}
+                >
+                  {t("errorRules.dialog.useTemplate")}
+                </Button>
+              </div>
+            </div>
+            <Textarea
+              id={`${idPrefix}-overrideResponse`}
+              value={overrideResponse}
+              onChange={(e) => onOverrideResponseChange(e.target.value)}
+              placeholder={DEFAULT_OVERRIDE_RESPONSE}
+              rows={6}
+              className={`font-mono text-sm ${jsonStatus.state === "invalid" ? "border-destructive" : ""}`}
+            />
+            {/* JSON 解析错误详情 */}
+            {jsonStatus.state === "invalid" && (
+              <p className="text-xs text-destructive">{jsonStatus.message}</p>
+            )}
+          </div>
+
+          <div className="grid gap-2">
+            <Label htmlFor={`${idPrefix}-overrideStatusCode`}>
+              {t("errorRules.dialog.overrideStatusCodeLabel")}
+            </Label>
+            <Input
+              id={`${idPrefix}-overrideStatusCode`}
+              type="number"
+              min={400}
+              max={599}
+              value={overrideStatusCode}
+              onChange={(e) => onOverrideStatusCodeChange(e.target.value)}
+              placeholder={t("errorRules.dialog.overrideStatusCodePlaceholder")}
+            />
+            <p className="text-xs text-muted-foreground">
+              {t("errorRules.dialog.overrideStatusCodeHint")}
+            </p>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}

+ 18 - 11
src/app/[locale]/settings/error-rules/page.tsx

@@ -5,6 +5,7 @@ import { SettingsPageHeader } from "../_components/settings-page-header";
 import { RuleListTable } from "./_components/rule-list-table";
 import { AddRuleDialog } from "./_components/add-rule-dialog";
 import { RefreshCacheButton } from "./_components/refresh-cache-button";
+import { ErrorRuleTester } from "./_components/error-rule-tester";
 
 export const dynamic = "force-dynamic";
 
@@ -16,17 +17,23 @@ export default async function ErrorRulesPage() {
     <>
       <SettingsPageHeader title={t("errorRules.title")} description={t("errorRules.description")} />
 
-      <Section
-        title={t("errorRules.section.title")}
-        actions={
-          <div className="flex gap-2">
-            <RefreshCacheButton stats={cacheStats} />
-            <AddRuleDialog />
-          </div>
-        }
-      >
-        <RuleListTable rules={rules} />
-      </Section>
+      <div className="space-y-6">
+        <Section title={t("errorRules.tester.title")} description={t("errorRules.tester.description")}>
+          <ErrorRuleTester />
+        </Section>
+
+        <Section
+          title={`${t("errorRules.section.title")} (${rules.length})`}
+          actions={
+            <div className="flex gap-2">
+              <RefreshCacheButton stats={cacheStats} />
+              <AddRuleDialog />
+            </div>
+          }
+        >
+          <RuleListTable rules={rules} />
+        </Section>
+      </div>
     </>
   );
 }

+ 130 - 2
src/app/v1/_lib/proxy/error-handler.ts

@@ -1,9 +1,15 @@
 import { updateMessageRequestDuration, updateMessageRequestDetails } from "@/repository/message";
 import { logger } from "@/lib/logger";
 import { ProxyResponses } from "./responses";
-import { ProxyError, RateLimitError, isRateLimitError } from "./errors";
+import { ProxyError, RateLimitError, isRateLimitError, getErrorOverride } from "./errors";
 import type { ProxySession } from "./session";
 import { ProxyStatusTracker } from "@/lib/proxy-status-tracker";
+import { isValidErrorOverrideResponse } from "@/lib/error-override-validator";
+
+/** 覆写状态码最小值 */
+const OVERRIDE_STATUS_CODE_MIN = 400;
+/** 覆写状态码最大值 */
+const OVERRIDE_STATUS_CODE_MAX = 599;
 
 export class ProxyErrorHandler {
   static async handle(session: ProxySession, error: unknown): Promise<Response> {
@@ -44,12 +50,134 @@ export class ProxyErrorHandler {
       }
     }
 
-    // 记录错误到数据库
+    // 记录错误到数据库(始终记录原始错误消息)
     await this.logErrorToDatabase(session, errorMessage, statusCode, null);
 
+    // 检测是否有覆写配置(响应体或状态码)
+    if (error instanceof Error) {
+      const override = getErrorOverride(error);
+      if (override) {
+        // 运行时校验覆写状态码范围(400-599),防止数据库脏数据导致 Response 抛 RangeError
+        let validatedStatusCode = override.statusCode;
+        if (
+          validatedStatusCode !== null &&
+          (!Number.isInteger(validatedStatusCode) ||
+            validatedStatusCode < OVERRIDE_STATUS_CODE_MIN ||
+            validatedStatusCode > OVERRIDE_STATUS_CODE_MAX)
+        ) {
+          logger.warn("ProxyErrorHandler: Invalid override status code, falling back to upstream", {
+            overrideStatusCode: validatedStatusCode,
+            upstreamStatusCode: statusCode,
+          });
+          validatedStatusCode = null;
+        }
+
+        // 使用覆写状态码,如果未配置或无效则使用上游状态码
+        const responseStatusCode = validatedStatusCode ?? statusCode;
+
+        // 提取上游 request_id(用于覆写场景透传)
+        const upstreamRequestId =
+          error instanceof ProxyError ? error.upstreamError?.requestId : undefined;
+        const safeRequestId =
+          typeof upstreamRequestId === "string" ? upstreamRequestId : undefined;
+
+        // 情况 1: 有响应体覆写 - 返回覆写的 JSON 响应
+        if (override.response) {
+          // 运行时守卫:验证覆写响应格式是否合法(双重保护,加载时已过滤一次)
+          // 防止数据库中存在畸形数据导致返回不合规响应
+          if (!isValidErrorOverrideResponse(override.response)) {
+            logger.warn("ProxyErrorHandler: Invalid override response in database, skipping", {
+              response: JSON.stringify(override.response).substring(0, 200),
+            });
+            // 跳过响应体覆写,但仍可应用状态码覆写
+            if (override.statusCode !== null) {
+              return ProxyResponses.buildError(
+                responseStatusCode,
+                errorMessage,
+                undefined,
+                undefined,
+                safeRequestId
+              );
+            }
+            // 两者都无效,返回原始错误(但仍透传 request_id,因为有覆写意图)
+            return ProxyResponses.buildError(
+              statusCode,
+              errorMessage,
+              undefined,
+              undefined,
+              safeRequestId
+            );
+          }
+
+          // 覆写消息为空时回退到原始错误消息
+          const overrideErrorObj = override.response.error as Record<string, unknown>;
+          const overrideMessage =
+            typeof overrideErrorObj?.message === "string" && overrideErrorObj.message.trim().length > 0
+              ? overrideErrorObj.message
+              : errorMessage;
+
+          // 构建最终响应:注入 request_id(如果有),并确保 message 不为空
+          // 移除覆写配置中的 request_id,只使用上游的 request_id
+          // eslint-disable-next-line @typescript-eslint/no-unused-vars
+          const { request_id: _ignoredRequestId, ...overrideWithoutRequestId } =
+            override.response as Record<string, unknown>;
+
+          const responseBody = {
+            ...overrideWithoutRequestId,
+            error: {
+              ...overrideErrorObj,
+              message: overrideMessage,
+            },
+            ...(safeRequestId ? { request_id: safeRequestId } : {}),
+          };
+
+          logger.info("ProxyErrorHandler: Applied error override response", {
+            original: errorMessage.substring(0, 200),
+            overrideType: override.response.error?.type,
+            statusCode: responseStatusCode,
+            hasRequestId: !!safeRequestId,
+          });
+
+          logger.error("ProxyErrorHandler: Request failed (overridden)", {
+            error: errorMessage,
+            statusCode: responseStatusCode,
+            overridden: true,
+          });
+
+          return new Response(JSON.stringify(responseBody), {
+            status: responseStatusCode,
+            headers: { "Content-Type": "application/json" },
+          });
+        }
+
+        // 情况 2: 仅状态码覆写 - 返回原始错误消息,但使用覆写的状态码
+        logger.info("ProxyErrorHandler: Applied status code override only", {
+          original: errorMessage.substring(0, 200),
+          originalStatusCode: statusCode,
+          overrideStatusCode: responseStatusCode,
+          hasRequestId: !!safeRequestId,
+        });
+
+        logger.error("ProxyErrorHandler: Request failed (status overridden)", {
+          error: errorMessage,
+          statusCode: responseStatusCode,
+          overridden: true,
+        });
+
+        return ProxyResponses.buildError(
+          responseStatusCode,
+          errorMessage,
+          undefined,
+          undefined,
+          safeRequestId
+        );
+      }
+    }
+
     logger.error("ProxyErrorHandler: Request failed", {
       error: errorMessage,
       statusCode,
+      overridden: false,
     });
 
     return ProxyResponses.buildError(statusCode, errorMessage);

+ 373 - 8
src/app/v1/_lib/proxy/errors.ts

@@ -6,7 +6,8 @@
  * 2. 智能截断:JSON 完整保存,文本限制 500 字符
  * 3. 可读性优先:纯文本格式化,便于排查问题
  */
-import { errorRuleDetector } from "@/lib/error-rule-detector";
+import { errorRuleDetector, type ErrorDetectionResult } from "@/lib/error-rule-detector";
+import type { ErrorOverrideResponse } from "@/repository/error-rules";
 
 export class ProxyError extends Error {
   constructor(
@@ -17,6 +18,7 @@ export class ProxyError extends Error {
       parsed?: unknown; // 解析后的 JSON(如果有)
       providerId?: number;
       providerName?: string;
+      requestId?: string; // 上游请求 ID(用于覆写响应时注入)
     }
   ) {
     super(message);
@@ -64,14 +66,291 @@ export class ProxyError extends Error {
     // 4. 智能截断响应体
     const truncatedBody = ProxyError.smartTruncate(body, parsed);
 
+    // 5. 提取 request_id(从响应体或响应头)
+    const requestId =
+      ProxyError.extractRequestIdFromBody(parsed) ||
+      ProxyError.extractRequestIdFromHeaders(response.headers);
+
     return new ProxyError(message, response.status, {
       body: truncatedBody,
       parsed,
       providerId: provider.id,
       providerName: provider.name,
+      requestId,
     });
   }
 
+  /**
+   * 从解析后的 JSON 响应体中提取 request_id
+   *
+   * 支持多种嵌套格式:
+   * 1. 顶层 request_id/requestId(标准 Claude/OpenAI 格式)
+   * 2. error 对象内的 request_id/requestId
+   * 3. error.upstream_error 对象内的 request_id/requestId(中继服务格式)
+   * 4. message 字段内嵌套 JSON 字符串中的 request_id(某些代理服务格式)
+   *
+   * @example
+   * // 标准格式
+   * { "request_id": "req_xxx" }
+   *
+   * // error 对象内
+   * { "error": { "request_id": "req_xxx" } }
+   *
+   * // upstream_error 格式
+   * { "error": { "upstream_error": { "request_id": "req_xxx" } } }
+   *
+   * // message 内嵌套 JSON
+   * { "error": { "message": "{\"request_id\":\"req_xxx\"}" } }
+   */
+  private static extractRequestIdFromBody(parsed: unknown): string | undefined {
+    if (!parsed || typeof parsed !== "object") return undefined;
+    return ProxyError.extractRequestIdFromObject(parsed as Record<string, unknown>);
+  }
+
+  /**
+   * 通用的 request_id 提取逻辑
+   *
+   * @param obj - 要提取的对象
+   * @param remainingDepth - 允许的嵌套 JSON 解析次数,防止循环/过度 JSON.parse
+   */
+  private static extractRequestIdFromObject(
+    obj: Record<string, unknown>,
+    remainingDepth: number = 2
+  ): string | undefined {
+    let depthBudget = remainingDepth;
+
+    // 1. 检查顶层 request_id/requestId
+    const flatRequestId = ProxyError.extractRequestIdFromFlat(obj);
+    if (flatRequestId) {
+      return flatRequestId;
+    }
+
+    // 2. 检查 error 对象内的各种格式
+    if (obj.error && typeof obj.error === "object") {
+      const errorObj = obj.error as Record<string, unknown>;
+
+      // 2.1 直接在 error 对象内的 request_id/requestId
+      const errorRequestId = ProxyError.extractRequestIdFromFlat(errorObj);
+      if (errorRequestId) {
+        return errorRequestId;
+      }
+
+      // 2.2 检查 error.upstream_error 对象(中继服务格式)
+      if (errorObj.upstream_error && typeof errorObj.upstream_error === "object") {
+        const upstreamError = errorObj.upstream_error as Record<string, unknown>;
+
+        // 2.2.1 直接在 upstream_error 对象内的 request_id
+        const upstreamRequestId = ProxyError.extractRequestIdFromFlat(upstreamError);
+        if (upstreamRequestId) {
+          return upstreamRequestId;
+        }
+
+        // 2.2.2 检查 upstream_error.error 对象(深层嵌套格式)
+        // 例如: { error: { upstream_error: { error: { message: "{...request_id...}" } } } }
+        if (upstreamError.error && typeof upstreamError.error === "object") {
+          const nestedError = upstreamError.error as Record<string, unknown>;
+
+          // 检查 upstream_error.error.request_id
+          const nestedRequestId = ProxyError.extractRequestIdFromFlat(nestedError);
+          if (nestedRequestId) {
+            return nestedRequestId;
+          }
+
+          // 检查 upstream_error.error.message 内的嵌套格式
+          if (typeof nestedError.message === "string" && depthBudget > 0) {
+            const msgRequestId = ProxyError.extractRequestIdFromJsonString(
+              nestedError.message,
+              depthBudget - 1
+            );
+            if (msgRequestId) {
+              return msgRequestId;
+            }
+          }
+        }
+      }
+
+      // 2.3 尝试从 error.message 字段解析嵌套 JSON
+      if (typeof errorObj.message === "string" && depthBudget > 0) {
+        const nestedRequestId = ProxyError.extractRequestIdFromJsonString(
+          errorObj.message,
+          depthBudget - 1
+        );
+        if (nestedRequestId) {
+          return nestedRequestId;
+        }
+        depthBudget -= 1; // 消耗一次尝试,避免重复解析同一层 message
+      }
+    }
+
+    // 3. 检查顶层 message 字段内的嵌套 JSON
+    if (typeof obj.message === "string" && depthBudget > 0) {
+      return ProxyError.extractRequestIdFromJsonString(obj.message, depthBudget - 1);
+    }
+
+    return undefined;
+  }
+
+  /**
+   * 从对象中直接提取 request_id(不递归)
+   */
+  private static extractRequestIdFromFlat(obj: Record<string, unknown>): string | undefined {
+    if (typeof obj.request_id === "string" && obj.request_id.trim()) {
+      return obj.request_id.trim();
+    }
+    if (typeof obj.requestId === "string" && obj.requestId.trim()) {
+      return obj.requestId.trim();
+    }
+    return undefined;
+  }
+
+  /**
+   * 从可能包含 JSON 或 request_id 的字符串中提取 request_id
+   *
+   * 支持的格式:
+   * 1. 纯 JSON 字符串: `{"request_id":"req_xxx"}`
+   * 2. JSON + 尾部文本: `{"request_id":"req_xxx"}(traceid: ...)`
+   * 3. 纯文本格式: `... (request id: xxx)` 或 `... (request_id: xxx)`
+   *
+   * @param str - 可能包含 JSON 或 request_id 的字符串
+   * @param remainingDepth - 允许的嵌套 JSON 解析次数
+   * @returns 提取的 request_id,如果未找到则返回 undefined
+   */
+  private static extractRequestIdFromJsonString(
+    str: string,
+    remainingDepth: number = 2
+  ): string | undefined {
+    const trimmed = str.trim();
+    if (remainingDepth < 0) {
+      return undefined;
+    }
+
+    // 策略 1: 尝试解析 JSON(可能以 { 开头)
+    if (trimmed.startsWith("{")) {
+      // 尝试直接解析整个字符串
+      try {
+        const parsed = JSON.parse(trimmed);
+        if (parsed && typeof parsed === "object") {
+          return ProxyError.extractRequestIdFromObject(
+            parsed as Record<string, unknown>,
+            remainingDepth
+          );
+        }
+      } catch {
+        // JSON.parse 失败,可能是 JSON + 尾部文本的情况
+        // 尝试提取 JSON 部分(找到匹配的 } 位置)
+        const jsonPart = ProxyError.extractJsonFromString(trimmed);
+        if (jsonPart) {
+          try {
+            const parsed = JSON.parse(jsonPart);
+            if (parsed && typeof parsed === "object") {
+              return ProxyError.extractRequestIdFromObject(
+                parsed as Record<string, unknown>,
+                remainingDepth
+              );
+            }
+          } catch {
+            // 提取的部分也不是有效 JSON,继续尝试其他策略
+          }
+        }
+      }
+    }
+
+    // 策略 2: 正则提取纯文本格式的 request_id
+    // 匹配: (request id: xxx) 或 (request_id: xxx) 或 request_id: xxx
+    return ProxyError.extractRequestIdFromText(str);
+  }
+
+  /**
+   * 从字符串中提取 JSON 对象部分
+   *
+   * 处理类似 `{"key":"value"}(额外文本)` 的情况
+   * 通过括号匹配找到 JSON 对象的结束位置
+   */
+  private static extractJsonFromString(str: string): string | null {
+    if (!str.startsWith("{")) {
+      return null;
+    }
+
+    let depth = 0;
+    let inString = false;
+    let escape = false;
+
+    for (let i = 0; i < str.length; i++) {
+      const char = str[i];
+
+      if (escape) {
+        escape = false;
+        continue;
+      }
+
+      if (char === "\\") {
+        escape = true;
+        continue;
+      }
+
+      if (char === '"') {
+        inString = !inString;
+        continue;
+      }
+
+      if (inString) {
+        continue;
+      }
+
+      if (char === "{") {
+        depth++;
+      } else if (char === "}") {
+        depth--;
+        if (depth === 0) {
+          // 找到匹配的结束括号
+          return str.substring(0, i + 1);
+        }
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * 从纯文本中提取 request_id
+   *
+   * 支持的格式:
+   * - (request id: xxx)
+   * - (request_id: xxx)
+   * - request_id: "xxx"
+   * - "request_id": "xxx"
+   */
+  private static extractRequestIdFromText(str: string): string | undefined {
+    // 模式 1: (request id: xxx) 或 (request_id: xxx) - 括号内格式
+    const parenMatch = str.match(/\(request[_ ]id:\s*([^)]+)\)/i);
+    if (parenMatch && parenMatch[1]) {
+      return parenMatch[1].trim();
+    }
+
+    // 模式 2: "request_id": "xxx" - JSON 字段格式(用于部分损坏的 JSON)
+    const jsonFieldMatch = str.match(/"request_id"\s*:\s*"([^"]+)"/);
+    if (jsonFieldMatch && jsonFieldMatch[1]) {
+      return jsonFieldMatch[1].trim();
+    }
+
+    return undefined;
+  }
+
+  /**
+   * 从响应头中提取 request_id
+   */
+  private static extractRequestIdFromHeaders(headers: Headers): string | undefined {
+    // 常见的 request_id 响应头名称
+    const headerNames = ["x-request-id", "request-id", "x-amzn-requestid"];
+    for (const name of headerNames) {
+      const value = headers.get(name);
+      if (value && value.trim()) {
+        return value.trim();
+      }
+    }
+    return undefined;
+  }
+
   /**
    * 从 JSON 中提取错误消息
    * 支持的格式:
@@ -166,17 +445,103 @@ export enum ErrorCategory {
   NON_RETRYABLE_CLIENT_ERROR, // 客户端输入错误(Prompt 超限、内容过滤、PDF 限制、Thinking 格式、参数缺失/额外参数、非法请求)→ 不计入熔断器 + 不重试 + 直接返回
 }
 
+/**
+ * 从错误对象中提取用于规则匹配的内容
+ *
+ * 优先使用整个响应体(upstreamError.body),这样规则可以匹配响应中的任何内容
+ * 如果没有响应体,则使用错误消息
+ */
+function extractErrorContentForDetection(error: Error): string {
+  // 优先使用整个响应体进行规则匹配
+  if (error instanceof ProxyError && error.upstreamError?.body) {
+    return error.upstreamError.body;
+  }
+  return error.message;
+}
+
+/**
+ * 错误规则检测结果缓存
+ *
+ * 使用 WeakMap 避免内存泄漏,同一个 Error 对象只检测一次
+ * 这样 isNonRetryableClientError 和 getErrorOverrideMessage 可以复用检测结果
+ */
+const errorDetectionCache = new WeakMap<Error, ErrorDetectionResult>();
+
+/**
+ * 检测错误规则(带缓存)
+ *
+ * 同一个 Error 对象只执行一次规则匹配,后续调用直接返回缓存结果
+ *
+ * 优化:避免在规则尚未初始化时缓存空结果
+ * - 如果规则已初始化,正常缓存结果
+ * - 如果规则未初始化,触发异步加载并返回同步结果(可能为空)
+ *   后续请求会自动获取正确的缓存结果
+ */
+function detectErrorRuleOnce(error: Error): ErrorDetectionResult {
+  const cached = errorDetectionCache.get(error);
+  if (cached) {
+    return cached;
+  }
+
+  const content = extractErrorContentForDetection(error);
+
+  // 避免在规则尚未初始化时缓存可能不完整的结果
+  if (!errorRuleDetector.hasInitialized()) {
+    // 触发异步初始化,但不阻塞当前请求
+    void errorRuleDetector
+      .detectAsync(content)
+      .then((result) => errorDetectionCache.set(error, result))
+      .catch(() => undefined);
+
+    // 返回同步结果(可能为空),不缓存以允许后续请求重新检测
+    return errorRuleDetector.detect(content);
+  }
+
+  const result = errorRuleDetector.detect(content);
+  errorDetectionCache.set(error, result);
+  return result;
+}
+
 export function isNonRetryableClientError(error: Error): boolean {
-  // 确定要检测的内容
-  // 优先使用整个响应体,这样规则可以匹配响应中的任何内容
-  let contentToCheck = error.message;
+  // 使用缓存的检测结果,避免重复执行规则匹配
+  return detectErrorRuleOnce(error).matched;
+}
 
-  if (error instanceof ProxyError && error.upstreamError?.body) {
-    contentToCheck = error.upstreamError.body;
+/**
+ * 错误覆写结果
+ */
+export interface ErrorOverrideResult {
+  /** 覆写的响应体(可选,null 表示不覆写响应体,仅覆写状态码) */
+  response: ErrorOverrideResponse | null;
+  /** 覆写的状态码(可选,null 表示透传上游状态码) */
+  statusCode: number | null;
+}
+
+/**
+ * 检测错误并返回覆写配置(如果配置了)
+ *
+ * 用于在返回错误响应时应用覆写,将复杂的上游错误转换为友好的用户提示
+ * 支持三种覆写模式:
+ * 1. 仅覆写响应体
+ * 2. 仅覆写状态码
+ * 3. 同时覆写响应体和状态码
+ *
+ * @param error - 错误对象
+ * @returns 覆写配置(如果配置了响应体或状态码),否则返回 undefined
+ */
+export function getErrorOverride(error: Error): ErrorOverrideResult | undefined {
+  // 使用缓存的检测结果,避免重复执行规则匹配
+  const result = detectErrorRuleOnce(error);
+
+  // 只要配置了响应体或状态码,就返回覆写配置
+  if (result.matched && (result.overrideResponse || result.overrideStatusCode)) {
+    return {
+      response: result.overrideResponse ?? null,
+      statusCode: result.overrideStatusCode ?? null,
+    };
   }
 
-  // 使用 ErrorRuleDetector 检测规则,支持数据库驱动的动态规则
-  return errorRuleDetector.detect(contentToCheck).matched;
+  return undefined;
 }
 
 /**

+ 8 - 1
src/app/v1/_lib/proxy/responses.ts

@@ -3,7 +3,8 @@ export class ProxyResponses {
     status: number,
     message: string,
     errorType?: string,
-    details?: Record<string, unknown>
+    details?: Record<string, unknown>,
+    requestId?: string
   ): Response {
     const payload: {
       error: {
@@ -12,6 +13,7 @@ export class ProxyResponses {
         code?: string;
         details?: Record<string, unknown>;
       };
+      request_id?: string;
     } = {
       error: {
         message,
@@ -29,6 +31,11 @@ export class ProxyResponses {
       payload.error.details = details;
     }
 
+    // 透传上游 request_id(可选)
+    if (requestId) {
+      payload.request_id = requestId;
+    }
+
     return new Response(JSON.stringify(payload), {
       status,
       headers: {

+ 6 - 0
src/drizzle/schema.ts

@@ -298,6 +298,12 @@ export const errorRules = pgTable('error_rules', {
     .$type<'regex' | 'contains' | 'exact'>(),
   category: varchar('category', { length: 50 }).notNull(),
   description: text('description'),
+  // 覆写响应体(JSONB):匹配成功时用此响应替换原始错误响应
+  // 格式参考 Claude API: { type: "error", error: { type: "...", message: "..." }, request_id?: "..." }
+  // null = 不覆写,保留原始错误响应
+  overrideResponse: jsonb('override_response'),
+  // 覆写状态码:null = 透传上游状态码
+  overrideStatusCode: integer('override_status_code'),
   isEnabled: boolean('is_enabled').notNull().default(true),
   isDefault: boolean('is_default').notNull().default(false),
   priority: integer('priority').notNull().default(0),

+ 88 - 0
src/lib/error-override-validator.ts

@@ -0,0 +1,88 @@
+/**
+ * 错误覆写响应验证工具
+ *
+ * 提供统一的 JSON 结构验证,防止纯文本或畸形数据透传给客户端。
+ * 在规则加载阶段和运行时响应阶段复用同一验证逻辑。
+ */
+
+import type { ErrorOverrideResponse } from "@/repository/error-rules";
+
+/** 覆写响应体最大字节数限制 (10KB) */
+const MAX_OVERRIDE_RESPONSE_BYTES = 10 * 1024;
+
+/**
+ * 验证错误覆写响应的 JSON 结构是否合法(返回具体错误消息)
+ *
+ * 必须满足的结构:
+ * {
+ *   "type": "error",
+ *   "error": {
+ *     "type": string,
+ *     "message": string
+ *   }
+ * }
+ *
+ * @param response - 待验证的响应对象
+ * @returns 错误消息(如果验证失败)或 null(验证通过)
+ */
+export function validateErrorOverrideResponse(response: unknown): string | null {
+  // 检查是否为纯对象(排除 null 和数组)
+  if (!response || typeof response !== "object" || Array.isArray(response)) {
+    return "覆写响应必须是对象";
+  }
+
+  const obj = response as Record<string, unknown>;
+
+  // 顶层 type 必须为 "error"
+  if (typeof obj.type !== "string" || obj.type.trim().length === 0) {
+    return "覆写响应缺少 type 字段";
+  }
+  if (obj.type !== "error") {
+    return '覆写响应 type 字段必须为 "error"';
+  }
+
+  // error 对象存在且不是数组
+  if (!obj.error || typeof obj.error !== "object" || Array.isArray(obj.error)) {
+    return "覆写响应缺少 error 对象";
+  }
+
+  const errorObj = obj.error as Record<string, unknown>;
+
+  if (typeof errorObj.type !== "string" || errorObj.type.trim().length === 0) {
+    return "覆写响应 error.type 字段缺失或为空";
+  }
+
+  // message 允许为空字符串,运行时将回退到原始错误消息
+  if (typeof errorObj.message !== "string") {
+    return "覆写响应 error.message 字段必须是字符串";
+  }
+
+  if (obj.request_id !== undefined && typeof obj.request_id !== "string") {
+    return "覆写响应 request_id 字段必须是字符串";
+  }
+
+  // 检查响应体大小限制
+  try {
+    const jsonString = JSON.stringify(response);
+    const byteLength = new TextEncoder().encode(jsonString).length;
+    if (byteLength > MAX_OVERRIDE_RESPONSE_BYTES) {
+      return `覆写响应体大小 (${Math.round(byteLength / 1024)}KB) 超过限制 (10KB)`;
+    }
+  } catch {
+    return "覆写响应无法序列化为 JSON";
+  }
+
+  return null;
+}
+
+/**
+ * 验证错误覆写响应的 JSON 结构是否合法(类型守卫)
+ *
+ * @param response - 待验证的响应对象
+ * @returns 是否为合法的 ErrorOverrideResponse
+ */
+export function isValidErrorOverrideResponse(
+  response: unknown
+): response is ErrorOverrideResponse {
+  return validateErrorOverrideResponse(response) === null;
+}

+ 69 - 11
src/lib/error-rule-detector.ts

@@ -10,9 +10,10 @@
  * - EventEmitter 驱动的自动缓存刷新
  */
 
-import { getActiveErrorRules } from "@/repository/error-rules";
+import { getActiveErrorRules, type ErrorOverrideResponse } from "@/repository/error-rules";
 import { logger } from "@/lib/logger";
 import { eventEmitter } from "@/lib/event-emitter";
+import { isValidErrorOverrideResponse } from "@/lib/error-override-validator";
 import safeRegex from "safe-regex";
 
 /**
@@ -23,6 +24,10 @@ export interface ErrorDetectionResult {
   category?: string; // 触发的错误分类
   pattern?: string; // 匹配的规则模式
   matchType?: string; // 匹配类型(regex/contains/exact)
+  /** 覆写响应体:如果配置了则用此响应替换原始错误响应 */
+  overrideResponse?: ErrorOverrideResponse;
+  /** 覆写状态码:如果配置了则用此状态码替换原始状态码 */
+  overrideStatusCode?: number;
 }
 
 /**
@@ -32,6 +37,8 @@ interface RegexPattern {
   pattern: RegExp;
   category: string;
   description?: string;
+  overrideResponse?: ErrorOverrideResponse;
+  overrideStatusCode?: number;
 }
 
 /**
@@ -41,6 +48,8 @@ interface ContainsPattern {
   text: string;
   category: string;
   description?: string;
+  overrideResponse?: ErrorOverrideResponse;
+  overrideStatusCode?: number;
 }
 
 /**
@@ -50,6 +59,8 @@ interface ExactPattern {
   text: string;
   category: string;
   description?: string;
+  overrideResponse?: ErrorOverrideResponse;
+  overrideStatusCode?: number;
 }
 
 /**
@@ -125,33 +136,51 @@ class ErrorRuleDetector {
         throw dbError;
       }
 
-      // 清空旧缓存
-      this.regexPatterns = [];
-      this.containsPatterns = [];
-      this.exactPatterns.clear();
+      // 使用局部变量收集新数据,避免 reload 期间 detect() 返回空结果
+      const newRegexPatterns: RegexPattern[] = [];
+      const newContainsPatterns: ContainsPattern[] = [];
+      const newExactPatterns = new Map<string, ExactPattern>();
 
       // 按类型分组加载规则
       let validRegexCount = 0;
       let skippedRedosCount = 0;
+      let skippedInvalidResponseCount = 0;
 
       for (const rule of rules) {
+        // 在加载阶段验证 overrideResponse 格式,过滤畸形数据
+        let validatedOverrideResponse: ErrorOverrideResponse | undefined = undefined;
+        if (rule.overrideResponse) {
+          if (isValidErrorOverrideResponse(rule.overrideResponse)) {
+            validatedOverrideResponse = rule.overrideResponse;
+          } else {
+            logger.warn(
+              `[ErrorRuleDetector] Invalid override_response for rule ${rule.id} (pattern: ${rule.pattern}), skipping response override`
+            );
+            skippedInvalidResponseCount++;
+          }
+        }
+
         switch (rule.matchType) {
           case "contains": {
             const lowerText = rule.pattern.toLowerCase();
-            this.containsPatterns.push({
+            newContainsPatterns.push({
               text: lowerText,
               category: rule.category,
               description: rule.description ?? undefined,
+              overrideResponse: validatedOverrideResponse,
+              overrideStatusCode: rule.overrideStatusCode ?? undefined,
             });
             break;
           }
 
           case "exact": {
             const lowerText = rule.pattern.toLowerCase();
-            this.exactPatterns.set(lowerText, {
+            newExactPatterns.set(lowerText, {
               text: lowerText,
               category: rule.category,
               description: rule.description ?? undefined,
+              overrideResponse: validatedOverrideResponse,
+              overrideStatusCode: rule.overrideStatusCode ?? undefined,
             });
             break;
           }
@@ -168,10 +197,12 @@ class ErrorRuleDetector {
               }
 
               const pattern = new RegExp(rule.pattern, "i");
-              this.regexPatterns.push({
+              newRegexPatterns.push({
                 pattern,
                 category: rule.category,
                 description: rule.description ?? undefined,
+                overrideResponse: validatedOverrideResponse,
+                overrideStatusCode: rule.overrideStatusCode ?? undefined,
               });
               validRegexCount++;
             } catch (error) {
@@ -185,13 +216,25 @@ class ErrorRuleDetector {
         }
       }
 
+      // 原子替换:确保 detect() 始终看到一致的数据集
+      this.regexPatterns = newRegexPatterns;
+      this.containsPatterns = newContainsPatterns;
+      this.exactPatterns = newExactPatterns;
+
       this.lastReloadTime = Date.now();
       this.isInitialized = true; // 标记为已初始化
 
+      const skippedInfo = [
+        skippedRedosCount > 0 ? `${skippedRedosCount} ReDoS` : "",
+        skippedInvalidResponseCount > 0 ? `${skippedInvalidResponseCount} invalid response` : "",
+      ]
+        .filter(Boolean)
+        .join(", ");
+
       logger.info(
         `[ErrorRuleDetector] Loaded ${rules.length} error rules: ` +
-          `contains=${this.containsPatterns.length}, exact=${this.exactPatterns.size}, ` +
-          `regex=${validRegexCount}${skippedRedosCount > 0 ? ` (skipped ${skippedRedosCount} ReDoS)` : ""}`
+        `contains=${newContainsPatterns.length}, exact=${newExactPatterns.size}, ` +
+        `regex=${validRegexCount}${skippedInfo ? ` (skipped: ${skippedInfo})` : ""}`
       );
     } catch (error) {
       logger.error("[ErrorRuleDetector] Failed to reload error rules:", error);
@@ -250,6 +293,8 @@ class ErrorRuleDetector {
           category: pattern.category,
           pattern: pattern.text,
           matchType: "contains",
+          overrideResponse: pattern.overrideResponse,
+          overrideStatusCode: pattern.overrideStatusCode,
         };
       }
     }
@@ -262,17 +307,21 @@ class ErrorRuleDetector {
         category: exactMatch.category,
         pattern: exactMatch.text,
         matchType: "exact",
+        overrideResponse: exactMatch.overrideResponse,
+        overrideStatusCode: exactMatch.overrideStatusCode,
       };
     }
 
     // 3. 正则匹配(最慢,但最灵活)
-    for (const { pattern, category } of this.regexPatterns) {
+    for (const { pattern, category, overrideResponse, overrideStatusCode } of this.regexPatterns) {
       if (pattern.test(errorMessage)) {
         return {
           matched: true,
           category,
           pattern: pattern.source,
           matchType: "regex",
+          overrideResponse,
+          overrideStatusCode,
         };
       }
     }
@@ -295,6 +344,15 @@ class ErrorRuleDetector {
     };
   }
 
+  /**
+   * 检查是否完成至少一次初始化
+   *
+   * 用于避免未加载完成时缓存空结果,导致后续请求无法命中规则
+   */
+  hasInitialized(): boolean {
+    return this.isInitialized;
+  }
+
   /**
    * 检查缓存是否为空
    */

+ 66 - 3
src/repository/error-rules.ts

@@ -4,6 +4,23 @@ import { db } from "@/drizzle/db";
 import { errorRules } from "@/drizzle/schema";
 import { eq, desc } from "drizzle-orm";
 import { eventEmitter } from "@/lib/event-emitter";
+import { validateErrorOverrideResponse } from "@/lib/error-override-validator";
+import { logger } from "@/lib/logger";
+
+/**
+ * 错误覆写响应体类型
+ * 参考 Claude API 错误格式: https://platform.claude.com/docs/en/api/errors
+ */
+export interface ErrorOverrideResponse {
+  type: string; // 通常为 "error"
+  error: {
+    type: string; // 错误类型,如 "invalid_request_error"
+    message: string; // 错误消息
+    [key: string]: unknown; // 其他可选字段
+  };
+  request_id?: string; // 请求 ID(会自动从上游注入)
+  [key: string]: unknown; // 其他可选字段
+}
 
 export interface ErrorRule {
   id: number;
@@ -11,6 +28,10 @@ export interface ErrorRule {
   matchType: "regex" | "contains" | "exact";
   category: string;
   description: string | null;
+  /** 覆写响应体(JSON):匹配成功时用此响应替换原始错误响应,null 表示不覆写 */
+  overrideResponse: ErrorOverrideResponse | null;
+  /** 覆写状态码:null 表示透传上游状态码 */
+  overrideStatusCode: number | null;
   isEnabled: boolean;
   isDefault: boolean;
   priority: number;
@@ -18,6 +39,33 @@ export interface ErrorRule {
   updatedAt: Date;
 }
 
+/**
+ * 验证并清理 overrideResponse 字段
+ *
+ * 从数据库读取的 JSONB 数据可能被手动修改为畸形格式,
+ * 此函数在 repository 层进行运行时验证,确保返回给上层的数据格式正确
+ *
+ * @param raw - 数据库中的原始值
+ * @param context - 调用上下文(用于日志)
+ * @returns 验证通过的 ErrorOverrideResponse 或 null
+ */
+function sanitizeOverrideResponse(
+  raw: unknown,
+  context: string
+): ErrorOverrideResponse | null {
+  if (raw === null || raw === undefined) {
+    return null;
+  }
+
+  const validationError = validateErrorOverrideResponse(raw);
+  if (validationError) {
+    logger.warn(`[ErrorRulesRepository] Invalid overrideResponse in ${context}: ${validationError}`);
+    return null;
+  }
+
+  return raw as ErrorOverrideResponse;
+}
+
 /**
  * 获取所有启用的错误规则(用于缓存加载和运行时检测)
  */
@@ -33,6 +81,8 @@ export async function getActiveErrorRules(): Promise<ErrorRule[]> {
     matchType: r.matchType as "regex" | "contains" | "exact",
     category: r.category,
     description: r.description,
+    overrideResponse: sanitizeOverrideResponse(r.overrideResponse, `getActiveErrorRules id=${r.id}`),
+    overrideStatusCode: r.overrideStatusCode,
     isEnabled: r.isEnabled,
     isDefault: r.isDefault,
     priority: r.priority,
@@ -59,6 +109,8 @@ export async function getErrorRuleById(id: number): Promise<ErrorRule | null> {
     matchType: result.matchType as "regex" | "contains" | "exact",
     category: result.category,
     description: result.description,
+    overrideResponse: sanitizeOverrideResponse(result.overrideResponse, `getErrorRuleById id=${result.id}`),
+    overrideStatusCode: result.overrideStatusCode,
     isEnabled: result.isEnabled,
     isDefault: result.isDefault,
     priority: result.priority,
@@ -81,6 +133,8 @@ export async function getAllErrorRules(): Promise<ErrorRule[]> {
     matchType: r.matchType as "regex" | "contains" | "exact",
     category: r.category,
     description: r.description,
+    overrideResponse: sanitizeOverrideResponse(r.overrideResponse, `getAllErrorRules id=${r.id}`),
+    overrideStatusCode: r.overrideStatusCode,
     isEnabled: r.isEnabled,
     isDefault: r.isDefault,
     priority: r.priority,
@@ -97,6 +151,8 @@ export async function createErrorRule(data: {
   matchType: "regex" | "contains" | "exact";
   category: string;
   description?: string;
+  overrideResponse?: ErrorOverrideResponse | null;
+  overrideStatusCode?: number | null;
   priority?: number;
 }): Promise<ErrorRule> {
   const [result] = await db
@@ -106,6 +162,8 @@ export async function createErrorRule(data: {
       matchType: data.matchType,
       category: data.category,
       description: data.description,
+      overrideResponse: data.overrideResponse,
+      overrideStatusCode: data.overrideStatusCode ?? null,
       priority: data.priority ?? 0,
     })
     .returning();
@@ -116,6 +174,8 @@ export async function createErrorRule(data: {
     matchType: result.matchType as "regex" | "contains" | "exact",
     category: result.category,
     description: result.description,
+    overrideResponse: sanitizeOverrideResponse(result.overrideResponse, `createErrorRule id=${result.id}`),
+    overrideStatusCode: result.overrideStatusCode,
     isEnabled: result.isEnabled,
     isDefault: result.isDefault,
     priority: result.priority,
@@ -134,6 +194,8 @@ export async function updateErrorRule(
     matchType: "regex" | "contains" | "exact";
     category: string;
     description: string;
+    overrideResponse: ErrorOverrideResponse | null;
+    overrideStatusCode: number | null;
     isEnabled: boolean;
     priority: number;
   }>
@@ -157,6 +219,8 @@ export async function updateErrorRule(
     matchType: result.matchType as "regex" | "contains" | "exact",
     category: result.category,
     description: result.description,
+    overrideResponse: sanitizeOverrideResponse(result.overrideResponse, `updateErrorRule id=${result.id}`),
+    overrideStatusCode: result.overrideStatusCode,
     isEnabled: result.isEnabled,
     isDefault: result.isDefault,
     priority: result.priority,
@@ -278,9 +342,8 @@ export async function syncDefaultErrorRules(): Promise<number> {
     }
   });
 
-  // 通知 ErrorRuleDetector 重新加载缓存
-  eventEmitter.emit("errorRulesUpdated");
-
+  // 注意:不在此处触发 eventEmitter,由调用方决定是否刷新缓存
+  // 这样可以避免调用方手动 reload() 时导致双重刷新
   return DEFAULT_ERROR_RULES.length;
 }