diff --git a/CHANGELOG.md b/CHANGELOG.md index bd5fcd22a..04cb925ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to CC Switch will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [3.13.0] - 2026-04-10 Development since v3.12.3 focuses on quota visibility, provider workflow upgrades, stronger proxy compatibility, and lower-overhead tray / session workflows. @@ -13,7 +13,7 @@ Development since v3.12.3 focuses on quota visibility, provider workflow upgrade - **Lightweight Mode**: Added a tray-only mode that destroys the main window and keeps CC Switch running from the system tray, with the window recreated when users reopen it. - **Provider Model Auto-Fetch**: Added OpenAI-compatible `/v1/models` discovery for Claude, Codex, Gemini, OpenCode, and OpenClaw provider forms, including grouped dropdown selection and failure-specific error messages. -- **Quota & Balance Visibility**: Added inline quota or balance display for official Claude / Codex / Gemini providers, GitHub Copilot premium interactions, Codex OAuth providers, Token Plan providers (Kimi / Zhipu GLM / MiniMax), and official balance queries for DeepSeek, StepFun, SiliconFlow, OpenRouter, and Novita AI. +- **Quota & Balance Visibility**: Added inline quota or balance display for official Claude / Codex / Gemini providers, GitHub Copilot premium interactions, Codex OAuth providers, Token Plan providers (Kimi / Zhipu GLM / MiniMax), and official balance queries for DeepSeek, StepFun, SiliconFlow, OpenRouter, and Novita AI. Copilot / ChatGPT OAuth and CLI subscription quota now only auto-poll for the currently active provider, preventing unnecessary API calls and misleading displays on non-current cards. - **Skills Discovery & Batch Updates**: Added SHA-256 based skill update detection, per-skill and batch update actions, a storage-location toggle between CC Switch and `~/.agents/skills`, and public `skills.sh` search integration. - **Session Workflow Upgrades**: Added batch delete in Session Manager, a directory picker before launching Claude terminal restore commands, usage import from Claude / Codex / Gemini session logs without requiring proxy interception, and per-app usage filtering for Claude / Codex / Gemini dashboards. - **Codex OAuth Reverse Proxy**: Added ChatGPT Plus / Pro based Codex OAuth reverse proxy support for Claude provider cards, including managed OAuth login and inline subscription quota display. @@ -21,6 +21,22 @@ Development since v3.12.3 focuses on quota visibility, provider workflow upgrade - **Full URL Endpoint Mode**: Added a provider option that treats `base_url` as a complete upstream endpoint so proxy forwarding and stream checks can work with vendors that require nonstandard URL layouts. - **OpenCode StepFun Step Plan Preset**: Added a StepFun Step Plan provider preset for OpenCode. - **Copilot Interaction Optimizer**: Added request classification and routing logic to reduce unnecessary GitHub Copilot premium interaction consumption. +- **First-Run Welcome Dialog**: Added a one-time welcome dialog on fresh installs explaining how existing configuration is preserved as a default provider and how the bundled official preset enables one-click revert. Upgrade users are excluded. +- **Official Provider Seeding**: Added automatic seeding of Claude Official, OpenAI Official, and Google Official provider entries on startup, giving every user a one-click path back to the official endpoint. +- **OpenCode / OpenClaw Auto-Import**: Added automatic startup import of live OpenCode and OpenClaw provider configurations, matching the auto-import behavior already present for Claude, Codex, and Gemini. +- **Common Config Editor Guidance**: Added an informational guide and empty-state prompt to the Common Config snippet editor modal for Claude, Codex, and Gemini, with i18n support. +- **Common Config First-Run Notice**: Added a one-time informational dialog explaining Common Config Snippets when users first open the provider add/edit form. +- **Claude Session Titles**: Added meaningful title extraction for Claude sessions using a priority chain: custom-title metadata, first real user message, then directory basename fallback. +- **Session Search Highlighting**: Added keyword highlighting in session titles and messages during Session Manager search. +- **URL-Based Provider Icons**: Added a dual rendering mode to the icon system supporting Vite URL imports for large SVGs and raster images (PNG, JPG, WebP), keeping small SVGs inlined. +- **Kaku Terminal Support**: Added Kaku as a selectable terminal for session launch on macOS, reusing the WezTerm-compatible launch path. +- **OMO Slim Council Support**: Restored first-class council support as a built-in oh-my-opencode-slim agent with updated metadata and UI copy. +- **TheRouter Provider Preset**: Added TheRouter provider presets across Claude, Codex, Gemini, OpenCode, and OpenClaw. +- **DDSHub Provider Preset**: Added DDSHub as a third-party partner provider for Claude with icon and partner promotion text. +- **LionCCAPI Provider Preset**: Added LionCCAPI as a third-party partner provider across all five apps with anthropic-messages protocol for OpenCode and OpenClaw. +- **Shengsuanyun Provider Preset**: Added Shengsuanyun (胜算云) as an aggregator partner provider across all five apps with URL-based icon and localized display name. +- **PIPELLM Provider Preset**: Added PIPELLM provider preset across Claude, Codex, OpenCode, and OpenClaw with full model definitions and icon. +- **E-FlowCode Provider Preset**: Added E-FlowCode provider preset across all five apps with per-app protocol configuration. ### Changed @@ -47,12 +63,19 @@ Development since v3.12.3 focuses on quota visibility, provider workflow upgrade - **Linux UI Unresponsive on Startup**: Fixed a bug where the window UI (including native title bar buttons) couldn't receive clicks on Linux until the user manually maximized and restored the window. Root causes: (1) Tauri webview did not acquire keyboard focus after `show()` on Linux, so the first click was consumed by X11/Wayland click-to-activate (Tauri #10746, wry #637); (2) GTK surface's input region failed to renegotiate on the `visible:false → show()` path under some WebKitGTK/compositor combinations, leaving the entire window unresponsive. Mitigations: set `WEBKIT_DISABLE_COMPOSITING_MODE=1` at startup, and added a new `linux_fix::nudge_main_window` helper that performs `set_focus` + a ±1px no-op resize ~200ms after show, equivalent to a visually invisible "maximize-and-restore". Wired into all window-re-show paths (normal startup, deeplink, single_instance, tray `show_main`, lightweight exit). - **Linux Drag Region on Header**: Removed `data-tauri-drag-region` from the top header bar on Linux to avoid triggering `gtk_window_begin_move_drag` paths affected by Tauri #13440 under Wayland. macOS drag behavior is preserved. - **OpenCode / OpenClaw Stream Check Edge Cases**: Fixed custom-header passthrough, OpenClaw custom auth-header detection, Bedrock error messaging, and OpenCode default `baseURL` fallback handling in Stream Check. +- **Duplicate Toast on Provider Switch**: Fixed double toast notifications (proxy-required warning followed by switch-success) when switching to Copilot, ChatGPT, or OpenAI-format providers with the proxy not running. +- **Session Search Accuracy & Chinese Support**: Fixed session search result truncation across providers and switched FlexSearch tokenizer to full mode for proper Chinese substring matching. +- **Adaptive Thinking Reasoning Effort**: Fixed `resolve_reasoning_effort()` mapping adaptive thinking to `xhigh` instead of incorrectly using `high` in OpenAI format conversions. +- **Thinking Model Fallback Display**: Fixed the Claude provider form showing an empty Thinking model field after saving only a main model by applying read-only fallback to ANTHROPIC_MODEL. +- **Auth Tab Localization**: Fixed missing i18n translation keys for the settings auth tab label across all locale bundles. +- **Schema Migration Guard**: Fixed database migrations failing when skills or model_pricing tables did not exist by adding table-existence checks before ALTER and UPDATE operations. ### Docs - **User Manual Refresh**: Updated the EN / ZH / JA manuals for tray submenus, lightweight mode, provider model fetching, session management, workspace files, WebDAV v2 behavior, OpenCode / OpenClaw activation, and other provider workflow improvements. - **Community & Contribution Docs**: Added `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, bilingual issue / PR templates, Dependabot config, and CI quality checks. - **Release Notes Risk Notice**: Added a Copilot reverse proxy risk notice and anchored highlight links in the v3.12.3 release notes across all three languages. +- **Sponsor Partners**: Added Shengsuanyun, LionCC, and DDS as sponsor partners in README across all languages. --- diff --git a/docs/release-notes/v3.13.0-en.md b/docs/release-notes/v3.13.0-en.md index a6d13524a..2dded22b3 100644 --- a/docs/release-notes/v3.13.0-en.md +++ b/docs/release-notes/v3.13.0-en.md @@ -8,11 +8,11 @@ ## Overview -CC Switch v3.13.0 is a major feature release centered on observability, provider workflow ergonomics, and proxy compatibility. It adds inline **quota and balance displays** across official Claude / Codex / Gemini providers plus Token Plan, Copilot, and third-party balance APIs; introduces a **Lightweight Mode** that keeps CC Switch running from the system tray without a main window; delivers **automatic model discovery** via OpenAI-compatible `/v1/models` across all five supported applications; ships a **Codex OAuth reverse proxy** for ChatGPT Plus / Pro subscribers; reorganizes the tray menu into **per-app submenus**; rebuilds the proxy forwarding stack on a **Hyper-based client**; and overhauls the **Skills workflow** with discovery, batch updates, and storage-location toggling. Additional improvements include full URL endpoint mode, session-log usage tracking without proxy interception, the Copilot interaction optimizer, a UTF-8 streaming chunk boundary fix for multi-byte output, and a Linux startup UI responsiveness fix. +CC Switch v3.13.0 is a major feature release centered on observability, provider workflow ergonomics, and proxy compatibility. It adds inline **quota and balance displays** across official Claude / Codex / Gemini providers plus Token Plan, Copilot, and third-party balance APIs; introduces a **Lightweight Mode** that keeps CC Switch running from the system tray without a main window; delivers **automatic model discovery** via OpenAI-compatible `/v1/models` across all five supported applications; ships a **Codex OAuth reverse proxy** for ChatGPT subscribers; reorganizes the tray menu into **per-app submenus**; rebuilds the proxy forwarding stack on a **Hyper-based client**; and overhauls the **Skills workflow** with discovery, batch updates, storage-location toggling, and built-in skills.sh search and install. Additional improvements include full URL endpoint mode, enhanced token usage tracking, the Copilot interaction optimizer, a UTF-8 streaming chunk boundary fix for multi-byte output, a Linux startup UI responsiveness fix, and a friendlier new-user onboarding experience. -**Release Date**: 2026-04-08 +**Release Date**: 2026-04-10 -**Update Scale**: 98 commits | 229 files changed | +23,891 / -2,305 lines +**Update Scale**: 139 commits | 280 files changed | +31,627 / -3,042 lines --- @@ -21,9 +21,9 @@ CC Switch v3.13.0 is a major feature release centered on observability, provider - **Lightweight Mode**: Tray-only operating mode that destroys the main window on exit to tray and recreates it on demand, reducing CC Switch's desktop footprint to near zero when idle - **Quota & Balance Visibility**: Inline quota or balance readout across provider cards — official Claude / Codex / Gemini subscriptions, GitHub Copilot premium interactions, Codex OAuth, Token Plan providers (Kimi / Zhipu GLM / MiniMax), plus official balance queries for DeepSeek, StepFun, SiliconFlow, OpenRouter, and Novita AI - **Provider Model Auto-Fetch**: OpenAI-compatible `/v1/models` discovery across Claude, Codex, Gemini, OpenCode, and OpenClaw provider forms, with grouped dropdown selection and failure-specific error messages -- **Codex OAuth Reverse Proxy**: ChatGPT Plus / Pro Codex reverse proxy exposed as a new Claude provider card with managed OAuth login and inline subscription quota display ([⚠️ Risk Notice](#️-risk-notice)) +- **Codex OAuth Reverse Proxy**: ChatGPT Codex reverse proxy exposed as a new Claude provider card type, allowing users to use their ChatGPT subscription in Claude Code. Includes managed OAuth login and inline subscription quota display ([⚠️ Risk Notice](#️-risk-notice)) - **Tray Per-App Submenus**: Reworked the tray menu into per-application submenus so it never overflows the screen and background provider switching scales to dozens of providers per app -- **Skills Discovery & Batch Updates**: SHA-256-based skill update detection, per-skill and "Update All" batch actions, public `skills.sh` search integration, and a storage-location toggle between CC Switch storage and `~/.agents/skills` +- **Skills Discovery & Batch Updates**: SHA-256-based skill update detection, per-skill and "Update All" batch actions, `skills.sh` search integration, and a storage-location toggle between CC Switch storage and `~/.agents/skills` - **Session Workflow Upgrades**: Batch session deletion, a directory picker before launching Claude terminal restore, usage import from Claude / Codex / Gemini session logs without proxy interception, precise Codex JSONL parsing, and per-app usage filtering - **OpenCode / OpenClaw Stream Check Coverage**: OpenCode detection via npm package mapping, OpenClaw `openai-completions` support, and the remaining OpenClaw protocol variants — with custom-header passthrough and auth-header detection fixes - **Full URL Endpoint Mode**: Provider option that treats `base_url` as a complete upstream endpoint, unblocking vendors that require nonstandard URL layouts @@ -31,6 +31,10 @@ CC Switch v3.13.0 is a major feature release centered on observability, provider - **Copilot Interaction Optimizer**: Request classification and routing logic that reduces unnecessary GitHub Copilot premium interaction consumption - **UTF-8 Stream Chunk Boundary Fix**: All four SSE streaming paths now preserve incomplete multi-byte UTF-8 sequences across TCP chunks, eliminating intermittent U+FFFD garbled output via the Copilot reverse proxy - **Linux Startup UI Fix**: Fixed the long-standing issue where the window UI couldn't receive clicks on Linux until the user manually maximized and restored the window +- **First-Run Onboarding**: One-time welcome dialog on fresh installs, automatic seeding of Claude / OpenAI / Google official presets, and auto-import of OpenCode / OpenClaw live configurations on startup +- **Claude Session Titles & Search Highlighting**: Meaningful title extraction for Claude sessions using a priority chain (custom-title metadata → first user message → directory basename), plus keyword highlighting in Session Manager search results +- **URL-Based Provider Icons**: Dual rendering mode supporting Vite URL imports for large SVGs and raster images (PNG, JPG, WebP), keeping small SVGs inlined +- **New Provider Presets**: TheRouter, DDSHub, LionCCAPI, Shengsuanyun (胜算云), PIPELLM, and E-FlowCode across supported applications --- @@ -50,9 +54,9 @@ Added inline quota and balance readouts to provider cards so users can see remai - **Official subscriptions**: Inline quota display for Claude, Codex, and Gemini official providers - **GitHub Copilot**: Premium interactions quota display on the Copilot provider card -- **Codex OAuth**: ChatGPT Plus / Pro subscription quota inline with the Codex OAuth provider card -- **Token Plan providers**: Kimi, Zhipu GLM, and MiniMax with corrected quota math and consistent 0% → 100% usage progression -- **Third-party balances**: Official balance queries for DeepSeek, StepFun, SiliconFlow, OpenRouter, and Novita AI +- **Codex OAuth**: ChatGPT subscription quota inline with the Codex OAuth provider card +- **Token Plan providers**: Kimi, Zhipu GLM, and MiniMax usage progression display (requires manual activation to avoid confusion) +- **Third-party balances**: Official balance queries for DeepSeek, StepFun, SiliconFlow, OpenRouter, and Novita AI (requires manual activation to avoid confusion) - Health-check and usage-config buttons are hidden for official providers to keep the card clean ### Provider Model Auto-Fetch @@ -66,13 +70,12 @@ Added OpenAI-compatible model discovery to every provider form, removing the man ### Codex OAuth Reverse Proxy -Added a reverse proxy path for ChatGPT Plus / Pro subscribers who want to route their Codex OAuth session through CC Switch. +Added a reverse proxy path for ChatGPT subscribers who want to use their ChatGPT subscription in Claude Code. -- Managed OAuth login flow with ChatGPT Plus / Pro authentication +- Managed OAuth login flow with ChatGPT authentication - Surfaces as a new Claude provider card type alongside API-key providers - Inline subscription quota display -- Integrated into the Auth Center with tightened copy, layout, and icon presentation -- Bumped the Codex OAuth preset to the GPT-5.4 model family +- Integrated into the Auth Center for unified token management - See the [⚠️ Risk Notice](#️-risk-notice) below before enabling ### Tray Per-App Submenus @@ -131,6 +134,54 @@ Added request classification and routing logic that reduces unnecessary GitHub C - Classifies incoming requests by intent and weight - Routes low-value requests away from premium interaction consumption paths - Designed to extend the usable lifetime of a Copilot subscription +- Note: Even with optimized consumption, using the Copilot API outside of Copilot still consumes more than using it within Copilot. + +### First-Run Welcome Dialog + +Added a one-time welcome dialog on fresh installs to guide new users through the CC Switch workflow. + +- Explains how existing live configuration is preserved as a default provider +- Introduces the bundled official preset that enables one-click revert to official endpoints +- Upgrade users are automatically excluded via empty provider check + +### Official Provider Seeding + +- Added automatic seeding of Claude Official, OpenAI Official, and Google Official provider entries on startup, giving every user a one-click path back to the official endpoint + +### OpenCode / OpenClaw Auto-Import + +- Added automatic startup import of live OpenCode and OpenClaw provider configurations, matching the auto-import behavior already present for Claude, Codex, and Gemini + +### Common Config Editor Guidance + +- Added an informational guide and empty-state prompt to the Common Config snippet editor modal for Claude, Codex, and Gemini +- Added a one-time informational dialog explaining Common Config Snippets when users first open the provider add/edit form + +### Claude Session Titles & Search Highlighting + +- Added meaningful title extraction for Claude sessions using a priority chain: custom-title metadata, first real user message, then directory basename fallback +- Added keyword highlighting in session titles and messages during Session Manager search + +### URL-Based Provider Icons + +- Added a dual rendering mode to the icon system: small SVGs are inlined as React components, while large SVGs and raster images (PNG, JPG, WebP) are loaded via Vite URL imports as `` tags + +### Kaku Terminal Support + +- Added Kaku as a selectable terminal for session launch on macOS, reusing the WezTerm-compatible launch path (#1983, thanks @yovinchen) + +### OMO Slim Council Support + +- Restored first-class council support as a built-in oh-my-opencode-slim agent with updated metadata and UI copy (#1982, thanks @yovinchen) + +### New Provider Presets + +- **TheRouter**: Added across Claude, Codex, Gemini, OpenCode, and OpenClaw (#1891, #1892, thanks @cmzz) +- **DDSHub**: Added as a third-party partner provider for Claude with icon and partner promotion text +- **LionCCAPI**: Added across all five apps with anthropic-messages protocol for OpenCode and OpenClaw +- **Shengsuanyun (胜算云)**: Added as an aggregator partner provider across all five apps with URL-based icon and localized display name +- **PIPELLM**: Added across Claude, Codex, OpenCode, and OpenClaw with full model definitions and icon +- **E-FlowCode**: Added across all five apps with per-app protocol configuration --- @@ -263,6 +314,31 @@ Fixed a long-standing Linux bug where the window UI (including native title bar - Bedrock error messaging - OpenCode default `baseURL` fallback handling +### Duplicate Toast on Provider Switch + +- Fixed double toast notifications (proxy-required warning followed by switch-success) when switching to Copilot, ChatGPT, or OpenAI-format providers with the proxy not running + +### Session Search Accuracy & Chinese Support + +- Fixed session search result truncation across providers +- Switched FlexSearch tokenizer to full mode for proper Chinese substring matching + +### Adaptive Thinking Reasoning Effort + +- Fixed `resolve_reasoning_effort()` mapping adaptive thinking to `xhigh` instead of incorrectly using `high` in OpenAI format conversions + +### Thinking Model Fallback Display + +- Fixed the Claude provider form showing an empty Thinking model field after saving only a main model by applying read-only fallback to ANTHROPIC_MODEL (#1984, thanks @yovinchen) + +### Auth Tab Localization + +- Fixed missing i18n translation keys for the settings auth tab label across all locale bundles (#1985, thanks @yovinchen) + +### Schema Migration Guard + +- Fixed database migrations failing when skills or model_pricing tables did not exist by adding table-existence checks before ALTER and UPDATE operations + --- ## Documentation @@ -282,16 +358,20 @@ Fixed a long-standing Linux bug where the window UI (including native title bar - Added a Copilot reverse proxy risk notice and anchored highlight links in the v3.12.3 release notes across all three languages +### Sponsor Partners + +- Added Shengsuanyun, LionCC, and DDS as sponsor partners in README across all languages + --- ## ⚠️ Risk Notice **Codex OAuth Reverse Proxy Disclaimer** -The Codex OAuth reverse proxy introduced in this release accesses ChatGPT Plus / Pro Codex services through reverse-engineered OAuth flows. Please be aware of the following risks before enabling this feature: +The Codex OAuth reverse proxy introduced in this release accesses ChatGPT Codex services through reverse-engineered OAuth flows. Please be aware of the following risks before enabling this feature: 1. **Terms of Service**: Using reverse-engineered OAuth flows to access OpenAI services may violate OpenAI's terms of service, which prohibit unauthorized automated access, service reproduction, and circumventing intended access paths. -2. **Account Risk**: OpenAI may flag unusual usage patterns as suspicious automated activity, potentially resulting in temporary or permanent restrictions on ChatGPT Plus / Pro access. +2. **Account Risk**: OpenAI may flag unusual usage patterns as suspicious automated activity, potentially resulting in temporary or permanent restrictions on ChatGPT access. 3. **No Guarantee**: OpenAI may update its authentication and detection mechanisms at any time, and usage patterns that work today may be flagged in the future. The **GitHub Copilot reverse proxy** introduced in v3.12.3 also remains subject to its existing risk notice — see the [v3.12.3 release notes](v3.12.3-en.md#️-risk-notice) for the full disclosure. diff --git a/docs/release-notes/v3.13.0-ja.md b/docs/release-notes/v3.13.0-ja.md index a15c0fdff..a3550039b 100644 --- a/docs/release-notes/v3.13.0-ja.md +++ b/docs/release-notes/v3.13.0-ja.md @@ -8,11 +8,11 @@ ## 概要 -CC Switch v3.13.0 は、可観測性、プロバイダーワークフローの使いやすさ、プロキシ互換性を中心とした大型機能リリースです。Claude / Codex / Gemini の公式プロバイダー、Token Plan、Copilot、サードパーティ残高 API にわたる**クォータと残高のインライン表示**を追加し、メインウィンドウなしでシステムトレイから CC Switch を動作させる**軽量モード**を導入しました。OpenAI 互換の `/v1/models` による**自動モデル発見**を 5 つのサポート対象アプリケーションすべてに提供し、ChatGPT Plus / Pro サブスクライバー向けの **Codex OAuth リバースプロキシ**を同梱しています。トレイメニューを**アプリ別サブメニュー**に再編成し、プロキシ転送スタックを **Hyper ベースのクライアント**に再構築し、**Skills ワークフロー**を発見、バッチ更新、ストレージ位置切り替えで刷新しました。さらに、フル URL エンドポイントモード、プロキシ傍受なしのセッションログ用量追跡、Copilot インタラクション最適化、マルチバイト UTF-8 ストリームチャンク境界修正、Linux 起動時の UI 応答性修正なども含まれます。 +CC Switch v3.13.0 は、可観測性、プロバイダーワークフローの使いやすさ、プロキシ互換性を中心とした大型機能リリースです。Claude / Codex / Gemini の公式プロバイダー、Token Plan、Copilot、サードパーティ残高 API にわたる**クォータと残高のインライン表示**を追加し、メインウィンドウなしでシステムトレイから CC Switch を動作させる**軽量モード**を導入しました。OpenAI 互換の `/v1/models` による**自動モデル発見**を 5 つのサポート対象アプリケーションすべてに提供し、ChatGPT サブスクライバー向けの **Codex OAuth リバースプロキシ**を同梱しています。トレイメニューを**アプリ別サブメニュー**に再編成し、プロキシ転送スタックを **Hyper ベースのクライアント**に再構築し、**Skills ワークフロー**を発見、バッチ更新、ストレージ位置切り替え、および組み込みの skills.sh 検索・インストールで刷新しました。さらに、フル URL エンドポイントモード、強化されたトークン用量追跡、Copilot インタラクション最適化、マルチバイト UTF-8 ストリームチャンク境界修正、Linux 起動時の UI 応答性修正、およびよりフレンドリーな新規ユーザーオンボーディングなども含まれます。 -**リリース日**: 2026-04-08 +**リリース日**: 2026-04-10 -**更新規模**: 98 commits | 229 files changed | +23,891 / -2,305 lines +**更新規模**: 139 commits | 280 files changed | +31,627 / -3,042 lines --- @@ -21,9 +21,9 @@ CC Switch v3.13.0 は、可観測性、プロバイダーワークフローの - **軽量モード**: トレイ専用の動作モード。トレイへの終了時にメインウィンドウを破棄し、必要時に再作成することで、アイドル時の CC Switch のデスクトップフットプリントを最小化 - **クォータと残高の可視化**: プロバイダーカードでのインラインクォータ/残高表示 — Claude / Codex / Gemini 公式サブスクリプション、GitHub Copilot premium interactions、Codex OAuth、Token Plan プロバイダー(Kimi / Zhipu GLM / MiniMax)、および DeepSeek / StepFun / SiliconFlow / OpenRouter / Novita AI の公式残高クエリをカバー - **プロバイダーモデル自動取得**: Claude / Codex / Gemini / OpenCode / OpenClaw のプロバイダーフォームに OpenAI 互換の `/v1/models` 発見機能を追加。グループ化ドロップダウンと失敗時の具体的なエラーメッセージ付き -- **Codex OAuth リバースプロキシ**: ChatGPT Plus / Pro の Codex リバースプロキシを新しい Claude プロバイダーカードタイプとして追加。マネージド OAuth ログインとサブスクリプションクォータのインライン表示を提供([⚠️ リスクに関する注意事項](#️-リスクに関する注意事項)) +- **Codex OAuth リバースプロキシ**: ChatGPT の Codex リバースプロキシを新しい Claude プロバイダーカードタイプとして追加。ユーザーは ChatGPT サブスクリプションを Claude Code で利用可能に。マネージド OAuth ログインとサブスクリプションクォータのインライン表示を提供([⚠️ リスクに関する注意事項](#️-リスクに関する注意事項)) - **トレイのアプリ別サブメニュー**: トレイメニューをアプリ別サブメニューに再編成し、プロバイダー数が多くてもメニューがオーバーフローせず、バックグラウンドのプロバイダー切り替えが長いリストでもスケール -- **Skills 発見とバッチ更新**: SHA-256 ベースの skill 更新検出、各 skill および「すべて更新」のバッチ更新、公開 `skills.sh` 検索統合、CC Switch ストレージと `~/.agents/skills` の間のストレージ位置切り替え +- **Skills 発見とバッチ更新**: SHA-256 ベースの skill 更新検出、各 skill および「すべて更新」のバッチ更新、`skills.sh` 検索統合、CC Switch ストレージと `~/.agents/skills` の間のストレージ位置切り替え - **セッションワークフローの改善**: Session Manager でのバッチ削除、Claude ターミナル復元前のディレクトリピッカー、プロキシ傍受なしでの Claude / Codex / Gemini セッションログからの用量インポート、正確な Codex JSONL 解析、アプリ別の用量フィルタリング - **OpenCode / OpenClaw Stream Check カバレッジ**: OpenCode の npm パッケージマッピング検出、OpenClaw `openai-completions` サポート、および残りの OpenClaw プロトコルバリアント - **フル URL エンドポイントモード**: `base_url` を完全な上流エンドポイントとして扱うプロバイダーオプションを追加し、非標準 URL レイアウトを要求するベンダーに対応 @@ -31,6 +31,10 @@ CC Switch v3.13.0 は、可観測性、プロバイダーワークフローの - **Copilot インタラクション最適化**: GitHub Copilot premium interaction の不要な消費を削減するリクエスト分類とルーティングロジックを追加 - **UTF-8 ストリームチャンク境界修正**: マルチバイト UTF-8 シーケンスが TCP チャンクを跨いで分割された際の Copilot リバースプロキシ経由での文字化け(U+FFFD 置換文字)を解消するため、すべての 4 つの SSE ストリーミングパスを修正 - **Linux 起動時 UI 修正**: ユーザーが手動でウィンドウを最大化・復元するまでウィンドウ UI がクリックを受け付けない長年の問題を修正 +- **初回起動オンボーディング**: 新規インストール時のワンタイムウェルカムダイアログ、Claude / OpenAI / Google 公式プリセットの自動シード、起動時の OpenCode / OpenClaw ライブ設定の自動インポート +- **Claude セッションタイトルと検索ハイライト**: カスタムタイトルメタデータ → 最初のユーザーメッセージ → ディレクトリベースネームの優先チェーンによる Claude セッションの意味のあるタイトル抽出、Session Manager 検索でのキーワードハイライト +- **URL ベースのプロバイダーアイコン**: 大きな SVG とラスター画像(PNG / JPG / WebP)を Vite URL import でロードし、小さな SVG はインライン保持するデュアルレンダリングモード +- **新プロバイダープリセット**: TheRouter、DDSHub、LionCCAPI、Shengsuanyun(胜算云)、PIPELLM、E-FlowCode を対応アプリケーションに追加 --- @@ -50,9 +54,9 @@ CC Switch のアイドル時のデスクトップフットプリントを大幅 - **公式サブスクリプション**: Claude / Codex / Gemini 公式プロバイダーのサブスクリプションクォータ表示 - **GitHub Copilot**: Copilot プロバイダーカードに premium interactions 残量を表示 -- **Codex OAuth**: Codex OAuth カードに ChatGPT Plus / Pro サブスクリプションクォータをインライン表示 -- **Token Plan プロバイダー**: Kimi、Zhipu GLM、MiniMax。クォータ計算と 0% → 100% 使用量進行を修正 -- **サードパーティ残高**: DeepSeek、StepFun、SiliconFlow、OpenRouter、Novita AI に公式残高クエリを追加 +- **Codex OAuth**: Codex OAuth カードに ChatGPT サブスクリプションクォータをインライン表示 +- **Token Plan プロバイダー**: Kimi、Zhipu GLM、MiniMax の使用量進行表示(混乱を避けるため手動で有効化が必要) +- **サードパーティ残高**: DeepSeek、StepFun、SiliconFlow、OpenRouter、Novita AI に公式残高クエリを追加(混乱を避けるため手動で有効化が必要) - 公式プロバイダーではヘルスチェックと用量設定ボタンを非表示にし、カードをクリーンに保つ ### プロバイダーモデル自動取得 @@ -66,13 +70,12 @@ CC Switch のアイドル時のデスクトップフットプリントを大幅 ### Codex OAuth リバースプロキシ -ChatGPT Plus / Pro のサブスクライバーが Codex OAuth セッションを CC Switch 経由で利用できるリバースプロキシパスを追加。 +ChatGPT サブスクライバーが ChatGPT サブスクリプションを Claude Code で利用できるリバースプロキシパスを追加。 -- ChatGPT Plus / Pro 認証を使ったマネージド OAuth ログインフロー +- ChatGPT 認証を使ったマネージド OAuth ログインフロー - API キー型プロバイダーと並ぶ新しい Claude プロバイダーカードタイプとして表示 - サブスクリプションクォータのインライン表示 -- Auth Center との緊密な統合と、コピー・レイアウト・アイコンの調整 -- Codex OAuth プリセットを GPT-5.4 モデルファミリーに更新 +- Auth Center との統合によるトークンの一元管理 - 有効化前に下記の [⚠️ リスクに関する注意事項](#️-リスクに関する注意事項) をご確認ください ### トレイのアプリ別サブメニュー @@ -131,6 +134,54 @@ GitHub Copilot premium interaction の不要な消費を削減するリクエス - 受信リクエストを意図と重要度で分類 - 価値の低いリクエストを premium interaction 消費パスから迂回 - Copilot サブスクリプションの使用可能期間を延長することを目的 +- 注意: 消費を最適化しても、Copilot 外で Copilot API を使用する場合、Copilot 内で使用するよりも消費量は多くなります。 + +### 初回起動ウェルカムダイアログ + +新規インストールのユーザーに CC Switch のワークフローを案内するワンタイムウェルカムダイアログを追加。 + +- 既存のライブ設定がデフォルトプロバイダーとして保持される仕組みを説明 +- 内蔵の公式プリセットによるワンクリックでの公式エンドポイント復帰を紹介 +- アップグレードユーザーは空プロバイダーチェックにより自動的にスキップ + +### 公式プロバイダーの自動シード + +- 起動時に Claude Official / OpenAI Official / Google Official プロバイダーエントリを自動シードし、すべてのユーザーにワンクリックで公式エンドポイントに戻るパスを提供 + +### OpenCode / OpenClaw 自動インポート + +- 起動時に OpenCode と OpenClaw のライブプロバイダー設定を自動インポート。Claude / Codex / Gemini で既にある自動インポート動作と同等に + +### Common Config エディタガイダンス + +- Claude / Codex / Gemini の Common Config スニペットエディタモーダルに情報ガイドと空状態プロンプトを追加 +- ユーザーがプロバイダー追加/編集フォームを初めて開く際、Common Config Snippets を説明するワンタイムダイアログを追加 + +### Claude セッションタイトルと検索ハイライト + +- Claude セッションの意味のあるタイトル抽出を追加。優先チェーン: カスタムタイトルメタデータ → 最初の実ユーザーメッセージ → ディレクトリベースネームフォールバック +- Session Manager 検索時にセッションタイトルとメッセージ内のキーワードをハイライト + +### URL ベースのプロバイダーアイコン + +- アイコンシステムにデュアルレンダリングモードを追加: 小さな SVG は React コンポーネントとしてインライン、大きな SVG とラスター画像(PNG / JPG / WebP)は Vite URL import で `` タグとしてロード + +### Kaku ターミナルサポート + +- macOS でセッション起動用の選択可能なターミナルとして Kaku を追加。WezTerm 互換の起動パスを再利用 (#1983, @yovinchen に感謝) + +### OMO Slim Council サポート + +- 内蔵 oh-my-opencode-slim エージェントとしての council のファーストクラスサポートを復元。メタデータと UI コピーを更新 (#1982, @yovinchen に感謝) + +### 新プロバイダープリセット + +- **TheRouter**: Claude / Codex / Gemini / OpenCode / OpenClaw の 5 アプリに追加 (#1891, #1892, @cmzz に感謝) +- **DDSHub**: Claude のサードパーティパートナープロバイダーとして追加。アイコンとパートナープロモーションテキスト付き +- **LionCCAPI**: 5 アプリすべてに追加。OpenCode / OpenClaw は anthropic-messages プロトコルを使用 +- **Shengsuanyun(胜算云)**: アグリゲーターパートナープロバイダーとして 5 アプリすべてに追加。URL ベースのアイコンとローカライズ名をサポート +- **PIPELLM**: Claude / Codex / OpenCode / OpenClaw に追加。完全なモデル定義とアイコン付き +- **E-FlowCode**: 5 アプリすべてに追加。アプリごとに異なるプロトコル設定 --- @@ -263,6 +314,31 @@ Claude Code で Copilot リバースプロキシ経由時、中国語文字や - Bedrock エラーメッセージを修正 - OpenCode デフォルト `baseURL` のフォールバック処理を修正 +### プロバイダー切り替え時の重複 Toast + +- プロキシ未実行時に Copilot / ChatGPT / OpenAI フォーマットプロバイダーに切り替えた際の二重 toast 通知(プロキシ必要警告 + 切り替え成功)を修正 + +### セッション検索精度と中国語サポート + +- プロバイダーをまたぐセッション検索結果の切り詰めを修正 +- FlexSearch トークナイザーを full モードに切り替え、中国語サブストリングマッチングを正しく動作させる + +### 適応的思考の推論エフォート + +- `resolve_reasoning_effort()` が適応的思考を `high` ではなく正しく `xhigh` にマッピングするよう修正(OpenAI フォーマット変換時) + +### Thinking モデルフォールバック表示 + +- Claude プロバイダーフォームでメインモデルのみ保存後に Thinking モデルフィールドが空で表示される問題を修正。ANTHROPIC_MODEL への読み取り専用フォールバックを適用 (#1984, @yovinchen に感謝) + +### Auth タブのローカライゼーション + +- 設定の auth タブラベルに不足していた i18n 翻訳キーをすべてのロケールバンドルで修正 (#1985, @yovinchen に感謝) + +### スキーマ移行ガード + +- skills または model_pricing テーブルが存在しない場合にデータベース移行が失敗する問題を修正。ALTER および UPDATE 操作の前にテーブル存在チェックを追加 + --- ## ドキュメント @@ -282,16 +358,20 @@ Claude Code で Copilot リバースプロキシ経由時、中国語文字や - v3.12.3 の release notes に Copilot リバースプロキシのリスク通知とハイライトリンクのアンカーを 3 言語すべてに追加 +### スポンサーパートナー + +- README の 3 言語すべてに Shengsuanyun、LionCC、DDS をスポンサーパートナーとして追加 + --- ## ⚠️ リスクに関する注意事項 **Codex OAuth リバースプロキシに関する免責事項** -本リリースで追加された Codex OAuth リバースプロキシ機能は、リバースエンジニアリングによる OAuth フローを通じて ChatGPT Plus / Pro の Codex サービスにアクセスします。この機能を有効にする前に、以下のリスクをご確認ください: +本リリースで追加された Codex OAuth リバースプロキシ機能は、リバースエンジニアリングによる OAuth フローを通じて ChatGPT の Codex サービスにアクセスします。この機能を有効にする前に、以下のリスクをご確認ください: 1. **利用規約違反の可能性**: リバースエンジニアリングされた OAuth フローを使用して OpenAI サービスにアクセスすることは、OpenAI の利用規約に違反する可能性があります。これらの規約では、未承認の自動アクセス、サービス複製、および意図されたアクセスパスの回避が禁止されています。 -2. **アカウントリスク**: OpenAI は異常な使用パターンを疑わしい自動化活動としてフラグ付けし、ChatGPT Plus / Pro へのアクセスに一時的または永久的な制限を科す可能性があります。 +2. **アカウントリスク**: OpenAI は異常な使用パターンを疑わしい自動化活動としてフラグ付けし、ChatGPT へのアクセスに一時的または永久的な制限を科す可能性があります。 3. **将来の利用保証なし**: OpenAI は認証および検出メカニズムをいつでも更新する可能性があり、現在動作する使用パターンが将来的にフラグ付けされる可能性があります。 v3.12.3 で導入された **GitHub Copilot リバースプロキシ**も、既存のリスク通知の対象となります — 詳細は [v3.12.3 リリースノート](v3.12.3-ja.md#️-リスクに関する注意事項) を参照してください。 diff --git a/docs/release-notes/v3.13.0-zh.md b/docs/release-notes/v3.13.0-zh.md index 925462127..4eb5eddd5 100644 --- a/docs/release-notes/v3.13.0-zh.md +++ b/docs/release-notes/v3.13.0-zh.md @@ -8,11 +8,11 @@ ## 概览 -CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供应商工作流与代理兼容性。本版本在各主要供应商卡片上新增了**配额与余额展示**,覆盖 Claude / Codex / Gemini 官方订阅、Token Plan(Kimi / Zhipu GLM / MiniMax)、Copilot premium interactions 以及 DeepSeek / StepFun / SiliconFlow / OpenRouter / Novita AI 等第三方余额查询;引入了**轻量模式**,让 CC Switch 可以仅驻留在系统托盘中运行;通过 OpenAI 兼容的 `/v1/models` 端点在 Claude / Codex / Gemini / OpenCode / OpenClaw 五个应用的供应商表单中实现了**模型自动发现**;为 ChatGPT Plus / Pro 订阅者提供了 **Codex OAuth 反向代理**;将托盘菜单重构为**按应用分级的子菜单**;将代理转发层重建在 **Hyper 客户端**之上;并完成了 **Skills 工作流**的发现、批量更新和存储位置切换改造。其他改进还包括完整 URL 端点模式、无需代理拦截的会话日志用量追踪、Copilot 调用优化器、多字节 UTF-8 流式分片边界修复以及 Linux 启动时 UI 无响应修复等。 +CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供应商工作流与代理兼容性。本版本在各主要供应商卡片上新增了**配额与余额展示**,覆盖 Claude / Codex / Gemini 官方订阅、Token Plan(Kimi / Zhipu GLM / MiniMax)、Copilot premium interactions 以及 DeepSeek / StepFun / SiliconFlow / OpenRouter / Novita AI 等第三方余额查询;引入了**轻量模式**,让 CC Switch 可以仅驻留在系统托盘中运行;通过 OpenAI 兼容的 `/v1/models` 端点在 Claude / Codex / Gemini / OpenCode / OpenClaw 五个应用的供应商表单中实现了**模型自动发现**;为 ChatGPT 订阅者提供了 **Codex OAuth 反向代理**;将托盘菜单重构为**按应用分级的子菜单**;将代理转发层重建在 **Hyper 客户端**之上;并完成了 **Skills 工作流**的发现、批量更新和存储位置切换改造,内置了 skills.sh 搜索安装。其他改进还包括完整 URL 端点模式、更完善的 token 用量追踪、Copilot 调用优化器、多字节 UTF-8 流式分片边界修复以及 Linux 启动时 UI 无响应修复,以及更友善的新用户引导等。 -**发布日期**:2026-04-08 +**发布日期**:2026-04-10 -**更新规模**:98 commits | 229 files changed | +23,891 / -2,305 lines +**更新规模**:139 commits | 280 files changed | +31,627 / -3,042 lines --- @@ -21,9 +21,9 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 - **轻量模式**:新增仅托盘运行模式,退出到托盘时销毁主窗口、按需重建,空闲时资源占用接近零 - **配额与余额展示**:供应商卡片上直接展示配额或余额 —— 覆盖 Claude / Codex / Gemini 官方订阅、GitHub Copilot premium interactions、Codex OAuth、Token Plan(Kimi / Zhipu GLM / MiniMax),以及 DeepSeek / StepFun / SiliconFlow / OpenRouter / Novita AI 的官方余额查询 - **供应商模型自动获取**:为 Claude / Codex / Gemini / OpenCode / OpenClaw 的供应商表单新增 OpenAI 兼容的 `/v1/models` 发现能力,按分组下拉展示并提供针对性错误提示 -- **Codex OAuth 反向代理**:新增 ChatGPT Plus / Pro 的 Codex 反向代理,作为新的 Claude 供应商卡片类型,包含受管 OAuth 登录流程和订阅配额展示([⚠️ 风险提示](#️-风险提示)) +- **Codex OAuth 反向代理**:新增 ChatGPT 的 Codex 反向代理,作为新的 Claude 供应商卡片类型,让用户在可以在 Claude Code 里面使用 ChatGPT 订阅。包含受管 OAuth 登录流程和订阅配额展示([⚠️ 风险提示](#️-风险提示)) - **托盘按应用分级菜单**:将托盘菜单重构为按应用分组的子菜单,防止供应商多时菜单溢出,让后台切换供应商在大量供应商场景下仍可用 -- **Skills 发现与批量更新**:基于 SHA-256 内容哈希的更新检测、单项和"全部更新"批量操作、`skills.sh` 公共注册表搜索集成,以及 CC Switch 与 `~/.agents/skills` 的存储位置切换 +- **Skills 发现与批量更新**:基于 SHA-256 内容哈希的更新检测、单项和"全部更新"批量操作、`skills.sh` 表搜索集成,以及 CC Switch 与 `~/.agents/skills` 的存储位置切换 - **会话工作流升级**:会话管理器批量删除、Claude 终端恢复前的目录选择器、无需代理拦截即可导入 Claude / Codex / Gemini 会话日志用量、精确的 Codex JSONL 解析、按应用筛选用量面板 - **OpenCode / OpenClaw 流式检测覆盖**:新增 OpenCode 的 npm 包映射检测、OpenClaw `openai-completions` 支持,以及其余所有 OpenClaw 协议变体 - **完整 URL 端点模式**:新增将 `base_url` 视作完整上游端点的供应商选项,支持非标准 URL 布局的厂商 @@ -31,6 +31,10 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 - **Copilot 调用优化器**:新增请求分类和路由逻辑,降低 GitHub Copilot premium interaction 的不必要消耗 - **UTF-8 流式分片边界修复**:所有 4 条 SSE 流式路径改为跨分片保留残留多字节序列,消除 Copilot 反代下中文/emoji 乱码 - **Linux 启动 UI 修复**:修复长期存在的 Linux 窗口初次无法响应点击、需用户手动最大化再还原才能操作的问题 +- **首次运行引导**:新安装时弹出一次性欢迎对话框、自动种入 Claude / OpenAI / Google 官方预设、启动时自动导入 OpenCode / OpenClaw 的 live 配置 +- **Claude 会话标题与搜索高亮**:从 Claude 会话中提取有意义的标题(自定义标题 → 首条用户消息 → 目录名),在会话管理器搜索时高亮匹配关键词 +- **URL 图标支持**:图标系统新增双渲染模式,大 SVG 和光栅图片(PNG / JPG / WebP)通过 Vite URL import 加载,小 SVG 保持内联 +- **新供应商预设**:新增 TheRouter、DDSHub、LionCCAPI、胜算云、PIPELLM、E-FlowCode 预设 --- @@ -50,9 +54,9 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 - **官方订阅**:Claude / Codex / Gemini 官方供应商的订阅配额展示 - **GitHub Copilot**:在 Copilot 供应商卡片上显示 premium interactions 剩余量 -- **Codex OAuth**:在 Codex OAuth 卡片上内联展示 ChatGPT Plus / Pro 订阅配额 -- **Token Plan 供应商**:Kimi、Zhipu GLM、MiniMax,修正配额数学与 0% → 100% 用量进度 -- **第三方余额**:为 DeepSeek、StepFun、SiliconFlow、OpenRouter、Novita AI 提供官方余额查询 +- **Codex OAuth**:在 Codex OAuth 卡片上内联展示 ChatGPT 订阅配额 +- **Token Plan 供应商**:Kimi、Zhipu GLM、MiniMax 用量进度显示(为避免混淆,需要手动开启) +- **第三方余额**:为 DeepSeek、StepFun、SiliconFlow、OpenRouter、Novita AI 提供官方余额查询(为避免混淆,需要手动开启) - 官方供应商的健康检查和用量配置按钮自动隐藏,保持卡片简洁 ### 供应商模型自动获取 @@ -66,13 +70,12 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 ### Codex OAuth 反向代理 -新增 ChatGPT Plus / Pro 订阅者的 Codex OAuth 反向代理路径,让 ChatGPT 订阅者可以在 Claude Code 中使用自己的订阅。 +新增 ChatGPT 订阅者的 Codex OAuth 反向代理路径,让 ChatGPT 订阅者可以在 Claude Code 中使用自己的订阅。 -- 受管 OAuth 登录流程,通过 ChatGPT Plus / Pro 认证 +- 受管 OAuth 登录流程,通过 ChatGPT 认证 - 作为新的 Claude 供应商卡片类型出现在列表中,与 API-key 型供应商并列 - 订阅配额内联展示 - 与 Auth Center UI 紧密集成,统一管理 Token -- Codex OAuth 预设升级到 GPT-5.4 系列 - 启用前请参见下文的 [⚠️ 风险提示](#️-风险提示) ### 托盘按应用分级菜单 @@ -131,6 +134,54 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 - 根据请求意图和权重进行分类 - 将低价值请求路由到非 premium 通道 - 旨在延长 Copilot 订阅的可用时长 +- 注意,即使优化过消耗以后,在 Copilot 外使用 Copilot 的 API 消耗仍然会高于在 Copilot 内使用。 + +### 首次运行欢迎对话框 + +新安装用户首次打开时显示一次性欢迎对话框,引导了解 CC Switch 工作流程。 + +- 说明已有 live 配置如何被保留为默认供应商 +- 介绍内置官方预设如何实现一键回滚到官方端点 +- 升级用户通过空供应商检查自动跳过 + +### 官方供应商自动种入 + +- 启动时自动种入 Claude Official / OpenAI Official / Google Official 供应商条目,为每位用户提供一键回滚到官方端点的路径 + +### OpenCode / OpenClaw 自动导入 + +- 启动时自动导入 OpenCode 和 OpenClaw 的 live 供应商配置,与 Claude / Codex / Gemini 已有的自动导入行为对齐 + +### Common Config 编辑器引导 + +- 在 Claude / Codex / Gemini 的 Common Config 代码片段编辑器弹窗中添加引导信息和空状态提示 +- 用户首次打开供应商添加/编辑表单时弹出一次性对话框说明 Common Config Snippets + +### Claude 会话标题与搜索高亮 + +- 为 Claude 会话新增有意义的标题提取,优先链:自定义标题元数据 → 首条真实用户消息 → 目录名回退 +- 在会话管理器搜索时高亮匹配关键词 + +### URL 图标支持 + +- 图标系统新增双渲染模式:小 SVG 以 React 组件内联,大 SVG 和光栅图片(PNG / JPG / WebP)通过 Vite URL import 以 `` 标签加载 + +### Kaku 终端支持 + +- macOS 上新增 Kaku 作为可选终端用于启动会话,复用 WezTerm 兼容的启动路径 (#1983, 感谢 @yovinchen) + +### OMO Slim Council 支持 + +- 恢复 council 作为内置 oh-my-opencode-slim agent 的一等支持,更新元数据和 UI 文案 (#1982, 感谢 @yovinchen) + +### 新供应商预设 + +- **TheRouter**:覆盖 Claude / Codex / Gemini / OpenCode / OpenClaw 五个应用 (#1891, #1892, 感谢 @cmzz) +- **DDSHub**:作为 Claude 的第三方合作伙伴供应商,含图标和推广文案 +- **LionCCAPI**:覆盖全部五个应用,OpenCode / OpenClaw 使用 anthropic-messages 协议 +- **胜算云 (Shengsuanyun)**:作为聚合类合作伙伴供应商覆盖全部五个应用,支持 URL 图标和本地化名称 +- **PIPELLM**:覆盖 Claude / Codex / OpenCode / OpenClaw,含完整模型定义和图标 +- **E-FlowCode**:覆盖全部五个应用,按应用配置不同协议 --- @@ -264,6 +315,31 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 - 修复 Bedrock 错误消息 - 修复 OpenCode 默认 `baseURL` 回退处理 +### 供应商切换时重复 Toast + +- 修复代理未运行时切换到 Copilot / ChatGPT / OpenAI 格式供应商时出现双重 toast 通知(代理必需警告 + 切换成功) + +### 会话搜索精度与中文支持 + +- 修复会话搜索结果在跨供应商时被截断的问题 +- 将 FlexSearch 分词器切换为 full 模式以支持中文子串匹配 + +### 自适应思维推理力度 + +- 修复 `resolve_reasoning_effort()` 将自适应思维错误映射为 `high`,应为 `xhigh`(OpenAI 格式转换场景) + +### Thinking 模型回退显示 + +- 修复 Claude 供应商表单仅填写主模型后 Thinking 模型字段显示为空,改为只读回退到 ANTHROPIC_MODEL (#1984, 感谢 @yovinchen) + +### Auth Tab 本地化 + +- 修复设置面板 auth tab 标签在所有语言包中缺失 i18n 翻译 key (#1985, 感谢 @yovinchen) + +### 数据库迁移守卫 + +- 修复 skills 或 model_pricing 表不存在时数据库迁移失败,在 ALTER 和 UPDATE 操作前添加表存在性检查 + --- ## 文档 @@ -283,16 +359,20 @@ CC Switch v3.13.0 是一次重要的功能版本,聚焦于可观测性、供 - 在三语 v3.12.3 release notes 中新增 Copilot 反代风险提示,并为重点内容添加锚点链接 +### 赞助商合作伙伴 + +- 在三语 README 中新增胜算云、LionCC、DDS 作为赞助商合作伙伴 + --- ## ⚠️ 风险提示 **Codex OAuth 反向代理免责声明** -本版本新增的 Codex OAuth 反向代理功能通过逆向工程的 OAuth 流程访问 ChatGPT Plus / Pro 的 Codex 服务。启用此功能前,请注意以下风险: +本版本新增的 Codex OAuth 反向代理功能通过逆向工程的 OAuth 流程访问 ChatGPT 的 Codex 服务。启用此功能前,请注意以下风险: 1. **违反服务条款**:使用逆向 OAuth 流程访问 OpenAI 服务可能违反 OpenAI 的服务条款,其中禁止未经授权的自动化访问、服务复制以及绕过既定的访问路径。 -2. **账号风险**:OpenAI 可能将异常使用模式标记为可疑的自动化行为,从而对 ChatGPT Plus / Pro 访问施加临时或永久限制。 +2. **账号风险**:OpenAI 可能将异常使用模式标记为可疑的自动化行为,从而对 ChatGPT 访问施加临时或永久限制。 3. **无法保证长期可用**:OpenAI 可能随时更新其认证和检测机制,当前可用的使用方式未来可能被标记。 v3.12.3 引入的 **GitHub Copilot 反向代理**同样适用原有风险提示 —— 详见 [v3.12.3 release notes](v3.12.3-zh.md#️-风险提示)。 @@ -307,26 +387,26 @@ v3.12.3 引入的 **GitHub Copilot 反向代理**同样适用原有风险提示 ### 系统要求 -| 系统 | 最低版本 | 架构 | -| ------- | ----------------------------- | ----------------------------------- | -| Windows | Windows 10 及以上 | x64 | -| macOS | macOS 12 (Monterey) 及以上 | Intel (x64) / Apple Silicon (arm64) | -| Linux | 见下表 | x64 | +| 系统 | 最低版本 | 架构 | +| ------- | -------------------------- | ----------------------------------- | +| Windows | Windows 10 及以上 | x64 | +| macOS | macOS 12 (Monterey) 及以上 | Intel (x64) / Apple Silicon (arm64) | +| Linux | 见下表 | x64 | ### Windows -| 文件 | 说明 | -| ------------------------------------------ | ----------------------------------- | -| `CC-Switch-v3.13.0-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 | -| `CC-Switch-v3.13.0-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 | +| 文件 | 说明 | +| ---------------------------------------- | ----------------------------------- | +| `CC-Switch-v3.13.0-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 | +| `CC-Switch-v3.13.0-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 | ### macOS -| 文件 | 说明 | -| ---------------------------------- | --------------------------------------------------------- | -| `CC-Switch-v3.13.0-macOS.dmg` | **推荐** - DMG 安装包,拖入 Applications 即可 | -| `CC-Switch-v3.13.0-macOS.zip` | 解压后拖入 Applications,Universal Binary | -| `CC-Switch-v3.13.0-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 | +| 文件 | 说明 | +| -------------------------------- | --------------------------------------------- | +| `CC-Switch-v3.13.0-macOS.dmg` | **推荐** - DMG 安装包,拖入 Applications 即可 | +| `CC-Switch-v3.13.0-macOS.zip` | 解压后拖入 Applications,Universal Binary | +| `CC-Switch-v3.13.0-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 | > macOS 版本已通过 Apple 代码签名和公证,可直接安装使用。 diff --git a/docs/working-directory-plan.md b/docs/working-directory-plan.md new file mode 100644 index 000000000..ae2426146 --- /dev/null +++ b/docs/working-directory-plan.md @@ -0,0 +1,597 @@ +# CC-Switch "工作目录" 功能 — 实施方案 + +## Context + +CC-Switch 管理 5 个 CLI 工具(Claude Code / Codex / Gemini CLI / OpenCode / OpenClaw)的供应商、MCP 服务器、Skills、提示词配置。当前所有启用状态是全局的——用户在不同项目间切换时需要手动 toggle。 + +本功能允许用户注册多个工作目录(项目文件夹),切换目录时自动保存/恢复各实体的启用状态。**不做数据隔离**——所有实体共享全局池,仅 "谁是激活的" 按目录区分。 + +--- + +## 一、需要按目录区分的实体(完整清单) + +| 实体 | 当前状态字段 | 存储方式 | 需要区分? | 理由 | +|------|-------------|---------|-----------|------| +| **Provider** | `is_current` | per `(id, app_type)` | **YES** | 不同项目用不同供应商 | +| **Provider (Failover)** | `in_failover_queue` | per `(id, app_type)` | **YES** | 备用供应商队列跟随主供应商配置 | +| **MCP Server** | `enabled_claude/codex/gemini/opencode` | per `id`, 4列 | **YES** | 不同项目需要不同 MCP 工具 | +| **Skill** | `enabled_claude/codex/gemini/opencode` | per `id`, 4列 | **YES** | 不同项目需要不同 Skills | +| **Prompt** | `enabled` | per `(id, app_type)`, 单选 | **YES** | 不同项目用不同系统提示词 | +| Proxy Config | `enabled`, thresholds | per `app_type` | NO | 基础设施级别,非项目相关 | +| Settings | key-value | flat table | NO | 全局用户偏好 | +| Provider Health | failures, errors | runtime | **CLEAR** | 切换时清除,重新计算 | +| Common Config | `common_config_{app}` | settings table | NO | 全局模板,非项目相关 | +| Usage/Logs | historical | various tables | NO | 历史数据,不应分区 | + +> 原计划遗漏了 **Failover Queue** 和 **Provider Health 清除**。 + +--- + +## 二、数据库变更(Schema v8 → v9) + +### 新增 5 张表 + +```sql +-- 1. 工作目录注册表 +CREATE TABLE IF NOT EXISTS working_directories ( + id TEXT PRIMARY KEY, + path TEXT NOT NULL UNIQUE, + name TEXT, + is_current BOOLEAN NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT 0 +); + +-- 2. Provider 状态快照 (is_current + in_failover_queue) +-- 每个目录保存所有 provider 的两个状态标志 +CREATE TABLE IF NOT EXISTS dir_provider_state ( + dir_id TEXT NOT NULL, + app_type TEXT NOT NULL, + provider_id TEXT NOT NULL, + is_current BOOLEAN NOT NULL DEFAULT 0, + in_failover_queue BOOLEAN NOT NULL DEFAULT 0, + PRIMARY KEY (dir_id, app_type, provider_id) +); + +-- 3. MCP 启用状态快照 (直接镜像 4 列,不做行展开) +CREATE TABLE IF NOT EXISTS dir_mcp_state ( + dir_id TEXT NOT NULL, + mcp_id TEXT NOT NULL, + enabled_claude BOOLEAN NOT NULL DEFAULT 0, + enabled_codex BOOLEAN NOT NULL DEFAULT 0, + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, + enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + PRIMARY KEY (dir_id, mcp_id) +); + +-- 4. Skill 启用状态快照 (直接镜像 4 列) +CREATE TABLE IF NOT EXISTS dir_skill_state ( + dir_id TEXT NOT NULL, + skill_id TEXT NOT NULL, + enabled_claude BOOLEAN NOT NULL DEFAULT 0, + enabled_codex BOOLEAN NOT NULL DEFAULT 0, + enabled_gemini BOOLEAN NOT NULL DEFAULT 0, + enabled_opencode BOOLEAN NOT NULL DEFAULT 0, + PRIMARY KEY (dir_id, skill_id) +); + +-- 5. Prompt 启用状态快照 (每个 app_type 只存激活的 prompt_id) +CREATE TABLE IF NOT EXISTS dir_prompt_state ( + dir_id TEXT NOT NULL, + app_type TEXT NOT NULL, + prompt_id TEXT NOT NULL, + PRIMARY KEY (dir_id, app_type) +); +``` + +### 设计决策说明 + +**MCP/Skill 用 4 列镜像而非 `(entity_id, app_type, enabled)` 行展开**: +- 与主表 `mcp_servers` / `skills` 结构一致,snapshot/apply 代码直接 copy 4 列 +- 避免 4 倍行膨胀(每个 MCP 服务器 1 行 vs 4 行) +- 未来增加新 app 时,两边同步加列即可 + +**Prompt 只存 `(dir_id, app_type, prompt_id)`**: +- 每个 app_type 最多一个 enabled prompt,不需要存 boolean +- 无记录 = 该 app 无激活 prompt + +**Provider 合并 `is_current` + `in_failover_queue`**: +- 两个标志都是 per `(app_type, provider_id)` 的状态 +- 存在同一表中避免多表 JOIN + +### 迁移脚本 + +在 `schema.rs` 中: +- `create_tables_on_conn()` 添加 5 个 CREATE TABLE +- 新增 `migrate_v8_to_v9(conn)`: 创建 5 张表 + 插入 `__default__` 行 +- `SCHEMA_VERSION` 升至 9 +- 迁移循环添加 `7 => ...` 后加 `8 => { Self::migrate_v8_to_v9(conn)?; Self::set_user_version(conn, 9)?; }` + +```rust +fn migrate_v8_to_v9(conn: &Connection) -> Result<(), AppError> { + // 创建 5 张表(使用 IF NOT EXISTS,幂等) + // ... + // 插入 __default__ 虚拟目录,代表"全局默认"状态 + conn.execute( + "INSERT OR IGNORE INTO working_directories (id, path, name, is_current, created_at) \ + VALUES ('__default__', '__default__', NULL, 0, ?1)", + [crate::database::get_unix_timestamp()?], + )?; + Ok(()) +} +``` + +--- + +## 三、后端实现 + +### 3.1 DAO 层 — `src-tauri/src/database/dao/working_dir.rs` + +所有方法都是 `impl Database` 块,遵循现有 DAO 模式。 + +**关键方法签名**(需要 `_on_conn` 变体支持事务): + +```rust +// ═══ 工作目录 CRUD ═══ +pub fn list_working_directories(&self) -> Result, AppError> +pub fn add_working_directory(&self, id: &str, path: &str, name: Option<&str>) -> Result<(), AppError> +pub fn delete_working_directory(&self, id: &str) -> Result<(), AppError> +pub fn rename_working_directory(&self, id: &str, name: &str) -> Result<(), AppError> +pub fn get_current_working_directory(&self) -> Result, AppError> + +// 使用 _on_conn 变体,在 Service 层的事务中调用 +fn set_current_working_directory_on_conn(conn: &Connection, id: &str) -> Result<(), AppError> + +// ═══ 快照写入 ═══ (都有 _on_conn 变体) +fn snapshot_providers_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError> +fn snapshot_mcp_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError> +fn snapshot_skills_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError> +fn snapshot_prompts_on_conn(conn: &Connection, dir_id: &str) -> Result<(), AppError> + +// ═══ 快照恢复 ═══ (都有 _on_conn 变体, 返回 bool = 是否有快照) +fn apply_provider_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result +fn apply_mcp_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result +fn apply_skill_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result +fn apply_prompt_snapshot_on_conn(conn: &Connection, dir_id: &str) -> Result +``` + +**snapshot_providers 实现思路**: +```sql +-- 先清除旧快照 +DELETE FROM dir_provider_state WHERE dir_id = ?1; +-- 从主表复制当前状态 +INSERT INTO dir_provider_state (dir_id, app_type, provider_id, is_current, in_failover_queue) +SELECT ?1, app_type, id, is_current, in_failover_queue +FROM providers +WHERE is_current = 1 OR in_failover_queue = 1; +``` + +**apply_provider_snapshot 实现思路**: +```sql +-- 检查是否有快照 +SELECT COUNT(*) FROM dir_provider_state WHERE dir_id = ?1; -- 如果 0,返回 false + +-- 在事务中:先清除主表所有 is_current 和 in_failover_queue +UPDATE providers SET is_current = 0; +UPDATE providers SET in_failover_queue = 0; + +-- 从快照恢复 +UPDATE providers SET is_current = 1 +WHERE (id, app_type) IN (SELECT provider_id, app_type FROM dir_provider_state WHERE dir_id = ?1 AND is_current = 1); + +UPDATE providers SET in_failover_queue = 1 +WHERE (id, app_type) IN (SELECT provider_id, app_type FROM dir_provider_state WHERE dir_id = ?1 AND in_failover_queue = 1); +``` + +**snapshot_mcp / snapshot_skills 实现思路**(直接镜像 4 列): +```sql +DELETE FROM dir_mcp_state WHERE dir_id = ?1; +INSERT INTO dir_mcp_state (dir_id, mcp_id, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode) +SELECT ?1, id, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode +FROM mcp_servers; +``` + +**apply_mcp_snapshot 实现思路**: +```sql +-- 先全部禁用 +UPDATE mcp_servers SET enabled_claude = 0, enabled_codex = 0, enabled_gemini = 0, enabled_opencode = 0; + +-- 从快照恢复 +UPDATE mcp_servers SET + enabled_claude = (SELECT enabled_claude FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id), + enabled_codex = (SELECT enabled_codex FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id), + enabled_gemini = (SELECT enabled_gemini FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id), + enabled_opencode = (SELECT enabled_opencode FROM dir_mcp_state WHERE dir_id = ?1 AND mcp_id = mcp_servers.id) +WHERE id IN (SELECT mcp_id FROM dir_mcp_state WHERE dir_id = ?1); +``` + +### 3.2 Service 层 — `src-tauri/src/services/working_dir.rs` + +```rust +use crate::store::AppState; +use crate::error::AppError; +use crate::database::lock_conn; +use crate::app_config::AppType; +use crate::services::{McpService, ProviderService, SkillService}; +use crate::config::write_text_file; +use crate::prompt_files::prompt_file_path; + +pub struct WorkingDirService; + +impl WorkingDirService { + /// 核心切换逻辑 + pub fn switch(state: &AppState, target_dir_id: &str) -> Result<(), AppError> { + // ═══ 前置检查 ═══ + // 1. 检查代理接管状态,若活跃则拒绝切换 + // 使用 db.is_live_takeover_active() 或同步检查 proxy_config.live_takeover_active + // (因为 ProxyService::is_running() 是 async,而此函数是 sync) + Self::check_proxy_not_active(state)?; + + // ═══ Phase 1: 回填 Prompt ═══ + // 在 snapshot 之前,将 live 文件内容回填到当前 enabled prompt + // 这样即使用户手动编辑了 live 文件,内容也不会丢失 + Self::backfill_prompt_content(state)?; + + // ═══ Phase 2: 数据库操作(事务) ═══ + { + let conn = lock_conn!(state.db.conn); + conn.execute("BEGIN IMMEDIATE", [])?; + + let result = (|| -> Result<(), AppError> { + // 获取当前工作目录 + let current = Self::get_current_dir_id_on_conn(&conn)?; + + // 保存当前状态到旧目录 + if let Some(old_id) = ¤t { + Database::snapshot_providers_on_conn(&conn, old_id)?; + Database::snapshot_mcp_on_conn(&conn, old_id)?; + Database::snapshot_skills_on_conn(&conn, old_id)?; + Database::snapshot_prompts_on_conn(&conn, old_id)?; + } else { + // 无当前目录 = 全局模式,保存到 __default__ + Database::snapshot_providers_on_conn(&conn, "__default__")?; + Database::snapshot_mcp_on_conn(&conn, "__default__")?; + Database::snapshot_skills_on_conn(&conn, "__default__")?; + Database::snapshot_prompts_on_conn(&conn, "__default__")?; + } + + // 加载目标目录快照(如果有的话) + // 如果无快照(首次进入),保持主表不变 + Database::apply_provider_snapshot_on_conn(&conn, target_dir_id)?; + Database::apply_mcp_snapshot_on_conn(&conn, target_dir_id)?; + Database::apply_skill_snapshot_on_conn(&conn, target_dir_id)?; + Database::apply_prompt_snapshot_on_conn(&conn, target_dir_id)?; + + // 更新 is_current 标记 + Database::set_current_working_directory_on_conn(&conn, target_dir_id)?; + + Ok(()) + })(); + + match result { + Ok(()) => conn.execute("COMMIT", [])?, + Err(e) => { + let _ = conn.execute("ROLLBACK", []); + return Err(e); + } + }; + } + // conn 锁在此处释放 + + // ═══ Phase 3: 同步 live 配置文件 ═══ + Self::sync_all_live(state)?; + + // ═══ Phase 4: 清除 Provider Health ═══ + state.db.clear_all_provider_health()?; + + Ok(()) + } + + /// 回填 live prompt 文件内容到 DB(切换前调用) + fn backfill_prompt_content(state: &AppState) -> Result<(), AppError> { + for app in AppType::all() { + let path = prompt_file_path(&app)?; + if !path.exists() { continue; } + let live_content = std::fs::read_to_string(&path).unwrap_or_default(); + if live_content.trim().is_empty() { continue; } + + let mut prompts = state.db.get_prompts(app.as_str())?; + if let Some((_, prompt)) = prompts.iter_mut().find(|(_, p)| p.enabled) { + prompt.content = live_content; + prompt.updated_at = Some(get_unix_timestamp()?); + state.db.save_prompt(app.as_str(), prompt)?; + } + } + Ok(()) + } + + /// 将 DB 中的 enabled prompt 内容写入 live 文件(切换后调用) + /// 注意:不做回填!只写入。区别于 PromptService::enable_prompt() + fn write_prompts_to_live(state: &AppState) -> Result<(), AppError> { + for app in AppType::all() { + let path = prompt_file_path(&app)?; + let prompts = state.db.get_prompts(app.as_str())?; + if let Some(prompt) = prompts.values().find(|p| p.enabled) { + write_text_file(&path, &prompt.content)?; + } + // 无 enabled prompt 时不清空文件(保留现状) + } + Ok(()) + } + + /// 同步所有 live 配置(Provider + MCP + Skill + Prompt) + fn sync_all_live(state: &AppState) -> Result<(), AppError> { + // 1. Provider → live files + ProviderService::sync_current_to_live(state)?; + // sync_current_to_live 内部已调用 McpService::sync_all_enabled() + + // 2. Skills → app dirs (循环每个 app) + for app in AppType::all() { + let _ = SkillService::sync_to_app(&state.db, &app); + } + + // 3. Prompts → live files + Self::write_prompts_to_live(state)?; + + Ok(()) + } + + /// 检查代理是否活跃(同步检查数据库标志) + fn check_proxy_not_active(state: &AppState) -> Result<(), AppError> { + // 检查 proxy_config 表中 live_takeover_active 列 + // 如果有任何 app 的 live_takeover_active = 1,拒绝切换 + let conn = lock_conn!(state.db.conn); + let active: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM proxy_config WHERE live_takeover_active = 1)", + [], |r| r.get(0) + ).unwrap_or(false); + + if active { + return Err(AppError::Message( + "代理接管模式运行中,请先停止代理再切换工作目录".into() + )); + } + Ok(()) + } +} +``` + +### 3.3 Command 层 — `src-tauri/src/commands/working_dir.rs` + +遵循现有模式:`State<'_, AppState>` + `Result` + `.map_err(|e| e.to_string())`。 + +```rust +#[tauri::command] +pub fn list_working_directories(state: State<'_, AppState>) -> Result, String> + +#[tauri::command] +pub fn add_working_directory(state: State<'_, AppState>, path: String, name: Option) -> Result + +#[tauri::command] +pub fn delete_working_directory(state: State<'_, AppState>, id: String) -> Result<(), String> + +#[tauri::command] +pub fn rename_working_directory(state: State<'_, AppState>, id: String, name: String) -> Result<(), String> + +#[tauri::command] +pub fn switch_working_directory(state: State<'_, AppState>, id: String) -> Result<(), String> +// 调用 WorkingDirService::switch() + +#[tauri::command] +pub fn get_current_working_directory(state: State<'_, AppState>) -> Result, String> +``` + +### 3.4 需修改的现有文件 + +| 文件 | 修改内容 | +|------|---------| +| `src-tauri/src/database/schema.rs` | 添加 5 个 CREATE TABLE + `migrate_v8_to_v9()` | +| `src-tauri/src/database/mod.rs` | `SCHEMA_VERSION = 9` + 迁移循环加 `8 => ...` + `pub mod working_dir` in dao | +| `src-tauri/src/database/dao/mod.rs` | 添加 `pub mod working_dir;` | +| `src-tauri/src/services/mod.rs` | 添加 `pub mod working_dir;` + `pub use working_dir::WorkingDirService;` | +| `src-tauri/src/commands/mod.rs` | 添加 `mod working_dir;` + `pub use working_dir::*;` | +| `src-tauri/src/lib.rs` | invoke_handler 注册 6 个新命令 | + +### 3.5 可能需要新增的 DAO 辅助方法 + +`src-tauri/src/database/dao/failover.rs`: +```rust +/// 清除所有 provider_health 记录(切换目录时调用) +pub fn clear_all_provider_health(&self) -> Result<(), AppError> +``` + +--- + +## 四、前端实现 + +### 4.1 API — `src/lib/api/workingDir.ts` + +```typescript +import { invoke } from "@tauri-apps/api/core"; + +export interface WorkingDirectory { + id: string; + path: string; + name?: string; + isCurrent: boolean; + createdAt: number; +} + +export const workingDirApi = { + list: () => invoke("list_working_directories"), + add: (path: string, name?: string) => + invoke("add_working_directory", { path, name }), + delete: (id: string) => invoke("delete_working_directory", { id }), + rename: (id: string, name: string) => + invoke("rename_working_directory", { id, name }), + switch: (id: string) => invoke("switch_working_directory", { id }), + getCurrent: () => + invoke("get_current_working_directory"), +}; +``` + +### 4.2 组件 — `src/components/WorkingDirSwitcher.tsx` + +**位置**:Header toolbar,靠近 AppSwitcher。 + +**功能**: +- 下拉菜单显示已注册目录列表 +- 当前目录高亮 +- "浏览…" 按钮调用 Tauri 文件夹选择对话框 +- 右键菜单:重命名、删除 +- "__default__(全局)" 选项恢复到全局状态 +- 切换后 invalidate 所有相关 React Query + +**切换后的 Query Invalidation**: +```typescript +// 需要验证实际的 queryKey 名称 +queryClient.invalidateQueries({ queryKey: ["providers"] }); +queryClient.invalidateQueries({ queryKey: ["mcp-servers"] }); +queryClient.invalidateQueries({ queryKey: ["installed-skills"] }); +queryClient.invalidateQueries({ queryKey: ["prompts"] }); +queryClient.invalidateQueries({ queryKey: ["workingDirectories"] }); +``` + +### 4.3 i18n + +三个文件都需更新: +- `src/i18n/locales/zh.json` +- `src/i18n/locales/en.json` +- `src/i18n/locales/ja.json` + +--- + +## 五、切换流程时序 + +``` +用户选择目录 B + │ + ├── 1. check_proxy_not_active() + │ → 如果代理接管中,返回错误,终止 + │ + ├── 2. backfill_prompt_content() + │ → 读 live prompt 文件 → 更新 DB 中已启用 prompt 的 content + │ → 保护用户手动编辑的 prompt 不丢失 + │ + ├── 3. BEGIN TRANSACTION + │ ├── snapshot(old_dir / __default__) + │ │ ├── providers → dir_provider_state (is_current + in_failover_queue) + │ │ ├── mcp_servers → dir_mcp_state (4 列直接复制) + │ │ ├── skills → dir_skill_state (4 列直接复制) + │ │ └── prompts → dir_prompt_state (enabled prompt_id) + │ │ + │ ├── apply(target_dir) + │ │ ├── dir_provider_state → providers + │ │ ├── dir_mcp_state → mcp_servers + │ │ ├── dir_skill_state → skills + │ │ └── dir_prompt_state → prompts + │ │ + │ └── set_current_working_directory(target_dir) + │ + ├── COMMIT + │ + ├── 4. sync_all_live() + │ ├── ProviderService::sync_current_to_live(state) + │ │ └── 内部已调用 McpService::sync_all_enabled() + │ ├── for app in AppType::all() { SkillService::sync_to_app(&db, &app) } + │ └── write_prompts_to_live() ← 无回填,直接写 + │ + └── 5. clear_all_provider_health() + → 清除运行时熔断器状态 +``` + +--- + +## 六、边界情况处理 + +| 场景 | 处理方式 | +|------|---------| +| **首次进入目录(无快照)** | `apply_*_snapshot()` 返回 false,主表保持不变。用户调整后,下次切走时自动保存。 | +| **全局模式 → 目录** | 自动将当前状态 snapshot 到 `__default__` 虚拟目录。`__default__` 在 v9 迁移中预创建。 | +| **目录 → 全局模式** | 用户选择 `__default__`,恢复全局状态。 | +| **新增 MCP/Skill/Provider** | 新实体在 dir_*_state 中无记录。apply 时只更新有记录的实体,新增的保持 DB 默认值。 | +| **删除 MCP/Skill/Provider** | dir_*_state 中对应记录在 apply 时找不到主表行,UPDATE 影响 0 行,静默跳过。 | +| **删除工作目录** | 级联删除 dir_*_state 中所有 `dir_id` 匹配的行。若为当前目录,回退到 `__default__`。 | +| **代理接管中切换** | `check_proxy_not_active()` 检测到 `live_takeover_active = 1`,拒绝切换并提示用户先停止代理。 | +| **切换中途崩溃** | 事务保护 DB 操作的原子性。最坏情况:DB 已更新但 live 文件未同步。下次启动可添加恢复检查(Phase 2 优化)。 | +| **用户手动编辑了 prompt 文件** | `backfill_prompt_content()` 在切换前读取 live 文件回填到 DB,保护手动修改。 | + +--- + +## 七、实施顺序 + +### Phase 1: 数据库 +1. `database/schema.rs` — 5 个 CREATE TABLE + `migrate_v8_to_v9()` +2. `database/mod.rs` — `SCHEMA_VERSION = 9` + 迁移分支 +3. `database/dao/working_dir.rs` — 全部 DAO 方法(`_on_conn` 变体) +4. `database/dao/failover.rs` — 新增 `clear_all_provider_health()` +5. `database/dao/mod.rs` — 注册模块 + +### Phase 2: 服务 + 命令 +6. `services/working_dir.rs` — `WorkingDirService::switch()` 等 +7. `commands/working_dir.rs` — 6 个 Tauri 命令 +8. `services/mod.rs` — 注册模块 +9. `commands/mod.rs` — 注册模块 +10. `lib.rs` — invoke_handler 注册 + +### Phase 3: 前端 +11. `src/lib/api/workingDir.ts` — API 封装 +12. `src/types.ts` — WorkingDirectory 类型 +13. `src/components/WorkingDirSwitcher.tsx` — UI 组件 +14. `src/App.tsx` — 集成到 header toolbar +15. `src/i18n/locales/{zh,en,ja}.json` — 国际化 + +### Phase 4: 优化(可选) +16. 启动恢复检查(DB 状态 vs live 文件一致性) +17. 托盘菜单显示当前工作目录 + +--- + +## 八、关键文件索引 + +### 新增文件(5 个) +- `src-tauri/src/database/dao/working_dir.rs` +- `src-tauri/src/services/working_dir.rs` +- `src-tauri/src/commands/working_dir.rs` +- `src/lib/api/workingDir.ts` +- `src/components/WorkingDirSwitcher.tsx` + +### 必须修改的文件(7 个) +- `src-tauri/src/database/schema.rs` — CREATE TABLE + 迁移 +- `src-tauri/src/database/mod.rs` — 版本号 + 迁移循环 +- `src-tauri/src/database/dao/mod.rs` — 模块注册 +- `src-tauri/src/database/dao/failover.rs` — clear_all_provider_health +- `src-tauri/src/services/mod.rs` — 模块注册 +- `src-tauri/src/commands/mod.rs` — 模块注册 +- `src-tauri/src/lib.rs` — invoke_handler + +### 必须修改的前端文件(4 个) +- `src/App.tsx` — 集成 WorkingDirSwitcher +- `src/types.ts` — WorkingDirectory 接口 +- `src/i18n/locales/zh.json` — 中文 +- `src/i18n/locales/en.json` — 英文 +- `src/i18n/locales/ja.json` — 日文 + +### 参考文件(理解现有模式) +- `src-tauri/src/services/mcp.rs` — `sync_all_enabled()` (line 165) +- `src-tauri/src/services/skill.rs` — `sync_to_app()` (line 1707) +- `src-tauri/src/services/provider/mod.rs` — `sync_current_to_live()` (line 1552) +- `src-tauri/src/services/prompt.rs` — `enable_prompt()` (line 73) — 理解回填逻辑 +- `src-tauri/src/prompt_files.rs` — prompt 文件路径 +- `src-tauri/src/config.rs` — `write_text_file()` (line 176) + +--- + +## 九、验证计划 + +### 后端验证 +1. `cargo test` — DAO 层单元测试(使用 `Database::memory()`) + - 快照/恢复往返一致性 + - 新增/删除实体后的 apply 行为 + - `__default__` 全局状态保护 + - 事务回滚测试 +2. 手动测试 — 启动应用,创建两个目录,切换并验证 live 文件变化 + +### 前端验证 +1. `pnpm typecheck` — TypeScript 类型检查 +2. `pnpm lint` — ESLint 检查 +3. 手动 UI 测试 — 工作目录切换器交互、query invalidation 后数据刷新 diff --git a/package.json b/package.json index 713c3f2b3..5301a971a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cc-switch", - "version": "3.12.3", + "version": "3.13.0", "description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI", "type": "module", "scripts": { diff --git a/scripts/generate-icon-index.js b/scripts/generate-icon-index.js deleted file mode 100644 index 8aa211975..000000000 --- a/scripts/generate-icon-index.js +++ /dev/null @@ -1,115 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const ICONS_DIR = path.join(__dirname, '../src/icons/extracted'); -const INDEX_FILE = path.join(ICONS_DIR, 'index.ts'); -const METADATA_FILE = path.join(ICONS_DIR, 'metadata.ts'); - -// Known metadata from previous configuration -const KNOWN_METADATA = { - openai: { name: 'openai', displayName: 'OpenAI', category: 'ai-provider', keywords: ['gpt', 'chatgpt'], defaultColor: '#00A67E' }, - anthropic: { name: 'anthropic', displayName: 'Anthropic', category: 'ai-provider', keywords: ['claude'], defaultColor: '#D4915D' }, - claude: { name: 'claude', displayName: 'Claude', category: 'ai-provider', keywords: ['anthropic'], defaultColor: '#D4915D' }, - google: { name: 'google', displayName: 'Google', category: 'ai-provider', keywords: ['gemini', 'bard'], defaultColor: '#4285F4' }, - gemini: { name: 'gemini', displayName: 'Gemini', category: 'ai-provider', keywords: ['google'], defaultColor: '#4285F4' }, - deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' }, - moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' }, - kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' }, - stepfun: { name: 'stepfun', displayName: 'StepFun', category: 'ai-provider', keywords: ['stepfun', 'step', 'jieyue', '阶跃星辰'], defaultColor: '#005AFF' }, - zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' }, - minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' }, - baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' }, - alibaba: { name: 'alibaba', displayName: 'Alibaba', category: 'ai-provider', keywords: ['qwen', 'tongyi'], defaultColor: '#FF6A00' }, - tencent: { name: 'tencent', displayName: 'Tencent', category: 'ai-provider', keywords: ['hunyuan'], defaultColor: '#00A4FF' }, - meta: { name: 'meta', displayName: 'Meta', category: 'ai-provider', keywords: ['facebook', 'llama'], defaultColor: '#0081FB' }, - microsoft: { name: 'microsoft', displayName: 'Microsoft', category: 'ai-provider', keywords: ['copilot', 'azure'], defaultColor: '#00A4EF' }, - cohere: { name: 'cohere', displayName: 'Cohere', category: 'ai-provider', keywords: ['cohere'], defaultColor: '#39594D' }, - perplexity: { name: 'perplexity', displayName: 'Perplexity', category: 'ai-provider', keywords: ['perplexity'], defaultColor: '#20808D' }, - packycode: { name: 'packycode', displayName: 'PackyCode', category: 'ai-provider', keywords: ['packycode', 'packy', 'packyapi'], defaultColor: 'currentColor' }, - mistral: { name: 'mistral', displayName: 'Mistral', category: 'ai-provider', keywords: ['mistral'], defaultColor: '#FF7000' }, - huggingface: { name: 'huggingface', displayName: 'Hugging Face', category: 'ai-provider', keywords: ['huggingface', 'hf'], defaultColor: '#FFD21E' }, - aws: { name: 'aws', displayName: 'AWS', category: 'cloud', keywords: ['amazon', 'cloud'], defaultColor: '#FF9900' }, - azure: { name: 'azure', displayName: 'Azure', category: 'cloud', keywords: ['microsoft', 'cloud'], defaultColor: '#0078D4' }, - huawei: { name: 'huawei', displayName: 'Huawei', category: 'cloud', keywords: ['huawei', 'cloud'], defaultColor: '#FF0000' }, - cloudflare: { name: 'cloudflare', displayName: 'Cloudflare', category: 'cloud', keywords: ['cloudflare', 'cdn'], defaultColor: '#F38020' }, - github: { name: 'github', displayName: 'GitHub', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#181717' }, - gitlab: { name: 'gitlab', displayName: 'GitLab', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#FC6D26' }, - docker: { name: 'docker', displayName: 'Docker', category: 'tool', keywords: ['container'], defaultColor: '#2496ED' }, - kubernetes: { name: 'kubernetes', displayName: 'Kubernetes', category: 'tool', keywords: ['k8s', 'container'], defaultColor: '#326CE5' }, - vscode: { name: 'vscode', displayName: 'VS Code', category: 'tool', keywords: ['editor', 'ide'], defaultColor: '#007ACC' }, - settings: { name: 'settings', displayName: 'Settings', category: 'other', keywords: ['config', 'preferences'], defaultColor: '#6B7280' }, - folder: { name: 'folder', displayName: 'Folder', category: 'other', keywords: ['directory'], defaultColor: '#6B7280' }, - file: { name: 'file', displayName: 'File', category: 'other', keywords: ['document'], defaultColor: '#6B7280' }, - link: { name: 'link', displayName: 'Link', category: 'other', keywords: ['url', 'hyperlink'], defaultColor: '#6B7280' }, -}; - -// Get all SVG files -const files = fs.readdirSync(ICONS_DIR).filter(file => file.endsWith('.svg')); - -console.log(`Found ${files.length} SVG files.`); - -// Generate index.ts -const indexContent = `// Auto-generated icon index -// Do not edit manually - -export const icons: Record = { -${files.map(file => { - const name = path.basename(file, '.svg'); - const svg = fs.readFileSync(path.join(ICONS_DIR, file), 'utf-8'); - const escaped = svg.replace(/`/g, '\\`').replace(/\$/g, '\\$'); - return ` '${name}': \`${escaped}\`,`; -}).join('\n')} -}; - -export const iconList = Object.keys(icons); - -export function getIcon(name: string): string { - return icons[name.toLowerCase()] || ''; -} - -export function hasIcon(name: string): boolean { - return name.toLowerCase() in icons; -} -`; - -fs.writeFileSync(INDEX_FILE, indexContent); -console.log(`Generated ${INDEX_FILE}`); - -// Generate metadata.ts -const metadataEntries = files.map(file => { - const name = path.basename(file, '.svg').toLowerCase(); - const known = KNOWN_METADATA[name]; - - if (known) { - return ` ${name}: ${JSON.stringify(known)},`; - } - - // Default metadata for unknown icons - return ` '${name}': { name: '${name}', displayName: '${name}', category: 'other', keywords: [], defaultColor: 'currentColor' },`; -}); - -const metadataContent = `// Icon metadata for search and categorization -import { IconMetadata } from '@/types/icon'; - -export const iconMetadata: Record = { -${metadataEntries.join('\n')} -}; - -export function getIconMetadata(name: string): IconMetadata | undefined { - return iconMetadata[name.toLowerCase()]; -} - -export function searchIcons(query: string): string[] { - const lowerQuery = query.toLowerCase(); - return Object.values(iconMetadata) - .filter(meta => - meta.name.includes(lowerQuery) || - meta.displayName.toLowerCase().includes(lowerQuery) || - meta.keywords.some(k => k.includes(lowerQuery)) - ) - .map(meta => meta.name); -} -`; - -fs.writeFileSync(METADATA_FILE, metadataContent); -console.log(`Generated ${METADATA_FILE}`); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1ae679efc..1443b2388 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -735,7 +735,7 @@ dependencies = [ [[package]] name = "cc-switch" -version = "3.12.3" +version = "3.13.0" dependencies = [ "anyhow", "arboard", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9ac164580..e62bc1e36 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cc-switch" -version = "3.12.3" +version = "3.13.0" description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI" authors = ["Jason Young"] license = "MIT" diff --git a/src-tauri/src/auto_launch.rs b/src-tauri/src/auto_launch.rs index c69ae46ad..2f44e8971 100644 --- a/src-tauri/src/auto_launch.rs +++ b/src-tauri/src/auto_launch.rs @@ -70,6 +70,7 @@ pub fn is_auto_launch_enabled() -> Result { #[cfg(test)] mod tests { + #[allow(unused_imports)] use super::*; #[cfg(target_os = "macos")] diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index 5001e1f37..623166e5a 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -948,6 +948,7 @@ exec bash --norc --noprofile "kitty" => launch_macos_open_app("kitty", &script_file, false), "ghostty" => launch_macos_open_app("Ghostty", &script_file, true), "wezterm" => launch_macos_open_app("WezTerm", &script_file, true), + "kaku" => launch_macos_open_app("Kaku", &script_file, true), _ => launch_macos_terminal_app(&script_file), // "terminal" or default }; diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 2a9e1ded7..d755116fd 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -1072,8 +1072,15 @@ impl Database { /// v6 -> v7: Skills 更新检测支持(content_hash + updated_at) fn migrate_v6_to_v7(conn: &Connection) -> Result<(), AppError> { - Self::add_column_if_missing(conn, "skills", "content_hash", "TEXT")?; - Self::add_column_if_missing(conn, "skills", "updated_at", "INTEGER NOT NULL DEFAULT 0")?; + if Self::table_exists(conn, "skills")? { + Self::add_column_if_missing(conn, "skills", "content_hash", "TEXT")?; + Self::add_column_if_missing( + conn, + "skills", + "updated_at", + "INTEGER NOT NULL DEFAULT 0", + )?; + } log::info!("v6 -> v7 迁移完成:已添加 content_hash 和 updated_at 列"); Ok(()) } @@ -1103,32 +1110,34 @@ impl Database { .map_err(|e| AppError::Database(format!("创建 session_log_sync 表失败: {e}")))?; // 3. 修正国产模型定价:之前误将 CNY 值存为 USD 字段,统一转换为 USD - let pricing_fixes: &[(&str, &str, &str, &str, &str)] = &[ - ("deepseek-v3.2", "0.28", "0.42", "0.028", "0"), - ("deepseek-v3.1", "0.55", "1.67", "0.055", "0"), - ("deepseek-v3", "0.28", "1.11", "0.028", "0"), - ("doubao-seed-code", "0.17", "1.11", "0.02", "0"), - ("kimi-k2-thinking", "0.55", "2.20", "0.10", "0"), - ("kimi-k2-0905", "0.55", "2.20", "0.10", "0"), - ("kimi-k2-turbo", "1.11", "8.06", "0.14", "0"), - ("minimax-m2.1", "0.27", "0.95", "0.03", "0"), - ("minimax-m2.1-lightning", "0.27", "2.33", "0.03", "0"), - ("minimax-m2", "0.27", "0.95", "0.03", "0"), - ("glm-4.7", "0.39", "1.75", "0.04", "0"), - ("glm-4.6", "0.28", "1.11", "0.03", "0"), - ("mimo-v2-flash", "0.09", "0.29", "0.009", "0"), - ]; - for (model_id, input, output, cache_read, cache_creation) in pricing_fixes { - conn.execute( - "UPDATE model_pricing SET - input_cost_per_million = ?2, - output_cost_per_million = ?3, - cache_read_cost_per_million = ?4, - cache_creation_cost_per_million = ?5 - WHERE model_id = ?1", - rusqlite::params![model_id, input, output, cache_read, cache_creation], - ) - .map_err(|e| AppError::Database(format!("更新模型 {model_id} 定价失败: {e}")))?; + if Self::table_exists(conn, "model_pricing")? { + let pricing_fixes: &[(&str, &str, &str, &str, &str)] = &[ + ("deepseek-v3.2", "0.28", "0.42", "0.028", "0"), + ("deepseek-v3.1", "0.55", "1.67", "0.055", "0"), + ("deepseek-v3", "0.28", "1.11", "0.028", "0"), + ("doubao-seed-code", "0.17", "1.11", "0.02", "0"), + ("kimi-k2-thinking", "0.55", "2.20", "0.10", "0"), + ("kimi-k2-0905", "0.55", "2.20", "0.10", "0"), + ("kimi-k2-turbo", "1.11", "8.06", "0.14", "0"), + ("minimax-m2.1", "0.27", "0.95", "0.03", "0"), + ("minimax-m2.1-lightning", "0.27", "2.33", "0.03", "0"), + ("minimax-m2", "0.27", "0.95", "0.03", "0"), + ("glm-4.7", "0.39", "1.75", "0.04", "0"), + ("glm-4.6", "0.28", "1.11", "0.03", "0"), + ("mimo-v2-flash", "0.09", "0.29", "0.009", "0"), + ]; + for (model_id, input, output, cache_read, cache_creation) in pricing_fixes { + conn.execute( + "UPDATE model_pricing SET + input_cost_per_million = ?2, + output_cost_per_million = ?3, + cache_read_cost_per_million = ?4, + cache_creation_cost_per_million = ?5 + WHERE model_id = ?1", + rusqlite::params![model_id, input, output, cache_read, cache_creation], + ) + .map_err(|e| AppError::Database(format!("更新模型 {model_id} 定价失败: {e}")))?; + } } log::info!("v7 -> v8 迁移完成:data_source 列、session_log_sync 表、修正 13 个模型定价"); diff --git a/src-tauri/src/proxy/providers/transform.rs b/src-tauri/src/proxy/providers/transform.rs index 2b660f334..7380fb860 100644 --- a/src-tauri/src/proxy/providers/transform.rs +++ b/src-tauri/src/proxy/providers/transform.rs @@ -35,7 +35,7 @@ pub fn supports_reasoning_effort(model: &str) -> bool { /// `low`/`medium`/`high` map 1:1; `max` maps to `xhigh` /// (supported by mainstream GPT models). Unknown values are ignored. /// 2. Fallback: `thinking.type` + `budget_tokens`: -/// - `adaptive` → `high` (mirrors optimizer semantics where adaptive ≈ max effort) +/// - `adaptive` → `xhigh` (adaptive = maximum reasoning effort) /// - `enabled` with budget → `low` (<4 000) / `medium` (4 000–15 999) / `high` (≥16 000) /// - `enabled` without budget → `high` (conservative default) /// - `disabled` / absent → `None` @@ -57,7 +57,7 @@ pub fn resolve_reasoning_effort(body: &Value) -> Option<&'static str> { // --- Priority 2: thinking.type + budget_tokens fallback --- let thinking = body.get("thinking")?; match thinking.get("type").and_then(|t| t.as_str()) { - Some("adaptive") => Some("high"), + Some("adaptive") => Some("xhigh"), Some("enabled") => { let budget = thinking.get("budget_tokens").and_then(|b| b.as_u64()); match budget { @@ -1019,9 +1019,9 @@ mod tests { } #[test] - fn test_thinking_adaptive_maps_high() { + fn test_thinking_adaptive_maps_xhigh() { let body = json!({"thinking": {"type": "adaptive"}}); - assert_eq!(resolve_reasoning_effort(&body), Some("high")); + assert_eq!(resolve_reasoning_effort(&body), Some("xhigh")); } #[test] @@ -1100,7 +1100,7 @@ mod tests { }); let result = anthropic_to_openai(input, None).unwrap(); - assert_eq!(result["reasoning_effort"], "high"); + assert_eq!(result["reasoning_effort"], "xhigh"); } #[test] diff --git a/src-tauri/src/proxy/providers/transform_responses.rs b/src-tauri/src/proxy/providers/transform_responses.rs index 6af18eac1..a61bf7f9b 100644 --- a/src-tauri/src/proxy/providers/transform_responses.rs +++ b/src-tauri/src/proxy/providers/transform_responses.rs @@ -1047,7 +1047,7 @@ mod tests { } #[test] - fn test_responses_thinking_adaptive_sets_reasoning_high() { + fn test_responses_thinking_adaptive_sets_reasoning_xhigh() { let input = json!({ "model": "gpt-5.4", "max_tokens": 1024, @@ -1056,7 +1056,7 @@ mod tests { }); let result = anthropic_to_responses(input, None, false).unwrap(); - assert_eq!(result["reasoning"]["effort"], "high"); + assert_eq!(result["reasoning"]["effort"], "xhigh"); } #[test] diff --git a/src-tauri/src/session_manager/providers/claude.rs b/src-tauri/src/session_manager/providers/claude.rs index 865b9efa9..226613d13 100644 --- a/src-tauri/src/session_manager/providers/claude.rs +++ b/src-tauri/src/session_manager/providers/claude.rs @@ -12,6 +12,7 @@ use super::utils::{ }; const PROVIDER_ID: &str = "claude"; +const TITLE_MAX_CHARS: usize = 80; pub fn scan_sessions() -> Vec { let root = get_claude_config_dir().join("projects"); @@ -129,8 +130,9 @@ fn parse_session(path: &Path) -> Option { let mut session_id: Option = None; let mut project_dir: Option = None; let mut created_at: Option = None; + let mut first_user_message: Option = None; - // Extract metadata from head lines + // Extract metadata and first user message from head lines for line in &head { let value: Value = match serde_json::from_str(line) { Ok(parsed) => parsed, @@ -151,11 +153,41 @@ fn parse_session(path: &Path) -> Option { if created_at.is_none() { created_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } + // Extract first real user message as title candidate + // Skip system-injected caveats and slash commands (e.g. /clear, /compact) + if first_user_message.is_none() { + let is_user = value.get("type").and_then(Value::as_str) == Some("user") + || value + .get("message") + .and_then(|m| m.get("role")) + .and_then(Value::as_str) + == Some("user"); + if is_user { + if let Some(message) = value.get("message") { + let text = message.get("content").map(extract_text).unwrap_or_default(); + let trimmed = text.trim(); + if !trimmed.is_empty() + && !trimmed.contains("") + && !trimmed.starts_with("") + { + first_user_message = Some(trimmed.to_string()); + } + } + } + } + if session_id.is_some() + && project_dir.is_some() + && created_at.is_some() + && first_user_message.is_some() + { + break; + } } - // Extract last_active_at and summary from tail lines (reverse order) + // Extract last_active_at, summary, and custom-title from tail lines (reverse order) let mut last_active_at: Option = None; let mut summary: Option = None; + let mut custom_title: Option = None; for line in tail.iter().rev() { let value: Value = match serde_json::from_str(line) { @@ -165,6 +197,16 @@ fn parse_session(path: &Path) -> Option { if last_active_at.is_none() { last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms); } + // Look for custom-title entry (take the last one, i.e. first in reverse) + if custom_title.is_none() + && value.get("type").and_then(Value::as_str) == Some("custom-title") + { + custom_title = value + .get("customTitle") + .and_then(Value::as_str) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + } if summary.is_none() { if value.get("isMeta").and_then(Value::as_bool) == Some(true) { continue; @@ -176,7 +218,7 @@ fn parse_session(path: &Path) -> Option { } } } - if last_active_at.is_some() && summary.is_some() { + if last_active_at.is_some() && summary.is_some() && custom_title.is_some() { break; } } @@ -184,10 +226,16 @@ fn parse_session(path: &Path) -> Option { let session_id = session_id.or_else(|| infer_session_id_from_filename(path)); let session_id = session_id?; - let title = project_dir - .as_deref() - .and_then(path_basename) - .map(|value| value.to_string()); + // Title priority: custom-title > first user message > directory basename + let title = custom_title + .map(|t| truncate_summary(&t, TITLE_MAX_CHARS)) + .or_else(|| first_user_message.map(|t| truncate_summary(&t, TITLE_MAX_CHARS))) + .or_else(|| { + project_dir + .as_deref() + .and_then(path_basename) + .map(|v| v.to_string()) + }); let summary = summary.map(|text| truncate_summary(&text, 160)); @@ -336,4 +384,117 @@ mod tests { assert_eq!(msgs[0].role, "user"); assert!(msgs[0].content.contains("Please continue")); } + + #[test] + fn parse_session_uses_first_user_message_as_title() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("session-abc.jsonl"); + std::fs::write( + &path, + concat!( + "{\"sessionId\":\"session-abc\",\"cwd\":\"/tmp/project\",\"timestamp\":\"2026-03-06T10:00:00Z\"}\n", + "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"How do I deploy?\"},\"sessionId\":\"session-abc\",\"timestamp\":\"2026-03-06T10:01:00Z\"}\n", + "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":\"Here is how...\"},\"timestamp\":\"2026-03-06T10:02:00Z\"}\n", + ), + ) + .expect("write"); + + let meta = parse_session(&path).unwrap(); + assert_eq!(meta.title.as_deref(), Some("How do I deploy?")); + } + + #[test] + fn parse_session_custom_title_overrides_first_message() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("session-def.jsonl"); + std::fs::write( + &path, + concat!( + "{\"sessionId\":\"session-def\",\"cwd\":\"/tmp/project\",\"timestamp\":\"2026-03-06T10:00:00Z\"}\n", + "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"fix something\"},\"sessionId\":\"session-def\",\"timestamp\":\"2026-03-06T10:01:00Z\"}\n", + "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":\"Done.\"},\"timestamp\":\"2026-03-06T10:02:00Z\"}\n", + "{\"type\":\"custom-title\",\"customTitle\":\"fix-login-bug\",\"sessionId\":\"session-def\"}\n", + ), + ) + .expect("write"); + + let meta = parse_session(&path).unwrap(); + assert_eq!(meta.title.as_deref(), Some("fix-login-bug")); + } + + #[test] + fn parse_session_falls_back_to_dir_basename() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("session-ghi.jsonl"); + std::fs::write( + &path, + concat!( + "{\"sessionId\":\"session-ghi\",\"cwd\":\"/tmp/my-project\",\"timestamp\":\"2026-03-06T10:00:00Z\"}\n", + "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":\"Hello\"},\"timestamp\":\"2026-03-06T10:01:00Z\"}\n", + ), + ) + .expect("write"); + + let meta = parse_session(&path).unwrap(); + // No user message and no custom-title → falls back to dir basename + assert_eq!(meta.title.as_deref(), Some("my-project")); + } + + #[test] + fn parse_session_truncates_long_title() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("session-trunc.jsonl"); + let long_msg = "a".repeat(200); + std::fs::write( + &path, + format!( + "{{\"sessionId\":\"session-trunc\",\"cwd\":\"/tmp/p\",\"timestamp\":\"2026-03-06T10:00:00Z\"}}\n\ + {{\"type\":\"user\",\"message\":{{\"role\":\"user\",\"content\":\"{long_msg}\"}},\"sessionId\":\"session-trunc\",\"timestamp\":\"2026-03-06T10:01:00Z\"}}\n", + ), + ) + .expect("write"); + + let meta = parse_session(&path).unwrap(); + let title = meta.title.unwrap(); + assert!(title.len() <= TITLE_MAX_CHARS + 3); // +3 for "..." + assert!(title.ends_with("...")); + } + + #[test] + fn parse_session_new_format_with_snapshot() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("session-new.jsonl"); + std::fs::write( + &path, + concat!( + "{\"type\":\"file-history-snapshot\",\"messageId\":\"msg-1\",\"snapshot\":{},\"isSnapshotUpdate\":false}\n", + "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"请帮我重构这个函数\"},\"sessionId\":\"session-new\",\"timestamp\":\"2026-03-06T10:00:00Z\",\"cwd\":\"/tmp/project\"}\n", + "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":\"OK\"},\"timestamp\":\"2026-03-06T10:01:00Z\",\"cwd\":\"/tmp/project\"}\n", + ), + ) + .expect("write"); + + let meta = parse_session(&path).unwrap(); + assert_eq!(meta.title.as_deref(), Some("请帮我重构这个函数")); + } + + #[test] + fn parse_session_skips_command_caveat_and_slash_commands() { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join("session-clear.jsonl"); + std::fs::write( + &path, + concat!( + "{\"type\":\"file-history-snapshot\",\"messageId\":\"msg-1\",\"snapshot\":{},\"isSnapshotUpdate\":false}\n", + "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"Caveat: The messages below were generated by the user while running local commands.\"},\"sessionId\":\"session-clear\",\"timestamp\":\"2026-03-06T10:00:00Z\",\"cwd\":\"/tmp/project\"}\n", + "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"/clear\\nclear\"},\"sessionId\":\"session-clear\",\"timestamp\":\"2026-03-06T10:00:01Z\",\"cwd\":\"/tmp/project\"}\n", + "{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":\"Done.\"},\"timestamp\":\"2026-03-06T10:00:02Z\",\"cwd\":\"/tmp/project\"}\n", + "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"帮我看看工作区的改动\"},\"sessionId\":\"session-clear\",\"timestamp\":\"2026-03-06T10:01:00Z\",\"cwd\":\"/tmp/project\"}\n", + ), + ) + .expect("write"); + + let meta = parse_session(&path).unwrap(); + assert_eq!(meta.title.as_deref(), Some("帮我看看工作区的改动")); + } } diff --git a/src-tauri/src/session_manager/terminal/mod.rs b/src-tauri/src/session_manager/terminal/mod.rs index ab13466ba..3e35df30e 100644 --- a/src-tauri/src/session_manager/terminal/mod.rs +++ b/src-tauri/src/session_manager/terminal/mod.rs @@ -20,6 +20,7 @@ pub fn launch_terminal( "ghostty" => launch_ghostty(command, cwd), "kitty" => launch_kitty(command, cwd), "wezterm" => launch_wezterm(command, cwd), + "kaku" => launch_kaku(command, cwd), "alacritty" => launch_alacritty(command, cwd), "custom" => launch_custom(command, cwd, custom_config), _ => Err(format!("Unsupported terminal target: {target}")), @@ -153,25 +154,10 @@ fn launch_kitty(command: &str, cwd: Option<&str>) -> Result<(), String> { fn launch_wezterm(command: &str, cwd: Option<&str>) -> Result<(), String> { // wezterm start --cwd ... -- command // To invoke via `open`, we use `open -na "WezTerm" --args start ...` - - let full_command = build_shell_command(command, None); - - let mut args = vec!["-na", "WezTerm", "--args", "start"]; - - if let Some(dir) = cwd { - args.push("--cwd"); - args.push(dir); - } - - // Invoke shell to run the command string (to handle pipes, etc) - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); - args.push("--"); - args.push(&shell); - args.push("-c"); - args.push(&full_command); + let args = build_wezterm_compatible_args("WezTerm", command, cwd); let status = Command::new("open") - .args(&args) + .args(args.iter().map(String::as_str)) .status() .map_err(|e| format!("Failed to launch WezTerm: {e}"))?; @@ -182,6 +168,54 @@ fn launch_wezterm(command: &str, cwd: Option<&str>) -> Result<(), String> { } } +fn launch_kaku(command: &str, cwd: Option<&str>) -> Result<(), String> { + // Kaku is a WezTerm-derived terminal and keeps a compatible `start` entrypoint. + let args = build_wezterm_compatible_args("Kaku", command, cwd); + + let status = Command::new("open") + .args(args.iter().map(String::as_str)) + .status() + .map_err(|e| format!("Failed to launch Kaku: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err("Failed to launch Kaku.".to_string()) + } +} + +fn build_wezterm_compatible_args(app_name: &str, command: &str, cwd: Option<&str>) -> Vec { + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); + build_wezterm_compatible_args_with_shell(app_name, command, cwd, &shell) +} + +fn build_wezterm_compatible_args_with_shell( + app_name: &str, + command: &str, + cwd: Option<&str>, + shell: &str, +) -> Vec { + let full_command = build_shell_command(command, None); + let mut args = vec![ + "-na".to_string(), + app_name.to_string(), + "--args".to_string(), + "start".to_string(), + ]; + + if let Some(dir) = cwd { + args.push("--cwd".to_string()); + args.push(dir.to_string()); + } + + // Invoke shell to run the command string (to handle pipes, etc) + args.push("--".to_string()); + args.push(shell.to_string()); + args.push("-c".to_string()); + args.push(full_command); + args +} + fn launch_alacritty(command: &str, cwd: Option<&str>) -> Result<(), String> { // Alacritty: open -na Alacritty --args --working-directory ... -e shell -c command let full_command = build_shell_command(command, None); @@ -305,4 +339,30 @@ mod tests { "raw:echo foo\\\\\\\\bar\\npwd\\n" ); } + + #[test] + fn wezterm_compatible_terminals_use_start_and_cwd_arguments() { + let args = build_wezterm_compatible_args_with_shell( + "Kaku", + "claude --resume abc-123", + Some("/tmp/project dir"), + "/bin/zsh", + ); + + assert_eq!( + args, + vec![ + "-na".to_string(), + "Kaku".to_string(), + "--args".to_string(), + "start".to_string(), + "--cwd".to_string(), + "/tmp/project dir".to_string(), + "--".to_string(), + "/bin/zsh".to_string(), + "-c".to_string(), + "claude --resume abc-123".to_string(), + ] + ); + } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 22e415a4c..c9b4a4b41 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -273,7 +273,7 @@ pub struct AppSettings { // ===== 终端设置 ===== /// 首选终端应用(可选,默认使用系统默认终端) - /// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" + /// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "kaku" /// - Windows: "cmd" | "powershell" | "wt" (Windows Terminal) /// - Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty" #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1551b39fc..f3354cb75 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CC Switch", - "version": "3.12.3", + "version": "3.13.0", "identifier": "com.ccswitch.desktop", "build": { "frontendDist": "../dist", diff --git a/src-tauri/tests/skill_sync.rs b/src-tauri/tests/skill_sync.rs index 3d4cf6814..b1e0c3a61 100644 --- a/src-tauri/tests/skill_sync.rs +++ b/src-tauri/tests/skill_sync.rs @@ -110,6 +110,8 @@ fn sync_to_app_removes_disabled_and_orphaned_ssot_symlinks() { opencode: false, }, installed_at: 0, + content_hash: None, + updated_at: 0, }) .expect("save disabled skill"); @@ -154,6 +156,8 @@ fn uninstall_skill_creates_backup_before_removing_ssot() { opencode: false, }, installed_at: 123, + content_hash: None, + updated_at: 0, }) .expect("save skill"); @@ -222,6 +226,8 @@ fn restore_skill_backup_restores_files_to_ssot_and_current_app() { opencode: false, }, installed_at: 456, + content_hash: None, + updated_at: 0, }) .expect("save skill"); @@ -303,6 +309,8 @@ fn delete_skill_backup_removes_backup_directory() { opencode: false, }, installed_at: 789, + content_hash: None, + updated_at: 0, }) .expect("save skill"); diff --git a/src/components/ProviderIcon.tsx b/src/components/ProviderIcon.tsx index 9b575af8d..ddc9ed64d 100644 --- a/src/components/ProviderIcon.tsx +++ b/src/components/ProviderIcon.tsx @@ -1,5 +1,11 @@ import React, { useMemo } from "react"; -import { getIcon, hasIcon, getIconMetadata } from "@/icons/extracted"; +import { + getIcon, + hasIcon, + getIconMetadata, + getIconUrl, + isUrlIcon, +} from "@/icons/extracted"; import { cn } from "@/lib/utils"; interface ProviderIconProps { @@ -19,21 +25,28 @@ export const ProviderIcon: React.FC = ({ className, showFallback = true, }) => { - // 获取图标 SVG + // 获取内联 SVG 字符串 const iconSvg = useMemo(() => { - if (icon && hasIcon(icon)) { + if (icon && !isUrlIcon(icon) && hasIcon(icon)) { return getIcon(icon); } return ""; }, [icon]); + // 获取图标 URL(URL_ICONS 列表中的 SVG / 光栅图片) + const iconUrl = useMemo(() => { + if (icon && isUrlIcon(icon)) { + return getIconUrl(icon); + } + return ""; + }, [icon]); + // 计算尺寸样式 const sizeStyle = useMemo(() => { const sizeValue = typeof size === "number" ? `${size}px` : size; return { width: sizeValue, height: sizeValue, - // 内嵌 SVG 使用 1em 作为尺寸基准,这里同步 fontSize 让图标实际跟随 size 放大 fontSize: sizeValue, lineHeight: 1, }; @@ -41,14 +54,11 @@ export const ProviderIcon: React.FC = ({ // 获取有效颜色:优先使用传入的有效 color,否则从元数据获取 defaultColor const effectiveColor = useMemo(() => { - // 只有当 color 是有效的非空字符串时才使用 if (color && typeof color === "string" && color.trim() !== "") { return color; } - // 否则从元数据获取 defaultColor if (icon) { const metadata = getIconMetadata(icon); - // 只有当 defaultColor 不是 currentColor 时才使用 if (metadata?.defaultColor && metadata.defaultColor !== "currentColor") { return metadata.defaultColor; } @@ -56,7 +66,7 @@ export const ProviderIcon: React.FC = ({ return undefined; }, [color, icon]); - // 如果有图标,显示图标 + // 内联 SVG 渲染(支持 CSS currentColor 着色) if (iconSvg) { return ( = ({ ); } + // URL-based 图标(大型 SVG / 光栅图片):以 渲染 + if (iconUrl) { + return ( + {name} + ); + } + // Fallback:显示首字母 if (showFallback) { const initials = name diff --git a/src/components/providers/forms/OmoFormFields.tsx b/src/components/providers/forms/OmoFormFields.tsx index 52cced9cb..c1a316515 100644 --- a/src/components/providers/forms/OmoFormFields.tsx +++ b/src/components/providers/forms/OmoFormFields.tsx @@ -1264,12 +1264,22 @@ export function OmoFormFields({ ) : undefined, maxHeightClass: "max-h-[500px]", children: ( -