Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b64bb6cfa1 | ||
|
|
d65513ae7d | ||
|
|
29e32f73f3 | ||
|
|
eb46ac8592 | ||
|
|
ba336fc416 | ||
|
|
7fa0a7b166 | ||
|
|
5e54656d45 | ||
|
|
74969ae968 | ||
|
|
1f3627add3 | ||
|
|
14ee122b27 | ||
|
|
7aecba14fe |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -5,6 +5,31 @@ 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.1] - 2025-11-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Skills third-party repository installation** (#268) - Fixed installation failure for skills repositories with custom subdirectories (e.g., `ComposioHQ/awesome-claude-skills`)
|
||||||
|
- **Gemini configuration persistence** - Resolved issue where settings.json edits were lost when switching providers
|
||||||
|
- **Dialog overlay click protection** - Prevented dialogs from closing when clicking outside, avoiding accidental form data loss (affects 11 dialog components)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Gemini configuration directory support** (#255) - Added custom configuration directory option for Gemini in settings
|
||||||
|
- **ArchLinux installation support** (#259) - Added AUR installation via `paru -S cc-switch-bin`
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- **Skills error messages i18n** - Added 28+ detailed error messages (English & Chinese) with specific resolution suggestions
|
||||||
|
- **Download timeout** - Extended from 15s to 60s to reduce network-related false positives
|
||||||
|
- **Code formatting** - Applied unified Rust (`cargo fmt`) and TypeScript (`prettier`) formatting standards
|
||||||
|
|
||||||
|
### Reverted
|
||||||
|
|
||||||
|
- **Auto-launch on system startup** - Temporarily reverted feature pending further testing and optimization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [3.7.0] - 2025-11-19
|
## [3.7.0] - 2025-11-19
|
||||||
|
|
||||||
### Major Features
|
### Major Features
|
||||||
|
|||||||
103
README.md
103
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# All-in-One Assistant for Claude Code, Codex & Gemini CLI
|
# All-in-One Assistant for Claude Code, Codex & Gemini CLI
|
||||||
|
|
||||||
[](https://github.com/farion1231/cc-switch/releases)
|
[](https://github.com/farion1231/cc-switch/releases)
|
||||||
[](https://github.com/trending/typescript)
|
[](https://github.com/trending/typescript)
|
||||||
[](https://github.com/farion1231/cc-switch/releases)
|
[](https://github.com/farion1231/cc-switch/releases)
|
||||||
[](https://tauri.app/)
|
[](https://tauri.app/)
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
English | [中文](README_ZH.md) | [Changelog](CHANGELOG.md)
|
English | [中文](README_ZH.md) | [Changelog](CHANGELOG.md)
|
||||||
|
|
||||||
A desktop application for managing and switching between different provider configurations & MCP for Claude Code and Codex.
|
**From Provider Switcher to All-in-One AI CLI Management Platform**
|
||||||
|
|
||||||
|
Unified management for Claude Code, Codex & Gemini CLI provider configurations, MCP servers, Skills extensions, and system prompts.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,6 +35,12 @@ Get 10% OFF the GLM CODING PLAN with [this link](https://z.ai/subscribe?ic=8JVLJ
|
|||||||
<td width="180"><img src="assets/partners/logos/packycode.png" alt="PackyCode" width="150"></td>
|
<td width="180"><img src="assets/partners/logos/packycode.png" alt="PackyCode" width="150"></td>
|
||||||
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using <a href="https://www.packyapi.com/register?aff=cc-switch">this link</a> and enter the "cc-switch" promo code during recharge to get 10% off.</td>
|
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using <a href="https://www.packyapi.com/register?aff=cc-switch">this link</a> and enter the "cc-switch" promo code during recharge to get 10% off.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="180"><img src="assets/partners/logos/sds-en.png" alt="ShanDianShuo" width="150"></td>
|
||||||
|
<td>Thanks to ShanDianShuo for sponsoring this project! ShanDianShuo is a local-first AI voice input: Millisecond latency, data stays on device, 4x faster than typing, AI-powered correction, Privacy-first, completely free. Doubles your coding efficiency with Claude Code! <a href="shandianshuo.cn">Free download</a> for Mac/Win</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
@@ -43,12 +51,49 @@ Get 10% OFF the GLM CODING PLAN with [this link](https://z.ai/subscribe?ic=8JVLJ
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Current Version: v3.7.0 | [Full Changelog](CHANGELOG.md)
|
### Current Version: v3.7.0 | [Full Changelog](CHANGELOG.md) | [📋 Release Notes](docs/release-note-v3.7.0-en.md)
|
||||||
|
|
||||||
|
**v3.7.0 Major Update (2025-11-19)**
|
||||||
|
|
||||||
|
**Six Core Features, 18,000+ Lines of New Code**
|
||||||
|
|
||||||
|
- **Gemini CLI Integration**
|
||||||
|
- Third supported AI CLI (Claude Code / Codex / Gemini)
|
||||||
|
- Dual-file configuration support (`.env` + `settings.json`)
|
||||||
|
- Complete MCP server management
|
||||||
|
- Presets: Google Official (OAuth) / PackyCode / Custom
|
||||||
|
|
||||||
|
- **Claude Skills Management System**
|
||||||
|
- Auto-scan skills from GitHub repositories (3 pre-configured curated repos)
|
||||||
|
- One-click install/uninstall to `~/.claude/skills/`
|
||||||
|
- Custom repository support + subdirectory scanning
|
||||||
|
- Complete lifecycle management (discover/install/update)
|
||||||
|
|
||||||
|
- **Prompts Management System**
|
||||||
|
- Multi-preset system prompt management (unlimited presets, quick switching)
|
||||||
|
- Cross-app support (Claude: `CLAUDE.md` / Codex: `AGENTS.md` / Gemini: `GEMINI.md`)
|
||||||
|
- Markdown editor (CodeMirror 6 + real-time preview)
|
||||||
|
- Smart backfill protection, preserves manual modifications
|
||||||
|
|
||||||
|
- **MCP v3.7.0 Unified Architecture**
|
||||||
|
- Single panel manages MCP servers across three applications
|
||||||
|
- New SSE (Server-Sent Events) transport type
|
||||||
|
- Smart JSON parser + Codex TOML format auto-correction
|
||||||
|
- Unified import/export + bidirectional sync
|
||||||
|
|
||||||
|
- **Deep Link Protocol**
|
||||||
|
- `ccswitch://` protocol registration (all platforms)
|
||||||
|
- One-click import provider configs via shared links
|
||||||
|
- Security validation + lifecycle integration
|
||||||
|
|
||||||
|
- **Environment Variable Conflict Detection**
|
||||||
|
- Auto-detect cross-app configuration conflicts (Claude/Codex/Gemini/MCP)
|
||||||
|
- Visual conflict indicators + resolution suggestions
|
||||||
|
- Override warnings + backup before changes
|
||||||
|
|
||||||
**Core Capabilities**
|
**Core Capabilities**
|
||||||
|
|
||||||
- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations
|
- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations
|
||||||
- **MCP Integration**: Centralized MCP server management with stdio/http support and real-time sync
|
|
||||||
- **Speed Testing**: Measure API endpoint latency with visual quality indicators
|
- **Speed Testing**: Measure API endpoint latency with visual quality indicators
|
||||||
- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent)
|
- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent)
|
||||||
- **i18n Support**: Complete Chinese/English localization (UI, errors, tray)
|
- **i18n Support**: Complete Chinese/English localization (UI, errors, tray)
|
||||||
@@ -61,7 +106,6 @@ Get 10% OFF the GLM CODING PLAN with [this link](https://z.ai/subscribe?ic=8JVLJ
|
|||||||
- Granular model configuration (4-tier: Haiku/Sonnet/Opus/Custom)
|
- Granular model configuration (4-tier: Haiku/Sonnet/Opus/Custom)
|
||||||
- WSL environment support with auto-sync on directory change
|
- WSL environment support with auto-sync on directory change
|
||||||
- 100% hooks test coverage & complete architecture refactoring
|
- 100% hooks test coverage & complete architecture refactoring
|
||||||
- New presets: DMXAPI, Azure Codex, AnyRouter, AiHubMix, MiniMax
|
|
||||||
|
|
||||||
**System Features**
|
**System Features**
|
||||||
|
|
||||||
@@ -103,6 +147,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.
|
||||||
@@ -121,9 +173,36 @@ Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{ver
|
|||||||
### MCP Management
|
### MCP Management
|
||||||
|
|
||||||
- **Location**: Click "MCP" button in top-right corner
|
- **Location**: Click "MCP" button in top-right corner
|
||||||
- **Add Server**: Use built-in templates (mcp-fetch, mcp-filesystem) or custom config
|
- **Add Server**:
|
||||||
|
- Use built-in templates (mcp-fetch, mcp-filesystem, etc.)
|
||||||
|
- Support stdio / http / sse transport types
|
||||||
|
- Configure independent MCP servers for different apps
|
||||||
- **Enable/Disable**: Toggle switches to control which servers sync to live config
|
- **Enable/Disable**: Toggle switches to control which servers sync to live config
|
||||||
- **Sync**: Enabled servers auto-sync to `~/.claude.json` (Claude) or `~/.codex/config.toml` (Codex)
|
- **Sync**: Enabled servers auto-sync to each app's live files
|
||||||
|
- **Import/Export**: Import existing MCP servers from Claude/Codex/Gemini config files
|
||||||
|
|
||||||
|
### Skills Management (v3.7.0 New)
|
||||||
|
|
||||||
|
- **Location**: Click "Skills" button in top-right corner
|
||||||
|
- **Discover Skills**:
|
||||||
|
- Auto-scan pre-configured GitHub repositories (Anthropic official, ComposioHQ, community, etc.)
|
||||||
|
- Add custom repositories (supports subdirectory scanning)
|
||||||
|
- **Install Skills**: Click "Install" to one-click install to `~/.claude/skills/`
|
||||||
|
- **Uninstall Skills**: Click "Uninstall" to safely remove and clean up state
|
||||||
|
- **Manage Repositories**: Add/remove custom GitHub repositories
|
||||||
|
|
||||||
|
### Prompts Management (v3.7.0 New)
|
||||||
|
|
||||||
|
- **Location**: Click "Prompts" button in top-right corner
|
||||||
|
- **Create Presets**:
|
||||||
|
- Create unlimited system prompt presets
|
||||||
|
- Use Markdown editor to write prompts (syntax highlighting + real-time preview)
|
||||||
|
- **Switch Presets**: Select preset → Click "Activate" to apply immediately
|
||||||
|
- **Sync Mechanism**:
|
||||||
|
- Claude: `~/.claude/CLAUDE.md`
|
||||||
|
- Codex: `~/.codex/AGENTS.md`
|
||||||
|
- Gemini: `~/.gemini/GEMINI.md`
|
||||||
|
- **Protection Mechanism**: Auto-save current prompt content before switching, preserves manual modifications
|
||||||
|
|
||||||
### Configuration Files
|
### Configuration Files
|
||||||
|
|
||||||
@@ -141,13 +220,15 @@ Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{ver
|
|||||||
|
|
||||||
**Gemini**
|
**Gemini**
|
||||||
|
|
||||||
- Live config: `~/.gemini/.env` (API key) + `~/.gemini/settings.json` (auth type for quick switching)
|
- Live config: `~/.gemini/.env` (API key) + `~/.gemini/settings.json` (auth mode)
|
||||||
- API key field: `GEMINI_API_KEY` inside `.env`
|
- API key field: `GEMINI_API_KEY` or `GOOGLE_GEMINI_API_KEY` in `.env`
|
||||||
- Tray quick switch: each provider switch rewrites `~/.gemini/.env` so the Gemini CLI picks up the new credentials immediately
|
- Environment variables: Support `GOOGLE_GEMINI_BASE_URL`, `GEMINI_MODEL`, etc.
|
||||||
|
- MCP servers: `~/.gemini/settings.json` → `mcpServers`
|
||||||
|
- Tray quick switch: Each provider switch rewrites `~/.gemini/.env`, no need to restart Gemini CLI
|
||||||
|
|
||||||
**CC Switch Storage**
|
**CC Switch Storage**
|
||||||
|
|
||||||
- Main config (SSOT): `~/.cc-switch/config.json`
|
- Main config (SSOT): `~/.cc-switch/config.json` (includes providers, MCP, Prompts presets, etc.)
|
||||||
- Settings: `~/.cc-switch/settings.json`
|
- Settings: `~/.cc-switch/settings.json`
|
||||||
- Backups: `~/.cc-switch/backups/` (auto-rotate, keep 10)
|
- Backups: `~/.cc-switch/backups/` (auto-rotate, keep 10)
|
||||||
|
|
||||||
|
|||||||
103
README_ZH.md
103
README_ZH.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Claude Code / Codex / Gemini CLI 全方位辅助工具
|
# Claude Code / Codex / Gemini CLI 全方位辅助工具
|
||||||
|
|
||||||
[](https://github.com/farion1231/cc-switch/releases)
|
[](https://github.com/farion1231/cc-switch/releases)
|
||||||
[](https://github.com/trending/typescript)
|
[](https://github.com/trending/typescript)
|
||||||
[](https://github.com/farion1231/cc-switch/releases)
|
[](https://github.com/farion1231/cc-switch/releases)
|
||||||
[](https://tauri.app/)
|
[](https://tauri.app/)
|
||||||
@@ -10,9 +10,11 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/15372" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15372" alt="farion1231%2Fcc-switch | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/15372" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15372" alt="farion1231%2Fcc-switch | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[English](README.md) | 中文 | [更新日志](CHANGELOG.md)
|
[English](README.md) | 中文 | [更新日志](CHANGELOG.md) | [📋 v3.7.0 发布说明](docs/release-note-v3.7.0-zh.md)
|
||||||
|
|
||||||
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置、MCP的桌面应用。
|
**从供应商切换器到 AI CLI 一体化管理平台**
|
||||||
|
|
||||||
|
统一管理 Claude Code、Codex 与 Gemini CLI 的供应商配置、MCP 服务器、Skills 扩展和系统提示词。
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,6 +35,12 @@ CC Switch 已经预设了智谱GLM,只需要填写 key 即可一键导入编
|
|||||||
<td width="180"><img src="assets/partners/logos/packycode.png" alt="PackyCode" width="150"></td>
|
<td width="180"><img src="assets/partners/logos/packycode.png" alt="PackyCode" width="150"></td>
|
||||||
<td>感谢 PackyCode 赞助了本项目!PackyCode 是一家稳定、高效的API中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。PackyCode 为本软件的用户提供了特别优惠,使用<a href="https://www.packyapi.com/register?aff=cc-switch">此链接</a>注册并在充值时填写"cc-switch"优惠码,可以享受9折优惠。</td>
|
<td>感谢 PackyCode 赞助了本项目!PackyCode 是一家稳定、高效的API中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。PackyCode 为本软件的用户提供了特别优惠,使用<a href="https://www.packyapi.com/register?aff=cc-switch">此链接</a>注册并在充值时填写"cc-switch"优惠码,可以享受9折优惠。</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="180"><img src="assets/partners/logos/sds-zh.png" alt="ShanDianShuo" width="150"></td>
|
||||||
|
<td>感谢闪电说赞助了本项目!闪电说是本地优先的 AI 语音输入法:毫秒级响应,数据不离设备;打字速度提升 4 倍,AI 智能纠错;绝对隐私安全,完全免费,配合 Claude Code 写代码效率翻倍!支持 Mac/Win 双平台,<a href="shandianshuo.cn">免费下载</a></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 界面预览
|
## 界面预览
|
||||||
@@ -45,10 +53,47 @@ CC Switch 已经预设了智谱GLM,只需要填写 key 即可一键导入编
|
|||||||
|
|
||||||
### 当前版本:v3.7.0 | [完整更新日志](CHANGELOG.md)
|
### 当前版本:v3.7.0 | [完整更新日志](CHANGELOG.md)
|
||||||
|
|
||||||
|
**v3.7.0 重大更新(2025-11-19)**
|
||||||
|
|
||||||
|
**六大核心功能,18,000+ 行新增代码**
|
||||||
|
|
||||||
|
- **Gemini CLI 集成**
|
||||||
|
- 第三个支持的 AI CLI(Claude Code / Codex / Gemini)
|
||||||
|
- 双文件配置支持(`.env` + `settings.json`)
|
||||||
|
- 完整 MCP 服务器管理
|
||||||
|
- 预设:Google Official (OAuth) / PackyCode / 自定义
|
||||||
|
|
||||||
|
- **Claude Skills 管理系统**
|
||||||
|
- 从 GitHub 仓库自动扫描技能(预配置 3 个精选仓库)
|
||||||
|
- 一键安装/卸载到 `~/.claude/skills/`
|
||||||
|
- 自定义仓库支持 + 子目录扫描
|
||||||
|
- 完整生命周期管理(发现/安装/更新)
|
||||||
|
|
||||||
|
- **Prompts 管理系统**
|
||||||
|
- 多预设系统提示词管理(无限数量,快速切换)
|
||||||
|
- 跨应用支持(Claude: `CLAUDE.md` / Codex: `AGENTS.md` / Gemini: `GEMINI.md`)
|
||||||
|
- Markdown 编辑器(CodeMirror 6 + 实时预览)
|
||||||
|
- 智能回填保护,保留手动修改
|
||||||
|
|
||||||
|
- **MCP v3.7.0 统一架构**
|
||||||
|
- 单一面板管理三个应用的 MCP 服务器
|
||||||
|
- 新增 SSE (Server-Sent Events) 传输类型
|
||||||
|
- 智能 JSON 解析器 + Codex TOML 格式自动修正
|
||||||
|
- 统一导入/导出 + 双向同步
|
||||||
|
|
||||||
|
- **深度链接协议**
|
||||||
|
- `ccswitch://` 协议注册(全平台)
|
||||||
|
- 通过共享链接一键导入供应商配置
|
||||||
|
- 安全验证 + 生命周期集成
|
||||||
|
|
||||||
|
- **环境变量冲突检测**
|
||||||
|
- 自动检测跨应用配置冲突(Claude/Codex/Gemini/MCP)
|
||||||
|
- 可视化冲突指示器 + 解决建议
|
||||||
|
- 覆盖警告 + 更改前备份
|
||||||
|
|
||||||
**核心功能**
|
**核心功能**
|
||||||
|
|
||||||
- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置
|
- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置
|
||||||
- **MCP 集成**:集中管理 MCP 服务器,支持 stdio/http 类型和实时同步
|
|
||||||
- **速度测试**:测量 API 端点延迟,可视化连接质量指示器
|
- **速度测试**:测量 API 端点延迟,可视化连接质量指示器
|
||||||
- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个)
|
- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个)
|
||||||
- **国际化支持**:完整的中英文本地化(UI、错误、托盘)
|
- **国际化支持**:完整的中英文本地化(UI、错误、托盘)
|
||||||
@@ -61,7 +106,6 @@ CC Switch 已经预设了智谱GLM,只需要填写 key 即可一键导入编
|
|||||||
- 细粒度模型配置(四层:Haiku/Sonnet/Opus/自定义)
|
- 细粒度模型配置(四层:Haiku/Sonnet/Opus/自定义)
|
||||||
- WSL 环境支持,配置目录切换自动同步
|
- WSL 环境支持,配置目录切换自动同步
|
||||||
- 100% hooks 测试覆盖 & 完整架构重构
|
- 100% hooks 测试覆盖 & 完整架构重构
|
||||||
- 新增预设:DMXAPI、Azure Codex、AnyRouter、AiHubMix、MiniMax
|
|
||||||
|
|
||||||
**系统功能**
|
**系统功能**
|
||||||
|
|
||||||
@@ -103,6 +147,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` 安装包。
|
||||||
@@ -121,9 +173,36 @@ brew upgrade --cask cc-switch
|
|||||||
### MCP 管理
|
### MCP 管理
|
||||||
|
|
||||||
- **位置**:点击右上角"MCP"按钮
|
- **位置**:点击右上角"MCP"按钮
|
||||||
- **添加服务器**:使用内置模板(mcp-fetch、mcp-filesystem)或自定义配置
|
- **添加服务器**:
|
||||||
|
- 使用内置模板(mcp-fetch、mcp-filesystem 等)
|
||||||
|
- 支持 stdio / http / sse 三种传输类型
|
||||||
|
- 为不同应用配置独立的 MCP 服务器
|
||||||
- **启用/禁用**:切换开关以控制哪些服务器同步到 live 配置
|
- **启用/禁用**:切换开关以控制哪些服务器同步到 live 配置
|
||||||
- **同步**:启用的服务器自动同步到 `~/.claude.json`(Claude)或 `~/.codex/config.toml`(Codex)
|
- **同步**:启用的服务器自动同步到各应用的 live 文件
|
||||||
|
- **导入/导出**:支持从 Claude/Codex/Gemini 配置文件导入现有 MCP 服务器
|
||||||
|
|
||||||
|
### Skills 管理(v3.7.0 新增)
|
||||||
|
|
||||||
|
- **位置**:点击右上角"Skills"按钮
|
||||||
|
- **发现技能**:
|
||||||
|
- 自动扫描预配置的 GitHub 仓库(Anthropic 官方、ComposioHQ、社区等)
|
||||||
|
- 添加自定义仓库(支持子目录扫描)
|
||||||
|
- **安装技能**:点击"安装"一键安装到 `~/.claude/skills/`
|
||||||
|
- **卸载技能**:点击"卸载"安全移除并清理状态
|
||||||
|
- **管理仓库**:添加/删除自定义 GitHub 仓库
|
||||||
|
|
||||||
|
### Prompts 管理(v3.7.0 新增)
|
||||||
|
|
||||||
|
- **位置**:点击右上角"Prompts"按钮
|
||||||
|
- **创建预设**:
|
||||||
|
- 创建无限数量的系统提示词预设
|
||||||
|
- 使用 Markdown 编辑器编写提示词(语法高亮 + 实时预览)
|
||||||
|
- **切换预设**:选择预设 → 点击"激活"立即应用
|
||||||
|
- **同步机制**:
|
||||||
|
- Claude: `~/.claude/CLAUDE.md`
|
||||||
|
- Codex: `~/.codex/AGENTS.md`
|
||||||
|
- Gemini: `~/.gemini/GEMINI.md`
|
||||||
|
- **保护机制**:切换前自动保存当前提示词内容,保留手动修改
|
||||||
|
|
||||||
### 配置文件
|
### 配置文件
|
||||||
|
|
||||||
@@ -141,13 +220,15 @@ brew upgrade --cask cc-switch
|
|||||||
|
|
||||||
**Gemini**
|
**Gemini**
|
||||||
|
|
||||||
- Live 配置:`~/.gemini/.env`(API Key)+ `~/.gemini/settings.json`(保存认证模式,支持托盘快速切换)
|
- Live 配置:`~/.gemini/.env`(API Key)+ `~/.gemini/settings.json`(保存认证模式)
|
||||||
- API key 字段:`.env` 文件中的 `GEMINI_API_KEY`
|
- API key 字段:`.env` 文件中的 `GEMINI_API_KEY` 或 `GOOGLE_GEMINI_API_KEY`
|
||||||
- 托盘快速切换:每次切换供应商都会重写 `~/.gemini/.env`,Gemini CLI 无需额外操作即可使用新配置
|
- 环境变量:支持 `GOOGLE_GEMINI_BASE_URL`、`GEMINI_MODEL` 等自定义变量
|
||||||
|
- MCP 服务器:`~/.gemini/settings.json` → `mcpServers`
|
||||||
|
- 托盘快速切换:每次切换供应商都会重写 `~/.gemini/.env`,无需重启 Gemini CLI 即可生效
|
||||||
|
|
||||||
**CC Switch 存储**
|
**CC Switch 存储**
|
||||||
|
|
||||||
- 主配置(SSOT):`~/.cc-switch/config.json`
|
- 主配置(SSOT):`~/.cc-switch/config.json`(包含供应商、MCP、Prompts 预设等)
|
||||||
- 设置:`~/.cc-switch/settings.json`
|
- 设置:`~/.cc-switch/settings.json`
|
||||||
- 备份:`~/.cc-switch/backups/`(自动轮换,保留 10 个)
|
- 备份:`~/.cc-switch/backups/`(自动轮换,保留 10 个)
|
||||||
|
|
||||||
|
|||||||
BIN
assets/partners/logos/sds-en.png
Normal file
BIN
assets/partners/logos/sds-en.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
assets/partners/logos/sds-zh.png
Normal file
BIN
assets/partners/logos/sds-zh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
481
docs/release-note-v3.7.1-en.md
Normal file
481
docs/release-note-v3.7.1-en.md
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
# CC Switch v3.7.1
|
||||||
|
|
||||||
|
> Stability Enhancements and User Experience Improvements
|
||||||
|
|
||||||
|
**[中文更新说明 Chinese Documentation →](release-note-v3.7.1-zh.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.7.1 Updates
|
||||||
|
|
||||||
|
**Release Date**: 2025-11-22
|
||||||
|
**Code Changes**: 17 files, +524 / -81 lines
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Fix Third-Party Skills Installation Failure** (#268)
|
||||||
|
Fixed installation issues for skills repositories with custom subdirectories, now supports repos like `ComposioHQ/awesome-claude-skills` with subdirectories
|
||||||
|
|
||||||
|
- **Fix Gemini Configuration Persistence Issue**
|
||||||
|
Resolved the issue where settings.json edits in Gemini form were lost when switching providers
|
||||||
|
|
||||||
|
- **Prevent Dialogs from Closing on Overlay Click**
|
||||||
|
Added protection against clicking overlay/backdrop, preventing accidental form data loss across all 11 dialog components
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **Gemini Configuration Directory Support** (#255)
|
||||||
|
Added Gemini configuration directory option in settings, supports customizing `~/.gemini/` path
|
||||||
|
|
||||||
|
- **ArchLinux Installation Support** (#259)
|
||||||
|
Added AUR installation method: `paru -S cc-switch-bin`
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- **Skills Error Message i18n Enhancement**
|
||||||
|
Added 28+ detailed error messages (English & Chinese) with specific resolution suggestions, extended download timeout from 15s to 60s
|
||||||
|
|
||||||
|
- **Code Formatting**
|
||||||
|
Applied unified Rust and TypeScript code formatting standards
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the latest version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.7.0 Complete Release Notes
|
||||||
|
|
||||||
|
> From Provider Switcher to All-in-One AI CLI Management Platform
|
||||||
|
|
||||||
|
**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 `config.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+ / ArchLinux
|
||||||
|
|
||||||
|
### Download Links
|
||||||
|
|
||||||
|
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download:
|
||||||
|
|
||||||
|
- **Windows**: `CC-Switch-Windows.msi` or `-Portable.zip`
|
||||||
|
- **macOS**: `CC-Switch-macOS.tar.gz` or `.zip`
|
||||||
|
- **Linux**: `CC-Switch-Linux.AppImage` or `.deb`
|
||||||
|
- **ArchLinux**: `paru -S cc-switch-bin`
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
**ShanDianShuo** - Local-first AI voice input
|
||||||
|
[Free download](https://shandianshuo.cn) for Mac/Win
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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!**
|
||||||
481
docs/release-note-v3.7.1-zh.md
Normal file
481
docs/release-note-v3.7.1-zh.md
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
# CC Switch v3.7.1
|
||||||
|
|
||||||
|
> 稳定性增强与用户体验改进
|
||||||
|
|
||||||
|
**[English Version →](release-note-v3.7.1-en.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.7.1 更新内容
|
||||||
|
|
||||||
|
**发布日期**:2025-11-22
|
||||||
|
**代码变更**:17 个文件,+524 / -81 行
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
- **修复 Skills 第三方仓库安装失败** (#268)
|
||||||
|
修复使用自定义子目录的 skills 仓库无法安装的问题,支持类似 `ComposioHQ/awesome-claude-skills` 这样带子目录的仓库
|
||||||
|
|
||||||
|
- **修复 Gemini 配置持久化问题**
|
||||||
|
解决在 Gemini 表单中编辑 settings.json 后,切换供应商时修改丢失的问题
|
||||||
|
|
||||||
|
- **防止对话框意外关闭**
|
||||||
|
添加点击遮罩时的保护,避免误操作导致表单数据丢失,影响所有 11 个对话框组件
|
||||||
|
|
||||||
|
### 新增功能
|
||||||
|
|
||||||
|
- **Gemini 配置目录支持** (#255)
|
||||||
|
在设置中添加 Gemini 配置目录选项,支持自定义 `~/.gemini/` 路径
|
||||||
|
|
||||||
|
- **ArchLinux 安装支持** (#259)
|
||||||
|
添加 AUR 安装方式:`paru -S cc-switch-bin`
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
- **Skills 错误消息国际化增强**
|
||||||
|
新增 28+ 条详细错误消息(中英文),提供具体的解决建议,下载超时从 15 秒延长到 60 秒
|
||||||
|
|
||||||
|
- **代码格式化**
|
||||||
|
应用统一的 Rust 和 TypeScript 代码格式化标准
|
||||||
|
|
||||||
|
### 下载
|
||||||
|
|
||||||
|
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载最新版本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.7.0 完整更新说明
|
||||||
|
|
||||||
|
> 从供应商切换器到 AI CLI 一体化管理平台
|
||||||
|
|
||||||
|
**发布日期**: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、Table(shadcn/ui)
|
||||||
|
- **状态**:持久化存储在 `config.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 行)
|
||||||
|
- **前端**:PromptPanel(177)、PromptFormModal(160)、MarkdownEditor(159)
|
||||||
|
- **Hooks**:usePromptActions(152 行)
|
||||||
|
- **国际化**: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.52(Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术统计
|
||||||
|
|
||||||
|
```
|
||||||
|
总体变更:
|
||||||
|
- 提交数: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.15(Catalina)+
|
||||||
|
- **Linux**:Ubuntu 22.04+ / Debian 11+ / Fedora 34+ / ArchLinux
|
||||||
|
|
||||||
|
### 下载链接
|
||||||
|
|
||||||
|
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载:
|
||||||
|
|
||||||
|
- **Windows**:`CC-Switch-Windows.msi` 或 `-Portable.zip`
|
||||||
|
- **macOS**:`CC-Switch-macOS.tar.gz` 或 `.zip`
|
||||||
|
- **Linux**:`CC-Switch-Linux.AppImage` 或 `.deb`
|
||||||
|
- **ArchLinux**:`paru -S cc-switch-bin`
|
||||||
|
|
||||||
|
### Homebrew(macOS)
|
||||||
|
|
||||||
|
```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 & Gemini 集成实现
|
||||||
|
- [@farion1231](https://github.com/farion1231) - 从开发沦为 issue 回复机
|
||||||
|
- 社区成员的测试和反馈
|
||||||
|
|
||||||
|
### 赞助商
|
||||||
|
|
||||||
|
**智谱AI** - GLM CODING PLAN 赞助商
|
||||||
|
[使用此链接购买可享九折优惠](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII)
|
||||||
|
|
||||||
|
**PackyCode** - API 中转服务合作伙伴
|
||||||
|
[使用 "cc-switch" 优惠码注册享 9 折优惠](https://www.packyapi.com/register?aff=cc-switch)
|
||||||
|
|
||||||
|
**闪电说** - 本地优先的 AI 语音输入法
|
||||||
|
[免费下载](https://shandianshuo.cn) Mac/Win 双平台
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 反馈与支持
|
||||||
|
|
||||||
|
- **问题反馈**:[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 预览**(暂定):
|
||||||
|
|
||||||
|
- 本地代理功能
|
||||||
|
|
||||||
|
敬请期待更多更新!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Coding!**
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cc-switch",
|
"name": "cc-switch",
|
||||||
"version": "3.7.0",
|
"version": "3.7.1",
|
||||||
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm tauri dev",
|
"dev": "pnpm tauri dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.7.0"
|
version = "3.7.1"
|
||||||
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
||||||
authors = ["Jason Young"]
|
authors = ["Jason Young"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::error::format_skill_error;
|
||||||
use crate::services::skill::SkillState;
|
use crate::services::skill::SkillState;
|
||||||
use crate::services::{Skill, SkillRepo, SkillService};
|
use crate::services::{Skill, SkillRepo, SkillService};
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
@@ -45,24 +46,42 @@ pub async fn install_skill(
|
|||||||
let skill = skills
|
let skill = skills
|
||||||
.iter()
|
.iter()
|
||||||
.find(|s| s.directory.eq_ignore_ascii_case(&directory))
|
.find(|s| s.directory.eq_ignore_ascii_case(&directory))
|
||||||
.ok_or_else(|| "技能不存在".to_string())?;
|
.ok_or_else(|| {
|
||||||
|
format_skill_error(
|
||||||
|
"SKILL_NOT_FOUND",
|
||||||
|
&[("directory", &directory)],
|
||||||
|
Some("checkRepoUrl"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if !skill.installed {
|
if !skill.installed {
|
||||||
let repo = SkillRepo {
|
let repo = SkillRepo {
|
||||||
owner: skill
|
owner: skill
|
||||||
.repo_owner
|
.repo_owner
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| "缺少仓库信息".to_string())?,
|
.ok_or_else(|| {
|
||||||
|
format_skill_error(
|
||||||
|
"MISSING_REPO_INFO",
|
||||||
|
&[("directory", &directory), ("field", "owner")],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})?,
|
||||||
name: skill
|
name: skill
|
||||||
.repo_name
|
.repo_name
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| "缺少仓库信息".to_string())?,
|
.ok_or_else(|| {
|
||||||
|
format_skill_error(
|
||||||
|
"MISSING_REPO_INFO",
|
||||||
|
&[("directory", &directory), ("field", "name")],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})?,
|
||||||
branch: skill
|
branch: skill
|
||||||
.repo_branch
|
.repo_branch
|
||||||
.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
|
||||||
|
|||||||
@@ -94,3 +94,28 @@ impl From<AppError> for String {
|
|||||||
err.to_string()
|
err.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 格式化为 JSON 错误字符串,前端可解析为结构化错误
|
||||||
|
pub fn format_skill_error(
|
||||||
|
code: &str,
|
||||||
|
context: &[(&str, &str)],
|
||||||
|
suggestion: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
let mut ctx_map = serde_json::Map::new();
|
||||||
|
for (key, value) in context {
|
||||||
|
ctx_map.insert(key.to_string(), json!(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_obj = json!({
|
||||||
|
"code": code,
|
||||||
|
"context": ctx_map,
|
||||||
|
"suggestion": suggestion,
|
||||||
|
});
|
||||||
|
|
||||||
|
serde_json::to_string(&error_obj).unwrap_or_else(|_| {
|
||||||
|
// 如果 JSON 序列化失败,返回简单格式
|
||||||
|
format!("ERROR:{}", code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 官方),跳过验证
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1461,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() {
|
||||||
@@ -1477,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(¤t_id) {
|
if let Some(current) = manager.providers.get_mut(¤t_id) {
|
||||||
current.settings_config = live;
|
current.settings_config = live;
|
||||||
@@ -1495,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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
use crate::error::format_skill_error;
|
||||||
|
|
||||||
/// 技能对象
|
/// 技能对象
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Skill {
|
pub struct Skill {
|
||||||
@@ -32,6 +34,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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 仓库配置
|
/// 仓库配置
|
||||||
@@ -130,7 +135,11 @@ impl SkillService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_install_dir() -> Result<PathBuf> {
|
fn get_install_dir() -> Result<PathBuf> {
|
||||||
let home = dirs::home_dir().context("无法获取用户主目录")?;
|
let home = dirs::home_dir().context(format_skill_error(
|
||||||
|
"GET_HOME_DIR_FAILED",
|
||||||
|
&[],
|
||||||
|
Some("checkPermission"),
|
||||||
|
))?;
|
||||||
Ok(home.join(".claude").join("skills"))
|
Ok(home.join(".claude").join("skills"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,9 +179,19 @@ impl SkillService {
|
|||||||
/// 从仓库获取技能列表
|
/// 从仓库获取技能列表
|
||||||
async fn fetch_repo_skills(&self, repo: &SkillRepo) -> Result<Vec<Skill>> {
|
async fn fetch_repo_skills(&self, repo: &SkillRepo) -> Result<Vec<Skill>> {
|
||||||
// 为单个仓库加载增加整体超时,避免无效链接长时间阻塞
|
// 为单个仓库加载增加整体超时,避免无效链接长时间阻塞
|
||||||
let temp_dir = timeout(std::time::Duration::from_secs(15), self.download_repo(repo))
|
let temp_dir = timeout(std::time::Duration::from_secs(60), self.download_repo(repo))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
|
.map_err(|_| {
|
||||||
|
anyhow!(format_skill_error(
|
||||||
|
"DOWNLOAD_TIMEOUT",
|
||||||
|
&[
|
||||||
|
("owner", &repo.owner),
|
||||||
|
("name", &repo.name),
|
||||||
|
("timeout", "60")
|
||||||
|
],
|
||||||
|
Some("checkNetwork"),
|
||||||
|
))
|
||||||
|
})??;
|
||||||
let mut skills = Vec::new();
|
let mut skills = Vec::new();
|
||||||
|
|
||||||
// 确定要扫描的目录路径
|
// 确定要扫描的目录路径
|
||||||
@@ -234,6 +253,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 +332,7 @@ impl SkillService {
|
|||||||
repo_owner: None,
|
repo_owner: None,
|
||||||
repo_name: None,
|
repo_name: None,
|
||||||
repo_branch: None,
|
repo_branch: None,
|
||||||
|
skills_path: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,7 +395,17 @@ impl SkillService {
|
|||||||
// 下载 ZIP
|
// 下载 ZIP
|
||||||
let response = self.http_client.get(url).send().await?;
|
let response = self.http_client.get(url).send().await?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(anyhow::anyhow!("下载失败: {}", response.status()));
|
let status = response.status().as_u16().to_string();
|
||||||
|
return Err(anyhow::anyhow!(format_skill_error(
|
||||||
|
"DOWNLOAD_FAILED",
|
||||||
|
&[("status", &status)],
|
||||||
|
match status.as_str() {
|
||||||
|
"403" => Some("http403"),
|
||||||
|
"404" => Some("http404"),
|
||||||
|
"429" => Some("http429"),
|
||||||
|
_ => Some("checkNetwork"),
|
||||||
|
},
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = response.bytes().await?;
|
let bytes = response.bytes().await?;
|
||||||
@@ -389,7 +420,11 @@ impl SkillService {
|
|||||||
let name = first_file.name();
|
let name = first_file.name();
|
||||||
name.split('/').next().unwrap_or("").to_string()
|
name.split('/').next().unwrap_or("").to_string()
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!("空的压缩包"));
|
return Err(anyhow::anyhow!(format_skill_error(
|
||||||
|
"EMPTY_ARCHIVE",
|
||||||
|
&[],
|
||||||
|
Some("checkRepoUrl"),
|
||||||
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解压所有文件
|
// 解压所有文件
|
||||||
@@ -436,18 +471,38 @@ impl SkillService {
|
|||||||
|
|
||||||
// 下载仓库时增加总超时,防止无效链接导致长时间卡住安装过程
|
// 下载仓库时增加总超时,防止无效链接导致长时间卡住安装过程
|
||||||
let temp_dir = timeout(
|
let temp_dir = timeout(
|
||||||
std::time::Duration::from_secs(15),
|
std::time::Duration::from_secs(60),
|
||||||
self.download_repo(&repo),
|
self.download_repo(&repo),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
|
.map_err(|_| {
|
||||||
|
anyhow!(format_skill_error(
|
||||||
|
"DOWNLOAD_TIMEOUT",
|
||||||
|
&[
|
||||||
|
("owner", &repo.owner),
|
||||||
|
("name", &repo.name),
|
||||||
|
("timeout", "60")
|
||||||
|
],
|
||||||
|
Some("checkNetwork"),
|
||||||
|
))
|
||||||
|
})??;
|
||||||
|
|
||||||
// 复制到安装目录
|
// 根据 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!(format_skill_error(
|
||||||
|
"SKILL_DIR_NOT_FOUND",
|
||||||
|
&[("path", &source.display().to_string())],
|
||||||
|
Some("checkRepoUrl"),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除旧版本
|
// 删除旧版本
|
||||||
|
|||||||
@@ -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.7.0",
|
"version": "3.7.1",
|
||||||
"identifier": "com.ccswitch.desktop",
|
"identifier": "com.ccswitch.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
|||||||
import { SkillCard } from "./SkillCard";
|
import { SkillCard } from "./SkillCard";
|
||||||
import { RepoManager } from "./RepoManager";
|
import { RepoManager } from "./RepoManager";
|
||||||
import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills";
|
import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills";
|
||||||
|
import { formatSkillError } from "@/lib/errors/skillErrorParser";
|
||||||
|
|
||||||
interface SkillsPageProps {
|
interface SkillsPageProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -27,9 +28,22 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
|
|||||||
afterLoad(data);
|
afterLoad(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("skills.loadFailed"), {
|
const errorMessage =
|
||||||
description: error instanceof Error ? error.message : t("common.error"),
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
// 传入 "skills.loadFailed" 作为标题
|
||||||
|
const { title, description } = formatSkillError(
|
||||||
|
errorMessage,
|
||||||
|
t,
|
||||||
|
"skills.loadFailed",
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.error(title, {
|
||||||
|
description,
|
||||||
|
duration: 8000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.error("Load skills failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -54,8 +68,26 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
|
|||||||
toast.success(t("skills.installSuccess", { name: directory }));
|
toast.success(t("skills.installSuccess", { name: directory }));
|
||||||
await loadSkills();
|
await loadSkills();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("skills.installFailed"), {
|
const errorMessage =
|
||||||
description: error instanceof Error ? error.message : t("common.error"),
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
// 使用错误解析器格式化错误,传入 "skills.installFailed"
|
||||||
|
const { title, description } = formatSkillError(
|
||||||
|
errorMessage,
|
||||||
|
t,
|
||||||
|
"skills.installFailed",
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.error(title, {
|
||||||
|
description,
|
||||||
|
duration: 10000, // 延长显示时间让用户看清
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打印到控制台方便调试
|
||||||
|
console.error("Install skill failed:", {
|
||||||
|
directory,
|
||||||
|
error,
|
||||||
|
message: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -66,8 +98,25 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
|
|||||||
toast.success(t("skills.uninstallSuccess", { name: directory }));
|
toast.success(t("skills.uninstallSuccess", { name: directory }));
|
||||||
await loadSkills();
|
await loadSkills();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("skills.uninstallFailed"), {
|
const errorMessage =
|
||||||
description: error instanceof Error ? error.message : t("common.error"),
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
// 使用错误解析器格式化错误,传入 "skills.uninstallFailed"
|
||||||
|
const { title, description } = formatSkillError(
|
||||||
|
errorMessage,
|
||||||
|
t,
|
||||||
|
"skills.uninstallFailed",
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.error(title, {
|
||||||
|
description,
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error("Uninstall skill failed:", {
|
||||||
|
directory,
|
||||||
|
error,
|
||||||
|
message: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ const DialogContent = React.forwardRef<
|
|||||||
zIndexMap[zIndex],
|
zIndexMap[zIndex],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onInteractOutside={(e) => {
|
||||||
|
// 防止点击遮罩层关闭对话框
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -672,6 +675,34 @@
|
|||||||
"installFailed": "Failed to install",
|
"installFailed": "Failed to install",
|
||||||
"uninstallSuccess": "Skill {{name}} uninstalled",
|
"uninstallSuccess": "Skill {{name}} uninstalled",
|
||||||
"uninstallFailed": "Failed to uninstall",
|
"uninstallFailed": "Failed to uninstall",
|
||||||
|
"error": {
|
||||||
|
"skillNotFound": "Skill not found: {{directory}}",
|
||||||
|
"missingRepoInfo": "Missing repository info (owner or name)",
|
||||||
|
"downloadTimeout": "Download repository {{owner}}/{{name}} timeout ({{timeout}}s)",
|
||||||
|
"downloadTimeoutHint": "Please check network connection or retry later",
|
||||||
|
"skillPathNotFound": "Skill path '{{path}}' not found in repository {{owner}}/{{name}}",
|
||||||
|
"skillDirNotFound": "Skill directory not found: {{path}}",
|
||||||
|
"emptyArchive": "Downloaded archive is empty",
|
||||||
|
"downloadFailed": "Download failed: HTTP {{status}}",
|
||||||
|
"allBranchesFailed": "All branches failed, tried: {{branches}}",
|
||||||
|
"httpError": "HTTP error {{status}}",
|
||||||
|
"http403": "GitHub access restricted, possibly rate limited",
|
||||||
|
"http404": "Repository or branch not found, please check URL",
|
||||||
|
"http429": "Too many requests, please wait and retry",
|
||||||
|
"parseMetadataFailed": "Failed to parse skill metadata",
|
||||||
|
"getHomeDirFailed": "Unable to get user home directory",
|
||||||
|
"networkError": "Network error",
|
||||||
|
"fsError": "File system error",
|
||||||
|
"unknownError": "Unknown error",
|
||||||
|
"suggestion": {
|
||||||
|
"checkNetwork": "Please check network connection",
|
||||||
|
"checkProxy": "Consider configuring HTTP proxy",
|
||||||
|
"retryLater": "Please retry later",
|
||||||
|
"checkRepoUrl": "Please check repository URL and branch name",
|
||||||
|
"checkDiskSpace": "Please check disk space",
|
||||||
|
"checkPermission": "Please check directory permissions"
|
||||||
|
}
|
||||||
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
"title": "Manage Skill Repositories",
|
"title": "Manage Skill Repositories",
|
||||||
"description": "Add or remove GitHub skill repository sources",
|
"description": "Add or remove GitHub skill repository sources",
|
||||||
|
|||||||
@@ -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": "检查更新",
|
||||||
@@ -672,6 +675,34 @@
|
|||||||
"installFailed": "安装失败",
|
"installFailed": "安装失败",
|
||||||
"uninstallSuccess": "技能 {{name}} 已卸载",
|
"uninstallSuccess": "技能 {{name}} 已卸载",
|
||||||
"uninstallFailed": "卸载失败",
|
"uninstallFailed": "卸载失败",
|
||||||
|
"error": {
|
||||||
|
"skillNotFound": "技能不存在:{{directory}}",
|
||||||
|
"missingRepoInfo": "缺少仓库信息(owner 或 name)",
|
||||||
|
"downloadTimeout": "下载仓库 {{owner}}/{{name}} 超时({{timeout}}秒)",
|
||||||
|
"downloadTimeoutHint": "请检查网络连接或稍后重试",
|
||||||
|
"skillPathNotFound": "仓库 {{owner}}/{{name}} 中未找到技能路径 '{{path}}'",
|
||||||
|
"skillDirNotFound": "技能目录不存在:{{path}}",
|
||||||
|
"emptyArchive": "下载的压缩包为空",
|
||||||
|
"downloadFailed": "下载失败:HTTP {{status}}",
|
||||||
|
"allBranchesFailed": "所有分支下载失败,尝试了:{{branches}}",
|
||||||
|
"httpError": "HTTP 错误 {{status}}",
|
||||||
|
"http403": "GitHub 访问受限,可能是请求频率过高",
|
||||||
|
"http404": "仓库或分支不存在,请检查地址",
|
||||||
|
"http429": "请求过于频繁,请等待后重试",
|
||||||
|
"parseMetadataFailed": "解析技能元数据失败",
|
||||||
|
"getHomeDirFailed": "无法获取用户主目录",
|
||||||
|
"networkError": "网络错误",
|
||||||
|
"fsError": "文件系统错误",
|
||||||
|
"unknownError": "未知错误",
|
||||||
|
"suggestion": {
|
||||||
|
"checkNetwork": "请检查网络连接",
|
||||||
|
"checkProxy": "建议配置 HTTP 代理",
|
||||||
|
"retryLater": "请稍后重试",
|
||||||
|
"checkRepoUrl": "请检查仓库地址和分支名称",
|
||||||
|
"checkDiskSpace": "请检查磁盘空间",
|
||||||
|
"checkPermission": "请检查目录权限"
|
||||||
|
}
|
||||||
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
"title": "管理技能仓库",
|
"title": "管理技能仓库",
|
||||||
"description": "添加或删除 GitHub 技能仓库源",
|
"description": "添加或删除 GitHub 技能仓库源",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
104
src/lib/errors/skillErrorParser.ts
Normal file
104
src/lib/errors/skillErrorParser.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { TFunction } from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结构化错误对象
|
||||||
|
*/
|
||||||
|
export interface SkillError {
|
||||||
|
code: string;
|
||||||
|
context: Record<string, string>;
|
||||||
|
suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试解析后端返回的错误字符串
|
||||||
|
* 如果是 JSON 格式,返回结构化错误;否则返回 null
|
||||||
|
*/
|
||||||
|
export function parseSkillError(errorString: string): SkillError | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(errorString);
|
||||||
|
if (parsed.code && parsed.context) {
|
||||||
|
return parsed as SkillError;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 不是 JSON 格式,返回 null
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将错误码映射到 i18n key
|
||||||
|
*/
|
||||||
|
function getErrorI18nKey(code: string): string {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
SKILL_NOT_FOUND: "skills.error.skillNotFound",
|
||||||
|
MISSING_REPO_INFO: "skills.error.missingRepoInfo",
|
||||||
|
DOWNLOAD_TIMEOUT: "skills.error.downloadTimeout",
|
||||||
|
DOWNLOAD_FAILED: "skills.error.downloadFailed",
|
||||||
|
SKILL_DIR_NOT_FOUND: "skills.error.skillDirNotFound",
|
||||||
|
EMPTY_ARCHIVE: "skills.error.emptyArchive",
|
||||||
|
GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed",
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapping[code] || "skills.error.unknownError";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将建议码映射到 i18n key
|
||||||
|
*/
|
||||||
|
function getSuggestionI18nKey(suggestion: string): string {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
checkNetwork: "skills.error.suggestion.checkNetwork",
|
||||||
|
checkProxy: "skills.error.suggestion.checkProxy",
|
||||||
|
retryLater: "skills.error.suggestion.retryLater",
|
||||||
|
checkRepoUrl: "skills.error.suggestion.checkRepoUrl",
|
||||||
|
checkPermission: "skills.error.suggestion.checkPermission",
|
||||||
|
http403: "skills.error.http403",
|
||||||
|
http404: "skills.error.http404",
|
||||||
|
http429: "skills.error.http429",
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapping[suggestion] || suggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化技能错误为用户友好的消息
|
||||||
|
* @param errorString 后端返回的错误字符串
|
||||||
|
* @param t i18next 翻译函数
|
||||||
|
* @param defaultTitle 默认标题的 i18n key(如 "skills.installFailed")
|
||||||
|
* @returns 包含标题和描述的对象
|
||||||
|
*/
|
||||||
|
export function formatSkillError(
|
||||||
|
errorString: string,
|
||||||
|
t: TFunction,
|
||||||
|
defaultTitle: string = "skills.installFailed",
|
||||||
|
): { title: string; description: string } {
|
||||||
|
const parsedError = parseSkillError(errorString);
|
||||||
|
|
||||||
|
if (!parsedError) {
|
||||||
|
// 如果不是结构化错误,返回原始错误字符串
|
||||||
|
return {
|
||||||
|
title: t(defaultTitle),
|
||||||
|
description: errorString || t("common.error"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, context, suggestion } = parsedError;
|
||||||
|
|
||||||
|
// 获取错误消息的 i18n key
|
||||||
|
const errorKey = getErrorI18nKey(code);
|
||||||
|
|
||||||
|
// 构建描述(错误消息 + 建议)
|
||||||
|
let description = t(errorKey, context);
|
||||||
|
|
||||||
|
// 如果有建议,追加到描述中
|
||||||
|
if (suggestion) {
|
||||||
|
const suggestionKey = getSuggestionI18nKey(suggestion);
|
||||||
|
const suggestionText = t(suggestionKey);
|
||||||
|
description += `\n\n${suggestionText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t(defaultTitle),
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 自定义端点列表
|
||||||
|
|||||||
Reference in New Issue
Block a user