18 Commits

Author SHA1 Message Date
YoVinchen
81a6c08673 fix(skills): resolve third-party skills installation failure
- Add skills_path field to Skill struct
- Use skills_path to construct correct source path during installation
- Fix installation for repos with custom skill subdirectories
2025-11-21 12:33:12 +08:00
Jason
74969ae968 fix(dialog): prevent dialogs from closing on overlay click
Add onInteractOutside handler to DialogContent to prevent accidental
dialog closure when users click on the overlay/backdrop. This prevents
data loss in forms and improves user experience across all 11 dialog
components in the application.

Users can still close dialogs using:
- Close button (X) in the top-right corner
- Cancel/Close buttons within the dialog
- ESC key
2025-11-20 23:29:57 +08:00
Jason
1f3627add3 fix(gemini): persist settings json edits 2025-11-20 20:23:22 +08:00
YoVinchen
14ee122b27 feat(settings): add Gemini configuration directory support (#255)
* style: apply code formatting across backend and frontend

Apply cargo fmt and prettier formatting to improve code readability.
No functional changes.

Changes:
- Rust: multi-line assertion formatting (gemini_config, env_checker)
- Rust: simplify chained method calls (provider)
- TypeScript: add trailing commas to function parameters (codexProviderPresets)

* feat(settings): add Gemini configuration directory support

Add custom configuration directory support for Gemini:
- Add geminiConfigDir field to Settings type
- Extend DirectorySettings component with Gemini input
- Update useDirectorySettings hook for Gemini directory management
- Add i18n translations for Gemini directory settings
2025-11-19 21:14:43 +08:00
wugeer
7aecba14fe docs: Add installation instructions for Arch Linux (#259) 2025-11-19 19:12:00 +08:00
Jason
99b5f881e8 docs: add v3.7.0 release documentation
- Update CHANGELOG.md with v3.7.0 entry covering six major features
- Add English release notes (docs/release-note-v3.7.0-en.md)
- Add Chinese release notes (docs/release-note-v3.7.0-zh.md)

Major features documented:
- Gemini CLI integration (third app support)
- MCP v3.7.0 unified architecture
- Claude Skills management system (~2,000 lines)
- Prompts management system (~1,300 lines)
- Deep link protocol (ccswitch://)
- Environment variable conflict detection
2025-11-19 12:39:45 +08:00
Jason
286bafbd67 test: simplify boolean assertions in import_export_sync tests
Replace verbose assert_eq!(value, true) with idiomatic assert!(value) for improved readability and adherence to Rust best practices
2025-11-19 11:45:07 +08:00
Jason
6046cf8767 update screen shots 2025-11-19 11:29:19 +08:00
YoVinchen
c88afa365f style: apply code formatting across backend and frontend (#252)
Apply cargo fmt and prettier formatting to improve code readability.
No functional changes.

Changes:
- Rust: multi-line assertion formatting (gemini_config, env_checker)
- Rust: simplify chained method calls (provider)
- TypeScript: add trailing commas to function parameters (codexProviderPresets)
2025-11-19 11:26:31 +08:00
farion1231
93fa5fe29a chore(release): bump version to v3.7.0 and rebrand to include Gemini CLI
- Update version from 3.6.2 to 3.7.0 across all config files
- Update project description to "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
- Update Chinese description to "Claude Code / Codex / Gemini CLI 全方位辅助工具"
- Sync version in package.json, Cargo.toml, tauri.conf.json
- Update branding in README.md, README_ZH.md and i18n locale files
2025-11-19 11:20:55 +08:00
farion1231
3d31ad64af feat: add DouBaoSeed provider and remove AnyRouter preset
- Add DouBaoSeed code preview provider preset with Volcengine ARK API configuration
- Remove AnyRouter provider from both Claude and Codex preset configurations
- Clean up trailing commas in codex provider presets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 11:04:44 +08:00
farion1231
bb0951552d feat: update Gemini default model and remove Google Official preset model
Updated default model from gemini-2.5-pro to gemini-3-pro-preview across:
- Provider presets (PackyCode, Custom)
- Form field placeholders
- Default configurations
- Test cases

Google Official preset now has empty env config, allowing users to choose
their own model or use application defaults, which is more appropriate for
OAuth-based authentication.

Changes:
- geminiProviderPresets.ts: updated model to gemini-3-pro-preview, removed model from Google Official
- GeminiFormFields.tsx: updated placeholder to gemini-3-pro-preview
- GeminiConfigSections.tsx: updated placeholder to gemini-3-pro-preview
- ProviderForm.tsx: updated default config to gemini-3-pro-preview
- gemini_config.rs: updated test examples to gemini-3-pro-preview
2025-11-19 10:53:33 +08:00
farion1231
00e3e6fa70 fix: read both .env and settings.json for Gemini live config
Previously, when editing a Gemini provider, only the .env file was read,
missing the settings.json file that contains MCP configuration and other
settings. This caused the config field in the edit form to be empty or
show outdated data.

This fix ensures both files are read and merged into the complete
structure { "env": {...}, "config": {...} }, matching the behavior
of Codex provider.

Fixed in:
- read_live_settings(): now reads both .env and settings.json
- import_default_config(): now reads both files when importing
2025-11-19 10:45:45 +08:00
farion1231
1ce007622e fix: resolve winreg API compatibility issue on Windows
- Update RegValue.to_string() usage to match current winreg API
  which returns String directly instead of Result
- Add conditional compilation for std::fs import (Unix only)
2025-11-19 10:06:52 +08:00
Jason
436f0e8e42 fix: sync Gemini form fields with env editor
The Gemini API key, base URL, and model inputs were not syncing with
the env editor below due to data source mismatch. The form was using
generic hooks (useApiKeyState, useBaseUrlState) that only updated
settingsConfig, while the env editor relied on geminiEnv from
useGeminiConfigState.

Changes:
- Use geminiApiKey/geminiBaseUrl from useGeminiConfigState instead of
  generic hooks
- Wrap handlers to maintain bidirectional sync between geminiEnv and
  settingsConfig
- Remove unused handleGeminiBaseUrlChange from useBaseUrlState to
  avoid naming conflicts

Now all Gemini form fields properly sync with the env editor in both
directions.
2025-11-19 09:28:53 +08:00
YoVinchen
3d69da5b66 feat: add model configuration support and fix Gemini deeplink bug (#251)
* feat(providers): add notes field for provider management

- Add notes field to Provider model (backend and frontend)
- Display notes with higher priority than URL in provider card
- Style notes as non-clickable text to differentiate from URLs
- Add notes input field in provider form
- Add i18n support (zh/en) for notes field

* chore: format code and clean up unused props

- Run cargo fmt on Rust backend code
- Format TypeScript imports and code style
- Remove unused appId prop from ProviderPresetSelector
- Clean up unused variables in tests
- Integrate notes field handling in provider dialogs

* feat(deeplink): implement ccswitch:// protocol for provider import

Add deep link support to enable one-click provider configuration import via ccswitch:// URLs.

Backend:
- Implement URL parsing and validation (src-tauri/src/deeplink.rs)
- Add Tauri commands for parse and import (src-tauri/src/commands/deeplink.rs)
- Register ccswitch:// protocol in macOS Info.plist
- Add comprehensive unit tests (src-tauri/tests/deeplink_import.rs)

Frontend:
- Create confirmation dialog with security review UI (src/components/DeepLinkImportDialog.tsx)
- Add API wrapper (src/lib/api/deeplink.ts)
- Integrate event listeners in App.tsx

Configuration:
- Update Tauri config for deep link handling
- Add i18n support for Chinese and English
- Include test page for deep link validation (deeplink-test.html)

Files: 15 changed, 1312 insertions(+)

* chore(deeplink): integrate deep link handling into app lifecycle

Wire up deep link infrastructure with app initialization and event handling.

Backend Integration:
- Register deep link module and commands in mod.rs
- Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url)
- Handle deep links from single instance callback (Windows/Linux CLI)
- Handle deep links from macOS system events
- Add tauri-plugin-deep-link dependency (Cargo.toml)

Frontend Integration:
- Listen for deeplink-import/deeplink-error events in App.tsx
- Update DeepLinkImportDialog component imports

Configuration:
- Enable deep link plugin in tauri.conf.json
- Update Cargo.lock for new dependencies

Localization:
- Add Chinese translations for deep link UI (zh.json)
- Add English translations for deep link UI (en.json)

Files: 9 changed, 359 insertions(+), 18 deletions(-)

* refactor(deeplink): enhance Codex provider template generation

Align deep link import with UI preset generation logic by:
- Adding complete config.toml template matching frontend defaults
- Generating safe provider name from sanitized input
- Including model_provider, reasoning_effort, and wire_api settings
- Removing minimal template that only contained base_url
- Cleaning up deprecated test file deeplink-test.html

* style: fix clippy uninlined_format_args warnings

Apply clippy --fix to use inline format arguments in:
- src/mcp.rs (8 fixes)
- src/services/env_manager.rs (10 fixes)

* style: apply code formatting and cleanup

- Format TypeScript files with Prettier (App.tsx, EnvWarningBanner.tsx, formatters.ts)
- Organize Rust imports and module order alphabetically
- Add newline at end of JSON files (en.json, zh.json)
- Update Cargo.lock for dependency changes

* feat: add model name configuration support for Codex and fix Gemini model handling

- Add visual model name input field for Codex providers
  - Add model name extraction and update utilities in providerConfigUtils
  - Implement model name state management in useCodexConfigState hook
  - Add conditional model field rendering in CodexFormFields (non-official only)
  - Integrate model name sync with TOML config in ProviderForm

- Fix Gemini deeplink model injection bug
  - Correct environment variable name from GOOGLE_GEMINI_MODEL to GEMINI_MODEL
  - Add test cases for Gemini model injection (with/without model)
  - All tests passing (9/9)

- Fix Gemini model field binding in edit mode
  - Add geminiModel state to useGeminiConfigState hook
  - Extract model value during initialization and reset
  - Sync model field with geminiEnv state to prevent data loss on submit
  - Fix missing model value display when editing Gemini providers

Changes:
  - 6 files changed, 245 insertions(+), 13 deletions(-)
2025-11-19 09:03:18 +08:00
Jason
0ae9ed5a17 fix: resolve JSON syntax error in i18n locale files
Fixed missing closing braces in the error object of both en.json and zh.json locale files that caused Vite parse errors. The envManager.error object was not properly closed, causing the subsequent skills object to be parsed incorrectly.
2025-11-19 08:52:26 +08:00
冰子
5ff689af82 Add Gemini environment variable detection (#250)
* feat(env): add environment variable conflict detection and management

实现了系统环境变量冲突检测与管理功能:

核心功能:
- 自动检测会影响 Claude/Codex 的系统环境变量
- 支持 Windows 注册表和 Unix shell 配置文件检测
- 提供可视化的环境变量冲突警告横幅
- 支持批量选择和删除环境变量
- 删除前自动备份,支持后续恢复

技术实现:
- Rust 后端: 跨平台环境变量检测与管理
- React 前端: EnvWarningBanner 组件交互界面
- 国际化支持: 中英文界面
- 类型安全: 完整的 TypeScript 类型定义

* refactor(env): remove unused imports and function

Remove unused HashMap and PathBuf imports, and delete the unused get_source_description function to clean up the code.

* feat: Add Gemini environment variable detection
2025-11-19 08:33:02 +08:00
33 changed files with 1501 additions and 182 deletions

View File

@@ -5,6 +5,222 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.7.0] - 2025-11-19
### Major Features
#### Gemini CLI Integration
- **Complete Gemini CLI support** - Third major application added alongside Claude Code and Codex
- **Dual-file configuration** - Support for both `.env` and `settings.json` file formats
- **Environment variable detection** - Auto-detect `GOOGLE_GEMINI_BASE_URL`, `GEMINI_MODEL`, etc.
- **MCP management** - Full MCP configuration capabilities for Gemini
- **Provider presets**
- Google Official (OAuth authentication)
- PackyCode (partner integration)
- Custom endpoint support
- **Deep link support** - Import Gemini providers via `ccswitch://` protocol
- **System tray integration** - Quick-switch Gemini providers from tray menu
- **Backend modules** - New `gemini_config.rs` (20KB) and `gemini_mcp.rs`
#### MCP v3.7.0 Unified Architecture
- **Unified management panel** - Single interface for Claude/Codex/Gemini MCP servers
- **SSE transport type** - New Server-Sent Events support alongside stdio/http
- **Smart JSON parser** - Fault-tolerant parsing of various MCP config formats
- **Extended field support** - Preserve custom fields in Codex TOML conversion
- **Codex format correction** - Proper `[mcp_servers]` format (auto-cleanup of incorrect `[mcp.servers]`)
- **Import/export system** - Unified import from Claude/Codex/Gemini live configs
- **UX improvements**
- Default app selection in forms
- JSON formatter for config validation
- Improved layout and visual hierarchy
- Better validation error messages
#### Claude Skills Management System
- **GitHub repository integration** - Auto-scan and discover skills from GitHub repos
- **Pre-configured repositories**
- `ComposioHQ/awesome-claude-skills` (curated collection)
- `anthropics/skills` (official Anthropic skills)
- `cexll/myclaude` (community, with subdirectory scanning)
- **Lifecycle management**
- One-click install to `~/.claude/skills/`
- Safe uninstall with state tracking
- Update checking (infrastructure ready)
- **Custom repository support** - Add any GitHub repo as a skill source
- **Subdirectory scanning** - Optional `skillsPath` for repos with nested skill directories
- **Backend architecture** - `SkillService` (526 lines) with GitHub API integration
- **Frontend interface**
- SkillsPage: Browse and manage skills
- SkillCard: Visual skill presentation
- RepoManager: Repository management dialog
- **State persistence** - Installation state stored in `skills.json`
- **Full i18n support** - Complete Chinese/English translations (47+ keys)
#### Prompts (System Prompts) Management
- **Multi-preset management** - Create, edit, and switch between multiple system prompts
- **Cross-app support**
- Claude: `~/.claude/CLAUDE.md`
- Codex: `~/.codex/AGENTS.md`
- Gemini: `~/.gemini/GEMINI.md`
- **Markdown editor** - Full-featured CodeMirror 6 editor with syntax highlighting
- **Smart synchronization**
- Auto-write to live files on enable
- Content backfill protection (save current before switching)
- First-launch auto-import from live files
- **Single-active enforcement** - Only one prompt can be active at a time
- **Delete protection** - Cannot delete active prompts
- **Backend service** - `PromptService` (213 lines) with CRUD operations
- **Frontend components**
- PromptPanel: Main management interface (177 lines)
- PromptFormModal: Edit dialog with validation (160 lines)
- MarkdownEditor: CodeMirror integration (159 lines)
- usePromptActions: Business logic hook (152 lines)
- **Full i18n support** - Complete Chinese/English translations (41+ keys)
#### Deep Link Protocol (ccswitch://)
- **Protocol registration** - `ccswitch://` URL scheme for one-click imports
- **Provider import** - Import provider configurations from URLs or shared links
- **Lifecycle integration** - Deep link handling integrated into app startup
- **Cross-platform support** - Works on Windows, macOS, and Linux
#### Environment Variable Conflict Detection
- **Claude & Codex detection** - Identify conflicting environment variables
- **Gemini auto-detection** - Automatic environment variable discovery
- **Conflict management** - UI for resolving configuration conflicts
- **Prevention system** - Warn before overwriting existing configurations
### New Features
#### Provider Management
- **DouBaoSeed preset** - Added ByteDance's DouBao provider
- **Kimi For Coding** - Moonshot AI coding assistant
- **BaiLing preset** - BaiLing AI integration
- **Removed AnyRouter preset** - Discontinued provider
- **Model configuration** - Support for custom model names in Codex and Gemini
- **Provider notes field** - Add custom notes to providers for better organization
#### Configuration Management
- **Common config migration** - Moved Claude common config snippets from localStorage to `config.json`
- **Unified persistence** - Common config snippets now shared across all apps
- **Auto-import on first launch** - Automatically import configs from live files on first run
- **Backfill priority fix** - Correct priority handling when enabling prompts
#### UI/UX Improvements
- **macOS native design** - Migrated color scheme to macOS native design system
- **Window centering** - Default window position centered on screen
- **Password input fixes** - Disabled Edge/IE reveal and clear buttons
- **URL overflow prevention** - Fixed overflow in provider cards
- **Error notification enhancement** - Copy-to-clipboard for error messages
- **Tray menu sync** - Real-time sync after drag-and-drop sorting
### Improvements
#### Architecture
- **MCP v3.7.0 cleanup** - Removed legacy code and warnings
- **Unified structure** - Default initialization with v3.7.0 unified structure
- **Backward compatibility** - Compilation fixes for older configs
- **Code formatting** - Applied consistent formatting across backend and frontend
#### Platform Compatibility
- **Windows fix** - Resolved winreg API compatibility issue (v0.52)
- **Safe pattern matching** - Replaced `unwrap()` with safe patterns in tray menu
#### Configuration
- **MCP sync on switch** - Sync MCP configs for all apps when switching providers
- **Gemini form sync** - Fixed form fields syncing with environment editor
- **Gemini config reading** - Read from both `.env` and `settings.json`
- **Validation improvements** - Enhanced input validation and boundary checks
#### Internationalization
- **JSON syntax fixes** - Resolved syntax errors in locale files
- **App name i18n** - Added internationalization support for app names
- **Deduplicated labels** - Reused providerForm keys to reduce duplication
- **Gemini MCP title** - Added missing Gemini MCP panel title
### Bug Fixes
#### Critical Fixes
- **Usage script validation** - Added input validation and boundary checks
- **Gemini validation** - Relaxed validation when adding providers
- **TOML quote normalization** - Handle CJK quotes to prevent parsing errors
- **MCP field preservation** - Preserve custom fields in Codex TOML editor
- **Password input** - Fixed white screen crash (FormLabel → Label)
#### Stability
- **Tray menu safety** - Replaced unwrap with safe pattern matching
- **Error isolation** - Tray menu update failures don't block main operations
- **Import classification** - Set category to custom for imported default configs
#### UI Fixes
- **Model placeholders** - Removed misleading model input placeholders
- **Base URL population** - Auto-fill base URL for non-official providers
- **Drag sort sync** - Fixed tray menu order after drag-and-drop
### Technical Improvements
#### Code Quality
- **Type safety** - Complete TypeScript type coverage across codebase
- **Test improvements** - Simplified boolean assertions in tests
- **Clippy warnings** - Fixed `uninlined_format_args` warnings
- **Code refactoring** - Extracted templates, optimized logic flows
#### Dependencies
- **Tauri** - Updated to 2.8.x series
- **Rust dependencies** - Added `anyhow`, `zip`, `serde_yaml`, `tempfile` for Skills
- **Frontend dependencies** - Added CodeMirror 6 packages for Markdown editor
- **winreg** - Updated to v0.52 (Windows compatibility)
#### Performance
- **Startup optimization** - Removed legacy migration scanning
- **Lock management** - Improved RwLock usage to prevent deadlocks
- **Background query** - Enabled background mode for usage polling
### Statistics
- **Total commits**: 85 commits from v3.6.0 to v3.7.0
- **Code changes**: 152 files changed, 18,104 insertions(+), 3,732 deletions(-)
- **New modules**:
- Skills: 2,034 lines (21 files)
- Prompts: 1,302 lines (20 files)
- Gemini: ~1,000 lines (multiple files)
- MCP refactor: ~3,000 lines (refactored)
### Strategic Positioning
v3.7.0 represents a major evolution from "Provider Switcher" to **"All-in-One AI CLI Management Platform"**:
1. **Capability Extension** - Skills provide external ability integration
2. **Behavior Customization** - Prompts enable AI personality presets
3. **Configuration Unification** - MCP v3.7.0 eliminates app silos
4. **Ecosystem Openness** - Deep links enable community sharing
5. **Multi-AI Support** - Claude/Codex/Gemini trinity
6. **Intelligent Detection** - Auto-discovery of environment conflicts
### Notes
- Users upgrading from v3.1.0 or earlier should first upgrade to v3.2.x for one-time migration
- Skills and Prompts management are new features requiring no migration
- Gemini CLI support requires Gemini CLI to be installed separately
- MCP v3.7.0 unified structure is backward compatible with previous configs
## [3.6.0] - 2025-11-07 ## [3.6.0] - 2025-11-07
### ✨ New Features ### ✨ New Features
@@ -73,6 +289,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 🏗️ Technical Improvements (For Developers) ### 🏗️ Technical Improvements (For Developers)
**Backend Refactoring (Rust)** - Completed 5-phase refactoring: **Backend Refactoring (Rust)** - Completed 5-phase refactoring:
- **Phase 1**: Unified error handling (`AppError` + i18n error messages) - **Phase 1**: Unified error handling (`AppError` + i18n error messages)
- **Phase 2**: Command layer split by domain (`commands/{provider,mcp,config,settings,plugin,misc}.rs`) - **Phase 2**: Command layer split by domain (`commands/{provider,mcp,config,settings,plugin,misc}.rs`)
- **Phase 3**: Integration tests and transaction mechanism (config snapshot + failure rollback) - **Phase 3**: Integration tests and transaction mechanism (config snapshot + failure rollback)
@@ -80,17 +297,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Phase 5**: Concurrency optimization (`RwLock` instead of `Mutex`, scoped guard to avoid deadlock) - **Phase 5**: Concurrency optimization (`RwLock` instead of `Mutex`, scoped guard to avoid deadlock)
**Frontend Refactoring (React + TypeScript)** - Completed 4-stage refactoring: **Frontend Refactoring (React + TypeScript)** - Completed 4-stage refactoring:
- **Stage 1**: Test infrastructure (vitest + MSW + @testing-library/react) - **Stage 1**: Test infrastructure (vitest + MSW + @testing-library/react)
- **Stage 2**: Extracted custom hooks (`useProviderActions`, `useMcpActions`, `useSettings`, `useImportExport`, etc.) - **Stage 2**: Extracted custom hooks (`useProviderActions`, `useMcpActions`, `useSettings`, `useImportExport`, etc.)
- **Stage 3**: Component splitting and business logic extraction - **Stage 3**: Component splitting and business logic extraction
- **Stage 4**: Code cleanup and formatting unification - **Stage 4**: Code cleanup and formatting unification
**Testing System**: **Testing System**:
- Hooks unit tests 100% coverage - Hooks unit tests 100% coverage
- Integration tests covering key processes (App, SettingsDialog, MCP Panel) - Integration tests covering key processes (App, SettingsDialog, MCP Panel)
- MSW mocking backend API to ensure test independence - MSW mocking backend API to ensure test independence
**Code Quality**: **Code Quality**:
- Unified parameter format: All Tauri commands migrated to camelCase (Tauri 2 specification) - Unified parameter format: All Tauri commands migrated to camelCase (Tauri 2 specification)
- `AppType` renamed to `AppId`: Semantically clearer - `AppType` renamed to `AppId`: Semantically clearer
- Unified parsing with `FromStr` trait: Centralized `app` parameter parsing - Unified parsing with `FromStr` trait: Centralized `app` parameter parsing
@@ -98,6 +318,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove unused code: `missing_param` helper function, deprecated `tauri-api.ts`, redundant `KimiModelSelector` component - Remove unused code: `missing_param` helper function, deprecated `tauri-api.ts`, redundant `KimiModelSelector` component
**Internal Optimizations**: **Internal Optimizations**:
- **Removed Legacy Migration Logic**: v3.6 removed v1 config auto-migration and copy file scanning logic - **Removed Legacy Migration Logic**: v3.6 removed v1 config auto-migration and copy file scanning logic
-**Impact**: Improved startup performance, cleaner code -**Impact**: Improved startup performance, cleaner code
-**Compatibility**: v2 format configs fully compatible, no action required -**Compatibility**: v2 format configs fully compatible, no action required
@@ -361,6 +582,7 @@ For users upgrading from v2.x (Electron version):
- Basic provider management - Basic provider management
- Claude Code integration - Claude Code integration
- Configuration file handling - Configuration file handling
## [Unreleased] ## [Unreleased]
### ⚠️ Breaking Changes ### ⚠️ Breaking Changes

View File

@@ -1,8 +1,8 @@
<div align="center"> <div align="center">
# Claude Code & Codex Provider Switcher # All-in-One Assistant for Claude Code, Codex & Gemini CLI
[![Version](https://img.shields.io/badge/version-3.6.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Version](https://img.shields.io/badge/version-3.7.0-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Trending](https://img.shields.io/badge/🔥_TypeScript_Trending-Daily%20%7C%20Weekly%20%7C%20Monthly-ff6b6b.svg)](https://github.com/trending/typescript) [![Trending](https://img.shields.io/badge/🔥_TypeScript_Trending-Daily%20%7C%20Weekly%20%7C%20Monthly-ff6b6b.svg)](https://github.com/trending/typescript)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
[![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/)
@@ -43,7 +43,7 @@ Get 10% OFF the GLM CODING PLAN with [this link](https://z.ai/subscribe?ic=8JVLJ
## Features ## Features
### Current Version: v3.6.2 | [Full Changelog](CHANGELOG.md) ### Current Version: v3.7.0 | [Full Changelog](CHANGELOG.md)
**Core Capabilities** **Core Capabilities**
@@ -103,6 +103,14 @@ Download `CC-Switch-v{version}-macOS.zip` from the [Releases](../../releases) pa
> **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it first, then go to "System Settings" → "Privacy & Security" → click "Open Anyway", and you'll be able to open it normally afterwards. > **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it first, then go to "System Settings" → "Privacy & Security" → click "Open Anyway", and you'll be able to open it normally afterwards.
### ArchLinux 用户
**Install via paru (Recommended)**
```bash
paru -S cc-switch-bin
```
### Linux Users ### Linux Users
Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{version}-Linux.AppImage` from the [Releases](../../releases) page. Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{version}-Linux.AppImage` from the [Releases](../../releases) page.

View File

@@ -1,8 +1,8 @@
<div align="center"> <div align="center">
# Claude Code & Codex 供应商管理器 # Claude Code / Codex / Gemini CLI 全方位辅助工具
[![Version](https://img.shields.io/badge/version-3.6.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Version](https://img.shields.io/badge/version-3.7.0-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Trending](https://img.shields.io/badge/🔥_TypeScript_Trending-Daily%20%7C%20Weekly%20%7C%20Monthly-ff6b6b.svg)](https://github.com/trending/typescript) [![Trending](https://img.shields.io/badge/🔥_TypeScript_Trending-Daily%20%7C%20Weekly%20%7C%20Monthly-ff6b6b.svg)](https://github.com/trending/typescript)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
[![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/)
@@ -43,7 +43,7 @@ CC Switch 已经预设了智谱GLM只需要填写 key 即可一键导入编
## 功能特性 ## 功能特性
### 当前版本v3.6.2 | [完整更新日志](CHANGELOG.md) ### 当前版本v3.7.0 | [完整更新日志](CHANGELOG.md)
**核心功能** **核心功能**
@@ -103,6 +103,14 @@ brew upgrade --cask cc-switch
> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开 > **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开
### ArchLinux 用户
**通过 paru 安装(推荐)**
```bash
paru -S cc-switch-bin
```
### Linux 用户 ### Linux 用户
从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Linux.deb` 包或者 `CC-Switch-v{版本号}-Linux.AppImage` 安装包。 从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Linux.deb` 包或者 `CC-Switch-v{版本号}-Linux.AppImage` 安装包。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -0,0 +1,439 @@
# CC Switch v3.7.0
> From Provider Switcher to All-in-One AI CLI Management Platform
**[中文更新说明 Chinese Documentation →](release-note-v3.7.0-zh.md)**
---
## Overview
CC Switch v3.7.0 introduces six major features with over 18,000 lines of new code.
**Release Date**: 2025-11-19
**Commits**: 85 from v3.6.0
**Code Changes**: 152 files, +18,104 / -3,732 lines
---
## New Features
### Gemini CLI Integration
Complete support for Google Gemini CLI, becoming the third supported application (Claude Code, Codex, Gemini).
**Core Capabilities**:
- **Dual-file configuration** - Support for both `.env` and `settings.json` formats
- **Auto-detection** - Automatically detect `GOOGLE_GEMINI_BASE_URL`, `GEMINI_MODEL`, etc.
- **Full MCP support** - Complete MCP server management for Gemini
- **Deep link integration** - Import via `ccswitch://` protocol
- **System tray** - Quick-switch from tray menu
**Provider Presets**:
- **Google Official** - OAuth authentication support
- **PackyCode** - Partner integration
- **Custom** - Full customization support
**Technical Implementation**:
- New backend modules: `gemini_config.rs` (20KB), `gemini_mcp.rs`
- Form synchronization with environment editor
- Dual-file atomic writes
---
### MCP v3.7.0 Unified Architecture
Complete refactoring of MCP management system for cross-application unification.
**Architecture Improvements**:
- **Unified panel** - Single interface for Claude/Codex/Gemini MCP servers
- **SSE transport** - New Server-Sent Events support
- **Smart parser** - Fault-tolerant JSON parsing
- **Format correction** - Auto-fix Codex `[mcp_servers]` format
- **Extended fields** - Preserve custom TOML fields
**User Experience**:
- Default app selection in forms
- JSON formatter for validation
- Improved visual hierarchy
- Better error messages
**Import/Export**:
- Unified import from all three apps
- Bidirectional synchronization
- State preservation
---
### Claude Skills Management System
**Approximately 2,000 lines of code** - A complete skill ecosystem platform.
**GitHub Integration**:
- Auto-scan skills from GitHub repositories
- Pre-configured repos:
- `ComposioHQ/awesome-claude-skills` - Curated collection
- `anthropics/skills` - Official Anthropic skills
- `cexll/myclaude` - Community contributions
- Add custom repositories
- Subdirectory scanning support (`skillsPath`)
**Lifecycle Management**:
- **Discover** - Auto-detect `SKILL.md` files
- **Install** - One-click to `~/.claude/skills/`
- **Uninstall** - Safe removal with tracking
- **Update** - Check for updates (infrastructure ready)
**Technical Architecture**:
- **Backend**: `SkillService` (526 lines) with GitHub API integration
- **Frontend**: SkillsPage, SkillCard, RepoManager
- **UI Components**: Badge, Card, Table (shadcn/ui)
- **State**: Persistent storage in `skills.json`
- **i18n**: 47+ translation keys
---
### Prompts Management System
**Approximately 1,300 lines of code** - Complete system prompt management.
**Multi-Preset Management**:
- Create unlimited prompt presets
- Quick switch between presets
- One active prompt at a time
- Delete protection for active prompts
**Cross-App Support**:
- **Claude**: `~/.claude/CLAUDE.md`
- **Codex**: `~/.codex/AGENTS.md`
- **Gemini**: `~/.gemini/GEMINI.md`
**Markdown Editor**:
- Full-featured CodeMirror 6 integration
- Syntax highlighting
- Dark theme (One Dark)
- Real-time preview
**Smart Synchronization**:
- **Auto-write** - Immediately write to live files
- **Backfill protection** - Save current content before switching
- **Auto-import** - Import from live files on first launch
- **Modification protection** - Preserve manual modifications
**Technical Implementation**:
- **Backend**: `PromptService` (213 lines)
- **Frontend**: PromptPanel (177), PromptFormModal (160), MarkdownEditor (159)
- **Hooks**: usePromptActions (152 lines)
- **i18n**: 41+ translation keys
---
### Deep Link Protocol (ccswitch://)
One-click provider configuration import via URL scheme.
**Features**:
- Protocol registration on all platforms
- Import from shared links
- Lifecycle integration
- Security validation
---
### Environment Variable Conflict Detection
Intelligent detection and management of configuration conflicts.
**Detection Scope**:
- **Claude & Codex** - Cross-app conflicts
- **Gemini** - Auto-discovery
- **MCP** - Server configuration conflicts
**Management Features**:
- Visual conflict indicators
- Resolution suggestions
- Override warnings
- Backup before changes
---
## Improvements
### Provider Management
**New Presets**:
- **DouBaoSeed** - ByteDance's DouBao
- **Kimi For Coding** - Moonshot AI
- **BaiLing** - BaiLing AI
- **Removed AnyRouter** - To avoid confusion
**Enhancements**:
- Model name configuration for Codex and Gemini
- Provider notes field for organization
- Enhanced preset metadata
### Configuration Management
- **Common config migration** - From localStorage to `config.json`
- **Unified persistence** - Shared across all apps
- **Auto-import** - First launch configuration import
- **Backfill priority** - Correct handling of live files
### UI/UX Improvements
**Design System**:
- **macOS native** - System-aligned color scheme
- **Window centering** - Default centered position
- **Visual polish** - Improved spacing and hierarchy
**Interactions**:
- **Password input** - Fixed Edge/IE reveal buttons
- **URL overflow** - Fixed card overflow
- **Error copying** - Copy-to-clipboard errors
- **Tray sync** - Real-time drag-and-drop sync
---
## Bug Fixes
### Critical Fixes
- **Usage script validation** - Boundary checks
- **Gemini validation** - Relaxed constraints
- **TOML parsing** - CJK quote handling
- **MCP fields** - Custom field preservation
- **White screen** - FormLabel crash fix
### Stability
- **Tray safety** - Pattern matching instead of unwrap
- **Error isolation** - Tray failures don't block operations
- **Import classification** - Correct category assignment
### UI Fixes
- **Model placeholders** - Removed misleading hints
- **Base URL** - Auto-fill for third-party providers
- **Drag sort** - Tray menu synchronization
---
## Technical Improvements
### Architecture
**MCP v3.7.0**:
- Removed legacy code (~1,000 lines)
- Unified initialization structure
- Backward compatibility maintained
- Comprehensive code formatting
**Platform Compatibility**:
- Windows winreg API fix (v0.52)
- Safe pattern matching (no `unwrap()`)
- Cross-platform tray handling
### Configuration
**Synchronization**:
- MCP sync across all apps
- Gemini form-editor sync
- Dual-file reading (.env + settings.json)
**Validation**:
- Input boundary checks
- TOML quote normalization (CJK)
- Custom field preservation
- Enhanced error messages
### Code Quality
**Type Safety**:
- Complete TypeScript coverage
- Rust type refinements
- API contract validation
**Testing**:
- Simplified assertions
- Better test coverage
- Integration test updates
**Dependencies**:
- Tauri 2.8.x
- Rust: `anyhow`, `zip`, `serde_yaml`, `tempfile`
- Frontend: CodeMirror 6 packages
- winreg 0.52 (Windows)
---
## Technical Statistics
```
Total Changes:
- Commits: 85
- Files: 152 changed
- Additions: +18,104 lines
- Deletions: -3,732 lines
New Modules:
- Skills Management: 2,034 lines (21 files)
- Prompts Management: 1,302 lines (20 files)
- Gemini Integration: ~1,000 lines
- MCP Refactor: ~3,000 lines refactored
Code Distribution:
- Backend (Rust): ~4,500 lines new
- Frontend (React): ~3,000 lines new
- Configuration: ~1,500 lines refactored
- Tests: ~500 lines
```
---
## Strategic Positioning
### From Tool to Platform
v3.7.0 represents a shift in CC Switch's positioning:
| Aspect | v3.6 | v3.7.0 |
| ----------------- | ------------------------ | ---------------------------- |
| **Identity** | Provider Switcher | AI CLI Management Platform |
| **Scope** | Configuration Management | Ecosystem Management |
| **Applications** | Claude + Codex | Claude + Codex + Gemini |
| **Capabilities** | Switch configs | Extend capabilities (Skills) |
| **Customization** | Manual editing | Visual management (Prompts) |
| **Integration** | Isolated apps | Unified management (MCP) |
### Six Pillars of AI CLI Management
1. **Configuration Management** - Provider switching and management
2. **Capability Extension** - Skills installation and lifecycle
3. **Behavior Customization** - System prompt presets
4. **Ecosystem Integration** - Deep links and sharing
5. **Multi-AI Support** - Claude/Codex/Gemini
6. **Intelligent Detection** - Conflict prevention
---
## Download & Installation
### System Requirements
- **Windows**: Windows 10+
- **macOS**: macOS 10.15 (Catalina)+
- **Linux**: Ubuntu 22.04+ / Debian 11+ / Fedora 34+
### Download Links
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download:
- **Windows**: `CC-Switch-v3.7.0-Windows.msi` or `-Portable.zip`
- **macOS**: `CC-Switch-v3.7.0-macOS.tar.gz` or `.zip`
- **Linux**: `CC-Switch-v3.7.0-Linux.AppImage` or `.deb`
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
Update:
```bash
brew upgrade --cask cc-switch
```
---
## Migration Notes
### From v3.6.x
**Automatic migration** - No action required, configs are fully compatible
### From v3.1.x or Earlier
**Two-step migration required**:
1. First upgrade to v3.2.x (performs one-time migration)
2. Then upgrade to v3.7.0
### New Features
- **Skills**: No migration needed, start fresh
- **Prompts**: Auto-import from live files on first launch
- **Gemini**: Install Gemini CLI separately if needed
- **MCP v3.7.0**: Backward compatible with previous configs
---
## Acknowledgments
### Contributors
Thanks to all contributors who made this release possible:
- [@YoVinchen](https://github.com/YoVinchen) - Skills & Prompts & Gemini integration implementation
- [@farion1231](https://github.com/farion1231) - From developer to issue responder
- Community members for testing and feedback
### Sponsors
**Z.ai** - GLM CODING PLAN sponsor
[Get 10% OFF with this link](https://z.ai/subscribe?ic=8JVLJQFSKB)
**PackyCode** - API relay service partner
[Register with "cc-switch" code for 10% discount](https://www.packyapi.com/register?aff=cc-switch)
---
## Feedback & Support
- **Issues**: [GitHub Issues](https://github.com/farion1231/cc-switch/issues)
- **Discussions**: [GitHub Discussions](https://github.com/farion1231/cc-switch/discussions)
- **Documentation**: [README](../README.md)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
---
## What's Next
**v3.8.0 Preview** (Tentative):
- Local proxy functionality
Stay tuned for more updates!
---
**Happy Coding!**

View File

@@ -0,0 +1,435 @@
# CC Switch v3.7.0
> 从供应商切换器到 AI CLI 一体化管理平台
**[English Version →](release-note-v3.7.0-en.md)**
---
## 概览
CC Switch v3.7.0 新增六大核心功能,新增超过 18,000 行代码。
**发布日期**2025-11-19
**提交数量**:从 v3.6.0 开始 85 个提交
**代码变更**152 个文件,+18,104 / -3,732 行
---
## 新增功能
### Gemini CLI 集成
完整支持 Google Gemini CLI成为第三个支持的应用Claude Code、Codex、Gemini
**核心能力**
- **双文件配置** - 同时支持 `.env``settings.json` 格式
- **自动检测** - 自动检测 `GOOGLE_GEMINI_BASE_URL``GEMINI_MODEL` 等环境变量
- **完整 MCP 支持** - 为 Gemini 提供完整的 MCP 服务器管理
- **深度链接集成** - 通过 `ccswitch://` 协议导入配置
- **系统托盘** - 从托盘菜单快速切换
**供应商预设**
- **Google Official** - 支持 OAuth 认证
- **PackyCode** - 合作伙伴集成
- **自定义** - 完全自定义支持
**技术实现**
- 新增后端模块:`gemini_config.rs`20KB`gemini_mcp.rs`
- 表单与环境编辑器同步
- 双文件原子写入
---
### MCP v3.7.0 统一架构
MCP 管理系统完整重构,实现跨应用统一管理。
**架构改进**
- **统一管理面板** - 单一界面管理 Claude/Codex/Gemini MCP 服务器
- **SSE 传输类型** - 新增 Server-Sent Events 支持
- **智能解析器** - 容错性 JSON 解析
- **格式修正** - 自动修复 Codex `[mcp_servers]` 格式
- **扩展字段** - 保留自定义 TOML 字段
**用户体验**
- 表单中的默认应用选择
- JSON 格式化器用于验证
- 改进的视觉层次
- 更好的错误消息
**导入/导出**
- 统一从三个应用导入
- 双向同步
- 状态保持
---
### Claude Skills 管理系统
**约 2,000 行代码** - 完整的技能生态平台。
**GitHub 集成**
- 从 GitHub 仓库自动扫描技能
- 预配置仓库:
- `ComposioHQ/awesome-claude-skills` - 精选集合
- `anthropics/skills` - Anthropic 官方技能
- `cexll/myclaude` - 社区贡献
- 添加自定义仓库
- 子目录扫描支持(`skillsPath`
**生命周期管理**
- **发现** - 自动检测 `SKILL.md` 文件
- **安装** - 一键安装到 `~/.claude/skills/`
- **卸载** - 安全移除并跟踪状态
- **更新** - 检查更新(基础设施已就绪)
**技术架构**
- **后端**`SkillService`526 行)集成 GitHub API
- **前端**SkillsPage、SkillCard、RepoManager
- **UI 组件**Badge、Card、Tableshadcn/ui
- **状态**:持久化存储在 `skills.json`
- **国际化**47+ 个翻译键
---
### Prompts 管理系统
**约 1,300 行代码** - 完整的系统提示词管理。
**多预设管理**
- 创建无限数量的提示词预设
- 快速在预设间切换
- 同时只能激活一个提示词
- 活动提示词删除保护
**跨应用支持**
- **Claude**`~/.claude/CLAUDE.md`
- **Codex**`~/.codex/AGENTS.md`
- **Gemini**`~/.gemini/GEMINI.md`
**Markdown 编辑器**
- 完整的 CodeMirror 6 集成
- 语法高亮
- 暗色主题One Dark
- 实时预览
**智能同步**
- **自动写入** - 立即写入 live 文件
- **回填保护** - 切换前保存当前内容
- **自动导入** - 首次启动从 live 文件导入
- **修改保护** - 保留手动修改
**技术实现**
- **后端**`PromptService`213 行)
- **前端**PromptPanel177、PromptFormModal160、MarkdownEditor159
- **Hooks**usePromptActions152 行)
- **国际化**41+ 个翻译键
---
### 深度链接协议ccswitch://
通过 URL 方案一键导入供应商配置。
**功能特性**
- 所有平台的协议注册
- 从共享链接导入
- 生命周期集成
- 安全验证
---
### 环境变量冲突检测
智能检测和管理配置冲突。
**检测范围**
- **Claude & Codex** - 跨应用冲突
- **Gemini** - 自动发现
- **MCP** - 服务器配置冲突
**管理功能**
- 可视化冲突指示器
- 解决建议
- 覆盖警告
- 更改前备份
---
## 改进优化
### 供应商管理
**新增预设**
- **DouBaoSeed** - 字节跳动的豆包
- **Kimi For Coding** - 月之暗面
- **BaiLing** - 百灵 AI
- **移除 AnyRouter** - 避免误导
**增强功能**
- Codex 和 Gemini 的模型名称配置
- 供应商备注字段用于组织
- 增强的预设元数据
### 配置管理
- **通用配置迁移** - 从 localStorage 迁移到 `config.json`
- **统一持久化** - 跨所有应用共享
- **自动导入** - 首次启动配置导入
- **回填优先级** - 正确处理 live 文件
### UI/UX 改进
**设计系统**
- **macOS 原生** - 与系统对齐的配色方案
- **窗口居中** - 默认居中位置
- **视觉优化** - 改进的间距和层次
**交互优化**
- **密码输入** - 修复 Edge/IE 显示按钮
- **URL 溢出** - 修复卡片溢出
- **错误复制** - 可复制到剪贴板的错误
- **托盘同步** - 实时拖放同步
---
## Bug 修复
### 关键修复
- **用量脚本验证** - 边界检查
- **Gemini 验证** - 放宽约束
- **TOML 解析** - CJK 引号处理
- **MCP 字段** - 自定义字段保留
- **白屏** - FormLabel 崩溃修复
### 稳定性
- **托盘安全** - 模式匹配替代 unwrap
- **错误隔离** - 托盘失败不阻塞操作
- **导入分类** - 正确的类别分配
### UI 修复
- **模型占位符** - 移除误导性提示
- **Base URL** - 第三方供应商自动填充
- **拖拽排序** - 托盘菜单同步
---
## 技术改进
### 架构
**MCP v3.7.0**
- 移除遗留代码(约 1,000 行)
- 统一初始化结构
- 保持向后兼容性
- 全面的代码格式化
**平台兼容性**
- Windows winreg API 修复v0.52
- 安全模式匹配(无 `unwrap()`
- 跨平台托盘处理
### 配置
**同步机制**
- 跨所有应用的 MCP 同步
- Gemini 表单-编辑器同步
- 双文件读取(.env + settings.json
**验证增强**
- 输入边界检查
- TOML 引号规范化CJK
- 自定义字段保留
- 增强的错误消息
### 代码质量
**类型安全**
- 完整的 TypeScript 覆盖
- Rust 类型改进
- API 契约验证
**测试**
- 简化的断言
- 更好的测试覆盖
- 集成测试更新
**依赖项**
- Tauri 2.8.x
- Rust`anyhow``zip``serde_yaml``tempfile`
- 前端CodeMirror 6 包
- winreg 0.52Windows
---
## 技术统计
```
总体变更:
- 提交数85
- 文件数152 个文件变更
- 新增:+18,104 行
- 删除:-3,732 行
新增模块:
- Skills 管理2,034 行21 个文件)
- Prompts 管理1,302 行20 个文件)
- Gemini 集成:约 1,000 行
- MCP 重构:约 3,000 行重构
代码分布:
- 后端Rust约 4,500 行新增
- 前端React约 3,000 行新增
- 配置:约 1,500 行重构
- 测试:约 500 行
```
---
## 战略定位
### 从工具到平台
v3.7.0 代表了 CC Switch 定位的转变:
| 方面 | v3.6 | v3.7.0 |
| -------- | -------------- | ----------------------- |
| **身份** | 供应商切换器 | AI CLI 管理平台 |
| **范围** | 配置管理 | 生态系统管理 |
| **应用** | Claude + Codex | Claude + Codex + Gemini |
| **能力** | 切换配置 | 扩展能力Skills |
| **定制** | 手动编辑 | 可视化管理Prompts |
| **集成** | 孤立应用 | 统一管理MCP |
### AI CLI 管理六大支柱
1. **配置管理** - 供应商切换和管理
2. **能力扩展** - Skills 安装和生命周期
3. **行为定制** - 系统提示词预设
4. **生态集成** - 深度链接和共享
5. **多 AI 支持** - Claude/Codex/Gemini
6. **智能检测** - 冲突预防
---
## 下载与安装
### 系统要求
- **Windows**Windows 10+
- **macOS**macOS 10.15Catalina+
- **Linux**Ubuntu 22.04+ / Debian 11+ / Fedora 34+
### 下载链接
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载:
- **Windows**`CC-Switch-v3.7.0-Windows.msi``-Portable.zip`
- **macOS**`CC-Switch-v3.7.0-macOS.tar.gz``.zip`
- **Linux**`CC-Switch-v3.7.0-Linux.AppImage``.deb`
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
---
## 迁移说明
### 从 v3.6.x 升级
**自动迁移** - 无需任何操作,配置完全兼容
### 从 v3.1.x 或更早版本升级
**需要两步迁移**
1. 首先升级到 v3.2.x执行一次性迁移
2. 然后升级到 v3.7.0
### 新功能
- **Skills**:无需迁移,全新开始
- **Prompts**:首次启动时从 live 文件自动导入
- **Gemini**:需要单独安装 Gemini CLI
- **MCP v3.7.0**:与之前的配置向后兼容
---
## 致谢
### 贡献者
感谢所有让这个版本成为可能的贡献者:
- [@YoVinchen](https://github.com/YoVinchen) - Skills & Prompts & Geimini 集成实现
- [@farion1231](https://github.com/farion1231) - 从开发沦为 issue 回复机
- 社区成员的测试和反馈
### 赞助商
**Z.ai** - GLM CODING PLAN 赞助商
[通过此链接获得 10% 折扣](https://z.ai/subscribe?ic=8JVLJQFSKB)
**PackyCode** - API 中继服务合作伙伴
[使用 "cc-switch" 代码注册可享受 10% 折扣](https://www.packyapi.com/register?aff=cc-switch)
---
## 反馈与支持
- **问题反馈**[GitHub Issues](https://github.com/farion1231/cc-switch/issues)
- **讨论**[GitHub Discussions](https://github.com/farion1231/cc-switch/discussions)
- **文档**[README](../README_ZH.md)
- **更新日志**[CHANGELOG.md](../CHANGELOG.md)
---
## 未来展望
**v3.8.0 预览**(暂定):
- 本地代理功能
敬请期待更多更新!

View File

@@ -1,7 +1,7 @@
{ {
"name": "cc-switch", "name": "cc-switch",
"version": "3.6.2", "version": "3.7.0",
"description": "Claude Code & Codex 供应商切换工具", "description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"scripts": { "scripts": {
"dev": "pnpm tauri dev", "dev": "pnpm tauri dev",
"build": "pnpm tauri build", "build": "pnpm tauri build",

2
src-tauri/Cargo.lock generated
View File

@@ -595,7 +595,7 @@ dependencies = [
[[package]] [[package]]
name = "cc-switch" name = "cc-switch"
version = "3.6.2" version = "3.7.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "cc-switch" name = "cc-switch"
version = "3.6.2" version = "3.7.0"
description = "Claude Code & Codex 供应商配置管理工具" description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"] authors = ["Jason Young"]
license = "MIT" license = "MIT"
repository = "https://github.com/farion1231/cc-switch" repository = "https://github.com/farion1231/cc-switch"

View File

@@ -62,7 +62,7 @@ pub async fn install_skill(
.clone() .clone()
.unwrap_or_else(|| "main".to_string()), .unwrap_or_else(|| "main".to_string()),
enabled: true, enabled: true,
skills_path: None, // 安装时使用默认路径 skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path
}; };
service service

View File

@@ -236,6 +236,17 @@ pub fn validate_gemini_settings(settings: &Value) -> Result<(), AppError> {
} }
} }
// 如果有 config 字段,验证它是对象或 null
if let Some(config) = settings.get("config") {
if !(config.is_object() || config.is_null()) {
return Err(AppError::localized(
"gemini.validation.invalid_config",
"Gemini 配置格式错误: config 必须是对象",
"Gemini config invalid: config must be an object",
));
}
}
Ok(()) Ok(())
} }
@@ -244,6 +255,9 @@ pub fn validate_gemini_settings(settings: &Value) -> Result<(), AppError> {
/// 此函数在切换供应商时使用,确保配置包含所有必需的字段。 /// 此函数在切换供应商时使用,确保配置包含所有必需的字段。
/// 对于需要 API Key 的供应商(如 PackyCode会验证 GEMINI_API_KEY 字段。 /// 对于需要 API Key 的供应商(如 PackyCode会验证 GEMINI_API_KEY 字段。
pub fn validate_gemini_settings_strict(settings: &Value) -> Result<(), AppError> { pub fn validate_gemini_settings_strict(settings: &Value) -> Result<(), AppError> {
// 先做基础格式验证(包含 env/config 类型)
validate_gemini_settings(settings)?;
let env_map = json_to_env(settings)?; let env_map = json_to_env(settings)?;
// 如果 env 为空,表示使用 OAuth如 Google 官方),跳过验证 // 如果 env 为空,表示使用 OAuth如 Google 官方),跳过验证
@@ -368,7 +382,7 @@ mod tests {
# Comment line # Comment line
GOOGLE_GEMINI_BASE_URL=https://example.com GOOGLE_GEMINI_BASE_URL=https://example.com
GEMINI_API_KEY=sk-test123 GEMINI_API_KEY=sk-test123
GEMINI_MODEL=gemini-2.5-pro GEMINI_MODEL=gemini-3-pro-preview
# Another comment # Another comment
"#; "#;
@@ -381,19 +395,25 @@ GEMINI_MODEL=gemini-2.5-pro
Some(&"https://example.com".to_string()) Some(&"https://example.com".to_string())
); );
assert_eq!(map.get("GEMINI_API_KEY"), Some(&"sk-test123".to_string())); assert_eq!(map.get("GEMINI_API_KEY"), Some(&"sk-test123".to_string()));
assert_eq!(map.get("GEMINI_MODEL"), Some(&"gemini-2.5-pro".to_string())); assert_eq!(
map.get("GEMINI_MODEL"),
Some(&"gemini-3-pro-preview".to_string())
);
} }
#[test] #[test]
fn test_serialize_env_file() { fn test_serialize_env_file() {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("GEMINI_API_KEY".to_string(), "sk-test".to_string()); map.insert("GEMINI_API_KEY".to_string(), "sk-test".to_string());
map.insert("GEMINI_MODEL".to_string(), "gemini-2.5-pro".to_string()); map.insert(
"GEMINI_MODEL".to_string(),
"gemini-3-pro-preview".to_string(),
);
let content = serialize_env_file(&map); let content = serialize_env_file(&map);
assert!(content.contains("GEMINI_API_KEY=sk-test")); assert!(content.contains("GEMINI_API_KEY=sk-test"));
assert!(content.contains("GEMINI_MODEL=gemini-2.5-pro")); assert!(content.contains("GEMINI_MODEL=gemini-3-pro-preview"));
} }
#[test] #[test]
@@ -417,7 +437,7 @@ GEMINI_MODEL=gemini-2.5-pro
# Comment line # Comment line
GOOGLE_GEMINI_BASE_URL=https://example.com GOOGLE_GEMINI_BASE_URL=https://example.com
GEMINI_API_KEY=sk-test123 GEMINI_API_KEY=sk-test123
GEMINI_MODEL=gemini-2.5-pro GEMINI_MODEL=gemini-3-pro-preview
# Another comment # Another comment
"#; "#;
@@ -432,7 +452,10 @@ GEMINI_MODEL=gemini-2.5-pro
Some(&"https://example.com".to_string()) Some(&"https://example.com".to_string())
); );
assert_eq!(map.get("GEMINI_API_KEY"), Some(&"sk-test123".to_string())); assert_eq!(map.get("GEMINI_API_KEY"), Some(&"sk-test123".to_string()));
assert_eq!(map.get("GEMINI_MODEL"), Some(&"gemini-2.5-pro".to_string())); assert_eq!(
map.get("GEMINI_MODEL"),
Some(&"gemini-3-pro-preview".to_string())
);
} }
#[test] #[test]
@@ -598,7 +621,7 @@ KEY_WITH-DASH=value";
let settings = serde_json::json!({ let settings = serde_json::json!({
"env": { "env": {
"GEMINI_API_KEY": "sk-test123", "GEMINI_API_KEY": "sk-test123",
"GEMINI_MODEL": "gemini-2.5-pro" "GEMINI_MODEL": "gemini-3-pro-preview"
} }
}); });
@@ -611,7 +634,7 @@ KEY_WITH-DASH=value";
// 测试缺少 API Key 的非空配置在基本验证中可以通过(用户稍后填写) // 测试缺少 API Key 的非空配置在基本验证中可以通过(用户稍后填写)
let settings = serde_json::json!({ let settings = serde_json::json!({
"env": { "env": {
"GEMINI_MODEL": "gemini-2.5-pro" "GEMINI_MODEL": "gemini-3-pro-preview"
} }
}); });

View File

@@ -229,43 +229,23 @@ impl ConfigService {
provider_id: &str, provider_id: &str,
provider: &Provider, provider: &Provider,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
use crate::gemini_config::{ use crate::gemini_config::{env_to_json, read_gemini_env};
env_to_json, json_to_env, read_gemini_env, write_gemini_env_atomic,
ProviderService::write_gemini_live(provider)?;
// 读回实际写入的内容并更新到配置中(包含 settings.json
let live_after_env = read_gemini_env()?;
let settings_path = crate::gemini_config::get_gemini_settings_path();
let live_after_config = if settings_path.exists() {
crate::config::read_json_file(&settings_path)?
} else {
serde_json::json!({})
}; };
let mut live_after = env_to_json(&live_after_env);
let env_path = crate::gemini_config::get_gemini_env_path(); if let Some(obj) = live_after.as_object_mut() {
if let Some(parent) = env_path.parent() { obj.insert("config".to_string(), live_after_config);
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
} }
// 转换 JSON 配置为 .env 格式
let env_map = json_to_env(&provider.settings_config)?;
// Google 官方OAuth: env 为空,写入空文件并设置安全标志后返回
if env_map.is_empty() {
write_gemini_env_atomic(&env_map)?;
ProviderService::ensure_google_oauth_security_flag(provider)?;
let live_after_env = read_gemini_env()?;
let live_after = env_to_json(&live_after_env);
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(target) = manager.providers.get_mut(provider_id) {
target.settings_config = live_after;
}
}
return Ok(());
}
// 非 OAuth按常规写入并在必要时设置 Packycode 安全标志
write_gemini_env_atomic(&env_map)?;
ProviderService::ensure_packycode_security_flag(provider)?;
// 读回实际写入的内容并更新到配置中
let live_after_env = read_gemini_env()?;
let live_after = env_to_json(&live_after_env);
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) { if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(target) = manager.providers.get_mut(provider_id) { if let Some(target) = manager.providers.get_mut(provider_id) {
target.settings_config = live_after; target.settings_config = live_after;

View File

@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(not(target_os = "windows"))]
use std::fs; use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -35,6 +36,7 @@ fn get_keywords_for_app(app: &str) -> Vec<&str> {
match app.to_lowercase().as_str() { match app.to_lowercase().as_str() {
"claude" => vec!["ANTHROPIC"], "claude" => vec!["ANTHROPIC"],
"codex" => vec!["OPENAI"], "codex" => vec!["OPENAI"],
"gemini" => vec!["GEMINI", "GOOGLE_GEMINI"],
_ => vec![], _ => vec![],
} }
} }
@@ -48,17 +50,15 @@ fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") { if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
for (name, value) in hkcu.enum_values().filter_map(Result::ok) { for (name, value) in hkcu.enum_values().filter_map(Result::ok) {
if keywords.iter().any(|k| name.to_uppercase().contains(k)) { if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
if let Ok(val) = value.to_string() {
conflicts.push(EnvConflict { conflicts.push(EnvConflict {
var_name: name.clone(), var_name: name.clone(),
var_value: val, var_value: value.to_string(),
source_type: "system".to_string(), source_type: "system".to_string(),
source_path: "HKEY_CURRENT_USER\\Environment".to_string(), source_path: "HKEY_CURRENT_USER\\Environment".to_string(),
}); });
} }
} }
} }
}
// Check HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment // Check HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE) if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE)
@@ -66,17 +66,15 @@ fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
{ {
for (name, value) in hklm.enum_values().filter_map(Result::ok) { for (name, value) in hklm.enum_values().filter_map(Result::ok) {
if keywords.iter().any(|k| name.to_uppercase().contains(k)) { if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
if let Ok(val) = value.to_string() {
conflicts.push(EnvConflict { conflicts.push(EnvConflict {
var_name: name.clone(), var_name: name.clone(),
var_value: val, var_value: value.to_string(),
source_type: "system".to_string(), source_type: "system".to_string(),
source_path: "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment".to_string(), source_path: "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment".to_string(),
}); });
} }
} }
} }
}
Ok(conflicts) Ok(conflicts)
} }
@@ -161,6 +159,10 @@ mod tests {
fn test_get_keywords() { fn test_get_keywords() {
assert_eq!(get_keywords_for_app("claude"), vec!["ANTHROPIC"]); assert_eq!(get_keywords_for_app("claude"), vec!["ANTHROPIC"]);
assert_eq!(get_keywords_for_app("codex"), vec!["OPENAI"]); assert_eq!(get_keywords_for_app("codex"), vec!["OPENAI"]);
assert_eq!(
get_keywords_for_app("gemini"),
vec!["GEMINI", "GOOGLE_GEMINI"]
);
assert_eq!(get_keywords_for_app("unknown"), Vec::<&str>::new()); assert_eq!(get_keywords_for_app("unknown"), Vec::<&str>::new());
} }
} }

View File

@@ -30,6 +30,7 @@ enum LiveSnapshot {
}, },
Gemini { Gemini {
env: Option<HashMap<String, String>>, // 新增 env: Option<HashMap<String, String>>, // 新增
config: Option<Value>, // 新增settings.json 内容
}, },
} }
@@ -68,15 +69,30 @@ impl LiveSnapshot {
delete_file(&config_path)?; delete_file(&config_path)?;
} }
} }
LiveSnapshot::Gemini { env } => { LiveSnapshot::Gemini { env, .. } => {
// 新增 // 新增
use crate::gemini_config::{get_gemini_env_path, write_gemini_env_atomic}; use crate::gemini_config::{
get_gemini_env_path, get_gemini_settings_path, write_gemini_env_atomic,
};
let path = get_gemini_env_path(); let path = get_gemini_env_path();
if let Some(env_map) = env { if let Some(env_map) = env {
write_gemini_env_atomic(env_map)?; write_gemini_env_atomic(env_map)?;
} else if path.exists() { } else if path.exists() {
delete_file(&path)?; delete_file(&path)?;
} }
let settings_path = get_gemini_settings_path();
match self {
LiveSnapshot::Gemini {
config: Some(cfg), ..
} => {
write_json_file(&settings_path, cfg)?;
}
LiveSnapshot::Gemini { config: None, .. } if settings_path.exists() => {
delete_file(&settings_path)?;
}
_ => {}
}
} }
} }
Ok(()) Ok(())
@@ -612,7 +628,9 @@ impl ProviderService {
state.save()?; state.save()?;
} }
AppType::Gemini => { AppType::Gemini => {
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; use crate::gemini_config::{
env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let env_path = get_gemini_env_path(); let env_path = get_gemini_env_path();
if !env_path.exists() { if !env_path.exists() {
@@ -623,7 +641,18 @@ impl ProviderService {
)); ));
} }
let env_map = read_gemini_env()?; let env_map = read_gemini_env()?;
let live_after = env_to_json(&env_map); let mut live_after = env_to_json(&env_map);
let settings_path = get_gemini_settings_path();
let config_value = if settings_path.exists() {
read_json_file(&settings_path)?
} else {
json!({})
};
if let Some(obj) = live_after.as_object_mut() {
obj.insert("config".to_string(), config_value);
}
{ {
let mut guard = state.config.write().map_err(AppError::from)?; let mut guard = state.config.write().map_err(AppError::from)?;
@@ -670,14 +699,22 @@ impl ProviderService {
} }
AppType::Gemini => { AppType::Gemini => {
// 新增 // 新增
use crate::gemini_config::{get_gemini_env_path, read_gemini_env}; use crate::gemini_config::{
get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let path = get_gemini_env_path(); let path = get_gemini_env_path();
let env = if path.exists() { let env = if path.exists() {
Some(read_gemini_env()?) Some(read_gemini_env()?)
} else { } else {
None None
}; };
Ok(LiveSnapshot::Gemini { env }) let settings_path = get_gemini_settings_path();
let config = if settings_path.exists() {
Some(read_json_file(&settings_path)?)
} else {
None
};
Ok(LiveSnapshot::Gemini { env, config })
} }
} }
} }
@@ -847,19 +884,37 @@ impl ProviderService {
v v
} }
AppType::Gemini => { AppType::Gemini => {
// 新增 use crate::gemini_config::{
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let path = get_gemini_env_path(); // 读取 .env 文件(环境变量)
if !path.exists() { let env_path = get_gemini_env_path();
if !env_path.exists() {
return Err(AppError::localized( return Err(AppError::localized(
"gemini.live.missing", "gemini.live.missing",
"Gemini 配置文件不存在", "Gemini 配置文件不存在",
"Gemini configuration file is missing", "Gemini configuration file is missing",
)); ));
} }
let env_map = read_gemini_env()?; let env_map = read_gemini_env()?;
env_to_json(&env_map) let env_json = env_to_json(&env_map);
let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
// 读取 settings.json 文件MCP 配置等)
let settings_path = get_gemini_settings_path();
let config_obj = if settings_path.exists() {
read_json_file(&settings_path)?
} else {
json!({})
};
// 返回完整结构:{ "env": {...}, "config": {...} }
json!({
"env": env_obj,
"config": config_obj
})
} }
}; };
@@ -914,11 +969,13 @@ impl ProviderService {
read_json_file(&path) read_json_file(&path)
} }
AppType::Gemini => { AppType::Gemini => {
// 新增 use crate::gemini_config::{
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let path = get_gemini_env_path(); // 读取 .env 文件(环境变量)
if !path.exists() { let env_path = get_gemini_env_path();
if !env_path.exists() {
return Err(AppError::localized( return Err(AppError::localized(
"gemini.env.missing", "gemini.env.missing",
"Gemini .env 文件不存在", "Gemini .env 文件不存在",
@@ -927,7 +984,22 @@ impl ProviderService {
} }
let env_map = read_gemini_env()?; let env_map = read_gemini_env()?;
Ok(env_to_json(&env_map)) let env_json = env_to_json(&env_map);
let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
// 读取 settings.json 文件MCP 配置等)
let settings_path = get_gemini_settings_path();
let config_obj = if settings_path.exists() {
read_json_file(&settings_path)?
} else {
json!({})
};
// 返回完整结构:{ "env": {...}, "config": {...} }
Ok(json!({
"env": env_obj,
"config": config_obj
}))
} }
} }
} }
@@ -1426,7 +1498,9 @@ impl ProviderService {
config: &mut MultiAppConfig, config: &mut MultiAppConfig,
next_provider: &str, next_provider: &str,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; use crate::gemini_config::{
env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
};
let env_path = get_gemini_env_path(); let env_path = get_gemini_env_path();
if !env_path.exists() { if !env_path.exists() {
@@ -1442,7 +1516,18 @@ impl ProviderService {
} }
let env_map = read_gemini_env()?; let env_map = read_gemini_env()?;
let live = env_to_json(&env_map); let mut live = env_to_json(&env_map);
let settings_path = get_gemini_settings_path();
let config_value = if settings_path.exists() {
read_json_file(&settings_path)?
} else {
json!({})
};
if let Some(obj) = live.as_object_mut() {
obj.insert("config".to_string(), config_value);
}
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) { if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(current) = manager.providers.get_mut(&current_id) { if let Some(current) = manager.providers.get_mut(&current_id) {
current.settings_config = live; current.settings_config = live;
@@ -1460,36 +1545,71 @@ impl ProviderService {
Ok(()) Ok(())
} }
fn write_gemini_live(provider: &Provider) -> Result<(), AppError> { pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
use crate::gemini_config::{ use crate::gemini_config::{
json_to_env, validate_gemini_settings_strict, write_gemini_env_atomic, get_gemini_settings_path, json_to_env, validate_gemini_settings_strict,
write_gemini_env_atomic,
}; };
// 一次性检测认证类型,避免重复检测 // 一次性检测认证类型,避免重复检测
let auth_type = Self::detect_gemini_auth_type(provider); let auth_type = Self::detect_gemini_auth_type(provider);
let mut env_map = json_to_env(&provider.settings_config)?;
// 准备要写入 ~/.gemini/settings.json 的配置(缺省时保留现有文件内容)
let mut config_to_write = if let Some(config_value) = provider.settings_config.get("config")
{
if config_value.is_null() {
Some(json!({}))
} else if config_value.is_object() {
Some(config_value.clone())
} else {
return Err(AppError::localized(
"gemini.validation.invalid_config",
"Gemini 配置格式错误: config 必须是对象或 null",
"Gemini config invalid: config must be an object or null",
));
}
} else {
None
};
if config_to_write.is_none() {
let settings_path = get_gemini_settings_path();
if settings_path.exists() {
config_to_write = Some(read_json_file(&settings_path)?);
}
}
match auth_type { match auth_type {
GeminiAuthType::GoogleOfficial => { GeminiAuthType::GoogleOfficial => {
// Google 官方使用 OAuth清空 env // Google 官方使用 OAuth清空 env
let empty_env = std::collections::HashMap::new(); env_map.clear();
write_gemini_env_atomic(&empty_env)?; write_gemini_env_atomic(&env_map)?;
Self::ensure_google_oauth_security_flag(provider)?;
} }
GeminiAuthType::Packycode => { GeminiAuthType::Packycode => {
// PackyCode 供应商,使用 API Key切换时严格验证 // PackyCode 供应商,使用 API Key切换时严格验证
validate_gemini_settings_strict(&provider.settings_config)?; validate_gemini_settings_strict(&provider.settings_config)?;
let env_map = json_to_env(&provider.settings_config)?;
write_gemini_env_atomic(&env_map)?; write_gemini_env_atomic(&env_map)?;
Self::ensure_packycode_security_flag(provider)?;
} }
GeminiAuthType::Generic => { GeminiAuthType::Generic => {
// 通用供应商,使用 API Key切换时严格验证 // 通用供应商,使用 API Key切换时严格验证
validate_gemini_settings_strict(&provider.settings_config)?; validate_gemini_settings_strict(&provider.settings_config)?;
let env_map = json_to_env(&provider.settings_config)?;
write_gemini_env_atomic(&env_map)?; write_gemini_env_atomic(&env_map)?;
} }
} }
if let Some(config_value) = config_to_write {
let settings_path = get_gemini_settings_path();
write_json_file(&settings_path, &config_value)?;
}
match auth_type {
GeminiAuthType::GoogleOfficial => Self::ensure_google_oauth_security_flag(provider)?,
GeminiAuthType::Packycode => Self::ensure_packycode_security_flag(provider)?,
GeminiAuthType::Generic => {}
}
Ok(()) Ok(())
} }

View File

@@ -32,6 +32,9 @@ pub struct Skill {
/// 分支名称 /// 分支名称
#[serde(rename = "repoBranch")] #[serde(rename = "repoBranch")]
pub repo_branch: Option<String>, pub repo_branch: Option<String>,
/// 技能所在的子目录路径 (可选, 如 "skills")
#[serde(rename = "skillsPath")]
pub skills_path: Option<String>,
} }
/// 仓库配置 /// 仓库配置
@@ -234,6 +237,7 @@ impl SkillService {
repo_owner: Some(repo.owner.clone()), repo_owner: Some(repo.owner.clone()),
repo_name: Some(repo.name.clone()), repo_name: Some(repo.name.clone()),
repo_branch: Some(repo.branch.clone()), repo_branch: Some(repo.branch.clone()),
skills_path: repo.skills_path.clone(),
}); });
} }
Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e), Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e),
@@ -312,6 +316,7 @@ impl SkillService {
repo_owner: None, repo_owner: None,
repo_name: None, repo_name: None,
repo_branch: None, repo_branch: None,
skills_path: None,
}); });
} }
} }
@@ -442,12 +447,21 @@ impl SkillService {
.await .await
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??; .map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
// 复制到安装目录 // 根据 skills_path 确定源目录路径
let source = temp_dir.join(&directory); let source = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skills_path源路径为: temp_dir/skills_path/directory
temp_dir.join(skills_path.trim_matches('/')).join(&directory)
} else {
// 否则源路径为: temp_dir/directory
temp_dir.join(&directory)
};
if !source.exists() { if !source.exists() {
let _ = fs::remove_dir_all(&temp_dir); let _ = fs::remove_dir_all(&temp_dir);
return Err(anyhow::anyhow!("技能目录不存在")); return Err(anyhow::anyhow!(
"技能目录不存在: {}",
source.display()
));
} }
// 删除旧版本 // 删除旧版本

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch", "productName": "CC Switch",
"version": "3.6.2", "version": "3.7.0",
"identifier": "com.ccswitch.desktop", "identifier": "com.ccswitch.desktop",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",

View File

@@ -498,8 +498,8 @@ url = "https://example.com"
.expect("unified servers should exist"); .expect("unified servers should exist");
let echo = servers.get("echo_server").expect("echo server"); let echo = servers.get("echo_server").expect("echo server");
assert_eq!( assert!(
echo.apps.codex, true, echo.apps.codex,
"Codex app should be enabled for echo_server" "Codex app should be enabled for echo_server"
); );
let server_spec = echo.server.as_object().expect("server spec"); let server_spec = echo.server.as_object().expect("server spec");
@@ -512,8 +512,8 @@ url = "https://example.com"
); );
let http = servers.get("http_server").expect("http server"); let http = servers.get("http_server").expect("http server");
assert_eq!( assert!(
http.apps.codex, true, http.apps.codex,
"Codex app should be enabled for http_server" "Codex app should be enabled for http_server"
); );
let http_spec = http.server.as_object().expect("http spec"); let http_spec = http.server.as_object().expect("http spec");
@@ -577,10 +577,7 @@ command = "echo"
.expect("existing entry"); .expect("existing entry");
// 验证 Codex 应用已启用 // 验证 Codex 应用已启用
assert_eq!( assert!(entry.apps.codex, "Codex app should be enabled after import");
entry.apps.codex, true,
"Codex app should be enabled after import"
);
// 验证现有配置被保留server 不应被覆盖) // 验证现有配置被保留server 不应被覆盖)
let spec = entry.server.as_object().expect("server spec"); let spec = entry.server.as_object().expect("server spec");
@@ -702,8 +699,8 @@ fn import_from_claude_merges_into_config() {
.expect("entry exists"); .expect("entry exists");
// 验证 Claude 应用已启用 // 验证 Claude 应用已启用
assert_eq!( assert!(
entry.apps.claude, true, entry.apps.claude,
"Claude app should be enabled after import" "Claude app should be enabled after import"
); );

View File

@@ -61,7 +61,7 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
onBlur={onBlur} onBlur={onBlur}
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/ placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
GEMINI_API_KEY=sk-your-api-key-here GEMINI_API_KEY=sk-your-api-key-here
GEMINI_MODEL=gemini-2.5-pro`} GEMINI_MODEL=gemini-3-pro-preview`}
rows={6} rows={6}
className="w-full px-3 py-2 border border-border-default dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 transition-colors resize-y min-h-[8rem]" className="w-full px-3 py-2 border border-border-default dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 transition-colors resize-y min-h-[8rem]"
autoComplete="off" autoComplete="off"

View File

@@ -127,7 +127,7 @@ export function GeminiFormFields({
id="gemini-model" id="gemini-model"
value={model} value={model}
onChange={(e) => onModelChange(e.target.value)} onChange={(e) => onModelChange(e.target.value)}
placeholder="gemini-2.5-pro" placeholder="gemini-3-pro-preview"
/> />
</div> </div>
)} )}

View File

@@ -53,7 +53,7 @@ const GEMINI_DEFAULT_CONFIG = JSON.stringify(
env: { env: {
GOOGLE_GEMINI_BASE_URL: "", GOOGLE_GEMINI_BASE_URL: "",
GEMINI_API_KEY: "", GEMINI_API_KEY: "",
GEMINI_MODEL: "gemini-2.5-pro", GEMINI_MODEL: "gemini-3-pro-preview",
}, },
}, },
null, null,
@@ -171,14 +171,12 @@ export function ProviderForm({
}); });
// 使用 Base URL hook (Claude, Codex, Gemini) // 使用 Base URL hook (Claude, Codex, Gemini)
const { baseUrl, handleClaudeBaseUrlChange, handleGeminiBaseUrlChange } = const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({
useBaseUrlState({
appType: appId, appType: appId,
category, category,
settingsConfig: form.watch("settingsConfig"), settingsConfig: form.watch("settingsConfig"),
codexConfig: "", codexConfig: "",
onSettingsConfigChange: (config) => onSettingsConfigChange: (config) => form.setValue("settingsConfig", config),
form.setValue("settingsConfig", config),
onCodexConfigChange: () => { onCodexConfigChange: () => {
/* noop */ /* noop */
}, },
@@ -317,9 +315,13 @@ export function ProviderForm({
const { const {
geminiEnv, geminiEnv,
geminiConfig, geminiConfig,
geminiApiKey,
geminiBaseUrl,
geminiModel, geminiModel,
envError, envError,
configError: geminiConfigError, configError: geminiConfigError,
handleGeminiApiKeyChange: originalHandleGeminiApiKeyChange,
handleGeminiBaseUrlChange: originalHandleGeminiBaseUrlChange,
handleGeminiEnvChange, handleGeminiEnvChange,
handleGeminiConfigChange, handleGeminiConfigChange,
resetGeminiConfig, resetGeminiConfig,
@@ -329,6 +331,39 @@ export function ProviderForm({
initialData: appId === "gemini" ? initialData : undefined, initialData: appId === "gemini" ? initialData : undefined,
}); });
// 包装 Gemini handlers 以同步 settingsConfig
const handleGeminiApiKeyChange = useCallback(
(key: string) => {
originalHandleGeminiApiKeyChange(key);
// 同步更新 settingsConfig
try {
const config = JSON.parse(form.watch("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GEMINI_API_KEY = key.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
} catch {
// ignore
}
},
[originalHandleGeminiApiKeyChange, form],
);
const handleGeminiBaseUrlChange = useCallback(
(url: string) => {
originalHandleGeminiBaseUrlChange(url);
// 同步更新 settingsConfig
try {
const config = JSON.parse(form.watch("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GOOGLE_GEMINI_BASE_URL = url.trim().replace(/\/+$/, "");
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
} catch {
// ignore
}
},
[originalHandleGeminiBaseUrlChange, form],
);
// 使用 Gemini 通用配置 hook (仅 Gemini 模式) // 使用 Gemini 通用配置 hook (仅 Gemini 模式)
const { const {
useCommonConfig: useGeminiCommonConfigFlag, useCommonConfig: useGeminiCommonConfigFlag,
@@ -704,15 +739,15 @@ export function ProviderForm({
form.watch("settingsConfig"), form.watch("settingsConfig"),
isEditMode, isEditMode,
)} )}
apiKey={apiKey} apiKey={geminiApiKey}
onApiKeyChange={handleApiKeyChange} onApiKeyChange={handleGeminiApiKeyChange}
category={category} category={category}
shouldShowApiKeyLink={shouldShowGeminiApiKeyLink} shouldShowApiKeyLink={shouldShowGeminiApiKeyLink}
websiteUrl={geminiWebsiteUrl} websiteUrl={geminiWebsiteUrl}
isPartner={isGeminiPartner} isPartner={isGeminiPartner}
partnerPromotionKey={geminiPartnerPromotionKey} partnerPromotionKey={geminiPartnerPromotionKey}
shouldShowSpeedTest={shouldShowSpeedTest} shouldShowSpeedTest={shouldShowSpeedTest}
baseUrl={baseUrl} baseUrl={geminiBaseUrl}
onBaseUrlChange={handleGeminiBaseUrlChange} onBaseUrlChange={handleGeminiBaseUrlChange}
isEndpointModalOpen={isEndpointModalOpen} isEndpointModalOpen={isEndpointModalOpen}
onEndpointModalToggle={setIsEndpointModalOpen} onEndpointModalToggle={setIsEndpointModalOpen}

View File

@@ -14,6 +14,7 @@ interface DirectorySettingsProps {
onResetAppConfig: () => Promise<void>; onResetAppConfig: () => Promise<void>;
claudeDir?: string; claudeDir?: string;
codexDir?: string; codexDir?: string;
geminiDir?: string;
onDirectoryChange: (app: AppId, value?: string) => void; onDirectoryChange: (app: AppId, value?: string) => void;
onBrowseDirectory: (app: AppId) => Promise<void>; onBrowseDirectory: (app: AppId) => Promise<void>;
onResetDirectory: (app: AppId) => Promise<void>; onResetDirectory: (app: AppId) => Promise<void>;
@@ -27,6 +28,7 @@ export function DirectorySettings({
onResetAppConfig, onResetAppConfig,
claudeDir, claudeDir,
codexDir, codexDir,
geminiDir,
onDirectoryChange, onDirectoryChange,
onBrowseDirectory, onBrowseDirectory,
onResetDirectory, onResetDirectory,
@@ -104,6 +106,17 @@ export function DirectorySettings({
onBrowse={() => onBrowseDirectory("codex")} onBrowse={() => onBrowseDirectory("codex")}
onReset={() => onResetDirectory("codex")} onReset={() => onResetDirectory("codex")}
/> />
<DirectoryInput
label={t("settings.geminiConfigDir")}
description={undefined}
value={geminiDir}
resolvedValue={resolvedDirs.gemini}
placeholder={t("settings.browsePlaceholderGemini")}
onChange={(val) => onDirectoryChange("gemini", val)}
onBrowse={() => onBrowseDirectory("gemini")}
onReset={() => onResetDirectory("gemini")}
/>
</section> </section>
</> </>
); );

View File

@@ -220,6 +220,7 @@ export function SettingsDialog({
onResetAppConfig={resetAppConfigDir} onResetAppConfig={resetAppConfigDir}
claudeDir={settings.claudeConfigDir} claudeDir={settings.claudeConfigDir}
codexDir={settings.codexConfigDir} codexDir={settings.codexConfigDir}
geminiDir={settings.geminiConfigDir}
onDirectoryChange={updateDirectory} onDirectoryChange={updateDirectory}
onBrowseDirectory={browseDirectory} onBrowseDirectory={browseDirectory}
onResetDirectory={resetDirectory} onResetDirectory={resetDirectory}

View File

@@ -59,6 +59,10 @@ const DialogContent = React.forwardRef<
zIndexMap[zIndex], zIndexMap[zIndex],
className, className,
)} )}
onInteractOutside={(e) => {
// 防止点击遮罩层关闭对话框
e.preventDefault();
}}
{...props} {...props}
> >
{children} {children}

View File

@@ -230,6 +230,23 @@ export const providerPresets: ProviderPreset[] = [
}, },
category: "cn_official", category: "cn_official",
}, },
{
name: "DouBaoSeed",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl: "https://www.volcengine.com/product/doubao",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://ark.cn-beijing.volces.com/api/coding",
ANTHROPIC_AUTH_TOKEN: "",
API_TIMEOUT_MS: "3000000",
ANTHROPIC_MODEL: "doubao-seed-code-preview-latest",
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-code-preview-latest",
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-code-preview-latest",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-code-preview-latest",
},
},
category: "cn_official",
},
{ {
name: "BaiLing", name: "BaiLing",
websiteUrl: "https://alipaytbox.yuque.com/sxs0ba/ling/get_started", websiteUrl: "https://alipaytbox.yuque.com/sxs0ba/ling/get_started",
@@ -294,22 +311,4 @@ export const providerPresets: ProviderPreset[] = [
isPartner: true, // 合作伙伴 isPartner: true, // 合作伙伴
partnerPromotionKey: "packycode", // 促销信息 i18n key partnerPromotionKey: "packycode", // 促销信息 i18n key
}, },
{
name: "AnyRouter",
websiteUrl: "https://anyrouter.top",
apiKeyUrl: "https://anyrouter.top/register?aff=PCel",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://anyrouter.top",
ANTHROPIC_AUTH_TOKEN: "",
},
},
// 请求地址候选(用于地址管理/测速)
endpointCandidates: [
"https://q.quuvv.cn",
"https://pmpjfbhq.cn-nb1.rainapp.top",
"https://anyrouter.top",
],
category: "third_party",
},
]; ];

View File

@@ -143,20 +143,4 @@ requires_openai_auth = true`,
isPartner: true, // 合作伙伴 isPartner: true, // 合作伙伴
partnerPromotionKey: "packycode", // 促销信息 i18n key partnerPromotionKey: "packycode", // 促销信息 i18n key
}, },
{
name: "AnyRouter",
websiteUrl: "https://anyrouter.top",
category: "third_party",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"anyrouter",
"https://anyrouter.top/v1",
"gpt-5-codex",
),
endpointCandidates: [
"https://anyrouter.top/v1",
"https://q.quuvv.cn/v1",
"https://pmpjfbhq.cn-nb1.rainapp.top/v1",
],
},
]; ];

View File

@@ -33,14 +33,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
websiteUrl: "https://ai.google.dev/", websiteUrl: "https://ai.google.dev/",
apiKeyUrl: "https://aistudio.google.com/apikey", apiKeyUrl: "https://aistudio.google.com/apikey",
settingsConfig: { settingsConfig: {
env: { env: {},
GEMINI_MODEL: "gemini-2.5-pro",
},
}, },
description: "Google 官方 Gemini API (OAuth)", description: "Google 官方 Gemini API (OAuth)",
category: "official", category: "official",
partnerPromotionKey: "google-official", partnerPromotionKey: "google-official",
model: "gemini-2.5-pro",
theme: { theme: {
icon: "gemini", icon: "gemini",
backgroundColor: "#4285F4", backgroundColor: "#4285F4",
@@ -54,11 +51,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: { settingsConfig: {
env: { env: {
GOOGLE_GEMINI_BASE_URL: "https://www.packyapi.com", GOOGLE_GEMINI_BASE_URL: "https://www.packyapi.com",
GEMINI_MODEL: "gemini-2.5-pro", GEMINI_MODEL: "gemini-3-pro-preview",
}, },
}, },
baseURL: "https://www.packyapi.com", baseURL: "https://www.packyapi.com",
model: "gemini-2.5-pro", model: "gemini-3-pro-preview",
description: "PackyCode", description: "PackyCode",
category: "third_party", category: "third_party",
isPartner: true, isPartner: true,
@@ -74,10 +71,10 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: { settingsConfig: {
env: { env: {
GOOGLE_GEMINI_BASE_URL: "", GOOGLE_GEMINI_BASE_URL: "",
GEMINI_MODEL: "gemini-2.5-pro", GEMINI_MODEL: "gemini-3-pro-preview",
}, },
}, },
model: "gemini-2.5-pro", model: "gemini-3-pro-preview",
description: "自定义 Gemini API 端点", description: "自定义 Gemini API 端点",
category: "custom", category: "custom",
}, },

View File

@@ -5,12 +5,13 @@ import { homeDir, join } from "@tauri-apps/api/path";
import { settingsApi, type AppId } from "@/lib/api"; import { settingsApi, type AppId } from "@/lib/api";
import type { SettingsFormState } from "./useSettingsForm"; import type { SettingsFormState } from "./useSettingsForm";
type DirectoryKey = "appConfig" | "claude" | "codex"; type DirectoryKey = "appConfig" | "claude" | "codex" | "gemini";
export interface ResolvedDirectories { export interface ResolvedDirectories {
appConfig: string; appConfig: string;
claude: string; claude: string;
codex: string; codex: string;
gemini: string;
} }
const sanitizeDir = (value?: string | null): string | undefined => { const sanitizeDir = (value?: string | null): string | undefined => {
@@ -37,7 +38,8 @@ const computeDefaultConfigDir = async (
): Promise<string | undefined> => { ): Promise<string | undefined> => {
try { try {
const home = await homeDir(); const home = await homeDir();
const folder = app === "claude" ? ".claude" : ".codex"; const folder =
app === "claude" ? ".claude" : app === "codex" ? ".codex" : ".gemini";
return await join(home, folder); return await join(home, folder);
} catch (error) { } catch (error) {
console.error( console.error(
@@ -64,7 +66,11 @@ export interface UseDirectorySettingsResult {
browseAppConfigDir: () => Promise<void>; browseAppConfigDir: () => Promise<void>;
resetDirectory: (app: AppId) => Promise<void>; resetDirectory: (app: AppId) => Promise<void>;
resetAppConfigDir: () => Promise<void>; resetAppConfigDir: () => Promise<void>;
resetAllDirectories: (claudeDir?: string, codexDir?: string) => void; resetAllDirectories: (
claudeDir?: string,
codexDir?: string,
geminiDir?: string,
) => void;
} }
/** /**
@@ -89,6 +95,7 @@ export function useDirectorySettings({
appConfig: "", appConfig: "",
claude: "", claude: "",
codex: "", codex: "",
gemini: "",
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -96,6 +103,7 @@ export function useDirectorySettings({
appConfig: "", appConfig: "",
claude: "", claude: "",
codex: "", codex: "",
gemini: "",
}); });
const initialAppConfigDirRef = useRef<string | undefined>(undefined); const initialAppConfigDirRef = useRef<string | undefined>(undefined);
@@ -110,16 +118,20 @@ export function useDirectorySettings({
overrideRaw, overrideRaw,
claudeDir, claudeDir,
codexDir, codexDir,
geminiDir,
defaultAppConfig, defaultAppConfig,
defaultClaudeDir, defaultClaudeDir,
defaultCodexDir, defaultCodexDir,
defaultGeminiDir,
] = await Promise.all([ ] = await Promise.all([
settingsApi.getAppConfigDirOverride(), settingsApi.getAppConfigDirOverride(),
settingsApi.getConfigDir("claude"), settingsApi.getConfigDir("claude"),
settingsApi.getConfigDir("codex"), settingsApi.getConfigDir("codex"),
settingsApi.getConfigDir("gemini"),
computeDefaultAppConfigDir(), computeDefaultAppConfigDir(),
computeDefaultConfigDir("claude"), computeDefaultConfigDir("claude"),
computeDefaultConfigDir("codex"), computeDefaultConfigDir("codex"),
computeDefaultConfigDir("gemini"),
]); ]);
if (!active) return; if (!active) return;
@@ -130,6 +142,7 @@ export function useDirectorySettings({
appConfig: defaultAppConfig ?? "", appConfig: defaultAppConfig ?? "",
claude: defaultClaudeDir ?? "", claude: defaultClaudeDir ?? "",
codex: defaultCodexDir ?? "", codex: defaultCodexDir ?? "",
gemini: defaultGeminiDir ?? "",
}; };
setAppConfigDir(normalizedOverride); setAppConfigDir(normalizedOverride);
@@ -139,6 +152,7 @@ export function useDirectorySettings({
appConfig: normalizedOverride ?? defaultsRef.current.appConfig, appConfig: normalizedOverride ?? defaultsRef.current.appConfig,
claude: claudeDir || defaultsRef.current.claude, claude: claudeDir || defaultsRef.current.claude,
codex: codexDir || defaultsRef.current.codex, codex: codexDir || defaultsRef.current.codex,
gemini: geminiDir || defaultsRef.current.gemini,
}); });
} catch (error) { } catch (error) {
console.error( console.error(
@@ -167,7 +181,9 @@ export function useDirectorySettings({
onUpdateSettings( onUpdateSettings(
key === "claude" key === "claude"
? { claudeConfigDir: sanitized } ? { claudeConfigDir: sanitized }
: { codexConfigDir: sanitized }, : key === "codex"
? { codexConfigDir: sanitized }
: { geminiConfigDir: sanitized },
); );
} }
@@ -188,18 +204,24 @@ export function useDirectorySettings({
const updateDirectory = useCallback( const updateDirectory = useCallback(
(app: AppId, value?: string) => { (app: AppId, value?: string) => {
updateDirectoryState(app === "claude" ? "claude" : "codex", value); updateDirectoryState(
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini",
value,
);
}, },
[updateDirectoryState], [updateDirectoryState],
); );
const browseDirectory = useCallback( const browseDirectory = useCallback(
async (app: AppId) => { async (app: AppId) => {
const key: DirectoryKey = app === "claude" ? "claude" : "codex"; const key: DirectoryKey =
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini";
const currentValue = const currentValue =
key === "claude" key === "claude"
? (settings?.claudeConfigDir ?? resolvedDirs.claude) ? (settings?.claudeConfigDir ?? resolvedDirs.claude)
: (settings?.codexConfigDir ?? resolvedDirs.codex); : key === "codex"
? (settings?.codexConfigDir ?? resolvedDirs.codex)
: (settings?.geminiConfigDir ?? resolvedDirs.gemini);
try { try {
const picked = await settingsApi.selectConfigDirectory(currentValue); const picked = await settingsApi.selectConfigDirectory(currentValue);
@@ -240,7 +262,8 @@ export function useDirectorySettings({
const resetDirectory = useCallback( const resetDirectory = useCallback(
async (app: AppId) => { async (app: AppId) => {
const key: DirectoryKey = app === "claude" ? "claude" : "codex"; const key: DirectoryKey =
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini";
if (!defaultsRef.current[key]) { if (!defaultsRef.current[key]) {
const fallback = await computeDefaultConfigDir(app); const fallback = await computeDefaultConfigDir(app);
if (fallback) { if (fallback) {
@@ -269,13 +292,14 @@ export function useDirectorySettings({
}, [updateDirectoryState]); }, [updateDirectoryState]);
const resetAllDirectories = useCallback( const resetAllDirectories = useCallback(
(claudeDir?: string, codexDir?: string) => { (claudeDir?: string, codexDir?: string, geminiDir?: string) => {
setAppConfigDir(initialAppConfigDirRef.current); setAppConfigDir(initialAppConfigDirRef.current);
setResolvedDirs({ setResolvedDirs({
appConfig: appConfig:
initialAppConfigDirRef.current ?? defaultsRef.current.appConfig, initialAppConfigDirRef.current ?? defaultsRef.current.appConfig,
claude: claudeDir ?? defaultsRef.current.claude, claude: claudeDir ?? defaultsRef.current.claude,
codex: codexDir ?? defaultsRef.current.codex, codex: codexDir ?? defaultsRef.current.codex,
gemini: geminiDir ?? defaultsRef.current.gemini,
}); });
}, },
[], [],

View File

@@ -102,6 +102,7 @@ export function useSettings(): UseSettingsResult {
resetAllDirectories( resetAllDirectories(
sanitizeDir(data?.claudeConfigDir), sanitizeDir(data?.claudeConfigDir),
sanitizeDir(data?.codexConfigDir), sanitizeDir(data?.codexConfigDir),
sanitizeDir(data?.geminiConfigDir),
); );
setRequiresRestart(false); setRequiresRestart(false);
}, [ }, [
@@ -120,14 +121,17 @@ export function useSettings(): UseSettingsResult {
const sanitizedAppDir = sanitizeDir(appConfigDir); const sanitizedAppDir = sanitizeDir(appConfigDir);
const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir); const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir);
const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir); const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir);
const sanitizedGeminiDir = sanitizeDir(settings.geminiConfigDir);
const previousAppDir = initialAppConfigDir; const previousAppDir = initialAppConfigDir;
const previousClaudeDir = sanitizeDir(data?.claudeConfigDir); const previousClaudeDir = sanitizeDir(data?.claudeConfigDir);
const previousCodexDir = sanitizeDir(data?.codexConfigDir); const previousCodexDir = sanitizeDir(data?.codexConfigDir);
const previousGeminiDir = sanitizeDir(data?.geminiConfigDir);
const payload: Settings = { const payload: Settings = {
...settings, ...settings,
claudeConfigDir: sanitizedClaudeDir, claudeConfigDir: sanitizedClaudeDir,
codexConfigDir: sanitizedCodexDir, codexConfigDir: sanitizedCodexDir,
geminiConfigDir: sanitizedGeminiDir,
language: settings.language, language: settings.language,
}; };
@@ -170,10 +174,11 @@ export function useSettings(): UseSettingsResult {
console.warn("[useSettings] Failed to refresh tray menu", error); console.warn("[useSettings] Failed to refresh tray menu", error);
} }
// 如果 Claude/Codex 的目录覆盖发生变化,则立即将“当前使用的供应商”写回对应应用的 live 配置 // 如果 Claude/Codex/Gemini 的目录覆盖发生变化,则立即将“当前使用的供应商”写回对应应用的 live 配置
const claudeDirChanged = sanitizedClaudeDir !== previousClaudeDir; const claudeDirChanged = sanitizedClaudeDir !== previousClaudeDir;
const codexDirChanged = sanitizedCodexDir !== previousCodexDir; const codexDirChanged = sanitizedCodexDir !== previousCodexDir;
if (claudeDirChanged || codexDirChanged) { const geminiDirChanged = sanitizedGeminiDir !== previousGeminiDir;
if (claudeDirChanged || codexDirChanged || geminiDirChanged) {
const syncResult = await syncCurrentProvidersLiveSafe(); const syncResult = await syncCurrentProvidersLiveSafe();
if (!syncResult.ok) { if (!syncResult.ok) {
console.warn( console.warn(

View File

@@ -1,7 +1,7 @@
{ {
"app": { "app": {
"title": "CC Switch", "title": "CC Switch",
"description": "Claude Code & Codex Provider Switching Tool" "description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
}, },
"common": { "common": {
"add": "Add", "add": "Add",
@@ -179,8 +179,11 @@
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.", "claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.",
"codexConfigDir": "Codex Configuration Directory", "codexConfigDir": "Codex Configuration Directory",
"codexConfigDirDescription": "Override Codex configuration directory.", "codexConfigDirDescription": "Override Codex configuration directory.",
"geminiConfigDir": "Gemini Configuration Directory",
"geminiConfigDirDescription": "Override Gemini configuration directory (.env).",
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude", "browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
"browsePlaceholderCodex": "e.g., /home/<your-username>/.codex", "browsePlaceholderCodex": "e.g., /home/<your-username>/.codex",
"browsePlaceholderGemini": "e.g., /home/<your-username>/.gemini",
"browseDirectory": "Browse Directory", "browseDirectory": "Browse Directory",
"resetDefault": "Reset to default directory (takes effect after saving)", "resetDefault": "Reset to default directory (takes effect after saving)",
"checkForUpdates": "Check for Updates", "checkForUpdates": "Check for Updates",

View File

@@ -1,7 +1,7 @@
{ {
"app": { "app": {
"title": "CC Switch", "title": "CC Switch",
"description": "Claude Code & Codex 供应商切换工具" "description": "Claude Code / Codex / Gemini CLI 全方位辅助工具"
}, },
"common": { "common": {
"add": "添加", "add": "添加",
@@ -179,8 +179,11 @@
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。", "claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。",
"codexConfigDir": "Codex 配置目录", "codexConfigDir": "Codex 配置目录",
"codexConfigDirDescription": "覆盖 Codex 配置目录。", "codexConfigDirDescription": "覆盖 Codex 配置目录。",
"geminiConfigDir": "Gemini 配置目录",
"geminiConfigDirDescription": "覆盖 Gemini 配置目录 (.env)。",
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude", "browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
"browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex", "browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex",
"browsePlaceholderGemini": "例如:/home/<你的用户名>/.gemini",
"browseDirectory": "浏览目录", "browseDirectory": "浏览目录",
"resetDefault": "恢复默认目录(需保存后生效)", "resetDefault": "恢复默认目录(需保存后生效)",
"checkForUpdates": "检查更新", "checkForUpdates": "检查更新",

View File

@@ -10,6 +10,7 @@ export interface Skill {
repoOwner?: string; repoOwner?: string;
repoName?: string; repoName?: string;
repoBranch?: string; repoBranch?: string;
skillsPath?: string; // 技能所在的子目录路径,如 "skills"
} }
export interface SkillRepo { export interface SkillRepo {

View File

@@ -97,6 +97,8 @@ export interface Settings {
claudeConfigDir?: string; claudeConfigDir?: string;
// 覆盖 Codex 配置目录(可选) // 覆盖 Codex 配置目录(可选)
codexConfigDir?: string; codexConfigDir?: string;
// 覆盖 Gemini 配置目录(可选)
geminiConfigDir?: string;
// 首选语言(可选,默认中文) // 首选语言(可选,默认中文)
language?: "en" | "zh"; language?: "en" | "zh";
// Claude 自定义端点列表 // Claude 自定义端点列表