Commit Graph

4 Commits

  • refactor(proxy): replace panic-prone unwrap/expect with safe patterns
    Harden the proxy module against panics by removing optimistic
    unwrap()/expect() calls in favor of pattern matching and graceful
    fallbacks:
    
    - copilot_optimizer/cache_injector: bind Value::Array/String directly
      instead of is_array()+as_array().unwrap(); use is_none_or and in-place
      string mutation
    - hyper_client: gate the raw-write path with if-let + filter instead of
      has_cases + unwrap()
    - gemini_shadow: recover poisoned RwLock via into_inner() rather than
      panicking, with warn logging
    - streaming_codex_chat: replace expect("tool state exists") with
      let-else (return/continue)
    - merge_tool_results: early-return when messages is absent
    - sse: fall back to from_utf8_lossy on the UTF-8 boundary slice
    
    No behavior change on the happy path; all proxy tests pass.
  • fix(failover): patch P1-P3 reliability gaps surfaced by team review
    - Forwarder buffers non-streaming bodies and primes streaming first
      chunk before signaling success, so body timeouts and SSE first-chunk
      failures route through the circuit breaker instead of being recorded
      as success on response-header arrival
    - Atomic enable-failover: switch to P1 before persisting the flag, and
      roll back auto-added queue entries when the switch is rejected
      (e.g. official providers)
    - Hot-reload circuit breaker config on per-app proxy config change
      instead of waiting for a proxy restart
    - FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel
      require proxy takeover for the active app; the backend command also
      rejects enabling when takeover is off
    - ProviderHealthBadge consumes the backend is_healthy flag instead of
      hardcoding the 5-failure threshold
    
    Cleanup:
    - impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from
      the command layer
    - Collapse three identical TabsContent blocks into a single map
  • fix(copilot): 修复 GitHub Copilot 认证和代理问题 (#1854)
    * fix(copilot): 修复 GitHub Copilot 400 认证错误
    
    问题:使用 GitHub Copilot provider 时报错 400 bad request
    
    根因:与 copilot-api 项目对比发现多处差异
    
    修复内容:
    - 更新版本号 0.26.7 到 0.38.2
    - 更新 API 版本 2025-04-01 到 2025-10-01
    - 添加缺失的关键 headers
    - 修正 openai-intent 值
    - 添加动态 API endpoint 支持
    - 同步更新 stream_check.rs headers
    
    Closes #1777
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * fix: flush stream after write_all in hyper_client proxy
    
    Add explicit flush() calls after write_all() for TLS stream, plain TCP
    stream, and CONNECT tunnel requests to ensure buffered data is sent
    immediately, preventing connection hangs in Copilot auth header flow.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * 修复登录时的剪切板在mac与linux端可能没复制验证码
    
    * fix: flush stream after write_all in hyper_client proxy
    
    Add explicit flush() calls after write_all() for TLS stream, plain TCP
    stream, and CONNECT tunnel requests to ensure buffered data is sent
    immediately, preventing connection hangs in Copilot auth header flow.
    
    * 修复登录时的剪切板在mac与linux端可能没复制验证码
    
    * 1、修复不同类型的个人商业等不同类型的copilot账号问题
    2、将验证码复制改为异步操作
    
    * fix: address PR review comments for Copilot auth                                                      │
    │                                                                                                                      │
    │ - Fix clipboard blocking by using spawn_blocking for arboard ops                                                     │
    │ - Implement dynamic endpoint routing for enterprise Copilot users                                                    │
    │ - Add api_endpoints cache cleanup in remove_account() and clear_auth()                                               │
    │ - Change API endpoint log level from info to debug                                                                   │
    │ - Fix clear_auth() to continue cleanup even if file deletion fails                                                   │
    │ - Add 9 unit tests for Copilot detection and api_endpoints cachin
    
    * style: fix cargo fmt formatting
    
    * Fix Copilot dynamic endpoint handling
    
    * fix: restore clear_auth() memory-first cleanup order and fix cache leaks
    
    - Restore clear_auth() to clean memory state before deleting the storage
      file. The previous order (file deletion first) caused a regression where
      users could get stuck in a "cannot log out" state if file removal failed.
    
    - Add missing copilot_models.clear() in clear_auth() — this cache was
      cleaned in remove_account() but never in the full clear path.
    
    - Add endpoint_locks cleanup in both remove_account() and clear_auth()
      to prevent minor in-process memory leaks.
    
    - Update test to assert the correct behavior: memory should be cleaned
      even when file deletion fails.
    
    ---------
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com>
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • refactor(proxy): transparent header forwarding via hyper client (#1714)
    * style(frontend): reformat provider forms, constants and hooks
    
    Apply prettier formatting across 5 frontend files. No logic changes.
    
    Changed files:
    - AddProviderDialog.tsx: reformat generic type annotation and callback
    - ClaudeFormFields.tsx: consolidate multi-line useState and Collapsible props
    - CodexConfigSections.tsx: expand single-line React imports to multi-line,
      collapse removeCodexTopLevelField() call
    - constants.ts: merge TemplateType into single line
    - useSkills.ts: expand single-line TanStack Query imports to multi-line,
      reformat uninstallSkill mutationFn chain
    
    * deps(proxy): add hyper ecosystem crates and manual decompression libs
    
    reqwest internally normalizes all header names to lowercase and does not
    preserve insertion order, causing proxied requests to differ from the
    original client requests. To achieve transparent header forwarding with
    original casing and order, introduce lower-level hyper HTTP client libs.
    
    New dependencies:
    - hyper-util 0.1: TokioExecutor + legacy Client with
      preserve_header_case support for HTTP/1.1
    - hyper-rustls 0.27: rustls-based TLS connector for hyper
    - http 1 / http-body 1 / http-body-util 0.1: HTTP type crates for
      hyper 1.x request/response construction
    - flate2 1: manual gzip/deflate decompression (replaces reqwest auto)
    - brotli 7: manual brotli decompression
    
    Changed dependencies:
    - serde_json: enable preserve_order feature to keep JSON field order
    - reqwest: drop gzip feature to prevent reqwest from overriding the
      client's original accept-encoding header
    
    * refactor(proxy): use hyper client for header-case preserving forwarding
    
    Previously the proxy used reqwest for all upstream requests. reqwest
    normalizes header names to lowercase and reorders them internally,
    making proxied requests distinguishable from direct CLI requests.
    Some upstream providers are sensitive to these differences.
    
    This commit replaces reqwest with a hyper-based HTTP client on the
    default (non-proxy) path, achieving wire-level header fidelity:
    
    Server layer (server.rs):
    - Replace axum::serve with a manual hyper HTTP/1.1 accept loop
    - Enable preserve_header_case(true) so incoming header casing is
      captured in a HeaderCaseMap extension on each request
    - Bridge hyper requests to axum Router via tower::Service
    
    New hyper client module (hyper_client.rs):
    - Lazy-initialized hyper-util Client with preserve_header_case
    - ProxyResponse enum wrapping both hyper::Response and reqwest::Response
      behind a unified interface (status, headers, bytes, bytes_stream)
    - send_request() builds requests with ordered HeaderMap + case map
    
    Request handlers (handlers.rs):
    - Switch from (HeaderMap, Json<Value>) extractors to raw
      axum::extract::Request to preserve Extensions (containing the
      HeaderCaseMap from the accept loop)
    - Pass extensions through the forwarding chain
    
    Forwarder (forwarder.rs):
    - Remove HEADER_BLACKLIST array; replace with ordered header iteration
      that preserves original header sequence and casing
    - Build ordered_headers by iterating client headers, skipping only
      auth/host/content-length, and inserting auth headers at the original
      authorization position to maintain order
    - Handle anthropic-beta (ensure claude-code-20250219 tag) and
      anthropic-version (passthrough or default) inline during iteration
    - Remove should_force_identity_encoding() — accept-encoding is now
      transparently forwarded to upstream
    - Use hyper client by default; fall back to reqwest only when an
      HTTP/SOCKS5 proxy tunnel is configured
    
    Provider adapters (adapter.rs, claude.rs, codex.rs, gemini.rs):
    - Replace add_auth_headers(RequestBuilder) -> RequestBuilder with
      get_auth_headers(AuthInfo) -> Vec<(HeaderName, HeaderValue)>
    - Adapters now return header pairs instead of mutating a reqwest builder
    - Claude adapter: merge Anthropic/ClaudeAuth/Bearer into single branch;
      move Copilot fingerprint headers into get_auth_headers
    
    Response processing (response_processor.rs):
    - Add manual decompression (gzip/deflate/brotli via flate2 + brotli)
      for non-streaming responses, since reqwest auto-decompression is now
      disabled to allow accept-encoding passthrough
    - Add compressed-SSE warning log for streaming responses
    - Accept ProxyResponse instead of reqwest::Response
    
    HTTP client (http_client.rs):
    - Disable reqwest auto-decompression (.no_gzip/.no_brotli/.no_deflate)
      on both global and per-provider clients
    
    Streaming adapters (streaming.rs, streaming_responses.rs):
    - Generalize stream error type from reqwest::Error to generic E: Error
    
    Misc:
    - log_codes.rs: add SRV-005 (ACCEPT_ERR) and SRV-006 (CONN_ERR)
    - stream_check.rs: reformat copilot header lines
    - transform.rs: fix trailing whitespace alignment
    
    * fix(lint): resolve 35 clippy warnings across Rust codebase
    
    Fix all clippy warnings reported by `cargo clippy --lib`:
    
    - codex_config.rs: fix doc_overindented_list_items (3 spaces -> 2)
    - commands/copilot.rs: inline format args in 2 log::error! calls
    - commands/provider.rs: inline format args in 3 map_err closures
    - proxy/hyper_client.rs: inline format arg in log::debug! call
    - proxy/providers/copilot_auth.rs: inline format args in 16 locations
      (log macros, format! in headers, error constructors)
    - proxy/thinking_optimizer.rs: inline format args in 2 log::info! calls
    - services/skill.rs: inline format args in log::debug! call
    - services/webdav_sync.rs: inline format args in 6 format! calls
      (version compat messages, download limit messages)
    - services/webdav_sync/archive.rs: inline format args in 2 format! calls
    - session_manager/providers/opencode.rs: inline format args in
      source_path format!
    
    All fixes use the clippy::uninlined_format_args suggestion pattern:
      format!("msg: {}", var)  ->  format!("msg: {var}")
    
    * deps(proxy): add raw HTTP write and native TLS cert dependencies
    
    Add crates required for the raw TCP/TLS write path that bypasses
    hyper's header encoder to preserve original header name casing:
    
    - httparse: parse raw TCP peek bytes to capture header casings
    - tokio-rustls + rustls: direct TLS connections for raw write path
    - webpki-roots: Mozilla CA bundle baseline
    - rustls-native-certs: load system keychain CAs (trusts proxy MITM
      certificates from Clash, mitmproxy, etc.)
    
    * fix(proxy): address code review feedback on response handling
    
    Fixes from PR #1714 code review:
    
    - Extract `read_decoded_body()` and `strip_entity_headers_for_rebuilt_body()`
      in response_processor to properly clean content-encoding/content-length
      headers after decompression
    - Reuse `read_decoded_body()` in handlers.rs for Claude transform path,
      ensuring compressed responses are decoded before format conversion
    - Make `build_proxy_url_from_config()` public so forwarder can pass proxy
      URL to the hyper raw write path
    - Add `has_system_proxy_env()` utility with test coverage
    - Add 50ms backoff after accept() failures in server.rs to prevent
      tight-loop CPU spin on transient socket errors
    
    * feat(proxy): implement raw TCP/TLS write with HTTP CONNECT tunnel
    
    Rewrite hyper_client with a two-tier strategy for header case preservation:
    
    Primary path (raw write):
    - Peek raw TCP bytes in server.rs to capture OriginalHeaderCases before
      hyper lowercases them
    - Build raw HTTP/1.1 request bytes with exact original header name casing
    - Write directly to TLS stream, then use WriteFilter to let hyper parse
      the response while discarding its duplicate request writes
    - Support HTTP CONNECT tunneling through upstream proxies, so header case
      is preserved even when a proxy (Clash, V2Ray) is configured
    
    Fallback path (hyper-util Client):
    - Used when OriginalHeaderCases is empty or raw write fails
    - Configured with title_case_headers(true) for best-effort casing
    
    TLS improvements:
    - Load native system certificates alongside webpki roots so proxy MITM
      CAs (installed in system keychain) are trusted through CONNECT tunnels
    
    Key types added:
    - OriginalHeaderCases: maps lowercase name → original wire-casing bytes
    - WriteFilter<S>: AsyncRead+AsyncWrite wrapper that discards writes
    - connect_via_proxy(): HTTP CONNECT tunnel establishment
    - ExtensionDebugMarker: diagnostic marker for extension chain debugging
    
    * refactor(proxy): route requests through hyper with proxy-aware forwarding
    
    Rework forwarder request dispatch to always prefer the hyper raw write
    path (header case preservation) over reqwest:
    
    Request routing:
    - HTTP/HTTPS proxy: hyper raw write through CONNECT tunnel (case preserved)
    - SOCKS5 proxy: reqwest fallback (CONNECT not supported for SOCKS5)
    - No proxy: hyper raw write direct connection
    
    Header handling improvements:
    - Replace host header in-place at original position instead of
      skip-and-append, preserving client's header ordering
    - Preserve client's original accept-encoding for transparent passthrough;
      only force identity encoding when transform path needs decompression
    - Add should_force_identity_encoding() to centralize the decision
    - Remove hardcoded 'br, gzip, deflate' override that masked client values
    
    Proxy URL resolution (priority order):
    1. Provider-specific proxy config (if enabled)
    2. Global proxy URL configured in CC Switch
    3. Direct connection (no proxy)
    
    * chore(proxy): remove dead code, redundant tests and debug scaffolding
    
    - Inline should_force_identity_encoding() (was just `needs_transform`)
      and delete its 5 test cases
    - Remove ExtensionDebugMarker diagnostic type
    - Remove unused has_system_proxy_env() and its test
    - Remove strip_entity_headers test
    - Simplify hyper path: remove redundant is_socks_proxy ternary
    - Update hyper_client module doc to reflect CONNECT tunnel support
    
    * fix(proxy): block direct-connect fallback and complete CONNECT tunnel support
    
    * feat(hooks): improve proxy requirement warnings with specific reasons
    
    - Remove redundant OpenAI format hint toast messages
    - Add detailed reason detection for proxy requirements (OpenAI Chat, OpenAI Responses, full URL mode)
    - Update i18n files with new reason-specific keys
    
    * style(*): format code with prettier
    
    - Remove extra whitespace in http_client.rs
    - Fix formatting issues in useProviderActions.ts
    
    * fix(proxy): post-merge fixes for forward return type and clippy warnings
    
    - Restore forward() return type to (ProxyResponse, Option<String>)
      to pass claude_api_format through to callers
    - Inline format args in log::warn! macro (clippy::uninlined_format_args)
    - Suppress too_many_arguments for check_claude_stream
    
    * refactor(proxy): preserve original header wire order and add non-streaming body timeout
    
    - Rewrite build_raw_request to emit headers in original
      client-sent sequence instead of hash-map order
    - Remove unused OriginalHeaderCases::get_all method
    - Add body_timeout to read_decoded_body to prevent
      requests hanging when upstream stalls after headers