diff --git a/backend/internal/handler/antigravity_http.go b/backend/internal/handler/antigravity_http.go index 7d200985..05b28956 100644 --- a/backend/internal/handler/antigravity_http.go +++ b/backend/internal/handler/antigravity_http.go @@ -213,7 +213,7 @@ func (h *AntigravityHTTPHandler) GetModels(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "models": models, - "default_model": "claude-opus-4-6", + "default_model": "claude-opus-4-7", }) } diff --git a/backend/internal/service/language_server_service.go b/backend/internal/service/language_server_service.go index f0ddfaec..dec67519 100644 --- a/backend/internal/service/language_server_service.go +++ b/backend/internal/service/language_server_service.go @@ -310,6 +310,24 @@ type ModelConfig struct { // GetAvailableModels 获取可用模型列表 func (svc *LanguageServerService) GetAvailableModels(ctx context.Context) ([]ModelConfig, error) { models := []ModelConfig{ + { + Name: "claude-opus-4-7", + DisplayName: "Claude Opus 4.7", + MaxTokens: 200000, + SupportsThinking: true, + ThinkingBudget: 32000, + SupportsImages: true, + Provider: "anthropic", + }, + { + Name: "claude-sonnet-4-7", + DisplayName: "Claude Sonnet 4.7", + MaxTokens: 200000, + SupportsThinking: true, + ThinkingBudget: 16000, + SupportsImages: true, + Provider: "anthropic", + }, { Name: "claude-opus-4-6", DisplayName: "Claude Opus 4.6", diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3a714260..5d532ce1 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,57 +1,82 @@ # ============================================================================= -# Sub2API Docker Compose Configuration +# Sub2API Docker Compose Configuration (负载均衡版) # ============================================================================= # Quick Start: # 1. Copy .env.example to .env and configure -# 2. docker-compose up -d -# 3. Check logs: docker-compose logs -f sub2api -# 4. Access: http://localhost:8080 +# 2. docker compose up -d +# 3. Check logs: docker compose logs -f +# 4. Access: http://localhost (via nginx) # -# All configuration is done via environment variables. -# No Setup Wizard needed - the system auto-initializes on first run. +# 扩缩容: +# docker compose up -d --scale sub2api=5 # 扩到 5 个实例 +# docker compose up -d --scale sub2api=2 # 缩回 2 个实例 +# +# 注意事项: +# - JWT_SECRET / TOTP_ENCRYPTION_KEY 必须固定,多实例共享同一个值 +# - PostgreSQL / Redis 单实例,不参与水平扩展 +# - postgres 端口默认不对外暴露,如需调试取消注释 127.0.0.1:5433:5432 +# - redis 端口默认不对外暴露,如需调试取消注释 127.0.0.1:6380:6379 # ============================================================================= services: # =========================================================================== - # Sub2API Application + # Nginx 负载均衡(入口) + # =========================================================================== + nginx: + image: nginx:alpine + container_name: sub2api-nginx + restart: unless-stopped + ulimits: + nofile: + soft: 65535 + hard: 65535 + ports: + - "0.0.0.0:80:80" + - "0.0.0.0:443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/certs:/etc/nginx/certs:ro + depends_on: + sub2api: + condition: service_healthy + networks: + - sub2api-network + healthcheck: + test: [ "CMD", "wget", "-q", "-T", "3", "-O", "/dev/null", "http://localhost/health" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # =========================================================================== + # Sub2API Application(多实例,通过 --scale 控制数量) # =========================================================================== sub2api: - image: weishaw/sub2api:latest - container_name: sub2api + image: docker.io/zfc931912343/sub2api:latest restart: unless-stopped ulimits: nofile: soft: 100000 hard: 100000 - ports: - - "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080" + # 不直接暴露端口,由 nginx 代理 + expose: + - "8080" volumes: - # Data persistence (config.yaml will be auto-generated here) - sub2api_data:/app/data - # Optional: Mount custom config.yaml (uncomment and create the file first) - # Copy config.example.yaml to config.yaml, modify it, then uncomment: + # Optional: 挂载自定义 config.yaml(先从 config.example.yaml 复制并修改) # - ./config.yaml:/app/data/config.yaml - # Optional: Mount a custom Codex instructions template file, then point - # gateway.forced_codex_instructions_template_file at /app/data/codex-instructions.md.tmpl - # in config.yaml. + # Optional: 自定义 Codex instructions 模板 # - ./codex-instructions.md.tmpl:/app/data/codex-instructions.md.tmpl:ro environment: - # ======================================================================= - # Auto Setup (REQUIRED for Docker deployment) - # ======================================================================= - AUTO_SETUP=true - # ======================================================================= - # Server Configuration - # ======================================================================= + # --- Server --- - SERVER_HOST=0.0.0.0 - SERVER_PORT=8080 - SERVER_MODE=${SERVER_MODE:-release} - RUN_MODE=${RUN_MODE:-standard} - # ======================================================================= - # Database Configuration (PostgreSQL) - # ======================================================================= + # --- Database (PostgreSQL) --- - DATABASE_HOST=postgres - DATABASE_PORT=5432 - DATABASE_USER=${POSTGRES_USER:-sub2api} @@ -63,9 +88,7 @@ services: - DATABASE_CONN_MAX_LIFETIME_MINUTES=${DATABASE_CONN_MAX_LIFETIME_MINUTES:-30} - DATABASE_CONN_MAX_IDLE_TIME_MINUTES=${DATABASE_CONN_MAX_IDLE_TIME_MINUTES:-5} - # ======================================================================= - # Redis Configuration - # ======================================================================= + # --- Redis --- - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD=${REDIS_PASSWORD:-} @@ -74,74 +97,39 @@ services: - REDIS_MIN_IDLE_CONNS=${REDIS_MIN_IDLE_CONNS:-10} - REDIS_ENABLE_TLS=${REDIS_ENABLE_TLS:-false} - # ======================================================================= - # Admin Account (auto-created on first run) - # ======================================================================= + # --- Admin(仅首次启动生效)--- - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-} - # ======================================================================= - # JWT Configuration - # ======================================================================= - # IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being - # invalidated after container restarts. If left empty, a random secret - # will be generated on each startup. - # Generate a secure secret: openssl rand -hex 32 - - JWT_SECRET=${JWT_SECRET:-} + # --- JWT(多实例必须固定,否则重启后 session 失效)--- + # 生成: openssl rand -hex 32 + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required for multi-instance} - JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24} - # ======================================================================= - # TOTP (2FA) Configuration - # ======================================================================= - # IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty, - # a random key will be generated on each startup, causing all existing - # TOTP configurations to become invalid (users won't be able to login - # with 2FA). - # Generate a secure key: openssl rand -hex 32 - - TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-} + # --- TOTP 2FA(多实例必须固定,否则 2FA 失效)--- + # 生成: openssl rand -hex 32 + - TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:?TOTP_ENCRYPTION_KEY is required for multi-instance} - # ======================================================================= - # Timezone Configuration - # This affects ALL time operations in the application: - # - Database timestamps - # - Usage statistics "today" boundary - # - Subscription expiry times - # - Log timestamps - # Common values: Asia/Shanghai, America/New_York, Europe/London, UTC - # ======================================================================= + # --- Timezone --- - TZ=${TZ:-Asia/Shanghai} - # ======================================================================= - # Gemini OAuth Configuration (for Gemini accounts) - # ======================================================================= + # --- Gemini OAuth --- - GEMINI_OAUTH_CLIENT_ID=${GEMINI_OAUTH_CLIENT_ID:-} - GEMINI_OAUTH_CLIENT_SECRET=${GEMINI_OAUTH_CLIENT_SECRET:-} - GEMINI_OAUTH_SCOPES=${GEMINI_OAUTH_SCOPES:-} - GEMINI_QUOTA_POLICY=${GEMINI_QUOTA_POLICY:-} - - # Built-in OAuth client secrets (optional) - # SECURITY: This repo does not embed third-party client_secret. - GEMINI_CLI_OAUTH_CLIENT_SECRET=${GEMINI_CLI_OAUTH_CLIENT_SECRET:-} - ANTIGRAVITY_OAUTH_CLIENT_SECRET=${ANTIGRAVITY_OAUTH_CLIENT_SECRET:-} - # ======================================================================= - # Security Configuration (URL Allowlist) - # ======================================================================= - # Enable URL allowlist validation (false to skip allowlist checks) + # --- Security --- - SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false} - # Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https) - SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false} - # Allow private IP addresses for upstream/pricing/CRS (for internal deployments) - SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false} - # Upstream hosts whitelist (comma-separated, only used when enabled=true) - SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-} - # ======================================================================= - # Update Configuration (在线更新配置) - # ======================================================================= - # Proxy for accessing GitHub (online updates + pricing data) - # Examples: http://host:port, socks5://host:port + # --- Update Proxy(国内机器可配置代理访问 GitHub)--- - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-} + depends_on: postgres: condition: service_healthy @@ -150,17 +138,7 @@ services: networks: - sub2api-network healthcheck: - test: - [ - "CMD", - "wget", - "-q", - "-T", - "5", - "-O", - "/dev/null", - "http://localhost:8080/health", - ] + test: [ "CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health" ] interval: 30s timeout: 10s retries: 3 @@ -180,9 +158,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data environment: - # postgres:18-alpine 默认 PGDATA=/var/lib/postgresql/18/docker(位于镜像声明的匿名卷 /var/lib/postgresql 内)。 - # 若不显式设置 PGDATA,则即使挂载了 postgres_data 到 /var/lib/postgresql/data,数据也不会落盘到该命名卷, - # docker compose down/up 后会触发 initdb 重新初始化,导致用户/密码等数据丢失。 + # postgres:18-alpine 默认 PGDATA 在镜像内部匿名卷,必须显式指定才能持久化到命名卷 - PGDATA=/var/lib/postgresql/data - POSTGRES_USER=${POSTGRES_USER:-sub2api} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} @@ -191,19 +167,14 @@ services: networks: - sub2api-network healthcheck: - test: - [ - "CMD-SHELL", - "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}", - ] + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}" ] interval: 10s timeout: 5s retries: 5 start_period: 10s - ports: - - 5432:5432 - # 注意:不暴露端口到宿主机,应用通过内部网络连接 - # 如需调试,可临时添加:ports: ["127.0.0.1:5433:5432"] + # 默认不对外暴露,如需本地调试取消注释 + # ports: + # - "127.0.0.1:5433:5432" # =========================================================================== # Redis Cache @@ -227,18 +198,19 @@ services: ${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}' environment: - TZ=${TZ:-Asia/Shanghai} - # REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag) - REDISCLI_AUTH=${REDIS_PASSWORD:-} networks: - sub2api-network healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 5 start_period: 5s - ports: - - 6379:6379 + # 默认不对外暴露,如需本地调试取消注释 + # ports: + # - "127.0.0.1:6380:6379" + # ============================================================================= # Volumes # ============================================================================= diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 00000000..217989d8 --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,155 @@ +# ============================================================================= +# Nginx 负载均衡配置 for Sub2API(Cloudflare 前置 + Origin Certificate) +# ============================================================================= +# 架构:Cloudflare (HTTPS) → VPS:443 (Docker nginx + CF Origin Cert) → sub2api +# - SSL 由 Cloudflare Origin Certificate 端到端加密 +# - 真实客户端 IP 从 CF-Connecting-IP 头还原 +# ============================================================================= + +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 4096; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$realip_remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent ' + 'rt=$request_time urt=$upstream_response_time cf_ray=$http_cf_ray'; + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 75s; + keepalive_requests 1000; + server_tokens off; + + client_max_body_size 64m; + client_body_buffer_size 2m; # 避免请求体落盘(默认 8k/16k 对 AI 请求太小) + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 4; + gzip_min_length 1024; + gzip_types text/plain text/css application/json application/javascript + text/xml application/xml text/javascript; + + # ------------------------------------------------------------------------- + # 还原真实客户端 IP(Cloudflare IP 段) + # ------------------------------------------------------------------------- + set_real_ip_from 103.21.244.0/22; + set_real_ip_from 103.22.200.0/22; + set_real_ip_from 103.31.4.0/22; + set_real_ip_from 104.16.0.0/13; + set_real_ip_from 104.24.0.0/14; + set_real_ip_from 108.162.192.0/18; + set_real_ip_from 131.0.72.0/22; + set_real_ip_from 141.101.64.0/18; + set_real_ip_from 162.158.0.0/15; + set_real_ip_from 172.64.0.0/13; + set_real_ip_from 173.245.48.0/20; + set_real_ip_from 188.114.96.0/20; + set_real_ip_from 190.93.240.0/20; + set_real_ip_from 197.234.240.0/22; + set_real_ip_from 198.41.128.0/17; + set_real_ip_from 2400:cb00::/32; + set_real_ip_from 2606:4700::/32; + set_real_ip_from 2803:f800::/32; + set_real_ip_from 2405:b500::/32; + set_real_ip_from 2405:8100::/32; + set_real_ip_from 2a06:98c0::/29; + set_real_ip_from 2c0f:f248::/32; + real_ip_header CF-Connecting-IP; + real_ip_recursive on; + + # WebSocket upgrade 映射 + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # ------------------------------------------------------------------------- + # 上游实例池(least_conn) + # ------------------------------------------------------------------------- + upstream sub2api_backend { + # ip_hash 保证同一客户端 IP 始终路由到同一实例 + # 解决 OAuth session 内存不共享问题(session 在实例内存中) + ip_hash; + server sub2api:8080; + } + + # ========================================================================= + # HTTP → HTTPS 跳转 + # ========================================================================= + server { + listen 80; + server_name _; + + location = /health { + proxy_pass http://sub2api_backend; + proxy_set_header Host $host; + proxy_connect_timeout 5s; + proxy_read_timeout 10s; + access_log off; + } + + location / { + return 301 https://$host$request_uri; + } + } + + # ========================================================================= + # HTTPS(Cloudflare Origin Certificate) + # ========================================================================= + server { + listen 443 ssl; + http2 on; + server_name _; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + location = /health { + proxy_pass http://sub2api_backend; + proxy_set_header Host $host; + proxy_connect_timeout 5s; + proxy_read_timeout 10s; + access_log off; + } + + location / { + proxy_pass http://sub2api_backend; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + + proxy_buffering off; + proxy_cache off; + + proxy_connect_timeout 10s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + } +}