Compare commits
5 Commits
v3.7.0
...
fix/third-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a6c08673 | ||
|
|
74969ae968 | ||
|
|
1f3627add3 | ||
|
|
14ee122b27 | ||
|
|
7aecba14fe |
@@ -103,6 +103,14 @@ Download `CC-Switch-v{version}-macOS.zip` from the [Releases](../../releases) pa
|
||||
|
||||
> **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it first, then go to "System Settings" → "Privacy & Security" → click "Open Anyway", and you'll be able to open it normally afterwards.
|
||||
|
||||
### ArchLinux 用户
|
||||
|
||||
**Install via paru (Recommended)**
|
||||
|
||||
```bash
|
||||
paru -S cc-switch-bin
|
||||
```
|
||||
|
||||
### Linux Users
|
||||
|
||||
Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{version}-Linux.AppImage` from the [Releases](../../releases) page.
|
||||
|
||||
@@ -103,6 +103,14 @@ brew upgrade --cask cc-switch
|
||||
|
||||
> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开
|
||||
|
||||
### ArchLinux 用户
|
||||
|
||||
**通过 paru 安装(推荐)**
|
||||
|
||||
```bash
|
||||
paru -S cc-switch-bin
|
||||
```
|
||||
|
||||
### Linux 用户
|
||||
|
||||
从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch-v{版本号}-Linux.deb` 包或者 `CC-Switch-v{版本号}-Linux.AppImage` 安装包。
|
||||
|
||||
@@ -62,7 +62,7 @@ pub async fn install_skill(
|
||||
.clone()
|
||||
.unwrap_or_else(|| "main".to_string()),
|
||||
enabled: true,
|
||||
skills_path: None, // 安装时使用默认路径
|
||||
skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path
|
||||
};
|
||||
|
||||
service
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -244,6 +255,9 @@ pub fn validate_gemini_settings(settings: &Value) -> Result<(), AppError> {
|
||||
/// 此函数在切换供应商时使用,确保配置包含所有必需的字段。
|
||||
/// 对于需要 API Key 的供应商(如 PackyCode),会验证 GEMINI_API_KEY 字段。
|
||||
pub fn validate_gemini_settings_strict(settings: &Value) -> Result<(), AppError> {
|
||||
// 先做基础格式验证(包含 env/config 类型)
|
||||
validate_gemini_settings(settings)?;
|
||||
|
||||
let env_map = json_to_env(settings)?;
|
||||
|
||||
// 如果 env 为空,表示使用 OAuth(如 Google 官方),跳过验证
|
||||
|
||||
@@ -229,43 +229,23 @@ impl ConfigService {
|
||||
provider_id: &str,
|
||||
provider: &Provider,
|
||||
) -> Result<(), AppError> {
|
||||
use crate::gemini_config::{
|
||||
env_to_json, json_to_env, read_gemini_env, write_gemini_env_atomic,
|
||||
use crate::gemini_config::{env_to_json, read_gemini_env};
|
||||
|
||||
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 env_path = crate::gemini_config::get_gemini_env_path();
|
||||
if let Some(parent) = env_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||||
let mut live_after = env_to_json(&live_after_env);
|
||||
if let Some(obj) = live_after.as_object_mut() {
|
||||
obj.insert("config".to_string(), live_after_config);
|
||||
}
|
||||
|
||||
// 转换 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(target) = manager.providers.get_mut(provider_id) {
|
||||
target.settings_config = live_after;
|
||||
|
||||
@@ -30,6 +30,7 @@ enum LiveSnapshot {
|
||||
},
|
||||
Gemini {
|
||||
env: Option<HashMap<String, String>>, // 新增
|
||||
config: Option<Value>, // 新增:settings.json 内容
|
||||
},
|
||||
}
|
||||
|
||||
@@ -68,15 +69,30 @@ impl LiveSnapshot {
|
||||
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();
|
||||
if let Some(env_map) = env {
|
||||
write_gemini_env_atomic(env_map)?;
|
||||
} else if path.exists() {
|
||||
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(())
|
||||
@@ -612,7 +628,9 @@ impl ProviderService {
|
||||
state.save()?;
|
||||
}
|
||||
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();
|
||||
if !env_path.exists() {
|
||||
@@ -623,7 +641,18 @@ impl ProviderService {
|
||||
));
|
||||
}
|
||||
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)?;
|
||||
@@ -670,14 +699,22 @@ impl ProviderService {
|
||||
}
|
||||
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 env = if path.exists() {
|
||||
Some(read_gemini_env()?)
|
||||
} else {
|
||||
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,
|
||||
next_provider: &str,
|
||||
) -> 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();
|
||||
if !env_path.exists() {
|
||||
@@ -1477,7 +1516,18 @@ impl ProviderService {
|
||||
}
|
||||
|
||||
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(current) = manager.providers.get_mut(¤t_id) {
|
||||
current.settings_config = live;
|
||||
@@ -1495,36 +1545,71 @@ impl ProviderService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
||||
pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
||||
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 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 {
|
||||
GeminiAuthType::GoogleOfficial => {
|
||||
// Google 官方使用 OAuth,清空 env
|
||||
let empty_env = std::collections::HashMap::new();
|
||||
write_gemini_env_atomic(&empty_env)?;
|
||||
Self::ensure_google_oauth_security_flag(provider)?;
|
||||
env_map.clear();
|
||||
write_gemini_env_atomic(&env_map)?;
|
||||
}
|
||||
GeminiAuthType::Packycode => {
|
||||
// PackyCode 供应商,使用 API Key(切换时严格验证)
|
||||
validate_gemini_settings_strict(&provider.settings_config)?;
|
||||
let env_map = json_to_env(&provider.settings_config)?;
|
||||
write_gemini_env_atomic(&env_map)?;
|
||||
Self::ensure_packycode_security_flag(provider)?;
|
||||
}
|
||||
GeminiAuthType::Generic => {
|
||||
// 通用供应商,使用 API Key(切换时严格验证)
|
||||
validate_gemini_settings_strict(&provider.settings_config)?;
|
||||
let env_map = json_to_env(&provider.settings_config)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ pub struct Skill {
|
||||
/// 分支名称
|
||||
#[serde(rename = "repoBranch")]
|
||||
pub repo_branch: Option<String>,
|
||||
/// 技能所在的子目录路径 (可选, 如 "skills")
|
||||
#[serde(rename = "skillsPath")]
|
||||
pub skills_path: Option<String>,
|
||||
}
|
||||
|
||||
/// 仓库配置
|
||||
@@ -234,6 +237,7 @@ impl SkillService {
|
||||
repo_owner: Some(repo.owner.clone()),
|
||||
repo_name: Some(repo.name.clone()),
|
||||
repo_branch: Some(repo.branch.clone()),
|
||||
skills_path: repo.skills_path.clone(),
|
||||
});
|
||||
}
|
||||
Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e),
|
||||
@@ -312,6 +316,7 @@ impl SkillService {
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
repo_branch: None,
|
||||
skills_path: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -442,12 +447,21 @@ impl SkillService {
|
||||
.await
|
||||
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
|
||||
|
||||
// 复制到安装目录
|
||||
let source = temp_dir.join(&directory);
|
||||
// 根据 skills_path 确定源目录路径
|
||||
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() {
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
return Err(anyhow::anyhow!("技能目录不存在"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"技能目录不存在: {}",
|
||||
source.display()
|
||||
));
|
||||
}
|
||||
|
||||
// 删除旧版本
|
||||
|
||||
@@ -14,6 +14,7 @@ interface DirectorySettingsProps {
|
||||
onResetAppConfig: () => Promise<void>;
|
||||
claudeDir?: string;
|
||||
codexDir?: string;
|
||||
geminiDir?: string;
|
||||
onDirectoryChange: (app: AppId, value?: string) => void;
|
||||
onBrowseDirectory: (app: AppId) => Promise<void>;
|
||||
onResetDirectory: (app: AppId) => Promise<void>;
|
||||
@@ -27,6 +28,7 @@ export function DirectorySettings({
|
||||
onResetAppConfig,
|
||||
claudeDir,
|
||||
codexDir,
|
||||
geminiDir,
|
||||
onDirectoryChange,
|
||||
onBrowseDirectory,
|
||||
onResetDirectory,
|
||||
@@ -104,6 +106,17 @@ export function DirectorySettings({
|
||||
onBrowse={() => onBrowseDirectory("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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -220,6 +220,7 @@ export function SettingsDialog({
|
||||
onResetAppConfig={resetAppConfigDir}
|
||||
claudeDir={settings.claudeConfigDir}
|
||||
codexDir={settings.codexConfigDir}
|
||||
geminiDir={settings.geminiConfigDir}
|
||||
onDirectoryChange={updateDirectory}
|
||||
onBrowseDirectory={browseDirectory}
|
||||
onResetDirectory={resetDirectory}
|
||||
|
||||
@@ -59,6 +59,10 @@ const DialogContent = React.forwardRef<
|
||||
zIndexMap[zIndex],
|
||||
className,
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
// 防止点击遮罩层关闭对话框
|
||||
e.preventDefault();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -5,12 +5,13 @@ import { homeDir, join } from "@tauri-apps/api/path";
|
||||
import { settingsApi, type AppId } from "@/lib/api";
|
||||
import type { SettingsFormState } from "./useSettingsForm";
|
||||
|
||||
type DirectoryKey = "appConfig" | "claude" | "codex";
|
||||
type DirectoryKey = "appConfig" | "claude" | "codex" | "gemini";
|
||||
|
||||
export interface ResolvedDirectories {
|
||||
appConfig: string;
|
||||
claude: string;
|
||||
codex: string;
|
||||
gemini: string;
|
||||
}
|
||||
|
||||
const sanitizeDir = (value?: string | null): string | undefined => {
|
||||
@@ -37,7 +38,8 @@ const computeDefaultConfigDir = async (
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const home = await homeDir();
|
||||
const folder = app === "claude" ? ".claude" : ".codex";
|
||||
const folder =
|
||||
app === "claude" ? ".claude" : app === "codex" ? ".codex" : ".gemini";
|
||||
return await join(home, folder);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -64,7 +66,11 @@ export interface UseDirectorySettingsResult {
|
||||
browseAppConfigDir: () => Promise<void>;
|
||||
resetDirectory: (app: AppId) => 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: "",
|
||||
claude: "",
|
||||
codex: "",
|
||||
gemini: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -96,6 +103,7 @@ export function useDirectorySettings({
|
||||
appConfig: "",
|
||||
claude: "",
|
||||
codex: "",
|
||||
gemini: "",
|
||||
});
|
||||
const initialAppConfigDirRef = useRef<string | undefined>(undefined);
|
||||
|
||||
@@ -110,16 +118,20 @@ export function useDirectorySettings({
|
||||
overrideRaw,
|
||||
claudeDir,
|
||||
codexDir,
|
||||
geminiDir,
|
||||
defaultAppConfig,
|
||||
defaultClaudeDir,
|
||||
defaultCodexDir,
|
||||
defaultGeminiDir,
|
||||
] = await Promise.all([
|
||||
settingsApi.getAppConfigDirOverride(),
|
||||
settingsApi.getConfigDir("claude"),
|
||||
settingsApi.getConfigDir("codex"),
|
||||
settingsApi.getConfigDir("gemini"),
|
||||
computeDefaultAppConfigDir(),
|
||||
computeDefaultConfigDir("claude"),
|
||||
computeDefaultConfigDir("codex"),
|
||||
computeDefaultConfigDir("gemini"),
|
||||
]);
|
||||
|
||||
if (!active) return;
|
||||
@@ -130,6 +142,7 @@ export function useDirectorySettings({
|
||||
appConfig: defaultAppConfig ?? "",
|
||||
claude: defaultClaudeDir ?? "",
|
||||
codex: defaultCodexDir ?? "",
|
||||
gemini: defaultGeminiDir ?? "",
|
||||
};
|
||||
|
||||
setAppConfigDir(normalizedOverride);
|
||||
@@ -139,6 +152,7 @@ export function useDirectorySettings({
|
||||
appConfig: normalizedOverride ?? defaultsRef.current.appConfig,
|
||||
claude: claudeDir || defaultsRef.current.claude,
|
||||
codex: codexDir || defaultsRef.current.codex,
|
||||
gemini: geminiDir || defaultsRef.current.gemini,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -167,7 +181,9 @@ export function useDirectorySettings({
|
||||
onUpdateSettings(
|
||||
key === "claude"
|
||||
? { claudeConfigDir: sanitized }
|
||||
: { codexConfigDir: sanitized },
|
||||
: key === "codex"
|
||||
? { codexConfigDir: sanitized }
|
||||
: { geminiConfigDir: sanitized },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,18 +204,24 @@ export function useDirectorySettings({
|
||||
|
||||
const updateDirectory = useCallback(
|
||||
(app: AppId, value?: string) => {
|
||||
updateDirectoryState(app === "claude" ? "claude" : "codex", value);
|
||||
updateDirectoryState(
|
||||
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini",
|
||||
value,
|
||||
);
|
||||
},
|
||||
[updateDirectoryState],
|
||||
);
|
||||
|
||||
const browseDirectory = useCallback(
|
||||
async (app: AppId) => {
|
||||
const key: DirectoryKey = app === "claude" ? "claude" : "codex";
|
||||
const key: DirectoryKey =
|
||||
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini";
|
||||
const currentValue =
|
||||
key === "claude"
|
||||
? (settings?.claudeConfigDir ?? resolvedDirs.claude)
|
||||
: (settings?.codexConfigDir ?? resolvedDirs.codex);
|
||||
: key === "codex"
|
||||
? (settings?.codexConfigDir ?? resolvedDirs.codex)
|
||||
: (settings?.geminiConfigDir ?? resolvedDirs.gemini);
|
||||
|
||||
try {
|
||||
const picked = await settingsApi.selectConfigDirectory(currentValue);
|
||||
@@ -240,7 +262,8 @@ export function useDirectorySettings({
|
||||
|
||||
const resetDirectory = useCallback(
|
||||
async (app: AppId) => {
|
||||
const key: DirectoryKey = app === "claude" ? "claude" : "codex";
|
||||
const key: DirectoryKey =
|
||||
app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini";
|
||||
if (!defaultsRef.current[key]) {
|
||||
const fallback = await computeDefaultConfigDir(app);
|
||||
if (fallback) {
|
||||
@@ -269,13 +292,14 @@ export function useDirectorySettings({
|
||||
}, [updateDirectoryState]);
|
||||
|
||||
const resetAllDirectories = useCallback(
|
||||
(claudeDir?: string, codexDir?: string) => {
|
||||
(claudeDir?: string, codexDir?: string, geminiDir?: string) => {
|
||||
setAppConfigDir(initialAppConfigDirRef.current);
|
||||
setResolvedDirs({
|
||||
appConfig:
|
||||
initialAppConfigDirRef.current ?? defaultsRef.current.appConfig,
|
||||
claude: claudeDir ?? defaultsRef.current.claude,
|
||||
codex: codexDir ?? defaultsRef.current.codex,
|
||||
gemini: geminiDir ?? defaultsRef.current.gemini,
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -102,6 +102,7 @@ export function useSettings(): UseSettingsResult {
|
||||
resetAllDirectories(
|
||||
sanitizeDir(data?.claudeConfigDir),
|
||||
sanitizeDir(data?.codexConfigDir),
|
||||
sanitizeDir(data?.geminiConfigDir),
|
||||
);
|
||||
setRequiresRestart(false);
|
||||
}, [
|
||||
@@ -120,14 +121,17 @@ export function useSettings(): UseSettingsResult {
|
||||
const sanitizedAppDir = sanitizeDir(appConfigDir);
|
||||
const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir);
|
||||
const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir);
|
||||
const sanitizedGeminiDir = sanitizeDir(settings.geminiConfigDir);
|
||||
const previousAppDir = initialAppConfigDir;
|
||||
const previousClaudeDir = sanitizeDir(data?.claudeConfigDir);
|
||||
const previousCodexDir = sanitizeDir(data?.codexConfigDir);
|
||||
const previousGeminiDir = sanitizeDir(data?.geminiConfigDir);
|
||||
|
||||
const payload: Settings = {
|
||||
...settings,
|
||||
claudeConfigDir: sanitizedClaudeDir,
|
||||
codexConfigDir: sanitizedCodexDir,
|
||||
geminiConfigDir: sanitizedGeminiDir,
|
||||
language: settings.language,
|
||||
};
|
||||
|
||||
@@ -170,10 +174,11 @@ export function useSettings(): UseSettingsResult {
|
||||
console.warn("[useSettings] Failed to refresh tray menu", error);
|
||||
}
|
||||
|
||||
// 如果 Claude/Codex 的目录覆盖发生变化,则立即将“当前使用的供应商”写回对应应用的 live 配置
|
||||
// 如果 Claude/Codex/Gemini 的目录覆盖发生变化,则立即将“当前使用的供应商”写回对应应用的 live 配置
|
||||
const claudeDirChanged = sanitizedClaudeDir !== previousClaudeDir;
|
||||
const codexDirChanged = sanitizedCodexDir !== previousCodexDir;
|
||||
if (claudeDirChanged || codexDirChanged) {
|
||||
const geminiDirChanged = sanitizedGeminiDir !== previousGeminiDir;
|
||||
if (claudeDirChanged || codexDirChanged || geminiDirChanged) {
|
||||
const syncResult = await syncCurrentProvidersLiveSafe();
|
||||
if (!syncResult.ok) {
|
||||
console.warn(
|
||||
|
||||
@@ -179,8 +179,11 @@
|
||||
"claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.",
|
||||
"codexConfigDir": "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",
|
||||
"browsePlaceholderCodex": "e.g., /home/<your-username>/.codex",
|
||||
"browsePlaceholderGemini": "e.g., /home/<your-username>/.gemini",
|
||||
"browseDirectory": "Browse Directory",
|
||||
"resetDefault": "Reset to default directory (takes effect after saving)",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
|
||||
@@ -179,8 +179,11 @@
|
||||
"claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。",
|
||||
"codexConfigDir": "Codex 配置目录",
|
||||
"codexConfigDirDescription": "覆盖 Codex 配置目录。",
|
||||
"geminiConfigDir": "Gemini 配置目录",
|
||||
"geminiConfigDirDescription": "覆盖 Gemini 配置目录 (.env)。",
|
||||
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
||||
"browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex",
|
||||
"browsePlaceholderGemini": "例如:/home/<你的用户名>/.gemini",
|
||||
"browseDirectory": "浏览目录",
|
||||
"resetDefault": "恢复默认目录(需保存后生效)",
|
||||
"checkForUpdates": "检查更新",
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Skill {
|
||||
repoOwner?: string;
|
||||
repoName?: string;
|
||||
repoBranch?: string;
|
||||
skillsPath?: string; // 技能所在的子目录路径,如 "skills"
|
||||
}
|
||||
|
||||
export interface SkillRepo {
|
||||
|
||||
@@ -97,6 +97,8 @@ export interface Settings {
|
||||
claudeConfigDir?: string;
|
||||
// 覆盖 Codex 配置目录(可选)
|
||||
codexConfigDir?: string;
|
||||
// 覆盖 Gemini 配置目录(可选)
|
||||
geminiConfigDir?: string;
|
||||
// 首选语言(可选,默认中文)
|
||||
language?: "en" | "zh";
|
||||
// Claude 自定义端点列表
|
||||
|
||||
Reference in New Issue
Block a user