diff --git a/drizzle/0041_sticky_jackal.sql b/drizzle/0041_sticky_jackal.sql new file mode 100644 index 000000000..1d138ea7c --- /dev/null +++ b/drizzle/0041_sticky_jackal.sql @@ -0,0 +1,4 @@ +ALTER TABLE "request_filters" ADD COLUMN "binding_type" varchar(20) DEFAULT 'global' NOT NULL;--> statement-breakpoint +ALTER TABLE "request_filters" ADD COLUMN "provider_ids" jsonb;--> statement-breakpoint +ALTER TABLE "request_filters" ADD COLUMN "group_tags" jsonb;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_request_filters_binding" ON "request_filters" USING btree ("is_enabled","binding_type"); \ No newline at end of file diff --git a/drizzle/meta/0041_snapshot.json b/drizzle/meta/0041_snapshot.json new file mode 100644 index 000000000..c44b2e6fe --- /dev/null +++ b/drizzle/meta/0041_snapshot.json @@ -0,0 +1,1991 @@ +{ + "id": "bcbf2ce2-bc13-49f3-b014-8a34a2bf9a6a", + "prevId": "1da9ba1f-bef1-4e61-ac2c-43868c526b28", + "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": 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_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "provider_group": { + "name": "provider_group", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "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_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 + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "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 + }, + "ttfb_ms": { + "name": "ttfb_ms", + "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 + }, + "cache_creation_5m_input_tokens": { + "name": "cache_creation_5m_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_creation_1h_input_tokens": { + "name": "cache_creation_1h_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_applied": { + "name": "cache_ttl_applied", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_applied": { + "name": "context_1m_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack": { + "name": "error_stack", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_cause": { + "name": "error_cause", + "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_session_seq": { + "name": "idx_message_request_session_seq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sequence", + "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'" + }, + "preserve_client_ip": { + "name": "preserve_client_ip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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 + }, + "max_retry_attempts": { + "name": "max_retry_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "cache_ttl_preference": { + "name": "cache_ttl_preference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "context_1m_preference": { + "name": "context_1m_preference", + "type": "varchar(20)", + "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.request_filters": { + "name": "request_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "match_type": { + "name": "match_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "binding_type": { + "name": "binding_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'global'" + }, + "provider_ids": { + "name": "provider_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "group_tags": { + "name": "group_tags", + "type": "jsonb", + "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()" + } + }, + "indexes": { + "idx_request_filters_enabled": { + "name": "idx_request_filters_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": {} + }, + "idx_request_filters_scope": { + "name": "idx_request_filters_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_action": { + "name": "idx_request_filters_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_request_filters_binding": { + "name": "idx_request_filters_binding", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_type", + "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 + }, + "verbose_provider_error": { + "name": "verbose_provider_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_http2": { + "name": "enable_http2", + "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, + "default": "'default'" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "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_total_usd": { + "name": "limit_total_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "limit_concurrent_sessions": { + "name": "limit_concurrent_sessions", + "type": "integer", + "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'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_clients": { + "name": "allowed_clients", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "allowed_models": { + "name": "allowed_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "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_enabled_expires_at": { + "name": "idx_users_enabled_expires_at", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "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 0bfd887ea..b62252c5a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -288,6 +288,13 @@ "when": 1766509746306, "tag": "0040_bored_venus", "breakpoints": true + }, + { + "idx": 41, + "version": "7", + "when": 1766696732309, + "tag": "0041_sticky_jackal", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/en/settings.json b/messages/en/settings.json index 5b3044f2d..c6914b466 100644 --- a/messages/en/settings.json +++ b/messages/en/settings.json @@ -379,7 +379,7 @@ }, "notifications": { "title": "Push Notifications", - "description": "Configure WeChat Work robot push notifications", + "description": "Configure Webhook push notifications", "global": { "title": "Notification Master Switch", "description": "Enable or disable all push notification features", @@ -411,6 +411,9 @@ "enable": "Enable Cost Alert", "webhook": "Webhook URL", "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeWeCom": "WeCom", + "webhookTypeFeishu": "Feishu", + "webhookTypeUnknown": "Unknown platform. Please use WeCom or Feishu webhook URL", "threshold": "Alert Threshold", "thresholdLabel": "Alert Threshold: {percent}%", "thresholdHelp": "Alert when consumption reaches {percent}% of quota", @@ -426,7 +429,7 @@ "saveError": "Failed to save settings", "loadError": "Failed to load notification settings", "webhookRequired": "Please fill in Webhook URL first", - "testSuccess": "Test message sent, please check WeChat Work", + "testSuccess": "Test message sent", "testFailed": "Test failed", "testFailedRetry": "Test failed, please retry", "testError": "Test connection failed", @@ -574,6 +577,18 @@ "statusEnabled": "enabled", "statusDisabled": "disabled" }, + "inlineEdit": { + "save": "Save", + "cancel": "Cancel", + "saveSuccess": "Saved successfully", + "saveFailed": "Save failed", + "priorityLabel": "Priority", + "weightLabel": "Weight", + "costMultiplierLabel": "Cost Multiplier", + "priorityInvalid": "Please enter an integer >= 0", + "weightInvalid": "Please enter an integer between 1 and 100", + "costMultiplierInvalid": "Please enter a non-negative number" + }, "schedulingDialog": { "title": "Provider Scheduling Rules", "description": "Understand how the system intelligently selects upstream providers for high availability and cost optimization", @@ -1619,7 +1634,22 @@ "saving": "Saving...", "validation": { "fieldRequired": "Name and target are required" - } + }, + "bindingType": "Apply To", + "bindingGlobal": "All Providers (Global)", + "bindingProviders": "Specific Providers", + "bindingGroups": "Provider Groups", + "selectProviders": "Select providers...", + "selectGroups": "Select groups...", + "searchProviders": "Search providers...", + "searchGroups": "Search groups...", + "noProvidersFound": "No providers found", + "noGroupsFound": "No groups found", + "providersSelected": "{count} provider(s) selected", + "groupsSelected": "{count} group(s) selected", + "loading": "Loading...", + "clear": "Clear", + "selectAll": "Select All" }, "table": { "name": "Name", @@ -1628,6 +1658,7 @@ "target": "Target", "replacement": "Replacement", "priority": "Priority", + "apply": "Apply", "status": "Status", "createdAt": "Created At", "actions": "Actions" @@ -1641,7 +1672,10 @@ "set": "Set Header", "json_path": "JSON Path Replace", "text_replace": "Text Replace" - } + }, + "applyToAll": "Applied to all requests", + "providers": "Providers", + "groups": "Groups" }, "errorRules": { "nav": "Error Rules", diff --git a/messages/ja/settings.json b/messages/ja/settings.json index 58d493575..686a459e6 100644 --- a/messages/ja/settings.json +++ b/messages/ja/settings.json @@ -370,7 +370,7 @@ }, "notifications": { "title": "プッシュ通知", - "description": "WeCom(企業微信)ロボットのプッシュ通知を設定", + "description": "Webhook プッシュ通知を設定", "global": { "title": "通知マスタースイッチ", "description": "すべてのプッシュ通知機能を有効または無効にする", @@ -402,6 +402,9 @@ "enable": "コストアラートを有効にする", "webhook": "Webhook URL", "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeWeCom": "企業微信", + "webhookTypeFeishu": "飛書", + "webhookTypeUnknown": "不明なプラットフォーム。企業微信または飛書のWebhook URLを使用してください", "threshold": "アラートしきい値", "thresholdLabel": "アラートしきい値: {percent}%", "thresholdHelp": "消費がクォータの{percent}%に達した時にアラート", @@ -417,7 +420,7 @@ "saveError": "設定の保存に失敗しました", "loadError": "通知設定の読み込みに失敗しました", "webhookRequired": "まずWebhook URLを入力してください", - "testSuccess": "テストメッセージを送信しました。WeComを確認してください", + "testSuccess": "テストメッセージを送信しました", "testFailed": "テストに失敗しました", "testFailedRetry": "テストに失敗しました。再試行してください", "testError": "接続テストに失敗しました", @@ -1448,6 +1451,18 @@ "statusEnabled": "有効", "statusDisabled": "無効" }, + "inlineEdit": { + "save": "保存", + "cancel": "キャンセル", + "saveSuccess": "保存に成功しました", + "saveFailed": "保存に失敗しました", + "priorityLabel": "優先度", + "weightLabel": "重み", + "costMultiplierLabel": "コスト倍率", + "priorityInvalid": "0 以上の整数を入力してください", + "weightInvalid": "1〜100 の整数を入力してください", + "costMultiplierInvalid": "0以上の数値を入力してください" + }, "schedulingDialog": { "title": "プロバイダースケジューリングルール", "description": "システムが高可用性とコスト最適化のために上流プロバイダーをインテリジェントに選択する方法を理解する", @@ -1569,7 +1584,22 @@ "saving": "保存中...", "validation": { "fieldRequired": "名称と対象は必須です" - } + }, + "bindingType": "適用先", + "bindingGlobal": "全プロバイダー(グローバル)", + "bindingProviders": "特定のプロバイダー", + "bindingGroups": "プロバイダーグループ", + "selectProviders": "プロバイダーを選択...", + "selectGroups": "グループを選択...", + "searchProviders": "プロバイダー検索...", + "searchGroups": "グループ検索...", + "noProvidersFound": "プロバイダーが見つかりません", + "noGroupsFound": "グループが見つかりません", + "providersSelected": "{count}件のプロバイダーを選択", + "groupsSelected": "{count}件のグループを選択", + "loading": "読み込み中...", + "clear": "クリア", + "selectAll": "すべて選択" }, "table": { "name": "名称", @@ -1578,6 +1608,7 @@ "target": "対象", "replacement": "置換値", "priority": "優先度", + "apply": "適用", "status": "状態", "createdAt": "作成日時", "actions": "操作" @@ -1591,7 +1622,10 @@ "set": "ヘッダー設定", "json_path": "JSONパス置換", "text_replace": "テキスト置換" - } + }, + "applyToAll": "すべてのリクエストに適用", + "providers": "プロバイダー", + "groups": "グループ" }, "errorRules": { "nav": "エラールール", diff --git a/messages/ru/settings.json b/messages/ru/settings.json index 97ed8c673..e40b44475 100644 --- a/messages/ru/settings.json +++ b/messages/ru/settings.json @@ -370,7 +370,7 @@ }, "notifications": { "title": "Push-уведомления", - "description": "Настройка push-уведомлений робота WeChat Work", + "description": "Настройка push-уведомлений Webhook", "global": { "title": "Главный переключатель уведомлений", "description": "Включить или отключить все функции push-уведомлений", @@ -402,6 +402,9 @@ "enable": "Включить оповещение о расходах", "webhook": "Webhook URL", "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeWeCom": "WeCom", + "webhookTypeFeishu": "Feishu", + "webhookTypeUnknown": "Неизвестная платформа. Используйте URL вебхука WeCom или Feishu", "threshold": "Порог оповещения", "thresholdLabel": "Порог оповещения: {percent}%", "thresholdHelp": "Оповещение при достижении {percent}% квоты", @@ -417,7 +420,7 @@ "saveError": "Не удалось сохранить настройки", "loadError": "Не удалось загрузить настройки уведомлений", "webhookRequired": "Сначала заполните Webhook URL", - "testSuccess": "Тестовое сообщение отправлено, проверьте WeChat Work", + "testSuccess": "Тестовое сообщение отправлено", "testFailed": "Тест не пройден", "testFailedRetry": "Тест не пройден, попробуйте снова", "testError": "Ошибка тестирования подключения", @@ -1448,6 +1451,18 @@ "statusEnabled": "включен", "statusDisabled": "отключен" }, + "inlineEdit": { + "save": "Сохранить", + "cancel": "Отмена", + "saveSuccess": "Успешно сохранено", + "saveFailed": "Не удалось сохранить", + "priorityLabel": "Приоритет", + "weightLabel": "Вес", + "costMultiplierLabel": "Коэф цены", + "priorityInvalid": "Введите целое число >= 0", + "weightInvalid": "Введите целое число от 1 до 100", + "costMultiplierInvalid": "Введите число не меньше 0" + }, "schedulingDialog": { "title": "Правила планирования провайдеров", "description": "Узнайте, как система интеллектуально выбирает вышестоящих провайдеров для высокой доступности и оптимизации затрат", @@ -1569,7 +1584,22 @@ "saving": "Сохранение...", "validation": { "fieldRequired": "Название и цель обязательны" - } + }, + "bindingType": "Применить к", + "bindingGlobal": "Все провайдеры (глобально)", + "bindingProviders": "Конкретные провайдеры", + "bindingGroups": "Группы провайдеров", + "selectProviders": "Выберите провайдеров...", + "selectGroups": "Выберите группы...", + "searchProviders": "Поиск провайдеров...", + "searchGroups": "Поиск групп...", + "noProvidersFound": "Провайдеры не найдены", + "noGroupsFound": "Группы не найдены", + "providersSelected": "Выбрано провайдеров: {count}", + "groupsSelected": "Выбрано групп: {count}", + "loading": "Загрузка...", + "clear": "Очистить", + "selectAll": "Выбрать все" }, "table": { "name": "Название", @@ -1578,6 +1608,7 @@ "target": "Цель", "replacement": "Значение", "priority": "Приоритет", + "apply": "Область", "status": "Статус", "createdAt": "Создано", "actions": "Действия" @@ -1591,7 +1622,10 @@ "set": "Установить header", "json_path": "Замена по JSON пути", "text_replace": "Замена текста" - } + }, + "applyToAll": "Применяется ко всем запросам", + "providers": "Провайдеры", + "groups": "Группы" }, "errorRules": { "nav": "Правила ошибок", diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index 1e806e995..8ffa2ca75 100644 --- a/messages/zh-CN/settings.json +++ b/messages/zh-CN/settings.json @@ -187,6 +187,18 @@ "statusEnabled": "启用", "statusDisabled": "禁用" }, + "inlineEdit": { + "save": "保存", + "cancel": "取消", + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "priorityLabel": "优先级", + "weightLabel": "权重", + "costMultiplierLabel": "成本倍数", + "priorityInvalid": "请输入大于等于 0 的整数", + "weightInvalid": "请输入 1-100 之间的整数", + "costMultiplierInvalid": "请输入大于等于 0 的数字" + }, "schedulingDialog": { "title": "供应商调度规则说明", "description": "了解系统如何智能选择上游供应商,确保高可用性和成本优化", @@ -1296,7 +1308,22 @@ "saving": "保存中...", "validation": { "fieldRequired": "名称和目标为必填项" - } + }, + "bindingType": "应用范围", + "bindingGlobal": "所有Provider(全局)", + "bindingProviders": "指定Provider", + "bindingGroups": "Provider分组", + "selectProviders": "选择Provider...", + "selectGroups": "选择分组...", + "searchProviders": "搜索Provider...", + "searchGroups": "搜索分组...", + "noProvidersFound": "未找到Provider", + "noGroupsFound": "未找到分组", + "providersSelected": "已选 {count} 个Provider", + "groupsSelected": "已选 {count} 个分组", + "loading": "加载中...", + "clear": "清空", + "selectAll": "全选" }, "table": { "name": "名称", @@ -1305,6 +1332,7 @@ "target": "目标", "replacement": "替换值", "priority": "优先级", + "apply": "范围", "status": "状态", "createdAt": "创建时间", "actions": "操作" @@ -1318,7 +1346,10 @@ "set": "设置 Header", "json_path": "JSON 路径替换", "text_replace": "文本替换" - } + }, + "applyToAll": "应用于所有请求", + "providers": "供应商", + "groups": "组" }, "logs": { "title": "日志管理", @@ -1560,7 +1591,7 @@ }, "notifications": { "title": "消息推送", - "description": "配置企业微信机器人消息推送", + "description": "配置 Webhook 消息推送", "global": { "title": "通知总开关", "description": "启用或禁用所有消息推送功能", @@ -1592,6 +1623,9 @@ "enable": "启用成本预警", "webhook": "Webhook URL", "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeWeCom": "企业微信", + "webhookTypeFeishu": "飞书", + "webhookTypeUnknown": "未知平台,请使用企业微信或飞书的 Webhook URL", "threshold": "预警阈值", "thresholdLabel": "预警阈值: {percent}%", "thresholdHelp": "当消费达到配额的 {percent}% 时触发告警", @@ -1607,7 +1641,7 @@ "saveError": "保存设置失败", "loadError": "加载通知设置失败", "webhookRequired": "请先填写 Webhook URL", - "testSuccess": "测试消息已发送,请检查企业微信", + "testSuccess": "测试消息已发送", "testFailed": "测试失败", "testFailedRetry": "测试失败,请重试", "testError": "测试连接失败", diff --git a/messages/zh-TW/settings.json b/messages/zh-TW/settings.json index 1edfdb61c..807b73510 100644 --- a/messages/zh-TW/settings.json +++ b/messages/zh-TW/settings.json @@ -370,7 +370,7 @@ }, "notifications": { "title": "訊息推送", - "description": "設定企業微信機器人訊息推送", + "description": "設定 Webhook 訊息推送", "global": { "title": "通知總開關", "description": "啟用或停用所有訊息推送功能", @@ -402,6 +402,9 @@ "enable": "啟用成本預警", "webhook": "Webhook URL", "webhookPlaceholder": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...", + "webhookTypeWeCom": "企業微信", + "webhookTypeFeishu": "飛書", + "webhookTypeUnknown": "未知平台,請使用企業微信或飛書的 Webhook URL", "threshold": "預警閾值", "thresholdLabel": "預警閾值: {percent}%", "thresholdHelp": "當消費達到配額的 {percent}% 時觸發告警", @@ -417,7 +420,7 @@ "saveError": "儲存設定失敗", "loadError": "載入通知設定失敗", "webhookRequired": "請先填寫 Webhook URL", - "testSuccess": "測試訊息已發送,請檢查企業微信", + "testSuccess": "測試訊息已發送", "testFailed": "測試失敗", "testFailedRetry": "測試失敗,請重試", "testError": "測試連線失敗", @@ -1454,6 +1457,18 @@ "statusEnabled": "啟用", "statusDisabled": "禁用" }, + "inlineEdit": { + "save": "保存", + "cancel": "取消", + "saveSuccess": "保存成功", + "saveFailed": "保存失敗", + "priorityLabel": "優先級", + "weightLabel": "權重", + "costMultiplierLabel": "成本倍數", + "priorityInvalid": "請輸入大於等於 0 的整數", + "weightInvalid": "請輸入 1-100 之間的整數", + "costMultiplierInvalid": "請輸入大於等於 0 的數字" + }, "schedulingDialog": { "title": "供應商調度規則說明", "description": "了解系統如何智慧選擇上游供應商,確保高可用性和成本優化", @@ -1575,7 +1590,22 @@ "saving": "保存中...", "validation": { "fieldRequired": "名稱和目標為必填項" - } + }, + "bindingType": "套用範圍", + "bindingGlobal": "所有Provider(全域)", + "bindingProviders": "指定Provider", + "bindingGroups": "Provider分組", + "selectProviders": "選擇Provider...", + "selectGroups": "選擇分組...", + "searchProviders": "搜尋Provider...", + "searchGroups": "搜尋分組...", + "noProvidersFound": "找不到Provider", + "noGroupsFound": "找不到分組", + "providersSelected": "已選 {count} 個Provider", + "groupsSelected": "已選 {count} 個分組", + "loading": "載入中...", + "clear": "清空", + "selectAll": "全選" }, "table": { "name": "名稱", @@ -1584,6 +1614,7 @@ "target": "目標", "replacement": "替換值", "priority": "優先級", + "apply": "範圍", "status": "狀態", "createdAt": "建立時間", "actions": "操作" @@ -1597,7 +1628,10 @@ "set": "設定 Header", "json_path": "JSON 路徑替換", "text_replace": "文字替換" - } + }, + "applyToAll": "應用於所有請求", + "providers": "供應商", + "groups": "群組" }, "errorRules": { "nav": "錯誤規則", diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts index 48654b5ef..6587116ea 100644 --- a/src/actions/notifications.ts +++ b/src/actions/notifications.ts @@ -1,7 +1,9 @@ "use server"; +import type { NotificationJobType } from "@/lib/constants/notification.constants"; import { logger } from "@/lib/logger"; -import { WeChatBot } from "@/lib/wechat/bot"; +import { WebhookNotifier } from "@/lib/webhook"; +import { buildTestMessage } from "@/lib/webhook/templates/test-messages"; import { getNotificationSettings, type NotificationSettings, @@ -116,7 +118,8 @@ export async function updateNotificationSettingsAction( * 测试 Webhook 连通性 */ export async function testWebhookAction( - webhookUrl: string + webhookUrl: string, + type: NotificationJobType ): Promise<{ success: boolean; error?: string }> { if (!webhookUrl || !webhookUrl.trim()) { return { success: false, error: "Webhook URL 不能为空" }; @@ -135,10 +138,9 @@ export async function testWebhookAction( } try { - const bot = new WeChatBot(trimmedUrl); - const result = await bot.testConnection(); - - return result; + const notifier = new WebhookNotifier(trimmedUrl, { maxRetries: 1 }); + const testMessage = buildTestMessage(type); + return notifier.send(testMessage); } catch (error) { return { success: false, diff --git a/src/actions/request-filters.ts b/src/actions/request-filters.ts index 430d2c716..63d37a5e9 100644 --- a/src/actions/request-filters.ts +++ b/src/actions/request-filters.ts @@ -12,6 +12,7 @@ import { getRequestFilterById, type RequestFilter, type RequestFilterAction, + type RequestFilterBindingType, type RequestFilterMatchType, type RequestFilterScope, updateRequestFilter, @@ -31,6 +32,9 @@ function validatePayload(data: { target: string; matchType?: RequestFilterMatchType; replacement?: unknown; + bindingType?: RequestFilterBindingType; + providerIds?: number[] | null; + groupTags?: string[] | null; }): string | null { if (!data.name?.trim()) return "名称不能为空"; if (!data.target?.trim()) return "目标字段不能为空"; @@ -40,6 +44,34 @@ function validatePayload(data: { return "正则表达式存在 ReDoS 风险"; } } + + // Validate binding type constraints + const bindingType = data.bindingType ?? "global"; + if (bindingType === "providers") { + if (!data.providerIds || data.providerIds.length === 0) { + return "至少选择一个 Provider"; + } + if (data.groupTags && data.groupTags.length > 0) { + return "不能同时选择 Providers 和 Groups"; + } + } + if (bindingType === "groups") { + if (!data.groupTags || data.groupTags.length === 0) { + return "至少选择一个 Group Tag"; + } + if (data.providerIds && data.providerIds.length > 0) { + return "不能同时选择 Providers 和 Groups"; + } + } + if (bindingType === "global") { + if ( + (data.providerIds && data.providerIds.length > 0) || + (data.groupTags && data.groupTags.length > 0) + ) { + return "Global 类型不能指定 Providers 或 Groups"; + } + } + return null; } @@ -65,6 +97,9 @@ export async function createRequestFilterAction(data: { matchType?: RequestFilterMatchType; replacement?: unknown; priority?: number; + bindingType?: RequestFilterBindingType; + providerIds?: number[] | null; + groupTags?: string[] | null; }): Promise> { const session = await getSession(); if (!isAdmin(session)) return { ok: false, error: "权限不足" }; @@ -82,6 +117,9 @@ export async function createRequestFilterAction(data: { matchType: data.matchType ?? null, replacement: data.replacement ?? null, priority: data.priority ?? 0, + bindingType: data.bindingType ?? "global", + providerIds: data.providerIds ?? null, + groupTags: data.groupTags ?? null, }); revalidatePath(SETTINGS_PATH); @@ -104,6 +142,9 @@ export async function updateRequestFilterAction( replacement: unknown; priority: number; isEnabled: boolean; + bindingType: RequestFilterBindingType; + providerIds: number[] | null; + groupTags: string[] | null; }> ): Promise> { const session = await getSession(); @@ -133,6 +174,39 @@ export async function updateRequestFilterAction( } } + // Validate binding type constraints when updating binding-related fields + if ( + updates.bindingType !== undefined || + updates.providerIds !== undefined || + updates.groupTags !== undefined + ) { + // Need to merge updates with existing data + const existing = await getRequestFilterById(id); + if (!existing) { + return { ok: false, error: "记录不存在" }; + } + + const effectiveBindingType = updates.bindingType ?? existing.bindingType; + const effectiveProviderIds = + updates.providerIds !== undefined ? updates.providerIds : existing.providerIds; + const effectiveGroupTags = + updates.groupTags !== undefined ? updates.groupTags : existing.groupTags; + + const validationError = validatePayload({ + name: existing.name, + scope: existing.scope, + action: existing.action, + target: existing.target, + bindingType: effectiveBindingType, + providerIds: effectiveProviderIds, + groupTags: effectiveGroupTags, + }); + + if (validationError) { + return { ok: false, error: validationError }; + } + } + try { const updated = await updateRequestFilter(id, updates); if (!updated) { @@ -176,3 +250,58 @@ export async function refreshRequestFiltersCache(): Promise> +> { + const session = await getSession(); + if (!isAdmin(session)) return { ok: false, error: "权限不足" }; + + try { + const { findAllProviders } = await import("@/repository/provider"); + const providers = await findAllProviders(); + const simplified = providers.map((p) => ({ id: p.id, name: p.name })); + return { ok: true, data: simplified }; + } catch (error) { + logger.error("[RequestFiltersAction] Failed to list providers", { error }); + return { ok: false, error: "获取 Provider 列表失败" }; + } +} + +/** + * Get distinct provider group tags for filter binding selection + */ +export async function getDistinctProviderGroupsAction(): Promise> { + const session = await getSession(); + if (!isAdmin(session)) return { ok: false, error: "权限不足" }; + + try { + const { db } = await import("@/drizzle/db"); + const { providers } = await import("@/drizzle/schema"); + const { isNotNull } = await import("drizzle-orm"); + + const result = await db + .selectDistinct({ groupTag: providers.groupTag }) + .from(providers) + .where(isNotNull(providers.groupTag)); + + // Parse comma-separated tags and flatten into unique array + const allTags = new Set(); + for (const row of result) { + if (row.groupTag) { + const tags = row.groupTag.split(",").map((tag) => tag.trim()); + for (const tag of tags) { + if (tag) allTags.add(tag); + } + } + } + + return { ok: true, data: Array.from(allTags).sort() }; + } catch (error) { + logger.error("[RequestFiltersAction] Failed to get distinct group tags", { error }); + return { ok: false, error: "获取 Group Tags 失败" }; + } +} diff --git a/src/app/[locale]/settings/notifications/page.tsx b/src/app/[locale]/settings/notifications/page.tsx index b13d6dcbf..108bfdb10 100644 --- a/src/app/[locale]/settings/notifications/page.tsx +++ b/src/app/[locale]/settings/notifications/page.tsx @@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle, Bell, Loader2, TestTube, TrendingUp } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -12,6 +12,7 @@ import { testWebhookAction, updateNotificationSettingsAction, } from "@/actions/notifications"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -20,6 +21,7 @@ import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { Slider } from "@/components/ui/slider"; import { Switch } from "@/components/ui/switch"; +import type { NotificationJobType } from "@/lib/constants/notification.constants"; /** * 通知设置表单 Schema @@ -50,7 +52,7 @@ export default function NotificationsPage() { const t = useTranslations("settings"); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); - const [testingWebhook, setTestingWebhook] = useState(null); + const [testingWebhook, setTestingWebhook] = useState(null); const { register, @@ -67,6 +69,35 @@ export default function NotificationsPage() { const dailyLeaderboardEnabled = watch("dailyLeaderboardEnabled"); const costAlertEnabled = watch("costAlertEnabled"); const costAlertThreshold = watch("costAlertThreshold"); + const costAlertWebhook = watch("costAlertWebhook"); + const circuitBreakerWebhook = watch("circuitBreakerWebhook"); + const dailyLeaderboardWebhook = watch("dailyLeaderboardWebhook"); + + // Detect webhook platform type from URL + const detectWebhookType = useCallback((url: string | undefined): "wechat" | "feishu" | null => { + if (!url) return null; + try { + const parsed = new URL(url); + if (parsed.hostname === "qyapi.weixin.qq.com") return "wechat"; + if (parsed.hostname === "open.feishu.cn") return "feishu"; + return null; + } catch { + return null; + } + }, []); + + const costAlertWebhookType = useMemo( + () => detectWebhookType(costAlertWebhook), + [costAlertWebhook, detectWebhookType] + ); + const circuitBreakerWebhookType = useMemo( + () => detectWebhookType(circuitBreakerWebhook), + [circuitBreakerWebhook, detectWebhookType] + ); + const dailyLeaderboardWebhookType = useMemo( + () => detectWebhookType(dailyLeaderboardWebhook), + [dailyLeaderboardWebhook, detectWebhookType] + ); const loadSettings = useCallback(async () => { try { @@ -129,7 +160,7 @@ export default function NotificationsPage() { } }; - const handleTestWebhook = async (webhookUrl: string, type: string) => { + const handleTestWebhook = async (webhookUrl: string, type: NotificationJobType) => { if (!webhookUrl || !webhookUrl.trim()) { toast.error(t("notifications.form.webhookRequired")); return; @@ -138,7 +169,7 @@ export default function NotificationsPage() { setTestingWebhook(type); try { - const result = await testWebhookAction(webhookUrl); + const result = await testWebhookAction(webhookUrl, type); if (result.success) { toast.success(t("notifications.form.testSuccess")); @@ -211,9 +242,18 @@ export default function NotificationsPage() {
- +
+ + {circuitBreakerWebhookType && ( + + {circuitBreakerWebhookType === "wechat" + ? t("notifications.costAlert.webhookTypeWeCom") + : t("notifications.costAlert.webhookTypeFeishu")} + + )} +
{errors.circuitBreakerWebhook.message}

)} + {circuitBreakerWebhook && !circuitBreakerWebhookType && ( +

+ {t("notifications.costAlert.webhookTypeUnknown")} +

+ )}
+ + + +
+
{label}
+ +
+ setDraft(e.target.value)} + disabled={disabled || saving} + className="w-24 tabular-nums" + aria-label={label} + aria-invalid={validationError != null} + type="number" + inputMode="decimal" + step={type === "integer" ? "1" : "any"} + onPointerDown={stopPropagation} + onClick={stopPropagation} + onKeyDown={(e) => { + e.stopPropagation(); + if (e.key === "Escape") { + e.preventDefault(); + handleCancel(); + } + if (e.key === "Enter") { + e.preventDefault(); + void handleSave(); + } + }} + /> + {suffix && {suffix}} +
+ + {validationError &&
{validationError}
} + +
+ + +
+
+
+ + ); +} diff --git a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx index facf8a253..30aa6a0fd 100644 --- a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx @@ -42,7 +42,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Switch } from "@/components/ui/switch"; -import { PROVIDER_GROUP } from "@/lib/constants/provider.constants"; +import { PROVIDER_GROUP, PROVIDER_LIMITS } from "@/lib/constants/provider.constants"; import { getProviderTypeConfig, getProviderTypeTranslationKey } from "@/lib/provider-type-utils"; import { copyToClipboard, isClipboardSupported } from "@/lib/utils/clipboard"; import { getContrastTextColor, getGroupColor } from "@/lib/utils/color"; @@ -51,6 +51,7 @@ import { formatCurrency } from "@/lib/utils/currency"; import type { ProviderDisplay } from "@/types/provider"; import type { User } from "@/types/user"; import { ProviderForm } from "./forms/provider-form"; +import { InlineEditPopover } from "./inline-edit-popover"; interface ProviderRichListItemProps { provider: ProviderDisplay; @@ -95,6 +96,35 @@ export function ProviderRichListItem({ const tTypes = useTranslations("settings.providers.types"); const tList = useTranslations("settings.providers.list"); const tTimeout = useTranslations("settings.providers.form.sections.timeout"); + const tInline = useTranslations("settings.providers.inlineEdit"); + + const validatePriority = (raw: string) => { + if (raw.length === 0) return tInline("priorityInvalid"); + const value = Number(raw); + if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 2147483647) + return tInline("priorityInvalid"); + return null; + }; + + const validateWeight = (raw: string) => { + if (raw.length === 0) return tInline("weightInvalid"); + const value = Number(raw); + if ( + !Number.isFinite(value) || + !Number.isInteger(value) || + value < PROVIDER_LIMITS.WEIGHT.MIN || + value > PROVIDER_LIMITS.WEIGHT.MAX + ) + return tInline("weightInvalid"); + return null; + }; + + const validateCostMultiplier = (raw: string) => { + if (raw.length === 0) return tInline("costMultiplierInvalid"); + const value = Number(raw); + if (!Number.isFinite(value) || value < 0) return tInline("costMultiplierInvalid"); + return null; + }; // 获取供应商类型配置 const typeConfig = getProviderTypeConfig(provider.providerType); @@ -254,6 +284,32 @@ export function ProviderRichListItem({ }); }; + const createSaveHandler = (fieldName: "priority" | "weight" | "cost_multiplier") => { + return async (value: number) => { + try { + const res = await editProvider(provider.id, { [fieldName]: value } as Parameters< + typeof editProvider + >[1]); + if (res.ok) { + toast.success(tInline("saveSuccess")); + queryClient.invalidateQueries({ queryKey: ["providers"] }); + router.refresh(); + return true; + } + toast.error(tInline("saveFailed"), { description: res.error || tList("unknownError") }); + return false; + } catch (error) { + console.error(`更新 ${fieldName} 失败:`, error); + toast.error(tInline("saveFailed"), { description: tList("unknownError") }); + return false; + } + }; + }; + + const handleSavePriority = createSaveHandler("priority"); + const handleSaveWeight = createSaveHandler("weight"); + const handleSaveCostMultiplier = createSaveHandler("cost_multiplier"); + return ( <>
@@ -393,15 +449,52 @@ export function ProviderRichListItem({
{tList("priority")}
-
{provider.priority}
+
+ {canEdit ? ( + + ) : ( + {provider.priority} + )} +
{tList("weight")}
-
{provider.weight}
+
+ {canEdit ? ( + + ) : ( + {provider.weight} + )} +
{tList("costMultiplier")}
-
{provider.costMultiplier}x
+
+ {canEdit ? ( + + ) : ( + {provider.costMultiplier}x + )} +
diff --git a/src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx b/src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx index 4a3789e3a..15418b0eb 100644 --- a/src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx +++ b/src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx @@ -27,7 +27,13 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; -import type { RequestFilter, RequestFilterMatchType } from "@/repository/request-filters"; +import type { + RequestFilter, + RequestFilterBindingType, + RequestFilterMatchType, +} from "@/repository/request-filters"; +import { GroupMultiSelect } from "./group-multi-select"; +import { ProviderMultiSelect } from "./provider-multi-select"; type Mode = "create" | "edit"; @@ -62,6 +68,11 @@ export function FilterDialog({ mode, trigger, filter, open, onOpenChange }: Prop filter?.matchType ?? "contains" ); const [isEnabled, setIsEnabled] = useState(filter?.isEnabled ?? true); + const [bindingType, setBindingType] = useState( + filter?.bindingType ?? "global" + ); + const [providerIds, setProviderIds] = useState(filter?.providerIds ?? []); + const [groupTags, setGroupTags] = useState(filter?.groupTags ?? []); useEffect(() => { // Sync controlled open prop to internal state @@ -87,6 +98,9 @@ export function FilterDialog({ mode, trigger, filter, open, onOpenChange }: Prop setPriority(filter.priority); setMatchType(filter.matchType ?? "contains"); setIsEnabled(filter.isEnabled); + setBindingType(filter.bindingType ?? "global"); + setProviderIds(filter.providerIds ?? []); + setGroupTags(filter.groupTags ?? []); } }, [filter]); @@ -105,6 +119,19 @@ export function FilterDialog({ mode, trigger, filter, open, onOpenChange }: Prop const showMatchType = scope === "body" && action === "text_replace"; const showReplacement = action === "set" || action === "json_path" || action === "text_replace"; + const handleBindingTypeChange = (value: RequestFilterBindingType) => { + setBindingType(value); + // Clear selections when changing binding type + if (value === "global") { + setProviderIds([]); + setGroupTags([]); + } else if (value === "providers") { + setGroupTags([]); + } else if (value === "groups") { + setProviderIds([]); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -139,6 +166,9 @@ export function FilterDialog({ mode, trigger, filter, open, onOpenChange }: Prop replacement: showReplacement ? parsedReplacement : null, priority, isEnabled, + bindingType, + providerIds: bindingType === "providers" ? providerIds : null, + groupTags: bindingType === "groups" ? groupTags : null, } as const; const result = @@ -163,16 +193,16 @@ export function FilterDialog({ mode, trigger, filter, open, onOpenChange }: Prop }; const content = ( - -
- - - {mode === "create" ? t("dialog.createTitle") : t("dialog.editTitle")} - - {t("description")} - + + + + {mode === "create" ? t("dialog.createTitle") : t("dialog.editTitle")} + + {t("description")} + -
+ +
+
+
+ + +
+ + {bindingType === "providers" && ( +
+ + +
+ )} + + {bindingType === "groups" && ( +
+ + +
+ )} +
+