mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-09 12:41:29 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de0758eba4 | |||
| 22552f7a7f | |||
| 1c6689a0bc | |||
| 9404341f14 | |||
| 53dd0a90f3 | |||
| 779fefd86d | |||
| 096c1d57c4 | |||
| adb868d0cf | |||
| a6ad896db0 | |||
| d6cf4390ac |
@@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [3.10.2] - 2026-01-24
|
||||
|
||||
### Patch Release
|
||||
|
||||
This maintenance release adds skill sync options and includes important bug fixes.
|
||||
|
||||
### Added
|
||||
|
||||
- **Skills**: Add skill sync method setting with symlink/copy options
|
||||
- **Partners**: Add RightCode as official partner
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Prompts**: Clear prompt file when all prompts are disabled
|
||||
- **OpenCode**: Preserve extra model fields during serialization
|
||||
- **Provider Form**: Backfill model fields when editing Claude provider
|
||||
|
||||
---
|
||||
|
||||
## [3.10.1] - 2026-01-23
|
||||
|
||||
### Patch Release
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# 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/farion1231/cc-switch/releases)
|
||||
[](https://tauri.app/)
|
||||
[](https://github.com/farion1231/cc-switch/releases/latest)
|
||||
@@ -52,7 +52,7 @@ This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.GLM
|
||||
|
||||
## Features
|
||||
|
||||
### Current Version: v3.10.0 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.9.0-en.md)
|
||||
### Current Version: v3.10.2 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.9.0-en.md)
|
||||
|
||||
**v3.8.0 Major Update (2025-11-28)**
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
# Claude Code / Codex / Gemini CLI オールインワン・アシスタント
|
||||
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://tauri.app/)
|
||||
[](https://github.com/farion1231/cc-switch/releases/latest)
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
## 特長
|
||||
|
||||
### 現在のバージョン:v3.10.0 | [完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.9.0-ja.md)
|
||||
### 現在のバージョン:v3.10.2 | [完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.9.0-ja.md)
|
||||
|
||||
**v3.8.0 メジャーアップデート (2025-11-28)**
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
# Claude Code / Codex / Gemini CLI 全方位辅助工具
|
||||
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://tauri.app/)
|
||||
[](https://github.com/farion1231/cc-switch/releases/latest)
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 当前版本:v3.10.0 | [完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.9.0-zh.md)
|
||||
### 当前版本:v3.10.2 | [完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.9.0-zh.md)
|
||||
|
||||
**v3.8.0 重大更新(2025-11-28)**
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cc-switch",
|
||||
"version": "3.10.1",
|
||||
"version": "3.10.2",
|
||||
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -701,7 +701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc-switch"
|
||||
version = "3.10.1"
|
||||
version = "3.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cc-switch"
|
||||
version = "3.10.1"
|
||||
version = "3.10.2"
|
||||
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
||||
authors = ["Jason Young"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -596,6 +596,11 @@ pub struct OpenCodeModel {
|
||||
/// 模型额外选项(provider 路由等)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<HashMap<String, Value>>,
|
||||
|
||||
/// 额外字段(cost、modalities、thinking、variants 等)
|
||||
/// 使用 flatten 捕获所有未明确定义的字段
|
||||
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// OpenCode 模型限制
|
||||
|
||||
@@ -36,10 +36,22 @@ impl PromptService {
|
||||
|
||||
state.db.save_prompt(app.as_str(), &prompt)?;
|
||||
|
||||
// 如果是已启用的提示词,同步更新到对应的文件
|
||||
if is_enabled {
|
||||
// 启用提示词:写入内容到文件
|
||||
let target_path = prompt_file_path(&app)?;
|
||||
write_text_file(&target_path, &prompt.content)?;
|
||||
} else {
|
||||
// 禁用提示词:检查是否还有其他已启用的提示词
|
||||
let prompts = state.db.get_prompts(app.as_str())?;
|
||||
let any_enabled = prompts.values().any(|p| p.enabled);
|
||||
|
||||
if !any_enabled {
|
||||
// 所有提示词都已禁用,清空文件
|
||||
let target_path = prompt_file_path(&app)?;
|
||||
if target_path.exists() {
|
||||
write_text_file(&target_path, "")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+158
-14
@@ -21,6 +21,19 @@ use crate::error::format_skill_error;
|
||||
|
||||
// ========== 数据结构 ==========
|
||||
|
||||
/// Skill 同步方式
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SyncMethod {
|
||||
/// 自动选择:优先 symlink,失败时回退到 copy
|
||||
#[default]
|
||||
Auto,
|
||||
/// 符号链接(推荐,节省磁盘空间)
|
||||
Symlink,
|
||||
/// 文件复制(兼容模式)
|
||||
Copy,
|
||||
}
|
||||
|
||||
/// 可发现的技能(来自仓库)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscoverableSkill {
|
||||
@@ -239,6 +252,50 @@ impl SkillService {
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| skill.directory.clone());
|
||||
|
||||
// 检查数据库中是否已有同名 directory 的 skill(来自其他仓库)
|
||||
let existing_skills = db.get_all_installed_skills()?;
|
||||
for existing in existing_skills.values() {
|
||||
if existing.directory.eq_ignore_ascii_case(&install_name) {
|
||||
// 检查是否来自同一仓库
|
||||
let same_repo = existing.repo_owner.as_deref() == Some(&skill.repo_owner)
|
||||
&& existing.repo_name.as_deref() == Some(&skill.repo_name);
|
||||
if same_repo {
|
||||
// 同一仓库的同名 skill,返回现有记录(可能需要更新启用状态)
|
||||
let mut updated = existing.clone();
|
||||
updated.apps.set_enabled_for(current_app, true);
|
||||
db.save_skill(&updated)?;
|
||||
Self::sync_to_app_dir(&updated.directory, current_app)?;
|
||||
log::info!(
|
||||
"Skill {} 已存在,更新 {:?} 启用状态",
|
||||
updated.name,
|
||||
current_app
|
||||
);
|
||||
return Ok(updated);
|
||||
} else {
|
||||
// 不同仓库的同名 skill,报错
|
||||
return Err(anyhow!(format_skill_error(
|
||||
"SKILL_DIRECTORY_CONFLICT",
|
||||
&[
|
||||
("directory", &install_name),
|
||||
(
|
||||
"existing_repo",
|
||||
&format!(
|
||||
"{}/{}",
|
||||
existing.repo_owner.as_deref().unwrap_or("unknown"),
|
||||
existing.repo_name.as_deref().unwrap_or("unknown")
|
||||
)
|
||||
),
|
||||
(
|
||||
"new_repo",
|
||||
&format!("{}/{}", skill.repo_owner, skill.repo_name)
|
||||
),
|
||||
],
|
||||
Some("uninstallFirst"),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dest = ssot_dir.join(&install_name);
|
||||
|
||||
// 如果已存在则跳过下载
|
||||
@@ -305,7 +362,7 @@ impl SkillService {
|
||||
db.save_skill(&installed_skill)?;
|
||||
|
||||
// 同步到当前应用目录
|
||||
Self::copy_to_app(&install_name, current_app)?;
|
||||
Self::sync_to_app_dir(&install_name, current_app)?;
|
||||
|
||||
log::info!(
|
||||
"Skill {} 安装成功,已启用 {:?}",
|
||||
@@ -368,7 +425,7 @@ impl SkillService {
|
||||
|
||||
// 同步文件
|
||||
if enabled {
|
||||
Self::copy_to_app(&skill.directory, app)?;
|
||||
Self::sync_to_app_dir(&skill.directory, app)?;
|
||||
} else {
|
||||
Self::remove_from_app(&skill.directory, app)?;
|
||||
}
|
||||
@@ -566,8 +623,41 @@ impl SkillService {
|
||||
|
||||
// ========== 文件同步方法 ==========
|
||||
|
||||
/// 复制 Skill 到应用目录
|
||||
pub fn copy_to_app(directory: &str, app: &AppType) -> Result<()> {
|
||||
/// 创建符号链接(跨平台)
|
||||
///
|
||||
/// - Unix: 使用 std::os::unix::fs::symlink
|
||||
/// - Windows: 使用 std::os::windows::fs::symlink_dir
|
||||
#[cfg(unix)]
|
||||
fn create_symlink(src: &Path, dest: &Path) -> Result<()> {
|
||||
std::os::unix::fs::symlink(src, dest)
|
||||
.with_context(|| format!("创建符号链接失败: {} -> {}", src.display(), dest.display()))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn create_symlink(src: &Path, dest: &Path) -> Result<()> {
|
||||
std::os::windows::fs::symlink_dir(src, dest)
|
||||
.with_context(|| format!("创建符号链接失败: {} -> {}", src.display(), dest.display()))
|
||||
}
|
||||
|
||||
/// 检查路径是否为符号链接
|
||||
fn is_symlink(path: &Path) -> bool {
|
||||
path.symlink_metadata()
|
||||
.map(|m| m.file_type().is_symlink())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 获取当前同步方式配置
|
||||
fn get_sync_method() -> SyncMethod {
|
||||
crate::settings::get_skill_sync_method()
|
||||
}
|
||||
|
||||
/// 同步 Skill 到应用目录(使用 symlink 或 copy)
|
||||
///
|
||||
/// 根据配置和平台选择最佳同步方式:
|
||||
/// - Auto: 优先尝试 symlink,失败时回退到 copy
|
||||
/// - Symlink: 仅使用 symlink
|
||||
/// - Copy: 仅使用文件复制
|
||||
pub fn sync_to_app_dir(directory: &str, app: &AppType) -> Result<()> {
|
||||
let ssot_dir = Self::get_ssot_dir()?;
|
||||
let source = ssot_dir.join(directory);
|
||||
|
||||
@@ -580,25 +670,77 @@ impl SkillService {
|
||||
|
||||
let dest = app_dir.join(directory);
|
||||
|
||||
// 如果已存在则先删除
|
||||
if dest.exists() {
|
||||
fs::remove_dir_all(&dest)?;
|
||||
// 如果已存在则先删除(无论是 symlink 还是真实目录)
|
||||
if dest.exists() || Self::is_symlink(&dest) {
|
||||
Self::remove_path(&dest)?;
|
||||
}
|
||||
|
||||
Self::copy_dir_recursive(&source, &dest)?;
|
||||
let sync_method = Self::get_sync_method();
|
||||
|
||||
log::debug!("Skill {directory} 已复制到 {app:?}");
|
||||
match sync_method {
|
||||
SyncMethod::Auto => {
|
||||
// 优先尝试 symlink
|
||||
match Self::create_symlink(&source, &dest) {
|
||||
Ok(()) => {
|
||||
log::debug!("Skill {directory} 已通过 symlink 同步到 {app:?}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Symlink 创建失败,将回退到文件复制: {} -> {}. 错误: {err:#}",
|
||||
source.display(),
|
||||
dest.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
// Fallback 到 copy
|
||||
Self::copy_dir_recursive(&source, &dest)?;
|
||||
log::debug!("Skill {directory} 已通过复制同步到 {app:?}");
|
||||
}
|
||||
SyncMethod::Symlink => {
|
||||
Self::create_symlink(&source, &dest)?;
|
||||
log::debug!("Skill {directory} 已通过 symlink 同步到 {app:?}");
|
||||
}
|
||||
SyncMethod::Copy => {
|
||||
Self::copy_dir_recursive(&source, &dest)?;
|
||||
log::debug!("Skill {directory} 已通过复制同步到 {app:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从应用目录删除 Skill
|
||||
/// 复制 Skill 到应用目录(保留用于向后兼容)
|
||||
#[deprecated(note = "请使用 sync_to_app_dir() 代替")]
|
||||
pub fn copy_to_app(directory: &str, app: &AppType) -> Result<()> {
|
||||
Self::sync_to_app_dir(directory, app)
|
||||
}
|
||||
|
||||
/// 删除路径(支持 symlink 和真实目录)
|
||||
fn remove_path(path: &Path) -> Result<()> {
|
||||
if Self::is_symlink(path) {
|
||||
// 符号链接:仅删除链接本身,不影响源文件
|
||||
#[cfg(unix)]
|
||||
fs::remove_file(path)?;
|
||||
#[cfg(windows)]
|
||||
fs::remove_dir(path)?; // Windows 的目录 symlink 需要用 remove_dir
|
||||
} else if path.is_dir() {
|
||||
// 真实目录:递归删除
|
||||
fs::remove_dir_all(path)?;
|
||||
} else if path.exists() {
|
||||
// 普通文件
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从应用目录删除 Skill(支持 symlink 和真实目录)
|
||||
pub fn remove_from_app(directory: &str, app: &AppType) -> Result<()> {
|
||||
let app_dir = Self::get_app_skills_dir(app)?;
|
||||
let skill_path = app_dir.join(directory);
|
||||
|
||||
if skill_path.exists() {
|
||||
fs::remove_dir_all(&skill_path)?;
|
||||
if skill_path.exists() || Self::is_symlink(&skill_path) {
|
||||
Self::remove_path(&skill_path)?;
|
||||
log::debug!("Skill {directory} 已从 {app:?} 删除");
|
||||
}
|
||||
|
||||
@@ -611,7 +753,7 @@ impl SkillService {
|
||||
|
||||
for skill in skills.values() {
|
||||
if skill.apps.is_enabled_for(app) {
|
||||
Self::copy_to_app(&skill.directory, app)?;
|
||||
Self::sync_to_app_dir(&skill.directory, app)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,10 +977,12 @@ impl SkillService {
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// 去重技能列表
|
||||
/// 去重技能列表(基于完整 key,不同仓库的同名 skill 分开显示)
|
||||
fn deduplicate_discoverable_skills(skills: &mut Vec<DiscoverableSkill>) {
|
||||
let mut seen = HashMap::new();
|
||||
skills.retain(|skill| {
|
||||
// 使用完整 key(owner/repo:directory)作为唯一标识
|
||||
// 这样不同仓库的同名 skill 会分开显示
|
||||
let unique_key = skill.key.to_lowercase();
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = seen.entry(unique_key) {
|
||||
e.insert(true);
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::{OnceLock, RwLock};
|
||||
|
||||
use crate::app_config::AppType;
|
||||
use crate::error::AppError;
|
||||
use crate::services::skill::SyncMethod;
|
||||
|
||||
/// 自定义端点配置(历史兼容,实际存储在 provider.meta.custom_endpoints)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -108,6 +109,11 @@ pub struct AppSettings {
|
||||
/// 当前 OpenCode 供应商 ID(本地存储,对 OpenCode 可能无意义,但保持结构一致)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_provider_opencode: Option<String>,
|
||||
|
||||
// ===== Skill 同步设置 =====
|
||||
/// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
|
||||
#[serde(default)]
|
||||
pub skill_sync_method: SyncMethod,
|
||||
}
|
||||
|
||||
fn default_show_in_tray() -> bool {
|
||||
@@ -136,6 +142,7 @@ impl Default for AppSettings {
|
||||
current_provider_codex: None,
|
||||
current_provider_gemini: None,
|
||||
current_provider_opencode: None,
|
||||
skill_sync_method: SyncMethod::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,3 +389,16 @@ pub fn get_effective_current_provider(
|
||||
// Fallback 到数据库的 is_current
|
||||
db.get_current_provider(app_type.as_str())
|
||||
}
|
||||
|
||||
// ===== Skill 同步方式管理函数 =====
|
||||
|
||||
/// 获取 Skill 同步方式配置
|
||||
pub fn get_skill_sync_method() -> SyncMethod {
|
||||
settings_store()
|
||||
.read()
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("设置锁已毒化,使用恢复值: {e}");
|
||||
e.into_inner()
|
||||
})
|
||||
.skill_sync_method
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Switch",
|
||||
"version": "3.10.1",
|
||||
"version": "3.10.2",
|
||||
"identifier": "com.ccswitch.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
+6
-7
@@ -750,13 +750,6 @@ function App() {
|
||||
>
|
||||
CC Switch
|
||||
</a>
|
||||
<UpdateBadge
|
||||
onClick={() => {
|
||||
setSettingsDefaultTab("about");
|
||||
setCurrentView("settings");
|
||||
}}
|
||||
className="absolute -top-4 -right-4"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -770,6 +763,12 @@ function App() {
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<UpdateBadge
|
||||
onClick={() => {
|
||||
setSettingsDefaultTab("about");
|
||||
setCurrentView("settings");
|
||||
}}
|
||||
/>
|
||||
{isCurrentAppTakeoverActive && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useUpdate } from "@/contexts/UpdateContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowUpCircle } from "lucide-react";
|
||||
|
||||
interface UpdateBadgeProps {
|
||||
className?: string;
|
||||
@@ -30,17 +31,12 @@ export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
|
||||
aria-label={title}
|
||||
onClick={onClick}
|
||||
className={`
|
||||
relative h-6 w-6 rounded-full
|
||||
${isActive ? "text-blue-600 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-500/10" : "text-muted-foreground hover:bg-muted/60"}
|
||||
relative h-8 w-8 rounded-full
|
||||
${isActive ? "text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-500/10" : "text-muted-foreground hover:bg-muted/60"}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
absolute inset-0 m-auto h-2 w-2 rounded-full ring-1 ring-background
|
||||
${isActive ? "bg-blue-500 dark:bg-blue-400" : "bg-blue-300/70 dark:bg-blue-300/60"}
|
||||
`}
|
||||
/>
|
||||
<ArrowUpCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,42 @@ interface UseModelStateProps {
|
||||
onConfigChange: (config: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse model values from settings config JSON
|
||||
*/
|
||||
function parseModelsFromConfig(settingsConfig: string) {
|
||||
try {
|
||||
const cfg = settingsConfig ? JSON.parse(settingsConfig) : {};
|
||||
const env = cfg?.env || {};
|
||||
const model =
|
||||
typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
|
||||
const reasoning =
|
||||
typeof env.ANTHROPIC_REASONING_MODEL === "string"
|
||||
? env.ANTHROPIC_REASONING_MODEL
|
||||
: "";
|
||||
const small =
|
||||
typeof env.ANTHROPIC_SMALL_FAST_MODEL === "string"
|
||||
? env.ANTHROPIC_SMALL_FAST_MODEL
|
||||
: "";
|
||||
const haiku =
|
||||
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string"
|
||||
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||
: small || model;
|
||||
const sonnet =
|
||||
typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === "string"
|
||||
? env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
: model || small;
|
||||
const opus =
|
||||
typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === "string"
|
||||
? env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
: model || small;
|
||||
|
||||
return { model, reasoning, haiku, sonnet, opus };
|
||||
} catch {
|
||||
return { model: "", reasoning: "", haiku: "", sonnet: "", opus: "" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理模型选择状态
|
||||
* 支持 ANTHROPIC_MODEL, ANTHROPIC_REASONING_MODEL 和各类型默认模型
|
||||
@@ -13,11 +49,22 @@ export function useModelState({
|
||||
settingsConfig,
|
||||
onConfigChange,
|
||||
}: UseModelStateProps) {
|
||||
const [claudeModel, setClaudeModel] = useState("");
|
||||
const [reasoningModel, setReasoningModel] = useState("");
|
||||
const [defaultHaikuModel, setDefaultHaikuModel] = useState("");
|
||||
const [defaultSonnetModel, setDefaultSonnetModel] = useState("");
|
||||
const [defaultOpusModel, setDefaultOpusModel] = useState("");
|
||||
// Initialize state by parsing config directly (fixes edit mode backfill)
|
||||
const [claudeModel, setClaudeModel] = useState(
|
||||
() => parseModelsFromConfig(settingsConfig).model,
|
||||
);
|
||||
const [reasoningModel, setReasoningModel] = useState(
|
||||
() => parseModelsFromConfig(settingsConfig).reasoning,
|
||||
);
|
||||
const [defaultHaikuModel, setDefaultHaikuModel] = useState(
|
||||
() => parseModelsFromConfig(settingsConfig).haiku,
|
||||
);
|
||||
const [defaultSonnetModel, setDefaultSonnetModel] = useState(
|
||||
() => parseModelsFromConfig(settingsConfig).sonnet,
|
||||
);
|
||||
const [defaultOpusModel, setDefaultOpusModel] = useState(
|
||||
() => parseModelsFromConfig(settingsConfig).opus,
|
||||
);
|
||||
|
||||
const isUserEditingRef = useRef(false);
|
||||
const lastConfigRef = useRef(settingsConfig);
|
||||
|
||||
@@ -35,6 +35,7 @@ import { LanguageSettings } from "@/components/settings/LanguageSettings";
|
||||
import { ThemeSettings } from "@/components/settings/ThemeSettings";
|
||||
import { WindowSettings } from "@/components/settings/WindowSettings";
|
||||
import { AppVisibilitySettings } from "@/components/settings/AppVisibilitySettings";
|
||||
import { SkillSyncMethodSettings } from "@/components/settings/SkillSyncMethodSettings";
|
||||
import { DirectorySettings } from "@/components/settings/DirectorySettings";
|
||||
import { ImportExportSection } from "@/components/settings/ImportExportSection";
|
||||
import { AboutSection } from "@/components/settings/AboutSection";
|
||||
@@ -249,6 +250,12 @@ export function SettingsPage({
|
||||
settings={settings}
|
||||
onChange={handleAutoSave}
|
||||
/>
|
||||
<SkillSyncMethodSettings
|
||||
value={settings.skillSyncMethod ?? "auto"}
|
||||
onChange={(method) =>
|
||||
handleAutoSave({ skillSyncMethod: method })
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SkillSyncMethod } from "@/types";
|
||||
|
||||
export interface SkillSyncMethodSettingsProps {
|
||||
value: SkillSyncMethod;
|
||||
onChange: (value: SkillSyncMethod) => void;
|
||||
}
|
||||
|
||||
export function SkillSyncMethodSettings({
|
||||
value,
|
||||
onChange,
|
||||
}: SkillSyncMethodSettingsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Handle default values: undefined or "auto" defaults to symlink display
|
||||
const displayValue = value === "copy" ? "copy" : "symlink";
|
||||
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<header className="space-y-1">
|
||||
<h3 className="text-sm font-medium">{t("settings.skillSync.title")}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.skillSync.description")}
|
||||
</p>
|
||||
</header>
|
||||
<div className="inline-flex gap-1 rounded-md border border-border-default bg-background p-1">
|
||||
<SyncMethodButton
|
||||
active={displayValue === "symlink"}
|
||||
onClick={() => onChange("symlink")}
|
||||
>
|
||||
{t("settings.skillSync.symlink")}
|
||||
</SyncMethodButton>
|
||||
<SyncMethodButton
|
||||
active={displayValue === "copy"}
|
||||
onClick={() => onChange("copy")}
|
||||
>
|
||||
{t("settings.skillSync.copy")}
|
||||
</SyncMethodButton>
|
||||
</div>
|
||||
{displayValue === "symlink" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.skillSync.symlinkHint")}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface SyncMethodButtonProps {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SyncMethodButton({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: SyncMethodButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
variant={active ? "default" : "ghost"}
|
||||
className={cn(
|
||||
"min-w-[96px]",
|
||||
active
|
||||
? "shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -65,10 +65,17 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
|
||||
const addRepoMutation = useAddSkillRepo();
|
||||
const removeRepoMutation = useRemoveSkillRepo();
|
||||
|
||||
// 已安装的 directory 集合
|
||||
const installedDirs = useMemo(() => {
|
||||
// 已安装的 skill key 集合(使用 directory + repoOwner + repoName 组合判断)
|
||||
const installedKeys = useMemo(() => {
|
||||
if (!installedSkills) return new Set<string>();
|
||||
return new Set(installedSkills.map((s) => s.directory.toLowerCase()));
|
||||
return new Set(
|
||||
installedSkills.map((s) => {
|
||||
// 构建唯一 key:directory + repoOwner + repoName
|
||||
const owner = s.repoOwner?.toLowerCase() || "";
|
||||
const name = s.repoName?.toLowerCase() || "";
|
||||
return `${s.directory.toLowerCase()}:${owner}:${name}`;
|
||||
}),
|
||||
);
|
||||
}, [installedSkills]);
|
||||
|
||||
type DiscoverableSkillItem = DiscoverableSkill & { installed: boolean };
|
||||
@@ -80,12 +87,14 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
|
||||
const installName =
|
||||
d.directory.split("/").pop()?.toLowerCase() ||
|
||||
d.directory.toLowerCase();
|
||||
// 使用 directory + repoOwner + repoName 组合判断是否已安装
|
||||
const key = `${installName}:${d.repoOwner.toLowerCase()}:${d.repoName.toLowerCase()}`;
|
||||
return {
|
||||
...d,
|
||||
installed: installedDirs.has(installName),
|
||||
installed: installedKeys.has(key),
|
||||
};
|
||||
});
|
||||
}, [discoverableSkills, installedDirs]);
|
||||
}, [discoverableSkills, installedKeys]);
|
||||
|
||||
const loading = loadingDiscoverable || fetchingDiscoverable;
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
{
|
||||
name: "RightCode",
|
||||
websiteUrl: "https://www.right.codes",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=0bdf9bfa",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=CCSWITCH",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://www.right.codes/claude",
|
||||
@@ -460,6 +460,8 @@ export const providerPresets: ProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "rightcode",
|
||||
icon: "rc",
|
||||
iconColor: "#E96B2C",
|
||||
},
|
||||
|
||||
@@ -195,7 +195,7 @@ requires_openai_auth = true`,
|
||||
{
|
||||
name: "RightCode",
|
||||
websiteUrl: "https://www.right.codes",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=0bdf9bfa",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=CCSWITCH",
|
||||
auth: generateThirdPartyAuth(""),
|
||||
config: generateThirdPartyConfig(
|
||||
"rightcode",
|
||||
@@ -203,6 +203,8 @@ requires_openai_auth = true`,
|
||||
"gpt-5.2",
|
||||
),
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "rightcode",
|
||||
icon: "rc",
|
||||
iconColor: "#E96B2C",
|
||||
},
|
||||
|
||||
@@ -649,7 +649,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
{
|
||||
name: "RightCode",
|
||||
websiteUrl: "https://www.right.codes",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=0bdf9bfa",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=CCSWITCH",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/openai",
|
||||
name: "RightCode",
|
||||
@@ -663,6 +663,8 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "rightcode",
|
||||
icon: "rc",
|
||||
iconColor: "#E96B2C",
|
||||
templateValues: {
|
||||
|
||||
@@ -280,6 +280,13 @@
|
||||
"geminiDesc": "Google Gemini CLI",
|
||||
"opencodeDesc": "OpenCode CLI"
|
||||
},
|
||||
"skillSync": {
|
||||
"title": "Skill Sync Method",
|
||||
"description": "Choose how to sync Skills files",
|
||||
"symlink": "Symlink",
|
||||
"copy": "Copy Files",
|
||||
"symlinkHint": "Symlinks save disk space and enable real-time sync. Note: May require admin privileges or Developer Mode on Windows"
|
||||
},
|
||||
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
||||
"configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory to the one in WSL to keep provider data consistent with the main environment.",
|
||||
"appConfigDir": "CC Switch Configuration Directory",
|
||||
@@ -393,7 +400,8 @@
|
||||
"minimax_en": "MiniMax Coding Plan Black Friday, Starter is now $2/mo (80% OFF!)",
|
||||
"dmxapi": "Claude Code exclusive model 66% OFF now!",
|
||||
"cubence": "Cubence is an official partner of CC Switch. Register using this link and enter \"CCSWITCH\" promo code during recharge to get 10% off every top-up",
|
||||
"aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!"
|
||||
"aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!",
|
||||
"rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!"
|
||||
},
|
||||
"parameterConfig": "Parameter Config - {{name}} *",
|
||||
"mainModel": "Main Model (optional)",
|
||||
@@ -964,6 +972,7 @@
|
||||
"downloadTimeoutHint": "Please check network connection or retry later",
|
||||
"skillPathNotFound": "Skill path '{{path}}' not found in repository {{owner}}/{{name}}",
|
||||
"skillDirNotFound": "Skill directory not found: {{path}}",
|
||||
"directoryConflict": "Skill directory '{{directory}}' is already occupied by {{existing_repo}}, cannot install from {{new_repo}}",
|
||||
"emptyArchive": "Downloaded archive is empty",
|
||||
"downloadFailed": "Download failed: HTTP {{status}}",
|
||||
"allBranchesFailed": "All branches failed, tried: {{branches}}",
|
||||
@@ -982,7 +991,8 @@
|
||||
"retryLater": "Please retry later",
|
||||
"checkRepoUrl": "Please check repository URL and branch name",
|
||||
"checkDiskSpace": "Please check disk space",
|
||||
"checkPermission": "Please check directory permissions"
|
||||
"checkPermission": "Please check directory permissions",
|
||||
"uninstallFirst": "Please uninstall the existing skill with the same name first"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
|
||||
@@ -280,6 +280,13 @@
|
||||
"geminiDesc": "Google Gemini CLI",
|
||||
"opencodeDesc": "OpenCode CLI"
|
||||
},
|
||||
"skillSync": {
|
||||
"title": "スキル同期方式",
|
||||
"description": "スキルファイルの同期方法を選択",
|
||||
"symlink": "シンボリックリンク",
|
||||
"copy": "ファイルコピー",
|
||||
"symlinkHint": "シンボリックリンクはディスク容量を節約し、リアルタイム同期を有効にします。注意:Windowsでは管理者権限または開発者モードが必要な場合があります"
|
||||
},
|
||||
"configDirectoryOverride": "設定ディレクトリの上書き(詳細)",
|
||||
"configDirectoryDescription": "WSL などで Claude Code や Codex を使う場合、ここで設定ディレクトリを WSL 側に合わせるとデータを揃えられます。",
|
||||
"appConfigDir": "CC Switch 設定ディレクトリ",
|
||||
@@ -393,7 +400,8 @@
|
||||
"minimax_en": "MiniMax Coding Plan Black Friday、Starter が月額 $2(80% OFF)",
|
||||
"dmxapi": "Claude Code 専用モデル 66% OFF 実施中!",
|
||||
"cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ",
|
||||
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!"
|
||||
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!",
|
||||
"rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!"
|
||||
},
|
||||
"parameterConfig": "パラメーター設定 - {{name}} *",
|
||||
"mainModel": "メインモデル(任意)",
|
||||
@@ -964,6 +972,7 @@
|
||||
"downloadTimeoutHint": "ネットワークを確認するか、時間をおいて再試行してください",
|
||||
"skillPathNotFound": "リポジトリ {{owner}}/{{name}} にスキルパス '{{path}}' がありません",
|
||||
"skillDirNotFound": "スキルディレクトリが見つかりません: {{path}}",
|
||||
"directoryConflict": "スキルディレクトリ '{{directory}}' は既に {{existing_repo}} で使用されています。{{new_repo}} からインストールできません",
|
||||
"emptyArchive": "ダウンロードしたアーカイブが空です",
|
||||
"downloadFailed": "ダウンロードに失敗しました: HTTP {{status}}",
|
||||
"allBranchesFailed": "すべてのブランチで失敗しました。試行: {{branches}}",
|
||||
@@ -982,7 +991,8 @@
|
||||
"retryLater": "時間をおいて再試行してください",
|
||||
"checkRepoUrl": "リポジトリ URL とブランチ名を確認してください",
|
||||
"checkDiskSpace": "ディスク容量を確認してください",
|
||||
"checkPermission": "ディレクトリの権限を確認してください"
|
||||
"checkPermission": "ディレクトリの権限を確認してください",
|
||||
"uninstallFirst": "同名のスキルを先にアンインストールしてください"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
|
||||
@@ -280,6 +280,13 @@
|
||||
"geminiDesc": "Google Gemini CLI",
|
||||
"opencodeDesc": "OpenCode CLI"
|
||||
},
|
||||
"skillSync": {
|
||||
"title": "Skill 同步方式",
|
||||
"description": "选择 Skills 的文件同步策略",
|
||||
"symlink": "软连接",
|
||||
"copy": "文件复制",
|
||||
"symlinkHint": "软连接节省磁盘空间并支持实时同步。注意:Windows 可能需要管理员权限或开启开发者模式"
|
||||
},
|
||||
"configDirectoryOverride": "配置目录覆盖(高级)",
|
||||
"configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定为 WSL 里的配置目录,供应商数据与主环境保持一致。",
|
||||
"appConfigDir": "CC Switch 配置目录",
|
||||
@@ -393,7 +400,8 @@
|
||||
"minimax_en": "MiniMax Coding Plan 黑五特惠,Starter 套餐现仅 $2/月(2折优惠!)",
|
||||
"dmxapi": "Claude Code 专属模型 3.4 折优惠进行中!",
|
||||
"cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠",
|
||||
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!"
|
||||
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!",
|
||||
"rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!"
|
||||
},
|
||||
"parameterConfig": "参数配置 - {{name}} *",
|
||||
"mainModel": "主模型 (可选)",
|
||||
@@ -964,6 +972,7 @@
|
||||
"downloadTimeoutHint": "请检查网络连接或稍后重试",
|
||||
"skillPathNotFound": "仓库 {{owner}}/{{name}} 中未找到技能路径 '{{path}}'",
|
||||
"skillDirNotFound": "技能目录不存在:{{path}}",
|
||||
"directoryConflict": "技能目录 '{{directory}}' 已被 {{existing_repo}} 占用,无法从 {{new_repo}} 安装",
|
||||
"emptyArchive": "下载的压缩包为空",
|
||||
"downloadFailed": "下载失败:HTTP {{status}}",
|
||||
"allBranchesFailed": "所有分支下载失败,尝试了:{{branches}}",
|
||||
@@ -982,7 +991,8 @@
|
||||
"retryLater": "请稍后重试",
|
||||
"checkRepoUrl": "请检查仓库地址和分支名称",
|
||||
"checkDiskSpace": "请检查磁盘空间",
|
||||
"checkPermission": "请检查目录权限"
|
||||
"checkPermission": "请检查目录权限",
|
||||
"uninstallFirst": "请先卸载已安装的同名技能"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
|
||||
@@ -35,6 +35,7 @@ function getErrorI18nKey(code: string): string {
|
||||
DOWNLOAD_TIMEOUT: "skills.error.downloadTimeout",
|
||||
DOWNLOAD_FAILED: "skills.error.downloadFailed",
|
||||
SKILL_DIR_NOT_FOUND: "skills.error.skillDirNotFound",
|
||||
SKILL_DIRECTORY_CONFLICT: "skills.error.directoryConflict",
|
||||
EMPTY_ARCHIVE: "skills.error.emptyArchive",
|
||||
GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed",
|
||||
};
|
||||
@@ -52,6 +53,7 @@ function getSuggestionI18nKey(suggestion: string): string {
|
||||
retryLater: "skills.error.suggestion.retryLater",
|
||||
checkRepoUrl: "skills.error.suggestion.checkRepoUrl",
|
||||
checkPermission: "skills.error.suggestion.checkPermission",
|
||||
uninstallFirst: "skills.error.suggestion.uninstallFirst",
|
||||
http403: "skills.error.http403",
|
||||
http404: "skills.error.http404",
|
||||
http429: "skills.error.http429",
|
||||
|
||||
@@ -25,6 +25,9 @@ export const settingsSchema = z.object({
|
||||
currentProviderClaude: z.string().optional(),
|
||||
currentProviderCodex: z.string().optional(),
|
||||
currentProviderGemini: z.string().optional(),
|
||||
|
||||
// Skill 同步设置
|
||||
skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(),
|
||||
});
|
||||
|
||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||
|
||||
@@ -137,6 +137,9 @@ export interface ProviderMeta {
|
||||
proxyConfig?: ProviderProxyConfig;
|
||||
}
|
||||
|
||||
// Skill 同步方式
|
||||
export type SkillSyncMethod = "auto" | "symlink" | "copy";
|
||||
|
||||
// 主页面显示的应用配置
|
||||
export interface VisibleApps {
|
||||
claude: boolean;
|
||||
@@ -182,6 +185,10 @@ export interface Settings {
|
||||
currentProviderCodex?: string;
|
||||
// 当前 Gemini 供应商 ID(优先于数据库 is_current)
|
||||
currentProviderGemini?: string;
|
||||
|
||||
// ===== Skill 同步设置 =====
|
||||
// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
|
||||
skillSyncMethod?: SkillSyncMethod;
|
||||
}
|
||||
|
||||
// MCP 服务器连接参数(宽松:允许扩展字段)
|
||||
@@ -310,6 +317,8 @@ export interface OpenCodeModel {
|
||||
output?: number;
|
||||
};
|
||||
options?: Record<string, unknown>; // 模型级别额外选项(provider 路由等)
|
||||
// 支持任意额外字段(cost、modalities、thinking、variants 等)
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// OpenCode 供应商选项
|
||||
|
||||
Reference in New Issue
Block a user