diff --git a/CHANGELOG.md b/CHANGELOG.md index d233bab95..9b1b3c894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,50 @@ All notable changes to this project will be documented in this file. ### Added +- Add provider availability monitoring dashboard with real-time health status, metrics, and heatmap visualization (#216) @ding113 +- Add smart circuit breaker probing for faster provider recovery with configurable intervals (#216) @ding113 +- Add enhanced provider testing with three-layer validation and preset templates for relay service verification (#216) @ding113 - Add real-time monitoring big screen dashboard with live metrics, 24h trends, provider slots status, and activity stream (#184) @ding113 - Add dark mode support with theme switcher in Dashboard and settings pages (#171) @ding113 - Add MCP (Model Context Protocol) passthrough functionality to forward tool calls to third-party AI services (#193) @ding113 +- Add provider API test improvements with streaming response detection and enhanced error parsing (#199) @ding113 +- Add configurable API test timeout via `API_TEST_TIMEOUT_MS` environment variable (#199) @ding113 ### Changed +- Improve provider page performance by fixing N+1 queries and SQL full table scans (#216) @ding113 +- Enhance error parsing with nested error structure support for relay services (#216) @ding113 +- Adjust streaming idle timeout range from 1-120s to 60-600s (0 to disable) (#216) @ding113 +- Add provider-specific User-Agent headers to avoid Cloudflare detection (#210) @ding113 +- Increase provider dialog width to prevent horizontal scrollbar on long model redirect names (#210) @ding113 - Enhance data dashboard with comprehensive optimizations and improvements (#183) @ding113 +- Update default provider timeout to unlimited for better compatibility (#199) @ding113 +- Adjust streaming silent period timeout from 10s to 300s (#199) @ding113 +- Improve usage records status code color display for better visibility (#199) @ding113 +- Clarify provider response model labeling (#199) @ding113 ### Fixed +- Fix session binding not updating after provider failover, causing repeated attempts to failed providers (#220) @ding113 +- Fix provider availability monitoring page sorting order so high-availability providers rank first (#219) @ding113 +- Fix provider daily usage statistics JSONB field name error (#216) @ding113 +- Fix response content validation failure in provider testing (#216) @ding113 +- Fix login redirect displaying duplicate language prefix (#216) @ding113 +- Fix zh-TW missing 8 translation keys for apiTest (#216) @ding113 +- Remove DialogContent hardcoded `sm:max-w-lg` width constraint (#216) @ding113 +- Fix model test disclaimer display order (#210) @ding113 +- Fix provider statistics, group settings persistence, and usage records date filtering; consolidate migrations 0020-0025 into single idempotent file (#207) @ding113 - Fix database migration duplicate enum type creation error (#181) @ding113 - Fix error handling and status codes in response handler, improve user management page UX (#179) @ding113 - Fix infinite loop in leaderboard tab switching (#178) @ding113 - Fix CI failures: Prettier formatting and React Hooks ESLint error in theme-switcher (#173) @ding113 +- Fix Gemini model redirect not working correctly (#199) @ding113 +- Fix model redirect info not being saved to database (#199) @ding113 +- Fix provider multi-tag matching issue (#199) @ding113 +- Fix error rules regex matching and cache refresh issues (#199) @ding113 +- Fix proxy fallback "Body has already been read" error (#199) @ding113 +- Fix ErrorRuleDetector lazy initialization race condition (#199) @ding113 +- Fix Anthropic API test sending duplicate auth headers (#199) @ding113 +- Fix Codex API test request body format (#199) @ding113 +- Fix Pino logger timestamp configuration placement (#199) @ding113 +- Fix data import cross-version compatibility and error prompts (#199) @ding113 diff --git a/VERSION b/VERSION index 940ac09aa..9e29e1061 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.9 +0.3.15 diff --git a/drizzle.config.ts b/drizzle.config.ts index db671a415..f2b45079d 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,8 +1,12 @@ import { config } from "dotenv"; import { defineConfig } from "drizzle-kit"; -// Load environment variables from .env.local -config({ path: ".env.local" }); +// Load environment variables following Next.js priority order +// Priority: .env.development.local > .env.local > .env.development > .env +const envFiles = [".env.development.local", ".env.local", ".env.development", ".env"]; +for (const envFile of envFiles) { + config({ path: envFile }); +} export default defineConfig({ out: "./drizzle", diff --git a/drizzle/0021_broad_black_panther.sql b/drizzle/0021_broad_black_panther.sql new file mode 100644 index 000000000..4dfe8289d --- /dev/null +++ b/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; \ No newline at end of file diff --git a/drizzle/meta/0021_snapshot.json b/drizzle/meta/0021_snapshot.json new file mode 100644 index 000000000..e782c7d73 --- /dev/null +++ b/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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3e19e663a..d432a0d1f 100644 --- a/drizzle/meta/_journal.json +++ b/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 } ] } \ No newline at end of file diff --git a/messages/en/settings.json b/messages/en/settings.json index fa047b022..23541ad95 100644 --- a/messages/en/settings.json +++ b/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..." }, diff --git a/messages/ja/settings.json b/messages/ja/settings.json index 446652e36..efc849d8c 100644 --- a/messages/ja/settings.json +++ b/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": "保存中..." }, diff --git a/messages/ru/settings.json b/messages/ru/settings.json index bf9116338..c48c48b98 100644 --- a/messages/ru/settings.json +++ b/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": "Сохранение..." }, diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index 61175a967..7fd6f8212 100644 --- a/messages/zh-CN/settings.json +++ b/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": "保存中..." }, diff --git a/messages/zh-TW/settings.json b/messages/zh-TW/settings.json index c04e1a30e..5d074112d 100644 --- a/messages/zh-TW/settings.json +++ b/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": "儲存中..." }, diff --git a/public/seed/litellm-prices.json b/public/seed/litellm-prices.json index 901bceaa3..f07f1cbe5 100644 --- a/public/seed/litellm-prices.json +++ b/public/seed/litellm-prices.json @@ -249,6 +249,12 @@ "/v1/images/generations" ] }, + "amazon.nova-canvas-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 2600, + "mode": "image_generation", + "output_cost_per_image": 0.06 + }, "amazon.nova-lite-v1:0": { "input_cost_per_token": 6e-08, "litellm_provider": "bedrock_converse", @@ -678,6 +684,32 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "anthropic.claude-sonnet-4-20250514-v1:0": { "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, @@ -708,6 +740,36 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "anthropic.claude-v1": { "input_cost_per_token": 8e-06, "litellm_provider": "bedrock", @@ -1087,6 +1149,60 @@ "output_cost_per_token": 1.5e-05, "supports_function_calling": true }, + "azure/claude-haiku-4-5": { + "input_cost_per_token": 1e-06, + "litellm_provider": "azure_anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/claude-opus-4-1": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure_anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/claude-sonnet-4-5": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure_anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, "azure/computer-use-preview": { "input_cost_per_token": 3e-06, "litellm_provider": "azure", @@ -5581,8 +5697,12 @@ "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 1.65e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, "supports_function_calling": true, "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true @@ -5722,8 +5842,12 @@ "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 1.65e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, "supports_function_calling": true, "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true @@ -6458,6 +6582,31 @@ "supports_web_search": true, "tool_use_system_prompt_tokens": 346 }, + "claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "claude-opus-4-1": { "cache_creation_input_token_cost": 1.875e-05, "cache_creation_input_token_cost_above_1hr": 3e-05, @@ -6541,6 +6690,33 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "claude-opus-4-5-20251101": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_creation_input_token_cost_above_1hr": 1e-05, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "claude-sonnet-4-20250514": { "deprecation_date": "2026-05-14", "cache_creation_input_token_cost": 3.75e-06, @@ -9352,6 +9528,15 @@ "output_cost_per_token": 0.0, "supports_embedding_image_input": true }, + "embed-multilingual-light-v3.0": { + "input_cost_per_token": 1e-04, + "litellm_provider": "cohere", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "embedding", + "output_cost_per_token": 0.0, + "supports_embedding_image_input": true + }, "eu.amazon.nova-lite-v1:0": { "input_cost_per_token": 7.8e-08, "litellm_provider": "bedrock_converse", @@ -9961,6 +10146,19 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "fireworks_ai/accounts/fireworks/models/glm-4p6": { + "input_cost_per_token": 0.55e-06, + "output_cost_per_token": 2.19e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 202800, + "max_output_tokens": 202800, + "max_tokens": 202800, + "mode": "chat", + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, "fireworks_ai/accounts/fireworks/models/gpt-oss-120b": { "input_cost_per_token": 1.5e-07, "litellm_provider": "fireworks_ai", @@ -19828,6 +20026,25 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "openrouter/anthropic/claude-opus-4.5": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "openrouter/anthropic/claude-sonnet-4.5": { "input_cost_per_image": 0.0048, "cache_creation_input_token_cost": 3.75e-06, @@ -22235,6 +22452,16 @@ "output_cost_per_token": 0.0, "output_vector_size": 1536 }, + "text-embedding-ada-002-v2": { + "input_cost_per_token": 1e-07, + "input_cost_per_token_batches": 5e-08, + "litellm_provider": "openai", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_cost_per_token_batches": 0.0 + }, "text-embedding-large-exp-03-07": { "input_cost_per_character": 2.5e-08, "input_cost_per_token": 1e-07, @@ -23052,6 +23279,32 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "us.anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "us.anthropic.claude-sonnet-4-20250514-v1:0": { "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, @@ -24399,6 +24652,58 @@ "supports_tool_choice": true, "supports_vision": true }, + "vertex_ai/claude-opus-4-5": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-opus-4-5@20251101": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "vertex_ai/claude-sonnet-4-5": { "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, @@ -25929,6 +26234,15 @@ "supports_parallel_function_calling": false, "supports_vision": false }, + "watsonx/whisper-large-v3-turbo": { + "input_cost_per_second": 0.0001, + "output_cost_per_second": 0.0001, + "litellm_provider": "watsonx", + "mode": "audio_transcription", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, "whisper-1": { "input_cost_per_second": 0.0001, "litellm_provider": "openai", diff --git a/src/actions/error-rules.ts b/src/actions/error-rules.ts index dbeecd0b6..e3225dc63 100644 --- a/src/actions/error-rules.ts +++ b/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; +} + /** * 获取所有错误规则列表 */ @@ -42,6 +69,10 @@ export async function createErrorRuleAction(data: { | "cache_limit"; matchType?: "contains" | "exact" | "regex"; description?: string; + /** 覆写响应体(JSON 格式,符合 Claude API 错误格式) */ + overrideResponse?: repo.ErrorOverrideResponse | null; + /** 覆写状态码:null 表示透传上游状态码 */ + overrideStatusCode?: number | null; }): Promise> { 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 { }; } - // 刷新缓存 + // 刷新缓存(直接调用 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,151 @@ 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; + + // 3. 处理 message 为空的情况(运行时会回退到原始错误消息) + const overrideErrorObj = detection.overrideResponse.error as Record; + 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: "测试错误规则失败", + }; + } +} + /** * 获取缓存统计信息 */ diff --git a/src/app/[locale]/settings/data/_components/database-import.tsx b/src/app/[locale]/settings/data/_components/database-import.tsx index 8d93a7720..275f5fb2c 100644 --- a/src/app/[locale]/settings/data/_components/database-import.tsx +++ b/src/app/[locale]/settings/data/_components/database-import.tsx @@ -258,19 +258,21 @@ export function DatabaseImport() { {t('confirmTitle')} - -

- {cleanFirst ? t('confirmOverwrite') : t('confirmMerge')} -

-

- {cleanFirst ? t('warningOverwrite') : t('warningMerge')} -

-

- {t('backupFile')} {selectedFile?.name} -

-

- {t('backupRecommendation')} -

+ +
+

+ {cleanFirst ? t('confirmOverwrite') : t('confirmMerge')} +

+

+ {cleanFirst ? t('warningOverwrite') : t('warningMerge')} +

+

+ {t('backupFile')} {selectedFile?.name} +

+

+ {t('backupRecommendation')} +

+
diff --git a/src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx b/src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx index 7d40e2cef..e25b22a47 100644 --- a/src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx +++ b/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(""); 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() { /> + + {pattern && (
diff --git a/src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx b/src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx index 2c2f3900b..0dd557b42 100644 --- a/src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx +++ b/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(""); // 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 />
+ + {pattern && (
diff --git a/src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx b/src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx new file mode 100644 index 000000000..3dc3ffe28 --- /dev/null +++ b/src/app/[locale]/settings/error-rules/_components/error-rule-tester.tsx @@ -0,0 +1,193 @@ +"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(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 ( +
+
+ +