mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9a4c7cb86 | |||
| 00f78e4546 | |||
| 1ee1e9cb2e | |||
| 7db4b8d976 | |||
| 924ad44e6c | |||
| eecd6a3a2b |
+4
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
1. **安装依赖**:添加了 `react-i18next` 和 `i18next` 包
|
1. **安装依赖**:添加了 `react-i18next` 和 `i18next` 包
|
||||||
2. **配置国际化**:在 `src/i18n/` 目录下创建了配置文件
|
2. **配置国际化**:在 `src/i18n/` 目录下创建了配置文件
|
||||||
3. **翻译文件**:创建了英文和中文翻译文件
|
3. **翻译文件**:创建了英文、中文、日文翻译文件
|
||||||
4. **组件更新**:替换了主要组件中的硬编码文案
|
4. **组件更新**:替换了主要组件中的硬编码文案
|
||||||
5. **语言切换器**:添加了语言切换按钮
|
5. **语言切换器**:添加了语言切换按钮
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ src/
|
|||||||
│ ├── index.ts # 国际化配置文件
|
│ ├── index.ts # 国际化配置文件
|
||||||
│ └── locales/
|
│ └── locales/
|
||||||
│ ├── en.json # 英文翻译
|
│ ├── en.json # 英文翻译
|
||||||
|
│ ├── ja.json # 日文翻译
|
||||||
│ └── zh.json # 中文翻译
|
│ └── zh.json # 中文翻译
|
||||||
├── components/
|
├── components/
|
||||||
│ └── LanguageSwitcher.tsx # 语言切换组件
|
│ └── LanguageSwitcher.tsx # 语言切换组件
|
||||||
@@ -24,7 +25,7 @@ src/
|
|||||||
|
|
||||||
## 默认语言设置
|
## 默认语言设置
|
||||||
|
|
||||||
- **默认语言**:英文 (en)
|
- **默认语言**:中文 (zh)(无首选时根据浏览器/系统语言选择 zh/en/ja)
|
||||||
- **回退语言**:英文 (en)
|
- **回退语言**:英文 (en)
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
@@ -57,7 +58,7 @@ src/
|
|||||||
|
|
||||||
## 测试功能
|
## 测试功能
|
||||||
|
|
||||||
应用已添加了语言切换按钮(地球图标),点击可以在中英文之间切换,验证国际化功能是否正常工作。
|
应用已添加了语言切换按钮,支持中文、英文、日文三种语言切换,验证国际化功能是否正常工作。
|
||||||
|
|
||||||
## 已更新的组件
|
## 已更新的组件
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub fn switch_provider(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<(), AppError> {
|
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
||||||
ProviderService::import_default_config(state, app_type)
|
ProviderService::import_default_config(state, app_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result
|
|||||||
pub fn import_default_config_test_hook(
|
pub fn import_default_config_test_hook(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
app_type: AppType,
|
app_type: AppType,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<bool, AppError> {
|
||||||
import_default_config_internal(state, app_type)
|
import_default_config_internal(state, app_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +102,7 @@ pub fn import_default_config_test_hook(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<bool, String> {
|
pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<bool, String> {
|
||||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||||
import_default_config_internal(&state, app_type)
|
import_default_config_internal(&state, app_type).map_err(Into::into)
|
||||||
.map(|_| true)
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 查询供应商用量
|
/// 查询供应商用量
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ pub async fn install_skill(
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "main".to_string()),
|
.unwrap_or_else(|| "main".to_string()),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path
|
|
||||||
};
|
};
|
||||||
|
|
||||||
service
|
service
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ impl Database {
|
|||||||
pub fn get_skill_repos(&self) -> Result<Vec<SkillRepo>, AppError> {
|
pub fn get_skill_repos(&self) -> Result<Vec<SkillRepo>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare("SELECT owner, name, branch, enabled, skills_path FROM skill_repos ORDER BY owner ASC, name ASC")
|
.prepare(
|
||||||
|
"SELECT owner, name, branch, enabled FROM skill_repos ORDER BY owner ASC, name ASC",
|
||||||
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
let repo_iter = stmt
|
let repo_iter = stmt
|
||||||
@@ -68,7 +70,6 @@ impl Database {
|
|||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
branch: row.get(2)?,
|
branch: row.get(2)?,
|
||||||
enabled: row.get(3)?,
|
enabled: row.get(3)?,
|
||||||
skills_path: row.get(4)?,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
@@ -84,8 +85,8 @@ impl Database {
|
|||||||
pub fn save_skill_repo(&self, repo: &SkillRepo) -> Result<(), AppError> {
|
pub fn save_skill_repo(&self, repo: &SkillRepo) -> Result<(), AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)",
|
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled) VALUES (?1, ?2, ?3, ?4)",
|
||||||
params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path],
|
params![repo.owner, repo.name, repo.branch, repo.enabled],
|
||||||
).map_err(|e| AppError::Database(e.to_string()))?;
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,12 +153,14 @@ impl Database {
|
|||||||
tx: &rusqlite::Transaction<'_>,
|
tx: &rusqlite::Transaction<'_>,
|
||||||
config: &MultiAppConfig,
|
config: &MultiAppConfig,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let migrate_app_prompts =
|
let migrate_app_prompts = |prompts_map: &std::collections::HashMap<
|
||||||
|prompts_map: &std::collections::HashMap<String, crate::prompt::Prompt>,
|
String,
|
||||||
app_type: &str|
|
crate::prompt::Prompt,
|
||||||
-> Result<(), AppError> {
|
>,
|
||||||
for (id, prompt) in prompts_map {
|
app_type: &str|
|
||||||
tx.execute(
|
-> Result<(), AppError> {
|
||||||
|
for (id, prompt) in prompts_map {
|
||||||
|
tx.execute(
|
||||||
"INSERT OR REPLACE INTO prompts (
|
"INSERT OR REPLACE INTO prompts (
|
||||||
id, app_type, name, content, description, enabled, created_at, updated_at
|
id, app_type, name, content, description, enabled, created_at, updated_at
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||||
@@ -174,9 +176,9 @@ impl Database {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(format!("Migrate prompt failed: {e}")))?;
|
.map_err(|e| AppError::Database(format!("Migrate prompt failed: {e}")))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
migrate_app_prompts(&config.prompts.claude.prompts, "claude")?;
|
migrate_app_prompts(&config.prompts.claude.prompts, "claude")?;
|
||||||
migrate_app_prompts(&config.prompts.codex.prompts, "codex")?;
|
migrate_app_prompts(&config.prompts.codex.prompts, "codex")?;
|
||||||
@@ -200,8 +202,8 @@ impl Database {
|
|||||||
|
|
||||||
for repo in &config.skills.repos {
|
for repo in &config.skills.repos {
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)",
|
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled) VALUES (?1, ?2, ?3, ?4)",
|
||||||
params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path],
|
params![repo.owner, repo.name, repo.branch, repo.enabled],
|
||||||
).map_err(|e| AppError::Database(format!("Migrate skill repo failed: {e}")))?;
|
).map_err(|e| AppError::Database(format!("Migrate skill repo failed: {e}")))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,28 +115,21 @@ impl Database {
|
|||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查数据库是否为空(需要首次导入)
|
/// 检查 MCP 服务器表是否为空
|
||||||
///
|
pub fn is_mcp_table_empty(&self) -> Result<bool, AppError> {
|
||||||
/// 通过检查是否有任何 MCP 服务器、提示词、Skills 仓库或供应商来判断
|
|
||||||
pub fn is_empty_for_first_import(&self) -> Result<bool, AppError> {
|
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
let count: i64 = conn
|
||||||
let mcp_count: i64 = conn
|
|
||||||
.query_row("SELECT COUNT(*) FROM mcp_servers", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM mcp_servers", [], |row| row.get(0))
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
Ok(count == 0)
|
||||||
|
}
|
||||||
|
|
||||||
let prompt_count: i64 = conn
|
/// 检查提示词表是否为空
|
||||||
|
pub fn is_prompts_table_empty(&self) -> Result<bool, AppError> {
|
||||||
|
let conn = lock_conn!(self.conn);
|
||||||
|
let count: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM prompts", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM prompts", [], |row| row.get(0))
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
Ok(count == 0)
|
||||||
let skill_repo_count: i64 = conn
|
|
||||||
.query_row("SELECT COUNT(*) FROM skill_repos", [], |row| row.get(0))
|
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
||||||
|
|
||||||
let provider_count: i64 = conn
|
|
||||||
.query_row("SELECT COUNT(*) FROM providers", [], |row| row.get(0))
|
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(mcp_count == 0 && prompt_count == 0 && skill_repo_count == 0 && provider_count == 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ impl Database {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
branch TEXT NOT NULL DEFAULT 'main',
|
branch TEXT NOT NULL DEFAULT 'main',
|
||||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||||
skills_path TEXT,
|
|
||||||
PRIMARY KEY (owner, name)
|
PRIMARY KEY (owner, name)
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
@@ -226,14 +225,14 @@ impl Database {
|
|||||||
Self::add_column_if_missing(conn, "skills", "installed_at", "INTEGER NOT NULL DEFAULT 0")?;
|
Self::add_column_if_missing(conn, "skills", "installed_at", "INTEGER NOT NULL DEFAULT 0")?;
|
||||||
|
|
||||||
// skill_repos 表
|
// skill_repos 表
|
||||||
Self::add_column_if_missing(conn, "skill_repos", "branch", "TEXT NOT NULL DEFAULT 'main'")?;
|
|
||||||
Self::add_column_if_missing(
|
Self::add_column_if_missing(
|
||||||
conn,
|
conn,
|
||||||
"skill_repos",
|
"skill_repos",
|
||||||
"enabled",
|
"branch",
|
||||||
"BOOLEAN NOT NULL DEFAULT 1",
|
"TEXT NOT NULL DEFAULT 'main'",
|
||||||
)?;
|
)?;
|
||||||
Self::add_column_if_missing(conn, "skill_repos", "skills_path", "TEXT")?;
|
Self::add_column_if_missing(conn, "skill_repos", "enabled", "BOOLEAN NOT NULL DEFAULT 1")?;
|
||||||
|
// 注意: skills_path 字段已被移除,因为现在支持全仓库递归扫描
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -247,9 +246,7 @@ impl Database {
|
|||||||
|
|
||||||
pub(crate) fn set_user_version(conn: &Connection, version: i32) -> Result<(), AppError> {
|
pub(crate) fn set_user_version(conn: &Connection, version: i32) -> Result<(), AppError> {
|
||||||
if version < 0 {
|
if version < 0 {
|
||||||
return Err(AppError::Database(
|
return Err(AppError::Database("user_version 不能为负数".to_string()));
|
||||||
"user_version 不能为负数".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
let sql = format!("PRAGMA user_version = {version};");
|
let sql = format!("PRAGMA user_version = {version};");
|
||||||
conn.execute(&sql, [])
|
conn.execute(&sql, [])
|
||||||
@@ -261,10 +258,7 @@ impl Database {
|
|||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
return Err(AppError::Database(format!("{kind} 不能为空")));
|
return Err(AppError::Database(format!("{kind} 不能为空")));
|
||||||
}
|
}
|
||||||
if !s
|
if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||||
.chars()
|
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
|
||||||
{
|
|
||||||
return Err(AppError::Database(format!(
|
return Err(AppError::Database(format!(
|
||||||
"非法{kind}: {s},仅允许字母、数字和下划线"
|
"非法{kind}: {s},仅允许字母、数字和下划线"
|
||||||
)));
|
)));
|
||||||
@@ -292,7 +286,11 @@ impl Database {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_column(conn: &Connection, table: &str, column: &str) -> Result<bool, AppError> {
|
pub(crate) fn has_column(
|
||||||
|
conn: &Connection,
|
||||||
|
table: &str,
|
||||||
|
column: &str,
|
||||||
|
) -> Result<bool, AppError> {
|
||||||
Self::validate_identifier(table, "表名")?;
|
Self::validate_identifier(table, "表名")?;
|
||||||
Self::validate_identifier(column, "列名")?;
|
Self::validate_identifier(column, "列名")?;
|
||||||
|
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ fn migration_rejects_future_version() {
|
|||||||
Database::create_tables_on_conn(&conn).expect("create tables");
|
Database::create_tables_on_conn(&conn).expect("create tables");
|
||||||
Database::set_user_version(&conn, SCHEMA_VERSION + 1).expect("set future version");
|
Database::set_user_version(&conn, SCHEMA_VERSION + 1).expect("set future version");
|
||||||
|
|
||||||
let err = Database::apply_schema_migrations_on_conn(&conn)
|
let err =
|
||||||
.expect_err("should reject higher version");
|
Database::apply_schema_migrations_on_conn(&conn).expect_err("should reject higher version");
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string().contains("数据库版本过新"),
|
err.to_string().contains("数据库版本过新"),
|
||||||
"unexpected error: {err}"
|
"unexpected error: {err}"
|
||||||
@@ -168,10 +168,7 @@ fn migration_aligns_column_defaults_and_types() {
|
|||||||
let is_current = get_column_info(&conn, "providers", "is_current");
|
let is_current = get_column_info(&conn, "providers", "is_current");
|
||||||
assert_eq!(is_current.r#type, "BOOLEAN");
|
assert_eq!(is_current.r#type, "BOOLEAN");
|
||||||
assert_eq!(is_current.notnull, 1);
|
assert_eq!(is_current.notnull, 1);
|
||||||
assert_eq!(
|
assert_eq!(normalize_default(&is_current.default).as_deref(), Some("0"));
|
||||||
normalize_default(&is_current.default).as_deref(),
|
|
||||||
Some("0")
|
|
||||||
);
|
|
||||||
|
|
||||||
let tags = get_column_info(&conn, "mcp_servers", "tags");
|
let tags = get_column_info(&conn, "mcp_servers", "tags");
|
||||||
assert_eq!(tags.r#type, "TEXT");
|
assert_eq!(tags.r#type, "TEXT");
|
||||||
@@ -181,10 +178,7 @@ fn migration_aligns_column_defaults_and_types() {
|
|||||||
let enabled = get_column_info(&conn, "prompts", "enabled");
|
let enabled = get_column_info(&conn, "prompts", "enabled");
|
||||||
assert_eq!(enabled.r#type, "BOOLEAN");
|
assert_eq!(enabled.r#type, "BOOLEAN");
|
||||||
assert_eq!(enabled.notnull, 1);
|
assert_eq!(enabled.notnull, 1);
|
||||||
assert_eq!(
|
assert_eq!(normalize_default(&enabled.default).as_deref(), Some("1"));
|
||||||
normalize_default(&enabled.default).as_deref(),
|
|
||||||
Some("1")
|
|
||||||
);
|
|
||||||
|
|
||||||
let installed_at = get_column_info(&conn, "skills", "installed_at");
|
let installed_at = get_column_info(&conn, "skills", "installed_at");
|
||||||
assert_eq!(installed_at.r#type, "INTEGER");
|
assert_eq!(installed_at.r#type, "INTEGER");
|
||||||
|
|||||||
@@ -100,12 +100,8 @@ pub struct DeepLinkImportRequest {
|
|||||||
/// Skill directory name
|
/// Skill directory name
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub directory: Option<String>,
|
pub directory: Option<String>,
|
||||||
/// Repository branch (default: "main")
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub branch: Option<String>,
|
pub branch: Option<String>,
|
||||||
/// Skills subdirectory path (e.g., "skills")
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub skills_path: Option<String>,
|
|
||||||
|
|
||||||
// ============ Config file fields (v3.8+) ============
|
// ============ Config file fields (v3.8+) ============
|
||||||
/// Base64 encoded config content
|
/// Base64 encoded config content
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ fn parse_provider_deeplink(
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
config,
|
config,
|
||||||
config_format,
|
config_format,
|
||||||
config_url,
|
config_url,
|
||||||
@@ -204,7 +203,6 @@ fn parse_prompt_deeplink(
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
config: None,
|
config: None,
|
||||||
config_format: None,
|
config_format: None,
|
||||||
config_url: None,
|
config_url: None,
|
||||||
@@ -262,7 +260,6 @@ fn parse_mcp_deeplink(
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
config_url: None,
|
config_url: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -287,10 +284,6 @@ fn parse_skill_deeplink(
|
|||||||
|
|
||||||
let directory = params.get("directory").cloned();
|
let directory = params.get("directory").cloned();
|
||||||
let branch = params.get("branch").cloned();
|
let branch = params.get("branch").cloned();
|
||||||
let skills_path = params
|
|
||||||
.get("skills_path")
|
|
||||||
.or_else(|| params.get("skillsPath"))
|
|
||||||
.cloned();
|
|
||||||
|
|
||||||
Ok(DeepLinkImportRequest {
|
Ok(DeepLinkImportRequest {
|
||||||
version,
|
version,
|
||||||
@@ -298,7 +291,6 @@ fn parse_skill_deeplink(
|
|||||||
repo: Some(repo),
|
repo: Some(repo),
|
||||||
directory,
|
directory,
|
||||||
branch,
|
branch,
|
||||||
skills_path,
|
|
||||||
icon: None,
|
icon: None,
|
||||||
app: Some("claude".to_string()), // Skills are Claude-only
|
app: Some("claude".to_string()), // Skills are Claude-only
|
||||||
name: None,
|
name: None,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ pub fn import_skill_from_deeplink(
|
|||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
branch: request.branch.unwrap_or_else(|| "main".to_string()),
|
branch: request.branch.unwrap_or_else(|| "main".to_string()),
|
||||||
enabled: request.enabled.unwrap_or(true),
|
enabled: request.enabled.unwrap_or(true),
|
||||||
skills_path: request.skills_path,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save using Database
|
// Save using Database
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ fn test_build_gemini_provider_with_model() {
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
content: None,
|
content: None,
|
||||||
description: None,
|
description: None,
|
||||||
enabled: None,
|
enabled: None,
|
||||||
@@ -189,7 +188,6 @@ fn test_build_gemini_provider_without_model() {
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
content: None,
|
content: None,
|
||||||
description: None,
|
description: None,
|
||||||
enabled: None,
|
enabled: None,
|
||||||
@@ -231,7 +229,6 @@ fn test_parse_and_merge_config_claude() {
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
content: None,
|
content: None,
|
||||||
description: None,
|
description: None,
|
||||||
enabled: None,
|
enabled: None,
|
||||||
@@ -275,7 +272,6 @@ fn test_parse_and_merge_config_url_override() {
|
|||||||
repo: None,
|
repo: None,
|
||||||
directory: None,
|
directory: None,
|
||||||
branch: None,
|
branch: None,
|
||||||
skills_path: None,
|
|
||||||
content: None,
|
content: None,
|
||||||
description: None,
|
description: None,
|
||||||
enabled: None,
|
enabled: None,
|
||||||
@@ -307,8 +303,7 @@ fn test_import_prompt_allows_space_in_base64_content() {
|
|||||||
let db = Arc::new(Database::memory().expect("create memory db"));
|
let db = Arc::new(Database::memory().expect("create memory db"));
|
||||||
let state = AppState::new(db.clone());
|
let state = AppState::new(db.clone());
|
||||||
|
|
||||||
let prompt_id =
|
let prompt_id = import_prompt_from_deeplink(&state, request.clone()).expect("import prompt");
|
||||||
import_prompt_from_deeplink(&state, request.clone()).expect("import prompt");
|
|
||||||
|
|
||||||
let prompts = state.db.get_prompts("codex").expect("get prompts");
|
let prompts = state.db.get_prompts("codex").expect("get prompts");
|
||||||
let prompt = prompts.get(&prompt_id).expect("prompt saved");
|
let prompt = prompts.get(&prompt_id).expect("prompt saved");
|
||||||
@@ -373,12 +368,11 @@ fn test_parse_mcp_deeplink() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_skill_deeplink() {
|
fn test_parse_skill_deeplink() {
|
||||||
let url = "ccswitch://v1/import?resource=skill&repo=owner/repo&directory=skills&branch=dev&skills_path=src";
|
let url = "ccswitch://v1/import?resource=skill&repo=owner/repo&directory=skills&branch=dev";
|
||||||
let request = parse_deeplink_url(&url).unwrap();
|
let request = parse_deeplink_url(&url).unwrap();
|
||||||
|
|
||||||
assert_eq!(request.resource, "skill");
|
assert_eq!(request.resource, "skill");
|
||||||
assert_eq!(request.repo.unwrap(), "owner/repo");
|
assert_eq!(request.repo.unwrap(), "owner/repo");
|
||||||
assert_eq!(request.directory.unwrap(), "skills");
|
assert_eq!(request.directory.unwrap(), "skills");
|
||||||
assert_eq!(request.branch.unwrap(), "dev");
|
assert_eq!(request.branch.unwrap(), "dev");
|
||||||
assert_eq!(request.skills_path.unwrap(), "src");
|
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-83
@@ -69,6 +69,11 @@ impl TrayTexts {
|
|||||||
no_provider_hint: " (No providers yet, please add them from the main window)",
|
no_provider_hint: " (No providers yet, please add them from the main window)",
|
||||||
quit: "Quit",
|
quit: "Quit",
|
||||||
},
|
},
|
||||||
|
"ja" => Self {
|
||||||
|
show_main: "メインウィンドウを開く",
|
||||||
|
no_provider_hint: " (プロバイダーがまだありません。メイン画面から追加してください)",
|
||||||
|
quit: "終了",
|
||||||
|
},
|
||||||
_ => Self {
|
_ => Self {
|
||||||
show_main: "打开主界面",
|
show_main: "打开主界面",
|
||||||
no_provider_hint: " (无供应商,请在主界面添加)",
|
no_provider_hint: " (无供应商,请在主界面添加)",
|
||||||
@@ -223,11 +228,9 @@ fn create_tray_menu(
|
|||||||
let providers = app_state.db.get_all_providers(app_type_str)?;
|
let providers = app_state.db.get_all_providers(app_type_str)?;
|
||||||
|
|
||||||
// 使用有效的当前供应商 ID(验证存在性,自动清理失效 ID)
|
// 使用有效的当前供应商 ID(验证存在性,自动清理失效 ID)
|
||||||
let current_id = crate::settings::get_effective_current_provider(
|
let current_id =
|
||||||
&app_state.db,
|
crate::settings::get_effective_current_provider(&app_state.db, §ion.app_type)?
|
||||||
§ion.app_type,
|
.unwrap_or_default();
|
||||||
)?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let manager = crate::provider::ProviderManager {
|
let manager = crate::provider::ProviderManager {
|
||||||
providers,
|
providers,
|
||||||
@@ -614,54 +617,47 @@ pub fn run() {
|
|||||||
|
|
||||||
let app_state = AppState::new(db);
|
let app_state = AppState::new(db);
|
||||||
|
|
||||||
// 检查是否需要首次导入(数据库为空)
|
// ============================================================
|
||||||
let need_first_import = app_state
|
// 按表独立判断的导入逻辑(各类数据独立检查,互不影响)
|
||||||
.db
|
// ============================================================
|
||||||
.is_empty_for_first_import()
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
log::warn!("Failed to check if database is empty: {e}");
|
|
||||||
false
|
|
||||||
});
|
|
||||||
|
|
||||||
if need_first_import {
|
// 1. 初始化默认 Skills 仓库(已有内置检查:表非空则跳过)
|
||||||
// 数据库为空,尝试从用户现有的配置文件导入数据并初始化默认配置
|
match app_state.db.init_default_skill_repos() {
|
||||||
log::info!(
|
Ok(count) if count > 0 => {
|
||||||
"Empty database detected, importing existing configurations and initializing defaults..."
|
log::info!("✓ Initialized {count} default skill repositories");
|
||||||
);
|
|
||||||
|
|
||||||
// 1. 初始化默认 Skills 仓库(3个)
|
|
||||||
match app_state.db.init_default_skill_repos() {
|
|
||||||
Ok(count) if count > 0 => {
|
|
||||||
log::info!("✓ Initialized {count} default skill repositories");
|
|
||||||
}
|
|
||||||
Ok(_) => log::debug!("No default skill repositories to initialize"),
|
|
||||||
Err(e) => log::warn!("✗ Failed to initialize default skill repos: {e}"),
|
|
||||||
}
|
}
|
||||||
|
Ok(_) => {} // 表非空,静默跳过
|
||||||
|
Err(e) => log::warn!("✗ Failed to initialize default skill repos: {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 导入供应商配置(从 live 配置文件)
|
// 2. 导入供应商配置(已有内置检查:该应用已有供应商则跳过)
|
||||||
for app in [
|
for app in [
|
||||||
crate::app_config::AppType::Claude,
|
crate::app_config::AppType::Claude,
|
||||||
crate::app_config::AppType::Codex,
|
crate::app_config::AppType::Codex,
|
||||||
crate::app_config::AppType::Gemini,
|
crate::app_config::AppType::Gemini,
|
||||||
] {
|
] {
|
||||||
match crate::services::provider::ProviderService::import_default_config(
|
match crate::services::provider::ProviderService::import_default_config(
|
||||||
&app_state,
|
&app_state,
|
||||||
app.clone(),
|
app.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(true) => {
|
||||||
log::info!("✓ Imported default provider for {}", app.as_str());
|
log::info!("✓ Imported default provider for {}", app.as_str());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Ok(false) => {} // 已有供应商,静默跳过
|
||||||
log::debug!(
|
Err(e) => {
|
||||||
"○ No default provider to import for {}: {}",
|
log::debug!(
|
||||||
app.as_str(),
|
"○ No default provider to import for {}: {}",
|
||||||
e
|
app.as_str(),
|
||||||
);
|
e
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 导入 MCP 服务器配置(表空时触发)
|
||||||
|
if app_state.db.is_mcp_table_empty().unwrap_or(false) {
|
||||||
|
log::info!("MCP table empty, importing from live configurations...");
|
||||||
|
|
||||||
// 3. 导入 MCP 服务器配置
|
|
||||||
match crate::services::mcp::McpService::import_from_claude(&app_state) {
|
match crate::services::mcp::McpService::import_from_claude(&app_state) {
|
||||||
Ok(count) if count > 0 => {
|
Ok(count) if count > 0 => {
|
||||||
log::info!("✓ Imported {count} MCP server(s) from Claude");
|
log::info!("✓ Imported {count} MCP server(s) from Claude");
|
||||||
@@ -685,42 +681,28 @@ pub fn run() {
|
|||||||
Ok(_) => log::debug!("○ No Gemini MCP servers found to import"),
|
Ok(_) => log::debug!("○ No Gemini MCP servers found to import"),
|
||||||
Err(e) => log::warn!("✗ Failed to import Gemini MCP: {e}"),
|
Err(e) => log::warn!("✗ Failed to import Gemini MCP: {e}"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 导入提示词文件
|
// 4. 导入提示词文件(表空时触发)
|
||||||
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
|
if app_state.db.is_prompts_table_empty().unwrap_or(false) {
|
||||||
&app_state,
|
log::info!("Prompts table empty, importing from live configurations...");
|
||||||
|
|
||||||
|
for app in [
|
||||||
crate::app_config::AppType::Claude,
|
crate::app_config::AppType::Claude,
|
||||||
) {
|
|
||||||
Ok(count) if count > 0 => {
|
|
||||||
log::info!("✓ Imported {count} prompt(s) from Claude");
|
|
||||||
}
|
|
||||||
Ok(_) => log::debug!("○ No Claude prompt file found to import"),
|
|
||||||
Err(e) => log::warn!("✗ Failed to import Claude prompt: {e}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
|
|
||||||
&app_state,
|
|
||||||
crate::app_config::AppType::Codex,
|
crate::app_config::AppType::Codex,
|
||||||
) {
|
|
||||||
Ok(count) if count > 0 => {
|
|
||||||
log::info!("✓ Imported {count} prompt(s) from Codex");
|
|
||||||
}
|
|
||||||
Ok(_) => log::debug!("○ No Codex prompt file found to import"),
|
|
||||||
Err(e) => log::warn!("✗ Failed to import Codex prompt: {e}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
|
|
||||||
&app_state,
|
|
||||||
crate::app_config::AppType::Gemini,
|
crate::app_config::AppType::Gemini,
|
||||||
) {
|
] {
|
||||||
Ok(count) if count > 0 => {
|
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
|
||||||
log::info!("✓ Imported {count} prompt(s) from Gemini");
|
&app_state,
|
||||||
|
app.clone(),
|
||||||
|
) {
|
||||||
|
Ok(count) if count > 0 => {
|
||||||
|
log::info!("✓ Imported {count} prompt(s) for {}", app.as_str());
|
||||||
|
}
|
||||||
|
Ok(_) => log::debug!("○ No prompt file found for {}", app.as_str()),
|
||||||
|
Err(e) => log::warn!("✗ Failed to import prompt for {}: {e}", app.as_str()),
|
||||||
}
|
}
|
||||||
Ok(_) => log::debug!("○ No Gemini prompt file found to import"),
|
|
||||||
Err(e) => log::warn!("✗ Failed to import Gemini prompt: {e}"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("First-time import completed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 迁移旧的 app_config_dir 配置到 Store
|
// 迁移旧的 app_config_dir 配置到 Store
|
||||||
@@ -1033,21 +1015,19 @@ fn show_migration_error_dialog(app: &tauri::AppHandle, error: &str) -> bool {
|
|||||||
|
|
||||||
let message = if is_chinese_locale() {
|
let message = if is_chinese_locale() {
|
||||||
format!(
|
format!(
|
||||||
"从旧版本迁移配置时发生错误:\n\n{}\n\n\
|
"从旧版本迁移配置时发生错误:\n\n{error}\n\n\
|
||||||
您的数据尚未丢失,旧配置文件仍然保留。\n\
|
您的数据尚未丢失,旧配置文件仍然保留。\n\
|
||||||
建议回退到旧版本 CC Switch 以保护数据。\n\n\
|
建议回退到旧版本 CC Switch 以保护数据。\n\n\
|
||||||
点击「重试」重新尝试迁移\n\
|
点击「重试」重新尝试迁移\n\
|
||||||
点击「退出」关闭程序(可回退版本后重新打开)",
|
点击「退出」关闭程序(可回退版本后重新打开)"
|
||||||
error
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"An error occurred while migrating configuration:\n\n{}\n\n\
|
"An error occurred while migrating configuration:\n\n{error}\n\n\
|
||||||
Your data is NOT lost - the old config file is still preserved.\n\
|
Your data is NOT lost - the old config file is still preserved.\n\
|
||||||
Consider rolling back to an older CC Switch version.\n\n\
|
Consider rolling back to an older CC Switch version.\n\n\
|
||||||
Click 'Retry' to attempt migration again\n\
|
Click 'Retry' to attempt migration again\n\
|
||||||
Click 'Exit' to close the program",
|
Click 'Exit' to close the program"
|
||||||
error
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,12 @@ impl PromptService {
|
|||||||
state: &AppState,
|
state: &AppState,
|
||||||
app: AppType,
|
app: AppType,
|
||||||
) -> Result<usize, AppError> {
|
) -> Result<usize, AppError> {
|
||||||
|
// 幂等性保护:该应用已有提示词则跳过
|
||||||
|
let existing = state.db.get_prompts(app.as_str())?;
|
||||||
|
if !existing.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
let file_path = prompt_file_path(&app)?;
|
let file_path = prompt_file_path(&app)?;
|
||||||
|
|
||||||
// 检查文件是否存在
|
// 检查文件是否存在
|
||||||
|
|||||||
@@ -100,12 +100,13 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
|||||||
write_json_file(&path, &provider.settings_config)?;
|
write_json_file(&path, &provider.settings_config)?;
|
||||||
}
|
}
|
||||||
AppType::Codex => {
|
AppType::Codex => {
|
||||||
let obj = provider.settings_config.as_object().ok_or_else(|| {
|
let obj = provider
|
||||||
AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string())
|
.settings_config
|
||||||
})?;
|
.as_object()
|
||||||
let auth = obj.get("auth").ok_or_else(|| {
|
.ok_or_else(|| AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string()))?;
|
||||||
AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string())
|
let auth = obj
|
||||||
})?;
|
.get("auth")
|
||||||
|
.ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?;
|
||||||
let config_str = obj.get("config").and_then(|v| v.as_str()).ok_or_else(|| {
|
let config_str = obj.get("config").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||||
AppError::Config("Codex 供应商配置缺少 'config' 字段或不是字符串".to_string())
|
AppError::Config("Codex 供应商配置缺少 'config' 字段或不是字符串".to_string())
|
||||||
})?;
|
})?;
|
||||||
@@ -113,8 +114,7 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
|||||||
let auth_path = get_codex_auth_path();
|
let auth_path = get_codex_auth_path();
|
||||||
write_json_file(&auth_path, auth)?;
|
write_json_file(&auth_path, auth)?;
|
||||||
let config_path = get_codex_config_path();
|
let config_path = get_codex_config_path();
|
||||||
std::fs::write(&config_path, config_str)
|
std::fs::write(&config_path, config_str).map_err(|e| AppError::io(&config_path, e))?;
|
||||||
.map_err(|e| AppError::io(&config_path, e))?;
|
|
||||||
}
|
}
|
||||||
AppType::Gemini => {
|
AppType::Gemini => {
|
||||||
// Delegate to write_gemini_live which handles env file writing correctly
|
// Delegate to write_gemini_live which handles env file writing correctly
|
||||||
@@ -132,11 +132,11 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
|||||||
pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
||||||
for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||||
// Use validated effective current provider
|
// Use validated effective current provider
|
||||||
let current_id = match crate::settings::get_effective_current_provider(&state.db, &app_type)?
|
let current_id =
|
||||||
{
|
match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
if let Some(provider) = providers.get(¤t_id) {
|
if let Some(provider) = providers.get(¤t_id) {
|
||||||
@@ -215,11 +215,14 @@ pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Import default configuration from live files
|
/// Import default configuration from live files
|
||||||
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> {
|
///
|
||||||
|
/// Returns `Ok(true)` if a provider was actually imported,
|
||||||
|
/// `Ok(false)` if skipped (providers already exist for this app).
|
||||||
|
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
||||||
{
|
{
|
||||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
if !providers.is_empty() {
|
if !providers.is_empty() {
|
||||||
return Ok(());
|
return Ok(false); // 已有供应商,跳过
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +301,7 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(),
|
|||||||
.db
|
.db
|
||||||
.set_current_provider(app_type.as_str(), &provider.id)?;
|
.set_current_provider(app_type.as_str(), &provider.id)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(true) // 真正导入了
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write Gemini live configuration with authentication handling
|
/// Write Gemini live configuration with authentication handling
|
||||||
|
|||||||
@@ -225,7 +225,9 @@ impl ProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Import default configuration from live files (re-export)
|
/// Import default configuration from live files (re-export)
|
||||||
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> {
|
///
|
||||||
|
/// Returns `Ok(true)` if imported, `Ok(false)` if skipped.
|
||||||
|
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
||||||
import_default_config(state, app_type)
|
import_default_config(state, app_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,9 +168,7 @@ pub(crate) fn validate_usage_script(script: &UsageScript) -> Result<(), AppError
|
|||||||
if interval > 1440 {
|
if interval > 1440 {
|
||||||
return Err(AppError::localized(
|
return Err(AppError::localized(
|
||||||
"usage_script.interval_too_large",
|
"usage_script.interval_too_large",
|
||||||
format!(
|
format!("自动查询间隔不能超过 1440 分钟(24小时),当前值: {interval}"),
|
||||||
"自动查询间隔不能超过 1440 分钟(24小时),当前值: {interval}"
|
|
||||||
),
|
|
||||||
format!(
|
format!(
|
||||||
"Auto query interval cannot exceed 1440 minutes (24 hours), current: {interval}"
|
"Auto query interval cannot exceed 1440 minutes (24 hours), current: {interval}"
|
||||||
),
|
),
|
||||||
|
|||||||
+155
-120
@@ -34,9 +34,6 @@ 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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 仓库配置
|
/// 仓库配置
|
||||||
@@ -50,9 +47,6 @@ pub struct SkillRepo {
|
|||||||
pub branch: String,
|
pub branch: String,
|
||||||
/// 是否启用
|
/// 是否启用
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// 技能所在的子目录路径 (可选, 如 "skills", "my-skills/subdir")
|
|
||||||
#[serde(rename = "skillsPath")]
|
|
||||||
pub skills_path: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 技能安装状态
|
/// 技能安装状态
|
||||||
@@ -84,21 +78,18 @@ impl Default for SkillStore {
|
|||||||
name: "awesome-claude-skills".to_string(),
|
name: "awesome-claude-skills".to_string(),
|
||||||
branch: "main".to_string(),
|
branch: "main".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
skills_path: None, // 扫描根目录
|
|
||||||
},
|
},
|
||||||
SkillRepo {
|
SkillRepo {
|
||||||
owner: "anthropics".to_string(),
|
owner: "anthropics".to_string(),
|
||||||
name: "skills".to_string(),
|
name: "skills".to_string(),
|
||||||
branch: "main".to_string(),
|
branch: "main".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
skills_path: None, // 扫描根目录
|
|
||||||
},
|
},
|
||||||
SkillRepo {
|
SkillRepo {
|
||||||
owner: "cexll".to_string(),
|
owner: "cexll".to_string(),
|
||||||
name: "myclaude".to_string(),
|
name: "myclaude".to_string(),
|
||||||
branch: "master".to_string(),
|
branch: "master".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
skills_path: Some("skills".to_string()), // 扫描 skills 子目录
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -194,76 +185,11 @@ impl SkillService {
|
|||||||
})??;
|
})??;
|
||||||
let mut skills = Vec::new();
|
let mut skills = Vec::new();
|
||||||
|
|
||||||
// 确定要扫描的目录路径
|
// 扫描仓库根目录(支持全仓库递归扫描)
|
||||||
let scan_dir = if let Some(ref skills_path) = repo.skills_path {
|
let scan_dir = temp_dir.clone();
|
||||||
// 如果指定了 skillsPath,则扫描该子目录
|
|
||||||
let subdir = temp_dir.join(skills_path.trim_matches('/'));
|
|
||||||
if !subdir.exists() {
|
|
||||||
log::warn!(
|
|
||||||
"仓库 {}/{} 中指定的技能路径 '{}' 不存在",
|
|
||||||
repo.owner,
|
|
||||||
repo.name,
|
|
||||||
skills_path
|
|
||||||
);
|
|
||||||
let _ = fs::remove_dir_all(&temp_dir);
|
|
||||||
return Ok(skills);
|
|
||||||
}
|
|
||||||
subdir
|
|
||||||
} else {
|
|
||||||
// 否则扫描仓库根目录
|
|
||||||
temp_dir.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 遍历目标目录
|
// 递归扫描目录查找所有技能
|
||||||
for entry in fs::read_dir(&scan_dir)? {
|
self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?;
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if !path.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let skill_md = path.join("SKILL.md");
|
|
||||||
if !skill_md.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析技能元数据
|
|
||||||
match self.parse_skill_metadata(&skill_md) {
|
|
||||||
Ok(meta) => {
|
|
||||||
// 安全地获取目录名
|
|
||||||
let Some(dir_name) = path.file_name() else {
|
|
||||||
log::warn!("Failed to get directory name from path: {path:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let directory = dir_name.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// 构建 README URL(考虑 skillsPath)
|
|
||||||
let readme_path = if let Some(ref skills_path) = repo.skills_path {
|
|
||||||
format!("{}/{}", skills_path.trim_matches('/'), directory)
|
|
||||||
} else {
|
|
||||||
directory.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
skills.push(Skill {
|
|
||||||
key: format!("{}/{}:{}", repo.owner, repo.name, directory),
|
|
||||||
name: meta.name.unwrap_or_else(|| directory.clone()),
|
|
||||||
description: meta.description.unwrap_or_default(),
|
|
||||||
directory,
|
|
||||||
readme_url: Some(format!(
|
|
||||||
"https://github.com/{}/{}/tree/{}/{}",
|
|
||||||
repo.owner, repo.name, repo.branch, readme_path
|
|
||||||
)),
|
|
||||||
installed: false,
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理临时目录
|
// 清理临时目录
|
||||||
let _ = fs::remove_dir_all(&temp_dir);
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
@@ -271,6 +197,85 @@ impl SkillService {
|
|||||||
Ok(skills)
|
Ok(skills)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 递归扫描目录查找 SKILL.md
|
||||||
|
///
|
||||||
|
/// 规则:
|
||||||
|
/// 1. 如果当前目录存在 SKILL.md,则识别为技能,停止扫描其子目录(子目录视为功能文件夹)
|
||||||
|
/// 2. 如果当前目录不存在 SKILL.md,则递归扫描所有子目录
|
||||||
|
fn scan_dir_recursive(
|
||||||
|
&self,
|
||||||
|
current_dir: &Path,
|
||||||
|
base_dir: &Path,
|
||||||
|
repo: &SkillRepo,
|
||||||
|
skills: &mut Vec<Skill>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// 检查当前目录是否包含 SKILL.md
|
||||||
|
let skill_md = current_dir.join("SKILL.md");
|
||||||
|
|
||||||
|
if skill_md.exists() {
|
||||||
|
// 发现技能!获取相对路径作为目录名
|
||||||
|
let directory = if current_dir == base_dir {
|
||||||
|
// 根目录的 SKILL.md,使用仓库名
|
||||||
|
repo.name.clone()
|
||||||
|
} else {
|
||||||
|
// 子目录的 SKILL.md,使用相对路径
|
||||||
|
current_dir
|
||||||
|
.strip_prefix(base_dir)
|
||||||
|
.unwrap_or(current_dir)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(skill) = self.build_skill_from_metadata(&skill_md, &directory, repo) {
|
||||||
|
skills.push(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止扫描此目录的子目录(同级目录都是功能文件夹)
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未发现 SKILL.md,继续递归扫描所有子目录
|
||||||
|
for entry in fs::read_dir(current_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
// 只处理目录
|
||||||
|
if path.is_dir() {
|
||||||
|
self.scan_dir_recursive(&path, base_dir, repo, skills)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 SKILL.md 构建技能对象
|
||||||
|
fn build_skill_from_metadata(
|
||||||
|
&self,
|
||||||
|
skill_md: &Path,
|
||||||
|
directory: &str,
|
||||||
|
repo: &SkillRepo,
|
||||||
|
) -> Result<Skill> {
|
||||||
|
let meta = self.parse_skill_metadata(skill_md)?;
|
||||||
|
|
||||||
|
// 构建 README URL
|
||||||
|
let readme_path = directory.to_string();
|
||||||
|
|
||||||
|
Ok(Skill {
|
||||||
|
key: format!("{}/{}:{}", repo.owner, repo.name, directory),
|
||||||
|
name: meta.name.unwrap_or_else(|| directory.to_string()),
|
||||||
|
description: meta.description.unwrap_or_default(),
|
||||||
|
directory: directory.to_string(),
|
||||||
|
readme_url: Some(format!(
|
||||||
|
"https://github.com/{}/{}/tree/{}/{}",
|
||||||
|
repo.owner, repo.name, repo.branch, readme_path
|
||||||
|
)),
|
||||||
|
installed: false,
|
||||||
|
repo_owner: Some(repo.owner.clone()),
|
||||||
|
repo_name: Some(repo.name.clone()),
|
||||||
|
repo_branch: Some(repo.branch.clone()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 解析技能元数据
|
/// 解析技能元数据
|
||||||
fn parse_skill_metadata(&self, path: &Path) -> Result<SkillMetadata> {
|
fn parse_skill_metadata(&self, path: &Path) -> Result<SkillMetadata> {
|
||||||
let content = fs::read_to_string(path)?;
|
let content = fs::read_to_string(path)?;
|
||||||
@@ -302,25 +307,18 @@ impl SkillService {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in fs::read_dir(&self.install_dir)? {
|
// 收集所有本地技能
|
||||||
let entry = entry?;
|
let mut local_skills = Vec::new();
|
||||||
let path = entry.path();
|
self.scan_local_dir_recursive(&self.install_dir, &self.install_dir, &mut local_skills)?;
|
||||||
|
|
||||||
if !path.is_dir() {
|
// 处理找到的本地技能
|
||||||
continue;
|
for local_skill in local_skills {
|
||||||
}
|
let directory = &local_skill.directory;
|
||||||
|
|
||||||
// 安全地获取目录名
|
// 更新已安装状态(匹配远程技能)
|
||||||
let Some(dir_name) = path.file_name() else {
|
|
||||||
log::warn!("Failed to get directory name from path: {path:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let directory = dir_name.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// 更新已安装状态
|
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for skill in skills.iter_mut() {
|
for skill in skills.iter_mut() {
|
||||||
if skill.directory.eq_ignore_ascii_case(&directory) {
|
if skill.directory.eq_ignore_ascii_case(directory) {
|
||||||
skill.installed = true;
|
skill.installed = true;
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
@@ -329,23 +327,68 @@ impl SkillService {
|
|||||||
|
|
||||||
// 添加本地独有的技能(仅当在仓库中未找到时)
|
// 添加本地独有的技能(仅当在仓库中未找到时)
|
||||||
if !found {
|
if !found {
|
||||||
let skill_md = path.join("SKILL.md");
|
skills.push(local_skill);
|
||||||
if skill_md.exists() {
|
}
|
||||||
if let Ok(meta) = self.parse_skill_metadata(&skill_md) {
|
}
|
||||||
skills.push(Skill {
|
|
||||||
key: format!("local:{directory}"),
|
Ok(())
|
||||||
name: meta.name.unwrap_or_else(|| directory.clone()),
|
}
|
||||||
description: meta.description.unwrap_or_default(),
|
|
||||||
directory: directory.clone(),
|
/// 递归扫描本地目录查找 SKILL.md
|
||||||
readme_url: None,
|
fn scan_local_dir_recursive(
|
||||||
installed: true,
|
&self,
|
||||||
repo_owner: None,
|
current_dir: &Path,
|
||||||
repo_name: None,
|
base_dir: &Path,
|
||||||
repo_branch: None,
|
skills: &mut Vec<Skill>,
|
||||||
skills_path: None,
|
) -> Result<()> {
|
||||||
});
|
// 检查当前目录是否包含 SKILL.md
|
||||||
}
|
let skill_md = current_dir.join("SKILL.md");
|
||||||
}
|
|
||||||
|
if skill_md.exists() {
|
||||||
|
// 发现技能!获取相对路径作为目录名
|
||||||
|
let directory = if current_dir == base_dir {
|
||||||
|
// 如果是 install_dir 本身,使用最后一段路径名
|
||||||
|
current_dir
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
// 使用相对于 install_dir 的路径
|
||||||
|
current_dir
|
||||||
|
.strip_prefix(base_dir)
|
||||||
|
.unwrap_or(current_dir)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析元数据并创建本地技能对象
|
||||||
|
if let Ok(meta) = self.parse_skill_metadata(&skill_md) {
|
||||||
|
skills.push(Skill {
|
||||||
|
key: format!("local:{directory}"),
|
||||||
|
name: meta.name.unwrap_or_else(|| directory.clone()),
|
||||||
|
description: meta.description.unwrap_or_default(),
|
||||||
|
directory: directory.clone(),
|
||||||
|
readme_url: None,
|
||||||
|
installed: true,
|
||||||
|
repo_owner: None,
|
||||||
|
repo_name: None,
|
||||||
|
repo_branch: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止扫描此目录的子目录(同级目录都是功能文件夹)
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未发现 SKILL.md,继续递归扫描所有子目录
|
||||||
|
for entry in fs::read_dir(current_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
// 只处理目录
|
||||||
|
if path.is_dir() {
|
||||||
|
self.scan_local_dir_recursive(&path, base_dir, skills)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,16 +542,8 @@ impl SkillService {
|
|||||||
))
|
))
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
// 根据 skills_path 确定源目录路径
|
// 确定源目录路径(技能相对于仓库根目录的路径)
|
||||||
let source = if let Some(ref skills_path) = repo.skills_path {
|
let source = temp_dir.join(&directory);
|
||||||
// 如果指定了 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);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ impl AppSettings {
|
|||||||
.language
|
.language
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.trim())
|
.map(|s| s.trim())
|
||||||
.filter(|s| matches!(*s, "en" | "zh"))
|
.filter(|s| matches!(*s, "en" | "zh" | "ja"))
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ use std::fs;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cc_switch_lib::{
|
use cc_switch_lib::{
|
||||||
get_claude_settings_path, read_json_file, AppError, AppType, ConfigService,
|
get_claude_settings_path, read_json_file, AppError, AppType, ConfigService, MultiAppConfig,
|
||||||
MultiAppConfig, Provider, ProviderMeta,
|
Provider, ProviderMeta,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[path = "support.rs"]
|
#[path = "support.rs"]
|
||||||
mod support;
|
mod support;
|
||||||
use support::{create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
use support::{
|
||||||
|
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_claude_provider_writes_live_settings() {
|
fn sync_claude_provider_writes_live_settings() {
|
||||||
|
|||||||
@@ -42,9 +42,13 @@ fn import_default_config_claude_persists_provider() {
|
|||||||
.expect("import default config succeeds");
|
.expect("import default config succeeds");
|
||||||
|
|
||||||
// 验证内存状态
|
// 验证内存状态
|
||||||
let providers = state.db.get_all_providers(AppType::Claude.as_str())
|
let providers = state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Claude.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
let current_id = state.db.get_current_provider(AppType::Claude.as_str())
|
let current_id = state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Claude.as_str())
|
||||||
.expect("get current provider");
|
.expect("get current provider");
|
||||||
assert_eq!(current_id.as_deref(), Some("default"));
|
assert_eq!(current_id.as_deref(), Some("default"));
|
||||||
let default_provider = providers.get("default").expect("default provider");
|
let default_provider = providers.get("default").expect("default provider");
|
||||||
@@ -87,7 +91,9 @@ fn import_default_config_without_live_file_returns_error() {
|
|||||||
|
|
||||||
// 使用数据库架构,不再检查 config.json
|
// 使用数据库架构,不再检查 config.json
|
||||||
// 失败的导入不应该向数据库写入任何供应商
|
// 失败的导入不应该向数据库写入任何供应商
|
||||||
let providers = state.db.get_all_providers(AppType::Claude.as_str())
|
let providers = state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Claude.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
assert!(
|
assert!(
|
||||||
providers.is_empty(),
|
providers.is_empty(),
|
||||||
@@ -125,8 +131,7 @@ fn import_mcp_from_claude_creates_config_and_enables_servers() {
|
|||||||
"import should report inserted or normalized entries"
|
"import should report inserted or normalized entries"
|
||||||
);
|
);
|
||||||
|
|
||||||
let servers = state.db.get_all_mcp_servers()
|
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||||
.expect("get all mcp servers");
|
|
||||||
let entry = servers
|
let entry = servers
|
||||||
.get("echo")
|
.get("echo")
|
||||||
.expect("server imported into unified structure");
|
.expect("server imported into unified structure");
|
||||||
@@ -168,8 +173,7 @@ fn import_mcp_from_claude_invalid_json_preserves_state() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用数据库架构,检查 MCP 服务器未被写入
|
// 使用数据库架构,检查 MCP 服务器未被写入
|
||||||
let servers = state.db.get_all_mcp_servers()
|
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||||
.expect("get all mcp servers");
|
|
||||||
assert!(
|
assert!(
|
||||||
servers.is_empty(),
|
servers.is_empty(),
|
||||||
"failed import should not persist any MCP servers to database"
|
"failed import should not persist any MCP servers to database"
|
||||||
@@ -224,11 +228,8 @@ fn set_mcp_enabled_for_codex_writes_live_config() {
|
|||||||
McpService::toggle_app(&state, "codex-server", AppType::Codex, true)
|
McpService::toggle_app(&state, "codex-server", AppType::Codex, true)
|
||||||
.expect("toggle_app should succeed");
|
.expect("toggle_app should succeed");
|
||||||
|
|
||||||
let servers = state.db.get_all_mcp_servers()
|
let servers = state.db.get_all_mcp_servers().expect("get all mcp servers");
|
||||||
.expect("get all mcp servers");
|
let entry = servers.get("codex-server").expect("codex server exists");
|
||||||
let entry = servers
|
|
||||||
.get("codex-server")
|
|
||||||
.expect("codex server exists");
|
|
||||||
assert!(
|
assert!(
|
||||||
entry.apps.codex,
|
entry.apps.codex,
|
||||||
"server should have Codex app enabled after toggle"
|
"server should have Codex app enabled after toggle"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use cc_switch_lib::{
|
|||||||
|
|
||||||
#[path = "support.rs"]
|
#[path = "support.rs"]
|
||||||
mod support;
|
mod support;
|
||||||
use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use support::{create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn switch_provider_updates_codex_live_and_state() {
|
fn switch_provider_updates_codex_live_and_state() {
|
||||||
@@ -104,16 +104,22 @@ command = "say"
|
|||||||
"config.toml should contain synced MCP servers"
|
"config.toml should contain synced MCP servers"
|
||||||
);
|
);
|
||||||
|
|
||||||
let current_id = app_state.db.get_current_provider(AppType::Codex.as_str())
|
let current_id = app_state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Codex.as_str())
|
||||||
.expect("get current provider");
|
.expect("get current provider");
|
||||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
assert_eq!(
|
||||||
|
current_id.as_deref(),
|
||||||
|
Some("new-provider"),
|
||||||
|
"current provider updated"
|
||||||
|
);
|
||||||
|
|
||||||
let providers = app_state.db.get_all_providers(AppType::Codex.as_str())
|
let providers = app_state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Codex.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
|
|
||||||
let new_provider = providers
|
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||||
.get("new-provider")
|
|
||||||
.expect("new provider exists");
|
|
||||||
let new_config_text = new_provider
|
let new_config_text = new_provider
|
||||||
.settings_config
|
.settings_config
|
||||||
.get("config")
|
.get("config")
|
||||||
@@ -165,7 +171,9 @@ fn switch_provider_missing_provider_returns_error() {
|
|||||||
|
|
||||||
let err_str = err.to_string();
|
let err_str = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
err_str.contains("供应商不存在") || err_str.contains("Provider not found") || err_str.contains("missing-provider"),
|
err_str.contains("供应商不存在")
|
||||||
|
|| err_str.contains("Provider not found")
|
||||||
|
|| err_str.contains("missing-provider"),
|
||||||
"error message should mention missing provider, got: {err_str}"
|
"error message should mention missing provider, got: {err_str}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -241,11 +249,19 @@ fn switch_provider_updates_claude_live_and_state() {
|
|||||||
"live settings.json should reflect new provider auth"
|
"live settings.json should reflect new provider auth"
|
||||||
);
|
);
|
||||||
|
|
||||||
let current_id = app_state.db.get_current_provider(AppType::Claude.as_str())
|
let current_id = app_state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Claude.as_str())
|
||||||
.expect("get current provider");
|
.expect("get current provider");
|
||||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
assert_eq!(
|
||||||
|
current_id.as_deref(),
|
||||||
|
Some("new-provider"),
|
||||||
|
"current provider updated"
|
||||||
|
);
|
||||||
|
|
||||||
let providers = app_state.db.get_all_providers(AppType::Claude.as_str())
|
let providers = app_state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Claude.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
|
|
||||||
let legacy_provider = providers
|
let legacy_provider = providers
|
||||||
@@ -258,9 +274,7 @@ fn switch_provider_updates_claude_live_and_state() {
|
|||||||
"previous provider should be backfilled with live config"
|
"previous provider should be backfilled with live config"
|
||||||
);
|
);
|
||||||
|
|
||||||
let new_provider = providers
|
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||||
.get("new-provider")
|
|
||||||
.expect("new provider exists");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
new_provider
|
new_provider
|
||||||
.settings_config
|
.settings_config
|
||||||
@@ -283,7 +297,9 @@ fn switch_provider_updates_claude_live_and_state() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 验证当前供应商已更新
|
// 验证当前供应商已更新
|
||||||
let current_id = app_state.db.get_current_provider(AppType::Claude.as_str())
|
let current_id = app_state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Claude.as_str())
|
||||||
.expect("get current provider");
|
.expect("get current provider");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
current_id.as_deref(),
|
current_id.as_deref(),
|
||||||
@@ -328,7 +344,9 @@ fn switch_provider_codex_missing_auth_returns_error_and_keeps_state() {
|
|||||||
other => panic!("expected config error, got {other:?}"),
|
other => panic!("expected config error, got {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_id = app_state.db.get_current_provider(AppType::Codex.as_str())
|
let current_id = app_state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Codex.as_str())
|
||||||
.expect("get current provider");
|
.expect("get current provider");
|
||||||
// 切换失败后,由于数据库操作是先设置再验证,current 可能已被设为 "invalid"
|
// 切换失败后,由于数据库操作是先设置再验证,current 可能已被设为 "invalid"
|
||||||
// 但由于 live 配置写入失败,状态应该回滚
|
// 但由于 live 配置写入失败,状态应该回滚
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use cc_switch_lib::{
|
use cc_switch_lib::{
|
||||||
get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType,
|
get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType, McpApps,
|
||||||
McpApps, McpServer, MultiAppConfig, Provider, ProviderMeta, ProviderService,
|
McpServer, MultiAppConfig, Provider, ProviderMeta, ProviderService,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[path = "support.rs"]
|
#[path = "support.rs"]
|
||||||
mod support;
|
mod support;
|
||||||
use support::{create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex};
|
use support::{
|
||||||
|
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
|
||||||
|
};
|
||||||
|
|
||||||
fn sanitize_provider_name(name: &str) -> String {
|
fn sanitize_provider_name(name: &str) -> String {
|
||||||
name.chars()
|
name.chars()
|
||||||
@@ -69,7 +71,10 @@ command = "say"
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用新的统一 MCP 结构(v3.7.0+)
|
// 使用新的统一 MCP 结构(v3.7.0+)
|
||||||
let servers = initial_config.mcp.servers.get_or_insert_with(Default::default);
|
let servers = initial_config
|
||||||
|
.mcp
|
||||||
|
.servers
|
||||||
|
.get_or_insert_with(Default::default);
|
||||||
servers.insert(
|
servers.insert(
|
||||||
"echo-server".into(),
|
"echo-server".into(),
|
||||||
McpServer {
|
McpServer {
|
||||||
@@ -111,16 +116,22 @@ command = "say"
|
|||||||
"config.toml should contain synced MCP servers"
|
"config.toml should contain synced MCP servers"
|
||||||
);
|
);
|
||||||
|
|
||||||
let current_id = state.db.get_current_provider(AppType::Codex.as_str())
|
let current_id = state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Codex.as_str())
|
||||||
.expect("read current provider after switch");
|
.expect("read current provider after switch");
|
||||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
assert_eq!(
|
||||||
|
current_id.as_deref(),
|
||||||
|
Some("new-provider"),
|
||||||
|
"current provider updated"
|
||||||
|
);
|
||||||
|
|
||||||
let providers = state.db.get_all_providers(AppType::Codex.as_str())
|
let providers = state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Codex.as_str())
|
||||||
.expect("read providers after switch");
|
.expect("read providers after switch");
|
||||||
|
|
||||||
let new_provider = providers
|
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||||
.get("new-provider")
|
|
||||||
.expect("new provider exists");
|
|
||||||
let new_config_text = new_provider
|
let new_config_text = new_provider
|
||||||
.settings_config
|
.settings_config
|
||||||
.get("config")
|
.get("config")
|
||||||
@@ -385,11 +396,19 @@ fn provider_service_switch_claude_updates_live_and_state() {
|
|||||||
"live settings.json should reflect new provider auth"
|
"live settings.json should reflect new provider auth"
|
||||||
);
|
);
|
||||||
|
|
||||||
let providers = state.db.get_all_providers(AppType::Claude.as_str())
|
let providers = state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Claude.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
let current_id = state.db.get_current_provider(AppType::Claude.as_str())
|
let current_id = state
|
||||||
|
.db
|
||||||
|
.get_current_provider(AppType::Claude.as_str())
|
||||||
.expect("get current provider");
|
.expect("get current provider");
|
||||||
assert_eq!(current_id.as_deref(), Some("new-provider"), "current provider updated");
|
assert_eq!(
|
||||||
|
current_id.as_deref(),
|
||||||
|
Some("new-provider"),
|
||||||
|
"current provider updated"
|
||||||
|
);
|
||||||
|
|
||||||
let legacy_provider = providers
|
let legacy_provider = providers
|
||||||
.get("old-provider")
|
.get("old-provider")
|
||||||
@@ -509,7 +528,9 @@ fn provider_service_delete_codex_removes_provider_and_files() {
|
|||||||
ProviderService::delete(&app_state, AppType::Codex, "to-delete")
|
ProviderService::delete(&app_state, AppType::Codex, "to-delete")
|
||||||
.expect("delete provider should succeed");
|
.expect("delete provider should succeed");
|
||||||
|
|
||||||
let providers = app_state.db.get_all_providers(AppType::Codex.as_str())
|
let providers = app_state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Codex.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
assert!(
|
assert!(
|
||||||
!providers.contains_key("to-delete"),
|
!providers.contains_key("to-delete"),
|
||||||
@@ -567,7 +588,9 @@ fn provider_service_delete_claude_removes_provider_files() {
|
|||||||
|
|
||||||
ProviderService::delete(&app_state, AppType::Claude, "delete").expect("delete claude provider");
|
ProviderService::delete(&app_state, AppType::Claude, "delete").expect("delete claude provider");
|
||||||
|
|
||||||
let providers = app_state.db.get_all_providers(AppType::Claude.as_str())
|
let providers = app_state
|
||||||
|
.db
|
||||||
|
.get_all_providers(AppType::Claude.as_str())
|
||||||
.expect("get all providers");
|
.expect("get all providers");
|
||||||
assert!(
|
assert!(
|
||||||
!providers.contains_key("delete"),
|
!providers.contains_key("delete"),
|
||||||
@@ -608,15 +631,18 @@ fn provider_service_delete_current_provider_returns_error() {
|
|||||||
.expect_err("deleting current provider should fail");
|
.expect_err("deleting current provider should fail");
|
||||||
match err {
|
match err {
|
||||||
AppError::Localized { zh, .. } => assert!(
|
AppError::Localized { zh, .. } => assert!(
|
||||||
zh.contains("不能删除当前正在使用的供应商") || zh.contains("无法删除当前正在使用的供应商"),
|
zh.contains("不能删除当前正在使用的供应商")
|
||||||
|
|| zh.contains("无法删除当前正在使用的供应商"),
|
||||||
"unexpected message: {zh}"
|
"unexpected message: {zh}"
|
||||||
),
|
),
|
||||||
AppError::Config(msg) => assert!(
|
AppError::Config(msg) => assert!(
|
||||||
msg.contains("不能删除当前正在使用的供应商") || msg.contains("无法删除当前正在使用的供应商"),
|
msg.contains("不能删除当前正在使用的供应商")
|
||||||
|
|| msg.contains("无法删除当前正在使用的供应商"),
|
||||||
"unexpected message: {msg}"
|
"unexpected message: {msg}"
|
||||||
),
|
),
|
||||||
AppError::Message(msg) => assert!(
|
AppError::Message(msg) => assert!(
|
||||||
msg.contains("不能删除当前正在使用的供应商") || msg.contains("无法删除当前正在使用的供应商"),
|
msg.contains("不能删除当前正在使用的供应商")
|
||||||
|
|| msg.contains("无法删除当前正在使用的供应商"),
|
||||||
"unexpected message: {msg}"
|
"unexpected message: {msg}"
|
||||||
),
|
),
|
||||||
other => panic!("expected Config/Message error, got {other:?}"),
|
other => panic!("expected Config/Message error, got {other:?}"),
|
||||||
|
|||||||
@@ -176,10 +176,16 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(
|
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(
|
||||||
() => {
|
() => {
|
||||||
const existingScript = provider.meta?.usage_script;
|
const existingScript = provider.meta?.usage_script;
|
||||||
|
// 检测 NEW_API 模板(有 accessToken 或 userId)
|
||||||
if (existingScript?.accessToken || existingScript?.userId) {
|
if (existingScript?.accessToken || existingScript?.userId) {
|
||||||
return TEMPLATE_KEYS.NEW_API;
|
return TEMPLATE_KEYS.NEW_API;
|
||||||
}
|
}
|
||||||
return null;
|
// 检测 GENERAL 模板(有 apiKey 或 baseUrl)
|
||||||
|
if (existingScript?.apiKey || existingScript?.baseUrl) {
|
||||||
|
return TEMPLATE_KEYS.GENERAL;
|
||||||
|
}
|
||||||
|
// 新配置或无凭证:默认使用 GENERAL(与默认代码模板一致)
|
||||||
|
return TEMPLATE_KEYS.GENERAL;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -349,14 +355,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
footer={footer}
|
footer={footer}
|
||||||
>
|
>
|
||||||
<div className="glass rounded-xl border border-white/10 px-6 py-4 flex items-center justify-between gap-4">
|
<div className="glass rounded-xl border border-white/10 px-6 py-4 flex items-center justify-between gap-4">
|
||||||
<div className="space-y-1">
|
<p className="text-base font-medium leading-none text-foreground">
|
||||||
<p className="text-sm font-medium leading-none text-foreground">
|
{t("usageScript.enableUsageQuery")}
|
||||||
{t("usageScript.enableUsageQuery")}
|
</p>
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("usageScript.autoQueryIntervalHint")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.enabled}
|
checked={script.enabled}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
@@ -370,14 +371,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 预设模板选择 */}
|
{/* 预设模板选择 */}
|
||||||
<div className="space-y-4 glass rounded-xl border border-white/10 p-6">
|
<div className="space-y-4 glass rounded-xl border border-white/10 p-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<Label className="text-base font-medium">
|
||||||
<Label className="text-base font-medium">
|
{t("usageScript.presetTemplate")}
|
||||||
{t("usageScript.presetTemplate")}
|
</Label>
|
||||||
</Label>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{t("usageScript.variablesHint")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{Object.keys(PRESET_TEMPLATES).map((name) => {
|
{Object.keys(PRESET_TEMPLATES).map((name) => {
|
||||||
const isSelected = selectedTemplate === name;
|
const isSelected = selectedTemplate === name;
|
||||||
@@ -447,7 +443,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="usage-base-url">Base URL</Label>
|
<Label htmlFor="usage-base-url">
|
||||||
|
{t("usageScript.baseUrl")}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="usage-base-url"
|
id="usage-base-url"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -466,7 +464,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
{selectedTemplate === TEMPLATE_KEYS.NEW_API && (
|
{selectedTemplate === TEMPLATE_KEYS.NEW_API && (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="usage-newapi-base-url">Base URL</Label>
|
<Label htmlFor="usage-newapi-base-url">
|
||||||
|
{t("usageScript.baseUrl")}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="usage-newapi-base-url"
|
id="usage-newapi-base-url"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -545,141 +545,36 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 脚本配置 */}
|
{/* 通用配置(始终显示) */}
|
||||||
<div className="space-y-4 glass rounded-xl border border-white/10 p-6">
|
<div className="grid gap-4 md:grid-cols-2 pt-4 border-t border-white/10">
|
||||||
<div className="flex items-center justify-between">
|
{/* 超时时间 */}
|
||||||
<h4 className="text-base font-medium text-foreground">
|
|
||||||
{t("usageScript.scriptConfig")}
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("usageScript.variablesHint")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="usage-request-url">
|
<Label htmlFor="usage-timeout">
|
||||||
{t("usageScript.requestUrl")}
|
{t("usageScript.timeoutSeconds")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="usage-request-url"
|
id="usage-timeout"
|
||||||
type="text"
|
type="number"
|
||||||
value={script.request?.url || ""}
|
min={0}
|
||||||
onChange={(e) => {
|
value={script.timeout ?? 10}
|
||||||
|
onChange={(e) =>
|
||||||
setScript({
|
setScript({
|
||||||
...script,
|
...script,
|
||||||
request: { ...script.request, url: e.target.value },
|
timeout: validateTimeout(e.target.value),
|
||||||
});
|
})
|
||||||
}}
|
}
|
||||||
placeholder={t("usageScript.requestUrlPlaceholder")}
|
onBlur={(e) =>
|
||||||
|
setScript({
|
||||||
|
...script,
|
||||||
|
timeout: validateTimeout(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
className="border-white/10"
|
className="border-white/10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
{/* 自动查询间隔 */}
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="usage-method">
|
|
||||||
{t("usageScript.method")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="usage-method"
|
|
||||||
type="text"
|
|
||||||
value={script.request?.method || "GET"}
|
|
||||||
onChange={(e) => {
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
request: {
|
|
||||||
...script.request,
|
|
||||||
method: e.target.value.toUpperCase(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
placeholder="GET / POST"
|
|
||||||
className="border-white/10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="usage-timeout">
|
|
||||||
{t("usageScript.timeoutSeconds")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="usage-timeout"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
value={script.timeout ?? 10}
|
|
||||||
onChange={(e) =>
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
timeout: validateTimeout(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onBlur={(e) =>
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
timeout: validateTimeout(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="border-white/10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="usage-headers">
|
|
||||||
{t("usageScript.headers")}
|
|
||||||
</Label>
|
|
||||||
<JsonEditor
|
|
||||||
id="usage-headers"
|
|
||||||
value={
|
|
||||||
script.request?.headers
|
|
||||||
? JSON.stringify(script.request.headers, null, 2)
|
|
||||||
: "{}"
|
|
||||||
}
|
|
||||||
onChange={(value) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value || "{}");
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
request: { ...script.request, headers: parsed },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Invalid headers JSON", error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
height={180}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="usage-body">{t("usageScript.body")}</Label>
|
|
||||||
<JsonEditor
|
|
||||||
id="usage-body"
|
|
||||||
value={
|
|
||||||
script.request?.body
|
|
||||||
? JSON.stringify(script.request.body, null, 2)
|
|
||||||
: "{}"
|
|
||||||
}
|
|
||||||
onChange={(value) => {
|
|
||||||
try {
|
|
||||||
const parsed =
|
|
||||||
value?.trim() === "" ? undefined : JSON.parse(value);
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
request: { ...script.request, body: parsed },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
t("usageScript.invalidJson") || "Body 必须是合法 JSON",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
height={220}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="usage-interval">
|
<Label htmlFor="usage-interval">
|
||||||
{t("usageScript.autoIntervalMinutes")}
|
{t("usageScript.autoIntervalMinutes")}
|
||||||
@@ -708,9 +603,6 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|||||||
}
|
}
|
||||||
className="border-white/10"
|
className="border-white/10"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t("usageScript.autoQueryIntervalHint")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,22 +30,11 @@ export function SkillConfirmation({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div>
|
<label className="block text-sm font-medium text-muted-foreground">
|
||||||
<label className="block text-sm font-medium text-muted-foreground">
|
{t("deeplink.skill.branch")}
|
||||||
{t("deeplink.skill.branch")}
|
</label>
|
||||||
</label>
|
<div className="mt-1 text-sm">{request.branch || "main"}</div>
|
||||||
<div className="mt-1 text-sm">{request.branch || "main"}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{request.skillsPath && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-muted-foreground">
|
|
||||||
{t("deeplink.skill.skillsPath")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 text-sm">{request.skillsPath}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-blue-600 dark:text-blue-400 text-sm bg-blue-50 dark:bg-blue-950/30 p-3 rounded border border-blue-200 dark:border-blue-800">
|
<div className="text-blue-600 dark:text-blue-400 text-sm bg-blue-50 dark:bg-blue-950/30 p-3 rounded border border-blue-200 dark:border-blue-800">
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type LanguageOption = "zh" | "en" | "ja";
|
||||||
|
|
||||||
interface LanguageSettingsProps {
|
interface LanguageSettingsProps {
|
||||||
value: "zh" | "en";
|
value: LanguageOption;
|
||||||
onChange: (value: "zh" | "en") => void;
|
onChange: (value: LanguageOption) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LanguageSettings({ value, onChange }: LanguageSettingsProps) {
|
export function LanguageSettings({ value, onChange }: LanguageSettingsProps) {
|
||||||
@@ -25,6 +27,9 @@ export function LanguageSettings({ value, onChange }: LanguageSettingsProps) {
|
|||||||
<LanguageButton active={value === "en"} onClick={() => onChange("en")}>
|
<LanguageButton active={value === "en"} onClick={() => onChange("en")}>
|
||||||
{t("settings.languageOptionEnglish")}
|
{t("settings.languageOptionEnglish")}
|
||||||
</LanguageButton>
|
</LanguageButton>
|
||||||
|
<LanguageButton active={value === "ja"} onClick={() => onChange("ja")}>
|
||||||
|
{t("settings.languageOptionJapanese")}
|
||||||
|
</LanguageButton>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export function RepoManager({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [repoUrl, setRepoUrl] = useState("");
|
const [repoUrl, setRepoUrl] = useState("");
|
||||||
const [branch, setBranch] = useState("");
|
const [branch, setBranch] = useState("");
|
||||||
const [skillsPath, setSkillsPath] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const getSkillCount = (repo: SkillRepo) =>
|
const getSkillCount = (repo: SkillRepo) =>
|
||||||
@@ -80,12 +79,10 @@ export function RepoManager({
|
|||||||
name: parsed.name,
|
name: parsed.name,
|
||||||
branch: branch || "main",
|
branch: branch || "main",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
skillsPath: skillsPath.trim() || undefined, // 仅在有值时传递
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setRepoUrl("");
|
setRepoUrl("");
|
||||||
setBranch("");
|
setBranch("");
|
||||||
setSkillsPath("");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
|
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
|
||||||
}
|
}
|
||||||
@@ -130,13 +127,6 @@ export function RepoManager({
|
|||||||
onChange={(e) => setBranch(e.target.value)}
|
onChange={(e) => setBranch(e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
id="skills-path"
|
|
||||||
placeholder={t("skills.repo.pathPlaceholder")}
|
|
||||||
value={skillsPath}
|
|
||||||
onChange={(e) => setSkillsPath(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
className="w-full sm:w-auto sm:px-4"
|
className="w-full sm:w-auto sm:px-4"
|
||||||
@@ -171,12 +161,6 @@ export function RepoManager({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{t("skills.repo.branch")}: {repo.branch || "main"}
|
{t("skills.repo.branch")}: {repo.branch || "main"}
|
||||||
{repo.skillsPath && (
|
|
||||||
<>
|
|
||||||
<span className="mx-2">•</span>
|
|
||||||
{t("skills.repo.path")}: {repo.skillsPath}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
|
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
|
||||||
{t("skills.repo.skillCount", {
|
{t("skills.repo.skillCount", {
|
||||||
count: getSkillCount(repo),
|
count: getSkillCount(repo),
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export function RepoManagerPanel({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [repoUrl, setRepoUrl] = useState("");
|
const [repoUrl, setRepoUrl] = useState("");
|
||||||
const [branch, setBranch] = useState("");
|
const [branch, setBranch] = useState("");
|
||||||
const [skillsPath, setSkillsPath] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const getSkillCount = (repo: SkillRepo) =>
|
const getSkillCount = (repo: SkillRepo) =>
|
||||||
@@ -67,12 +66,10 @@ export function RepoManagerPanel({
|
|||||||
name: parsed.name,
|
name: parsed.name,
|
||||||
branch: branch || "main",
|
branch: branch || "main",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
skillsPath: skillsPath.trim() || undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setRepoUrl("");
|
setRepoUrl("");
|
||||||
setBranch("");
|
setBranch("");
|
||||||
setSkillsPath("");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
|
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
|
||||||
}
|
}
|
||||||
@@ -110,31 +107,17 @@ export function RepoManagerPanel({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div>
|
||||||
<div>
|
<Label htmlFor="branch" className="text-foreground">
|
||||||
<Label htmlFor="branch" className="text-foreground">
|
{t("skills.repo.branch")}
|
||||||
{t("skills.repo.branch")}
|
</Label>
|
||||||
</Label>
|
<Input
|
||||||
<Input
|
id="branch"
|
||||||
id="branch"
|
placeholder={t("skills.repo.branchPlaceholder")}
|
||||||
placeholder={t("skills.repo.branchPlaceholder")}
|
value={branch}
|
||||||
value={branch}
|
onChange={(e) => setBranch(e.target.value)}
|
||||||
onChange={(e) => setBranch(e.target.value)}
|
className="mt-2"
|
||||||
className="mt-2"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="skills-path" className="text-foreground">
|
|
||||||
{t("skills.repo.path")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="skills-path"
|
|
||||||
placeholder={t("skills.repo.pathPlaceholder")}
|
|
||||||
value={skillsPath}
|
|
||||||
onChange={(e) => setSkillsPath(e.target.value)}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||||
@@ -174,12 +157,6 @@ export function RepoManagerPanel({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{t("skills.repo.branch")}: {repo.branch || "main"}
|
{t("skills.repo.branch")}: {repo.branch || "main"}
|
||||||
{repo.skillsPath && (
|
|
||||||
<>
|
|
||||||
<span className="mx-2">•</span>
|
|
||||||
{t("skills.repo.path")}: {repo.skillsPath}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
|
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
|
||||||
{t("skills.repo.skillCount", {
|
{t("skills.repo.skillCount", {
|
||||||
count: getSkillCount(repo),
|
count: getSkillCount(repo),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from "./useDirectorySettings";
|
} from "./useDirectorySettings";
|
||||||
import { useSettingsMetadata } from "./useSettingsMetadata";
|
import { useSettingsMetadata } from "./useSettingsMetadata";
|
||||||
|
|
||||||
type Language = "zh" | "en";
|
type Language = "zh" | "en" | "ja";
|
||||||
|
|
||||||
interface SaveResult {
|
interface SaveResult {
|
||||||
requiresRestart: boolean;
|
requiresRestart: boolean;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useSettingsQuery } from "@/lib/query";
|
import { useSettingsQuery } from "@/lib/query";
|
||||||
import type { Settings } from "@/types";
|
import type { Settings } from "@/types";
|
||||||
|
|
||||||
type Language = "zh" | "en";
|
type Language = "zh" | "en" | "ja";
|
||||||
|
|
||||||
export type SettingsFormState = Omit<Settings, "language"> & {
|
export type SettingsFormState = Omit<Settings, "language"> & {
|
||||||
language: Language;
|
language: Language;
|
||||||
@@ -11,7 +11,8 @@ export type SettingsFormState = Omit<Settings, "language"> & {
|
|||||||
|
|
||||||
const normalizeLanguage = (lang?: string | null): Language => {
|
const normalizeLanguage = (lang?: string | null): Language => {
|
||||||
if (!lang) return "zh";
|
if (!lang) return "zh";
|
||||||
return lang === "en" ? "en" : "zh";
|
const normalized = lang.toLowerCase();
|
||||||
|
return normalized === "en" || normalized === "ja" ? normalized : "zh";
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeDir = (value?: string | null): string | undefined => {
|
const sanitizeDir = (value?: string | null): string | undefined => {
|
||||||
@@ -51,8 +52,8 @@ export function useSettingsForm(): UseSettingsFormResult {
|
|||||||
const readPersistedLanguage = useCallback((): Language => {
|
const readPersistedLanguage = useCallback((): Language => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const stored = window.localStorage.getItem("language");
|
const stored = window.localStorage.getItem("language");
|
||||||
if (stored === "en" || stored === "zh") {
|
if (stored === "en" || stored === "zh" || stored === "ja") {
|
||||||
return stored;
|
return stored as Language;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return normalizeLanguage(i18n.language);
|
return normalizeLanguage(i18n.language);
|
||||||
|
|||||||
+13
-3
@@ -2,15 +2,18 @@ import i18n from "i18next";
|
|||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
import en from "./locales/en.json";
|
import en from "./locales/en.json";
|
||||||
|
import ja from "./locales/ja.json";
|
||||||
import zh from "./locales/zh.json";
|
import zh from "./locales/zh.json";
|
||||||
|
|
||||||
const DEFAULT_LANGUAGE: "zh" | "en" = "zh";
|
type Language = "zh" | "en" | "ja";
|
||||||
|
|
||||||
const getInitialLanguage = (): "zh" | "en" => {
|
const DEFAULT_LANGUAGE: Language = "zh";
|
||||||
|
|
||||||
|
const getInitialLanguage = (): Language => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
try {
|
try {
|
||||||
const stored = window.localStorage.getItem("language");
|
const stored = window.localStorage.getItem("language");
|
||||||
if (stored === "zh" || stored === "en") {
|
if (stored === "zh" || stored === "en" || stored === "ja") {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -28,6 +31,10 @@ const getInitialLanguage = (): "zh" | "en" => {
|
|||||||
return "zh";
|
return "zh";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (navigatorLang?.startsWith("ja")) {
|
||||||
|
return "ja";
|
||||||
|
}
|
||||||
|
|
||||||
if (navigatorLang?.startsWith("en")) {
|
if (navigatorLang?.startsWith("en")) {
|
||||||
return "en";
|
return "en";
|
||||||
}
|
}
|
||||||
@@ -39,6 +46,9 @@ const resources = {
|
|||||||
en: {
|
en: {
|
||||||
translation: en,
|
translation: en,
|
||||||
},
|
},
|
||||||
|
ja: {
|
||||||
|
translation: ja,
|
||||||
|
},
|
||||||
zh: {
|
zh: {
|
||||||
translation: zh,
|
translation: zh,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
"autoReload": "Data will refresh automatically in 2 seconds...",
|
"autoReload": "Data will refresh automatically in 2 seconds...",
|
||||||
"languageOptionChinese": "中文",
|
"languageOptionChinese": "中文",
|
||||||
"languageOptionEnglish": "English",
|
"languageOptionEnglish": "English",
|
||||||
|
"languageOptionJapanese": "日本語",
|
||||||
"windowBehavior": "Window Behavior",
|
"windowBehavior": "Window Behavior",
|
||||||
"windowBehaviorHint": "Configure window minimize and Claude plugin integration policies.",
|
"windowBehaviorHint": "Configure window minimize and Claude plugin integration policies.",
|
||||||
"launchOnStartup": "Launch on Startup",
|
"launchOnStartup": "Launch on Startup",
|
||||||
@@ -376,6 +377,7 @@
|
|||||||
"templateGeneral": "General",
|
"templateGeneral": "General",
|
||||||
"templateNewAPI": "NewAPI",
|
"templateNewAPI": "NewAPI",
|
||||||
"credentialsConfig": "Credentials",
|
"credentialsConfig": "Credentials",
|
||||||
|
"baseUrl": "Base URL",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"accessTokenPlaceholder": "Generate in 'Security Settings'",
|
"accessTokenPlaceholder": "Generate in 'Security Settings'",
|
||||||
"userId": "User ID",
|
"userId": "User ID",
|
||||||
@@ -389,7 +391,7 @@
|
|||||||
"timeoutHint": "Range: 2-30 seconds",
|
"timeoutHint": "Range: 2-30 seconds",
|
||||||
"timeoutMustBeInteger": "Timeout must be an integer, decimal part ignored",
|
"timeoutMustBeInteger": "Timeout must be an integer, decimal part ignored",
|
||||||
"timeoutCannotBeNegative": "Timeout cannot be negative",
|
"timeoutCannotBeNegative": "Timeout cannot be negative",
|
||||||
"autoIntervalMinutes": "Auto query interval (minutes)",
|
"autoIntervalMinutes": "Auto query interval (minutes, 0 to disable)",
|
||||||
"autoQueryInterval": "Auto Query Interval (minutes)",
|
"autoQueryInterval": "Auto Query Interval (minutes)",
|
||||||
"autoQueryIntervalHint": "0 to disable; recommend 5-60 minutes",
|
"autoQueryIntervalHint": "0 to disable; recommend 5-60 minutes",
|
||||||
"intervalMustBeInteger": "Interval must be an integer, decimal part ignored",
|
"intervalMustBeInteger": "Interval must be an integer, decimal part ignored",
|
||||||
|
|||||||
@@ -0,0 +1,837 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "CC Switch",
|
||||||
|
"description": "Claude Code・Codex・Gemini CLI のためのオールインワンアシスタント"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"add": "追加",
|
||||||
|
"edit": "編集",
|
||||||
|
"delete": "削除",
|
||||||
|
"save": "保存",
|
||||||
|
"saving": "保存中...",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"confirm": "確認",
|
||||||
|
"close": "閉じる",
|
||||||
|
"done": "完了",
|
||||||
|
"settings": "設定",
|
||||||
|
"about": "バージョン情報",
|
||||||
|
"version": "バージョン",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"success": "成功",
|
||||||
|
"error": "エラー",
|
||||||
|
"unknown": "不明",
|
||||||
|
"enterValidValue": "有効な値を入力してください",
|
||||||
|
"clear": "クリア",
|
||||||
|
"toggleTheme": "テーマを切り替え",
|
||||||
|
"format": "フォーマット",
|
||||||
|
"formatSuccess": "整形しました",
|
||||||
|
"formatError": "整形に失敗しました: {{error}}",
|
||||||
|
"copy": "コピー",
|
||||||
|
"view": "表示",
|
||||||
|
"back": "戻る"
|
||||||
|
},
|
||||||
|
"apiKeyInput": {
|
||||||
|
"placeholder": "API Key を入力",
|
||||||
|
"show": "API Key を表示",
|
||||||
|
"hide": "API Key を隠す"
|
||||||
|
},
|
||||||
|
"jsonEditor": {
|
||||||
|
"mustBeObject": "設定はオブジェクト形式の JSON で入力してください(配列や他の型は不可)",
|
||||||
|
"invalidJson": "JSON 形式が正しくありません"
|
||||||
|
},
|
||||||
|
"claudeConfig": {
|
||||||
|
"configLabel": "Claude Code settings.json (JSON) *",
|
||||||
|
"writeCommonConfig": "共通設定を書き込む",
|
||||||
|
"editCommonConfig": "共通設定を編集",
|
||||||
|
"editCommonConfigTitle": "共通設定スニペットを編集",
|
||||||
|
"commonConfigHint": "「共通設定を書き込む」がオンのとき settings.json にマージされます",
|
||||||
|
"fullSettingsHint": "Claude Code の settings.json 全文"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"viewOnGithub": "GitHub で見る",
|
||||||
|
"toggleDarkMode": "ダークモードに切り替え",
|
||||||
|
"toggleLightMode": "ライトモードに切り替え",
|
||||||
|
"addProvider": "プロバイダーを追加",
|
||||||
|
"switchToChinese": "中国語に切り替え",
|
||||||
|
"switchToEnglish": "英語に切り替え",
|
||||||
|
"enterEditMode": "編集モードに入る",
|
||||||
|
"exitEditMode": "編集モードを終了"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"noProviders": "まだプロバイダーがありません",
|
||||||
|
"noProvidersDescription": "右上の「プロバイダーを追加」を押して最初の API プロバイダーを登録してください",
|
||||||
|
"currentlyUsing": "現在使用中",
|
||||||
|
"enable": "有効化",
|
||||||
|
"inUse": "使用中",
|
||||||
|
"editProvider": "プロバイダーを編集",
|
||||||
|
"editProviderHint": "保存すると現在のプロバイダーにすぐ反映されます。",
|
||||||
|
"deleteProvider": "プロバイダーを削除",
|
||||||
|
"addNewProvider": "新しいプロバイダーを追加",
|
||||||
|
"addClaudeProvider": "Claude Code プロバイダーを追加",
|
||||||
|
"addCodexProvider": "Codex プロバイダーを追加",
|
||||||
|
"addGeminiProvider": "Gemini プロバイダーを追加",
|
||||||
|
"addProviderHint": "一覧にすばやく切り替えられるよう、ここに情報を入力してください。",
|
||||||
|
"editClaudeProvider": "Claude Code プロバイダーを編集",
|
||||||
|
"editCodexProvider": "Codex プロバイダーを編集",
|
||||||
|
"configError": "設定エラー",
|
||||||
|
"notConfigured": "公式サイト用に未設定",
|
||||||
|
"applyToClaudePlugin": "Claude プラグインに適用",
|
||||||
|
"removeFromClaudePlugin": "Claude プラグインから解除",
|
||||||
|
"dragToReorder": "ドラッグで並べ替え",
|
||||||
|
"dragHandle": "ドラッグで並べ替え",
|
||||||
|
"duplicate": "複製",
|
||||||
|
"sortUpdateFailed": "並び順の更新に失敗しました",
|
||||||
|
"configureUsage": "利用状況を設定",
|
||||||
|
"name": "プロバイダー名",
|
||||||
|
"namePlaceholder": "例: Claude Official",
|
||||||
|
"websiteUrl": "Web サイト URL",
|
||||||
|
"notes": "メモ",
|
||||||
|
"notesPlaceholder": "例: 会社用アカウント",
|
||||||
|
"configJson": "Config JSON",
|
||||||
|
"writeCommonConfig": "共通設定を書き込む",
|
||||||
|
"editCommonConfigButton": "共通設定を編集",
|
||||||
|
"configJsonHint": "Claude Code の設定をすべて入力してください",
|
||||||
|
"editCommonConfigTitle": "共通設定スニペットを編集",
|
||||||
|
"editCommonConfigHint": "共通設定スニペットは、この機能をオンにしたすべてのプロバイダーへマージされます",
|
||||||
|
"addProvider": "プロバイダーを追加",
|
||||||
|
"sortUpdated": "並び順を更新しました",
|
||||||
|
"usageSaved": "利用状況の設定を保存しました",
|
||||||
|
"usageSaveFailed": "利用状況設定の保存に失敗しました",
|
||||||
|
"geminiConfig": "Gemini 設定",
|
||||||
|
"geminiConfigHint": ".env 形式で Gemini を設定してください",
|
||||||
|
"form": {
|
||||||
|
"gemini": {
|
||||||
|
"model": "モデル",
|
||||||
|
"oauthTitle": "OAuth 認証モード",
|
||||||
|
"oauthHint": "Google 公式は OAuth 個人認証を使用するため API Key は不要です。初回利用時にブラウザが開きます。",
|
||||||
|
"apiKeyPlaceholder": "Gemini API Key を入力"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"providerAdded": "プロバイダーを追加しました",
|
||||||
|
"providerSaved": "プロバイダー設定を保存しました",
|
||||||
|
"providerDeleted": "プロバイダーを削除しました",
|
||||||
|
"switchSuccess": "切り替え成功! {{appName}} ターミナルを再起動すると反映されます",
|
||||||
|
"switchFailedTitle": "切り替えに失敗しました",
|
||||||
|
"switchFailed": "切り替えに失敗しました: {{error}}",
|
||||||
|
"autoImported": "既存設定からデフォルトプロバイダーを自動作成しました",
|
||||||
|
"addFailed": "プロバイダーの追加に失敗しました: {{error}}",
|
||||||
|
"saveFailed": "保存に失敗しました: {{error}}",
|
||||||
|
"saveFailedGeneric": "保存に失敗しました。もう一度お試しください",
|
||||||
|
"appliedToClaudePlugin": "Claude プラグインに適用しました",
|
||||||
|
"removedFromClaudePlugin": "Claude プラグインから削除しました",
|
||||||
|
"syncClaudePluginFailed": "Claude プラグインとの同期に失敗しました",
|
||||||
|
"updateSuccess": "プロバイダーを更新しました",
|
||||||
|
"updateFailed": "プロバイダーの更新に失敗しました: {{error}}",
|
||||||
|
"deleteSuccess": "プロバイダーを削除しました",
|
||||||
|
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
|
||||||
|
"settingsSaved": "設定を保存しました",
|
||||||
|
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"deleteProvider": "プロバイダーを削除",
|
||||||
|
"deleteProviderMessage": "プロバイダー「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "設定",
|
||||||
|
"general": "一般",
|
||||||
|
"tabGeneral": "一般",
|
||||||
|
"tabAdvanced": "詳細",
|
||||||
|
"language": "言語",
|
||||||
|
"languageHint": "切り替えるとすぐにプレビューされ、保存後に永続化されます。",
|
||||||
|
"theme": "テーマ",
|
||||||
|
"themeHint": "アプリのテーマを選択します。すぐに反映されます。",
|
||||||
|
"themeLight": "ライト",
|
||||||
|
"themeDark": "ダーク",
|
||||||
|
"themeSystem": "システム",
|
||||||
|
"importExport": "SQL インポート/エクスポート",
|
||||||
|
"importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします。",
|
||||||
|
"exportConfig": "SQL バックアップをエクスポート",
|
||||||
|
"selectConfigFile": "SQL ファイルを選択",
|
||||||
|
"noFileSelected": "ファイルが選択されていません。",
|
||||||
|
"import": "インポート",
|
||||||
|
"importing": "インポート中...",
|
||||||
|
"importSuccess": "インポート成功!",
|
||||||
|
"importFailed": "インポート失敗",
|
||||||
|
"syncLiveFailed": "インポートしましたが、現在のプロバイダーへの同期に失敗しました。手動で再選択してください。",
|
||||||
|
"importPartialSuccess": "設定はインポートされましたが、現在のプロバイダーへの同期に失敗しました。",
|
||||||
|
"importPartialHint": "ライブ設定を更新するため、もう一度プロバイダーを選択してください。",
|
||||||
|
"configExported": "設定をエクスポートしました:",
|
||||||
|
"exportFailed": "エクスポートに失敗しました",
|
||||||
|
"selectFileFailed": "有効な SQL バックアップファイルを選択してください",
|
||||||
|
"configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります",
|
||||||
|
"backupId": "バックアップ ID",
|
||||||
|
"autoReload": "2 秒後に自動で再読み込みします...",
|
||||||
|
"languageOptionChinese": "中文",
|
||||||
|
"languageOptionEnglish": "English",
|
||||||
|
"languageOptionJapanese": "日本語",
|
||||||
|
"windowBehavior": "ウィンドウ動作",
|
||||||
|
"windowBehaviorHint": "最小化動作や Claude プラグイン連携を設定します。",
|
||||||
|
"launchOnStartup": "起動時に自動実行",
|
||||||
|
"launchOnStartupDescription": "システム起動時に CC Switch を自動起動します",
|
||||||
|
"autoLaunchFailed": "自動起動の設定に失敗しました",
|
||||||
|
"minimizeToTray": "閉じるときトレイへ最小化",
|
||||||
|
"minimizeToTrayDescription": "チェックすると閉じるボタンでトレイに隠し、オフならアプリを終了します。",
|
||||||
|
"enableClaudePluginIntegration": "Claude Code 拡張に適用",
|
||||||
|
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
|
||||||
|
"configDirectoryOverride": "設定ディレクトリの上書き(詳細)",
|
||||||
|
"configDirectoryDescription": "WSL などで Claude Code や Codex を使う場合、ここで設定ディレクトリを WSL 側に合わせるとデータを揃えられます。",
|
||||||
|
"appConfigDir": "CC Switch 設定ディレクトリ",
|
||||||
|
"appConfigDirDescription": "CC Switch の保存場所をカスタマイズします(クラウド同期フォルダを指定すると設定を同期できます)",
|
||||||
|
"browsePlaceholderApp": "例: C:\\\\Users\\\\Administrator\\\\.cc-switch",
|
||||||
|
"claudeConfigDir": "Claude Code 設定ディレクトリ",
|
||||||
|
"claudeConfigDirDescription": "Claude の設定ディレクトリ(settings.json)を上書きし、claude.json(MCP)も同じ場所に置きます。",
|
||||||
|
"codexConfigDir": "Codex 設定ディレクトリ",
|
||||||
|
"codexConfigDirDescription": "Codex の設定ディレクトリを上書きします。",
|
||||||
|
"geminiConfigDir": "Gemini 設定ディレクトリ",
|
||||||
|
"geminiConfigDirDescription": "Gemini の設定ディレクトリ(.env)を上書きします。",
|
||||||
|
"browsePlaceholderClaude": "例: /home/<your-username>/.claude",
|
||||||
|
"browsePlaceholderCodex": "例: /home/<your-username>/.codex",
|
||||||
|
"browsePlaceholderGemini": "例: /home/<your-username>/.gemini",
|
||||||
|
"browseDirectory": "ディレクトリを選択",
|
||||||
|
"resetDefault": "デフォルトに戻す(保存後に反映)",
|
||||||
|
"checkForUpdates": "アップデートを確認",
|
||||||
|
"updateTo": "v{{version}} に更新",
|
||||||
|
"updating": "更新中...",
|
||||||
|
"checking": "確認中...",
|
||||||
|
"upToDate": "最新バージョンです",
|
||||||
|
"aboutHint": "バージョン情報と更新状況を表示します。",
|
||||||
|
"portableMode": "ポータブルモード: 更新は手動ダウンロードが必要です。",
|
||||||
|
"updateAvailable": "新しいバージョンがあります: {{version}}",
|
||||||
|
"updateFailed": "更新のインストールに失敗しました。ダウンロードページを開こうとしました。",
|
||||||
|
"checkUpdateFailed": "更新の確認に失敗しました。時間をおいて再試行してください。",
|
||||||
|
"openReleaseNotesFailed": "リリースノートの表示に失敗しました",
|
||||||
|
"releaseNotes": "リリースノート",
|
||||||
|
"viewReleaseNotes": "このバージョンのリリースノートを見る",
|
||||||
|
"viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る",
|
||||||
|
"importFailedError": "設定のインポートに失敗しました: {{message}}",
|
||||||
|
"exportFailedError": "設定のエクスポートに失敗しました:",
|
||||||
|
"restartRequired": "再起動が必要です",
|
||||||
|
"restartRequiredMessage": "CC Switch の設定ディレクトリを変更すると再起動が必要です。今すぐ再起動しますか?",
|
||||||
|
"restartNow": "今すぐ再起動",
|
||||||
|
"restartLater": "後で再起動",
|
||||||
|
"restartFailed": "アプリの再起動に失敗しました。手動で閉じて再度開いてください。",
|
||||||
|
"devModeRestartHint": "開発モードでは自動再起動をサポートしていません。手動で再起動してください。",
|
||||||
|
"saving": "保存中..."
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"claude": "Claude Code",
|
||||||
|
"codex": "Codex",
|
||||||
|
"gemini": "Gemini"
|
||||||
|
},
|
||||||
|
"console": {
|
||||||
|
"providerSwitchReceived": "プロバイダー切り替えイベントを受信:",
|
||||||
|
"setupListenerFailed": "プロバイダー切り替えリスナーの設定に失敗:",
|
||||||
|
"updateProviderFailed": "プロバイダー更新に失敗:",
|
||||||
|
"autoImportFailed": "デフォルト設定の自動インポートに失敗:",
|
||||||
|
"openLinkFailed": "リンクを開けませんでした:",
|
||||||
|
"getVersionFailed": "バージョン情報の取得に失敗:",
|
||||||
|
"loadSettingsFailed": "設定の読み込みに失敗:",
|
||||||
|
"getConfigPathFailed": "設定パスの取得に失敗:",
|
||||||
|
"getConfigDirFailed": "設定ディレクトリの取得に失敗:",
|
||||||
|
"detectPortableFailed": "ポータブルモードの検出に失敗:",
|
||||||
|
"saveSettingsFailed": "設定の保存に失敗:",
|
||||||
|
"updateFailed": "更新に失敗:",
|
||||||
|
"checkUpdateFailed": "更新確認に失敗:",
|
||||||
|
"openConfigFolderFailed": "設定フォルダを開けませんでした:",
|
||||||
|
"selectConfigDirFailed": "設定ディレクトリの選択に失敗:",
|
||||||
|
"getDefaultConfigDirFailed": "デフォルト設定ディレクトリの取得に失敗:",
|
||||||
|
"openReleaseNotesFailed": "リリースノートを開けませんでした:"
|
||||||
|
},
|
||||||
|
"providerForm": {
|
||||||
|
"supplierName": "プロバイダー名",
|
||||||
|
"supplierNameRequired": "プロバイダー名 *",
|
||||||
|
"supplierNamePlaceholder": "例: Anthropic Official",
|
||||||
|
"websiteUrl": "Web サイト URL",
|
||||||
|
"websiteUrlPlaceholder": "https://example.com(任意)",
|
||||||
|
"apiEndpoint": "API エンドポイント",
|
||||||
|
"apiEndpointPlaceholder": "https://your-api-endpoint.com",
|
||||||
|
"codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1",
|
||||||
|
"manageAndTest": "管理・テスト",
|
||||||
|
"configContent": "設定内容",
|
||||||
|
"officialNoApiKey": "公式ログインは API Key 不要です。そのまま保存できます",
|
||||||
|
"codexOfficialNoApiKey": "公式は API Key 不要です。そのまま保存してください",
|
||||||
|
"codexApiKeyAutoFill": "ここに入力すれば auth.json も自動で埋まります",
|
||||||
|
"apiKeyAutoFill": "ここに入力すれば下の設定も自動で埋まります",
|
||||||
|
"cnOfficialApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||||
|
"aggregatorApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||||
|
"thirdPartyApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||||
|
"customApiKeyHint": "💡 カスタム設定では必要な項目をすべて手動で入力してください",
|
||||||
|
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
|
||||||
|
"getApiKey": "API Key を取得",
|
||||||
|
"partnerPromotion": {
|
||||||
|
"zhipu": "Zhipu GLM は CC Switch の公式パートナーです。リンク経由でチャージすると 10% 割引",
|
||||||
|
"packycode": "PackyCode は CC Switch の公式パートナーです。登録後チャージ時に \"cc-switch\" を入力すると 10% オフ",
|
||||||
|
"minimax": "MiniMax Coding Plan Black Friday、Starter が月額 $2(80% OFF)"
|
||||||
|
},
|
||||||
|
"parameterConfig": "パラメーター設定 - {{name}} *",
|
||||||
|
"mainModel": "メインモデル(任意)",
|
||||||
|
"mainModelPlaceholder": "例: GLM-4.6",
|
||||||
|
"fastModel": "高速モデル(任意)",
|
||||||
|
"fastModelPlaceholder": "例: GLM-4.5-Air",
|
||||||
|
"modelHint": "💡 空欄ならプロバイダーのデフォルトモデルを使用します",
|
||||||
|
"apiHint": "💡 Claude API 互換サービスのエンドポイントを入力してください",
|
||||||
|
"codexApiHint": "💡 OpenAI Response 互換のサービスエンドポイントを入力してください",
|
||||||
|
"fillSupplierName": "プロバイダー名を入力してください",
|
||||||
|
"fillConfigContent": "設定内容を入力してください",
|
||||||
|
"fillParameter": "{{label}} を入力してください",
|
||||||
|
"fillTemplateValue": "{{label}} を入力してください",
|
||||||
|
"endpointRequired": "公式以外は API エンドポイントが必須です",
|
||||||
|
"apiKeyRequired": "公式以外は API Key が必須です",
|
||||||
|
"configJsonError": "Config JSON の形式が正しくありません。構文を確認してください",
|
||||||
|
"authJsonRequired": "auth.json は JSON オブジェクトで入力してください",
|
||||||
|
"authJsonError": "auth.json の形式が正しくありません。JSON を確認してください",
|
||||||
|
"fillAuthJson": "auth.json の設定を入力してください",
|
||||||
|
"fillApiKey": "OPENAI_API_KEY を入力してください",
|
||||||
|
"visitWebsite": "{{url}} を開く",
|
||||||
|
"anthropicModel": "メインモデル",
|
||||||
|
"anthropicSmallFastModel": "高速モデル",
|
||||||
|
"anthropicDefaultHaikuModel": "既定 Haiku モデル",
|
||||||
|
"anthropicDefaultSonnetModel": "既定 Sonnet モデル",
|
||||||
|
"anthropicDefaultOpusModel": "既定 Opus モデル",
|
||||||
|
"modelPlaceholder": "",
|
||||||
|
"smallModelPlaceholder": "",
|
||||||
|
"haikuModelPlaceholder": "",
|
||||||
|
"modelHelper": "任意: 既定で使いたい Claude モデルを指定。空欄ならシステム既定を使用します。",
|
||||||
|
"categoryOfficial": "公式",
|
||||||
|
"categoryCnOfficial": "オープンソース公式",
|
||||||
|
"categoryAggregation": "アグリゲーター",
|
||||||
|
"categoryThirdParty": "サードパーティ"
|
||||||
|
},
|
||||||
|
"endpointTest": {
|
||||||
|
"title": "API エンドポイント管理",
|
||||||
|
"endpoints": "エンドポイント",
|
||||||
|
"autoSelect": "自動選択",
|
||||||
|
"testSpeed": "テスト",
|
||||||
|
"testing": "テスト中",
|
||||||
|
"addEndpointPlaceholder": "https://api.example.com",
|
||||||
|
"done": "完了",
|
||||||
|
"noEndpoints": "エンドポイントがありません",
|
||||||
|
"failed": "失敗",
|
||||||
|
"enterValidUrl": "有効な URL を入力してください",
|
||||||
|
"invalidUrlFormat": "URL 形式が正しくありません",
|
||||||
|
"onlyHttps": "HTTP/HTTPS のみサポートします",
|
||||||
|
"urlExists": "この URL はすでに存在します",
|
||||||
|
"saveFailed": "保存に失敗しました。もう一度お試しください",
|
||||||
|
"loadEndpointsFailed": "カスタムエンドポイントの読み込みに失敗:",
|
||||||
|
"addEndpointFailed": "カスタムエンドポイントの追加に失敗:",
|
||||||
|
"removeEndpointFailed": "カスタムエンドポイントの削除に失敗:",
|
||||||
|
"removeFailed": "削除に失敗しました: {{error}}",
|
||||||
|
"updateLastUsedFailed": "エンドポイントの最終使用時間の更新に失敗しました",
|
||||||
|
"pleaseAddEndpoint": "まずエンドポイントを追加してください",
|
||||||
|
"testUnavailable": "速度テストを実行できません",
|
||||||
|
"noResult": "結果がありません",
|
||||||
|
"testFailed": "速度テストに失敗しました: {{error}}",
|
||||||
|
"status": "ステータス: {{code}}"
|
||||||
|
},
|
||||||
|
"codexConfig": {
|
||||||
|
"authJson": "auth.json (JSON) *",
|
||||||
|
"authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}",
|
||||||
|
"authJsonHint": "Codex の auth.json 設定内容",
|
||||||
|
"configToml": "config.toml (TOML)",
|
||||||
|
"configTomlHint": "Codex の config.toml 設定内容",
|
||||||
|
"writeCommonConfig": "共通設定を書き込む",
|
||||||
|
"editCommonConfig": "共通設定を編集",
|
||||||
|
"editCommonConfigTitle": "Codex 共通設定スニペットを編集",
|
||||||
|
"commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます",
|
||||||
|
"apiUrlLabel": "API リクエスト URL"
|
||||||
|
},
|
||||||
|
"geminiConfig": {
|
||||||
|
"envFile": "環境変数 (.env)",
|
||||||
|
"envFileHint": ".env 形式で Gemini の環境変数を設定",
|
||||||
|
"configJson": "設定ファイル (config.json)",
|
||||||
|
"configJsonHint": "Gemini 拡張パラメーターを JSON 形式で設定(任意)",
|
||||||
|
"writeCommonConfig": "共通設定を書き込む",
|
||||||
|
"editCommonConfig": "共通設定を編集",
|
||||||
|
"editCommonConfigTitle": "Gemini 共通設定スニペットを編集",
|
||||||
|
"commonConfigHint": "共通設定スニペットは、この機能をオンにしたすべての Gemini プロバイダーへマージされます"
|
||||||
|
},
|
||||||
|
"providerPreset": {
|
||||||
|
"label": "プロバイダータイプ",
|
||||||
|
"custom": "カスタム設定",
|
||||||
|
"other": "その他",
|
||||||
|
"hint": "プリセットを選んだ後でも、下のフィールドで調整できます。"
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"queryFailed": "照会に失敗しました",
|
||||||
|
"refreshUsage": "利用状況を更新",
|
||||||
|
"planUsage": "プラン利用状況",
|
||||||
|
"invalid": "期限切れ",
|
||||||
|
"total": "合計:",
|
||||||
|
"used": "使用:",
|
||||||
|
"remaining": "残り:",
|
||||||
|
"justNow": "たった今",
|
||||||
|
"minutesAgo": "{{count}} 分前",
|
||||||
|
"hoursAgo": "{{count}} 時間前",
|
||||||
|
"daysAgo": "{{count}} 日前"
|
||||||
|
},
|
||||||
|
"usageScript": {
|
||||||
|
"title": "利用状況を設定",
|
||||||
|
"enableUsageQuery": "利用状況照会を有効にする",
|
||||||
|
"presetTemplate": "プリセットテンプレート",
|
||||||
|
"requestUrl": "リクエスト URL",
|
||||||
|
"requestUrlPlaceholder": "例: https://api.example.com",
|
||||||
|
"method": "HTTP メソッド",
|
||||||
|
"templateCustom": "カスタム",
|
||||||
|
"templateGeneral": "General",
|
||||||
|
"templateNewAPI": "NewAPI",
|
||||||
|
"credentialsConfig": "認証情報",
|
||||||
|
"baseUrl": "Base URL",
|
||||||
|
"accessToken": "Access Token",
|
||||||
|
"accessTokenPlaceholder": "「Security Settings」で生成",
|
||||||
|
"userId": "ユーザー ID",
|
||||||
|
"userIdPlaceholder": "例: 114514",
|
||||||
|
"defaultPlan": "デフォルトプラン",
|
||||||
|
"queryFailedMessage": "照会に失敗しました",
|
||||||
|
"queryScript": "照会スクリプト (JavaScript)",
|
||||||
|
"timeoutSeconds": "タイムアウト(秒)",
|
||||||
|
"headers": "ヘッダー",
|
||||||
|
"body": "ボディ",
|
||||||
|
"timeoutHint": "範囲: 2〜30 秒",
|
||||||
|
"timeoutMustBeInteger": "タイムアウトは整数で入力してください(小数は切り捨て)",
|
||||||
|
"timeoutCannotBeNegative": "タイムアウトは負の値にできません",
|
||||||
|
"autoIntervalMinutes": "自動照会間隔(分、0 で無効)",
|
||||||
|
"autoQueryInterval": "自動照会間隔(分)",
|
||||||
|
"autoQueryIntervalHint": "0 で無効。推奨 5〜60 分",
|
||||||
|
"intervalMustBeInteger": "間隔は整数で入力してください(小数は切り捨て)",
|
||||||
|
"intervalCannotBeNegative": "間隔は負の値にできません",
|
||||||
|
"intervalAdjusted": "間隔を {{value}} 分に調整しました",
|
||||||
|
"scriptHelp": "スクリプトの書き方:",
|
||||||
|
"configFormat": "設定の形式:",
|
||||||
|
"commentOptional": "任意",
|
||||||
|
"commentResponseIsJson": "response は API から返る JSON データです",
|
||||||
|
"extractorFormat": "抽出関数の返却形式(すべて任意):",
|
||||||
|
"tips": "💡 ヒント:",
|
||||||
|
"testing": "テスト中...",
|
||||||
|
"testScript": "スクリプトをテスト",
|
||||||
|
"format": "整形",
|
||||||
|
"saveConfig": "設定を保存",
|
||||||
|
"scriptEmpty": "スクリプト設定は空にできません",
|
||||||
|
"mustHaveReturn": "スクリプトには return 文が必要です",
|
||||||
|
"testSuccess": "テスト成功!",
|
||||||
|
"testFailed": "テストに失敗しました",
|
||||||
|
"formatSuccess": "整形に成功しました",
|
||||||
|
"formatFailed": "整形に失敗しました",
|
||||||
|
"variablesHint": "使用可能な変数: {{apiKey}}, {{baseUrl}} | extractor 関数には API 応答の JSON オブジェクトが渡されます",
|
||||||
|
"scriptConfig": "リクエスト設定",
|
||||||
|
"extractorCode": "抽出コード",
|
||||||
|
"extractorHint": "戻り値のオブジェクトに残り枠の項目を含めてください",
|
||||||
|
"fieldIsValid": "• isValid: Boolean。プランが有効かどうか",
|
||||||
|
"fieldInvalidMessage": "• invalidMessage: String。無効時の理由(isValid が false のとき表示)",
|
||||||
|
"fieldRemaining": "• remaining: Number。残り枠",
|
||||||
|
"fieldUnit": "• unit: String。単位(例: \"USD\")",
|
||||||
|
"fieldPlanName": "• planName: String。プラン名",
|
||||||
|
"fieldTotal": "• total: Number。総枠",
|
||||||
|
"fieldUsed": "• used: Number。使用量",
|
||||||
|
"fieldExtra": "• extra: String。自由記述の追加テキスト",
|
||||||
|
"tip1": "• 変数 {{apiKey}} と {{baseUrl}} は自動で置換されます",
|
||||||
|
"tip2": "• 抽出関数はサンドボックスで実行され、ES2020+ の構文を使えます",
|
||||||
|
"tip3": "• 全体を () で囲み、オブジェクトリテラル式にしてください"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"usage_query_failed": "利用状況の取得に失敗しました"
|
||||||
|
},
|
||||||
|
"presetSelector": {
|
||||||
|
"title": "設定タイプを選択",
|
||||||
|
"custom": "カスタム",
|
||||||
|
"customDescription": "手動で設定。完全な構成が必要",
|
||||||
|
"officialDescription": "公式ログイン。API Key 不要",
|
||||||
|
"presetDescription": "プリセットを使用。API Key だけ入力すれば OK"
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"title": "MCP 管理",
|
||||||
|
"claudeTitle": "Claude Code MCP 管理",
|
||||||
|
"codexTitle": "Codex MCP 管理",
|
||||||
|
"geminiTitle": "Gemini MCP 管理",
|
||||||
|
"unifiedPanel": {
|
||||||
|
"title": "MCP サーバー管理",
|
||||||
|
"addServer": "サーバーを追加",
|
||||||
|
"editServer": "サーバーを編集",
|
||||||
|
"deleteServer": "サーバーを削除",
|
||||||
|
"deleteConfirm": "サーバー「{{id}}」を削除しますか?この操作は元に戻せません。",
|
||||||
|
"noServers": "まだサーバーがありません",
|
||||||
|
"enabledApps": "有効なアプリ",
|
||||||
|
"apps": {
|
||||||
|
"claude": "Claude",
|
||||||
|
"codex": "Codex",
|
||||||
|
"gemini": "Gemini"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userLevelPath": "ユーザーレベルの MCP パス",
|
||||||
|
"serverList": "サーバー一覧",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"empty": "MCP サーバーがありません",
|
||||||
|
"emptyDescription": "右上のボタンから最初の MCP サーバーを追加してください",
|
||||||
|
"add": "MCP を追加",
|
||||||
|
"addServer": "MCP を追加",
|
||||||
|
"editServer": "MCP を編集",
|
||||||
|
"addClaudeServer": "Claude Code MCP を追加",
|
||||||
|
"editClaudeServer": "Claude Code MCP を編集",
|
||||||
|
"addCodexServer": "Codex MCP を追加",
|
||||||
|
"editCodexServer": "Codex MCP を編集",
|
||||||
|
"configPath": "設定パス",
|
||||||
|
"serverCount": "MCP サーバー: {{count}} 件",
|
||||||
|
"enabledCount": "{{count}} 件が有効",
|
||||||
|
"template": {
|
||||||
|
"fetch": "クイックテンプレート: mcp-fetch"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "MCP ID(ユニーク)",
|
||||||
|
"titlePlaceholder": "my-mcp-server",
|
||||||
|
"name": "表示名",
|
||||||
|
"namePlaceholder": "例: @modelcontextprotocol/server-time",
|
||||||
|
"enabledApps": "適用するアプリ",
|
||||||
|
"noAppsWarning": "少なくとも 1 つ選択してください",
|
||||||
|
"description": "説明",
|
||||||
|
"descriptionPlaceholder": "任意の説明",
|
||||||
|
"tags": "タグ(カンマ区切り)",
|
||||||
|
"tagsPlaceholder": "stdio, time, utility",
|
||||||
|
"homepage": "ホームページ",
|
||||||
|
"homepagePlaceholder": "https://example.com",
|
||||||
|
"docs": "ドキュメント",
|
||||||
|
"docsPlaceholder": "https://example.com/docs",
|
||||||
|
"additionalInfo": "追加情報",
|
||||||
|
"jsonConfig": "JSON 全設定",
|
||||||
|
"jsonConfigOrPrefix": "JSON 全設定、または",
|
||||||
|
"tomlConfigOrPrefix": "TOML 全設定、または",
|
||||||
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
|
"tomlConfig": "TOML 全設定",
|
||||||
|
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||||
|
"useWizard": "設定ウィザード",
|
||||||
|
"syncOtherSide": "{{target}} にも反映",
|
||||||
|
"syncOtherSideHint": "{{target}} に同じ設定を書き込みます。既存の同一 ID は上書きされます。",
|
||||||
|
"willOverwriteWarning": "{{target}} の既存設定を上書きします"
|
||||||
|
},
|
||||||
|
"wizard": {
|
||||||
|
"title": "MCP 設定ウィザード",
|
||||||
|
"hint": "MCP サーバーを素早く設定し JSON を自動生成します",
|
||||||
|
"type": "タイプ",
|
||||||
|
"typeStdio": "stdio",
|
||||||
|
"typeHttp": "http",
|
||||||
|
"typeSse": "sse",
|
||||||
|
"command": "コマンド",
|
||||||
|
"commandPlaceholder": "npx または uvx",
|
||||||
|
"args": "引数",
|
||||||
|
"argsPlaceholder": "arg1\narg2",
|
||||||
|
"env": "環境変数",
|
||||||
|
"envPlaceholder": "KEY1=value1\nKEY2=value2",
|
||||||
|
"url": "URL",
|
||||||
|
"urlPlaceholder": "https://api.example.com/mcp",
|
||||||
|
"urlRequired": "URL を入力してください",
|
||||||
|
"headers": "ヘッダー(任意)",
|
||||||
|
"headersPlaceholder": "Authorization: Bearer your_token_here\nContent-Type: application/json",
|
||||||
|
"preview": "設定プレビュー",
|
||||||
|
"apply": "設定を反映"
|
||||||
|
},
|
||||||
|
"id": "識別子(ユニーク)",
|
||||||
|
"type": "タイプ",
|
||||||
|
"command": "コマンド",
|
||||||
|
"validateCommand": "コマンドを検証",
|
||||||
|
"args": "引数",
|
||||||
|
"argsPlaceholder": "例: mcp-server-fetch --help",
|
||||||
|
"env": "環境変数(1 行に 1 件、KEY=VALUE)",
|
||||||
|
"envPlaceholder": "FOO=bar\nHELLO=world",
|
||||||
|
"reset": "リセット",
|
||||||
|
"notice": {
|
||||||
|
"restartClaude": "書き込みました。Claude を再起動すると反映されます。"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"saved": "保存しました",
|
||||||
|
"deleted": "削除しました",
|
||||||
|
"enabled": "有効化しました",
|
||||||
|
"disabled": "無効化しました",
|
||||||
|
"templateAdded": "テンプレートを追加しました"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"idRequired": "識別子を入力してください",
|
||||||
|
"idExists": "この識別子は既に存在します。別のものを選んでください。",
|
||||||
|
"jsonInvalid": "JSON 形式が無効です",
|
||||||
|
"tomlInvalid": "TOML 形式が無効です",
|
||||||
|
"commandRequired": "コマンドを入力してください",
|
||||||
|
"singleServerObjectRequired": "単一の MCP サーバーオブジェクトを貼り付けてください(トップレベルの mcpServers は不要)",
|
||||||
|
"saveFailed": "保存に失敗しました",
|
||||||
|
"deleteFailed": "削除に失敗しました"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"ok": "コマンドが見つかりました",
|
||||||
|
"fail": "コマンドが見つかりません"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"deleteTitle": "MCP サーバーを削除",
|
||||||
|
"deleteMessage": "MCP サーバー「{{id}}」を削除してもよろしいですか?この操作は元に戻せません。"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"title": "MCP タイプを選択",
|
||||||
|
"enable": "有効化",
|
||||||
|
"enabled": "有効",
|
||||||
|
"installed": "インストール済み",
|
||||||
|
"docs": "ドキュメント",
|
||||||
|
"requiresEnv": "環境変数が必要",
|
||||||
|
"fetch": {
|
||||||
|
"name": "mcp-server-fetch",
|
||||||
|
"description": "汎用 HTTP リクエストツール。GET/POST などに対応し、API テストや Web データ取得に最適です"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"name": "@modelcontextprotocol/server-time",
|
||||||
|
"description": "現在時刻、タイムゾーン変換、日付計算を提供する時間クエリツール"
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"name": "@modelcontextprotocol/server-memory",
|
||||||
|
"description": "エンティティ・関係・観測を扱うナレッジグラフ型メモリ。会話の重要情報を AI に記憶させます"
|
||||||
|
},
|
||||||
|
"sequential-thinking": {
|
||||||
|
"name": "@modelcontextprotocol/server-sequential-thinking",
|
||||||
|
"description": "複雑な問題をステップに分解して深く考えるためのシーケンシャル思考ツール"
|
||||||
|
},
|
||||||
|
"context7": {
|
||||||
|
"name": "@upstash/context7-mcp",
|
||||||
|
"description": "最新のライブラリドキュメントとコード例を提供する Context7 ドキュメント検索ツール。キー設定で上限が拡張されます"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"manage": "プロンプト",
|
||||||
|
"title": "{{appName}} プロンプト管理",
|
||||||
|
"claudeTitle": "Claude プロンプト管理",
|
||||||
|
"codexTitle": "Codex プロンプト管理",
|
||||||
|
"add": "プロンプトを追加",
|
||||||
|
"edit": "プロンプトを編集",
|
||||||
|
"addTitle": "{{appName}} プロンプトを追加",
|
||||||
|
"editTitle": "{{appName}} プロンプトを編集",
|
||||||
|
"import": "既存をインポート",
|
||||||
|
"count": "{{count}} 件のプロンプト",
|
||||||
|
"enabled": "有効",
|
||||||
|
"enable": "有効化",
|
||||||
|
"enabledName": "有効: {{name}}",
|
||||||
|
"noneEnabled": "有効なプロンプトがありません",
|
||||||
|
"currentFile": "現在の {{filename}} の内容",
|
||||||
|
"empty": "まだプロンプトがありません",
|
||||||
|
"emptyDescription": "上のボタンからプロンプトを追加またはインポートしてください",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"name": "名前",
|
||||||
|
"namePlaceholder": "例: デフォルトプロジェクトプロンプト",
|
||||||
|
"description": "説明",
|
||||||
|
"descriptionPlaceholder": "任意の説明",
|
||||||
|
"content": "内容",
|
||||||
|
"contentPlaceholder": "# {{filename}}\n\nここにプロンプト内容を入力...",
|
||||||
|
"loadFailed": "プロンプトの読み込みに失敗しました",
|
||||||
|
"saveSuccess": "保存しました",
|
||||||
|
"saveFailed": "保存に失敗しました",
|
||||||
|
"deleteSuccess": "削除しました",
|
||||||
|
"deleteFailed": "削除に失敗しました",
|
||||||
|
"enableSuccess": "有効化しました",
|
||||||
|
"enableFailed": "有効化に失敗しました",
|
||||||
|
"disableSuccess": "無効化しました",
|
||||||
|
"disableFailed": "無効化に失敗しました",
|
||||||
|
"importSuccess": "インポートしました",
|
||||||
|
"importFailed": "インポートに失敗しました",
|
||||||
|
"confirm": {
|
||||||
|
"deleteTitle": "削除の確認",
|
||||||
|
"deleteMessage": "プロンプト「{{name}}」を削除してもよろしいですか?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"warning": {
|
||||||
|
"title": "競合する環境変数を検出しました",
|
||||||
|
"description": "設定を上書きする可能性のある環境変数を {{count}} 件見つけました"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"expand": "詳細を表示",
|
||||||
|
"collapse": "折りたたむ",
|
||||||
|
"selectAll": "すべて選択",
|
||||||
|
"clearSelection": "選択を解除",
|
||||||
|
"deleteSelected": "選択 {{count}} 件を削除",
|
||||||
|
"deleting": "削除中..."
|
||||||
|
},
|
||||||
|
"field": {
|
||||||
|
"value": "値",
|
||||||
|
"source": "ソース"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"userRegistry": "ユーザー環境変数(レジストリ)",
|
||||||
|
"systemRegistry": "システム環境変数(レジストリ)",
|
||||||
|
"systemEnv": "システム環境変数"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"success": "環境変数を削除しました",
|
||||||
|
"error": "環境変数の削除に失敗しました"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"location": "バックアップ場所: {{path}}"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"title": "環境変数を削除しますか?",
|
||||||
|
"message": "{{count}} 件の環境変数を削除してもよろしいですか?",
|
||||||
|
"backupNotice": "削除前に自動バックアップを作成します。後で復元できます。再起動またはターミナル再起動後に反映されます。",
|
||||||
|
"confirm": "削除を確認"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"noSelection": "削除する環境変数を選択してください"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"manage": "Skills",
|
||||||
|
"title": "Claude スキル管理",
|
||||||
|
"description": "人気リポジトリから Claude Skills を探してインストールし、Claude Code/Codex を拡張",
|
||||||
|
"refresh": "更新",
|
||||||
|
"refreshing": "更新中...",
|
||||||
|
"repoManager": "リポジトリ管理",
|
||||||
|
"count": "{{count}} 個のスキル",
|
||||||
|
"empty": "スキルがありません",
|
||||||
|
"emptyDescription": "スキルリポジトリを追加して探索してください",
|
||||||
|
"addRepo": "スキルリポジトリを追加",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"installed": "インストール済み",
|
||||||
|
"install": "インストール",
|
||||||
|
"installing": "インストール中...",
|
||||||
|
"uninstall": "アンインストール",
|
||||||
|
"uninstalling": "アンインストール中...",
|
||||||
|
"view": "表示",
|
||||||
|
"noDescription": "説明なし",
|
||||||
|
"loadFailed": "読み込みに失敗しました",
|
||||||
|
"installSuccess": "スキル {{name}} をインストールしました",
|
||||||
|
"installFailed": "インストールに失敗しました",
|
||||||
|
"uninstallSuccess": "スキル {{name}} をアンインストールしました",
|
||||||
|
"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": "リポジトリまたはブランチが見つかりません。URL を確認してください",
|
||||||
|
"http429": "リクエストが多すぎます。時間をおいて再試行してください",
|
||||||
|
"parseMetadataFailed": "スキルメタデータの解析に失敗しました",
|
||||||
|
"getHomeDirFailed": "ユーザーのホームディレクトリを取得できません",
|
||||||
|
"networkError": "ネットワークエラー",
|
||||||
|
"fsError": "ファイルシステムエラー",
|
||||||
|
"unknownError": "不明なエラー",
|
||||||
|
"suggestion": {
|
||||||
|
"checkNetwork": "ネットワーク接続を確認してください",
|
||||||
|
"checkProxy": "HTTP プロキシの設定を検討してください",
|
||||||
|
"retryLater": "時間をおいて再試行してください",
|
||||||
|
"checkRepoUrl": "リポジトリ URL とブランチ名を確認してください",
|
||||||
|
"checkDiskSpace": "ディスク容量を確認してください",
|
||||||
|
"checkPermission": "ディレクトリの権限を確認してください"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"title": "スキルリポジトリを管理",
|
||||||
|
"description": "GitHub のスキルリポジトリソースを追加または削除します",
|
||||||
|
"url": "リポジトリ URL",
|
||||||
|
"urlPlaceholder": "owner/name または https://github.com/owner/name",
|
||||||
|
"branch": "ブランチ",
|
||||||
|
"branchPlaceholder": "main",
|
||||||
|
"path": "スキルパス",
|
||||||
|
"pathPlaceholder": "skills(任意。空欄はルート)",
|
||||||
|
"add": "リポジトリを追加",
|
||||||
|
"list": "追加済みリポジトリ",
|
||||||
|
"empty": "リポジトリがありません",
|
||||||
|
"invalidUrl": "リポジトリ URL の形式が無効です",
|
||||||
|
"addSuccess": "リポジトリ {{owner}}/{{name}} を追加しました。検出スキル: {{count}} 件",
|
||||||
|
"addFailed": "追加に失敗しました",
|
||||||
|
"removeSuccess": "リポジトリ {{owner}}/{{name}} を削除しました",
|
||||||
|
"removeFailed": "削除に失敗しました",
|
||||||
|
"skillCount": "{{count}} 件のスキルを検出"
|
||||||
|
},
|
||||||
|
"search": "スキルを検索",
|
||||||
|
"searchPlaceholder": "スキル名または説明で検索...",
|
||||||
|
"filter": {
|
||||||
|
"placeholder": "状態で絞り込み",
|
||||||
|
"all": "すべて",
|
||||||
|
"installed": "インストール済み",
|
||||||
|
"uninstalled": "未インストール"
|
||||||
|
},
|
||||||
|
"noResults": "一致するスキルが見つかりませんでした"
|
||||||
|
},
|
||||||
|
"deeplink": {
|
||||||
|
"confirmImport": "プロバイダーのインポートを確認",
|
||||||
|
"confirmImportDescription": "次の設定をディープリンクから CC Switch へインポートします",
|
||||||
|
"importPrompt": "プロンプトをインポート",
|
||||||
|
"importPromptDescription": "このシステムプロンプトをインポートするか確認してください",
|
||||||
|
"importMcp": "MCP サーバーをインポート",
|
||||||
|
"importMcpDescription": "これらの MCP サーバーをインポートするか確認してください",
|
||||||
|
"importSkill": "スキルリポジトリを追加",
|
||||||
|
"importSkillDescription": "このスキルリポジトリを追加するか確認してください",
|
||||||
|
"promptImportSuccess": "プロンプトをインポートしました",
|
||||||
|
"promptImportSuccessDescription": "インポートされたプロンプト: {{name}}",
|
||||||
|
"mcpImportSuccess": "MCP サーバーをインポートしました",
|
||||||
|
"mcpImportSuccessDescription": "{{count}} 件のサーバーをインポートしました",
|
||||||
|
"mcpPartialSuccess": "一部のみインポート成功",
|
||||||
|
"mcpPartialSuccessDescription": "成功: {{success}}、失敗: {{failed}}",
|
||||||
|
"skillImportSuccess": "スキルリポジトリを追加しました",
|
||||||
|
"skillImportSuccessDescription": "追加したリポジトリ: {{repo}}",
|
||||||
|
"app": "アプリ種別",
|
||||||
|
"providerName": "プロバイダー名",
|
||||||
|
"homepage": "ホームページ",
|
||||||
|
"endpoint": "API エンドポイント",
|
||||||
|
"apiKey": "API Key",
|
||||||
|
"icon": "アイコン",
|
||||||
|
"model": "モデル",
|
||||||
|
"haikuModel": "Haiku モデル",
|
||||||
|
"sonnetModel": "Sonnet モデル",
|
||||||
|
"opusModel": "Opus モデル",
|
||||||
|
"multiModel": "マルチモーダルモデル",
|
||||||
|
"notes": "メモ",
|
||||||
|
"import": "インポート",
|
||||||
|
"importing": "インポート中...",
|
||||||
|
"warning": "インポート前に内容を確認してください。後から一覧で編集・削除できます。",
|
||||||
|
"parseError": "ディープリンクの解析に失敗しました",
|
||||||
|
"importSuccess": "インポート成功",
|
||||||
|
"importSuccessDescription": "プロバイダー「{{name}}」をインポートしました",
|
||||||
|
"importError": "インポートに失敗しました",
|
||||||
|
"configSource": "設定ソース",
|
||||||
|
"configEmbedded": "埋め込み設定",
|
||||||
|
"configRemote": "リモート設定",
|
||||||
|
"configDetails": "設定の詳細",
|
||||||
|
"configUrl": "設定ファイル URL",
|
||||||
|
"configMergeError": "設定ファイルのマージに失敗しました",
|
||||||
|
"mcp": {
|
||||||
|
"title": "MCP サーバーを一括インポート",
|
||||||
|
"targetApps": "ターゲットアプリ",
|
||||||
|
"serverCount": "MCP サーバー({{count}} 件)",
|
||||||
|
"enabledWarning": "インポート後、指定したすべてのアプリに即座に書き込まれます"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"title": "システムプロンプトをインポート",
|
||||||
|
"app": "アプリ",
|
||||||
|
"name": "名前",
|
||||||
|
"description": "説明",
|
||||||
|
"contentPreview": "内容プレビュー",
|
||||||
|
"enabledWarning": "インポート後すぐにこのプロンプトが有効になり、他は無効になります"
|
||||||
|
},
|
||||||
|
"skill": {
|
||||||
|
"title": "Claude スキルリポジトリを追加",
|
||||||
|
"repo": "GitHub リポジトリ",
|
||||||
|
"directory": "対象ディレクトリ",
|
||||||
|
"branch": "ブランチ",
|
||||||
|
"skillsPath": "スキルパス",
|
||||||
|
"hint": "この操作でスキルリポジトリが一覧に追加されます。",
|
||||||
|
"hintDetail": "追加後、スキル管理ページから個別のスキルをインストールできます。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iconPicker": {
|
||||||
|
"search": "アイコンを検索",
|
||||||
|
"searchPlaceholder": "アイコン名を入力...",
|
||||||
|
"noResults": "一致するアイコンが見つかりません",
|
||||||
|
"category": {
|
||||||
|
"aiProvider": "AI プロバイダー",
|
||||||
|
"cloud": "クラウドプラットフォーム",
|
||||||
|
"tool": "開発ツール",
|
||||||
|
"other": "その他"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providerIcon": {
|
||||||
|
"label": "アイコン",
|
||||||
|
"colorLabel": "アイコンカラー",
|
||||||
|
"selectIcon": "アイコンを選択",
|
||||||
|
"preview": "プレビュー"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,7 @@
|
|||||||
"autoReload": "数据将在2秒后自动刷新...",
|
"autoReload": "数据将在2秒后自动刷新...",
|
||||||
"languageOptionChinese": "中文",
|
"languageOptionChinese": "中文",
|
||||||
"languageOptionEnglish": "English",
|
"languageOptionEnglish": "English",
|
||||||
|
"languageOptionJapanese": "日本語",
|
||||||
"windowBehavior": "窗口行为",
|
"windowBehavior": "窗口行为",
|
||||||
"windowBehaviorHint": "配置窗口最小化与 Claude 插件联动策略。",
|
"windowBehaviorHint": "配置窗口最小化与 Claude 插件联动策略。",
|
||||||
"launchOnStartup": "开机自启",
|
"launchOnStartup": "开机自启",
|
||||||
@@ -376,7 +377,8 @@
|
|||||||
"templateGeneral": "通用模板",
|
"templateGeneral": "通用模板",
|
||||||
"templateNewAPI": "NewAPI",
|
"templateNewAPI": "NewAPI",
|
||||||
"credentialsConfig": "凭证配置",
|
"credentialsConfig": "凭证配置",
|
||||||
"accessToken": "访问令牌",
|
"baseUrl": "请求地址",
|
||||||
|
"accessToken": "访问令牌(在个人安全设置里获取)",
|
||||||
"accessTokenPlaceholder": "在'安全设置'里生成",
|
"accessTokenPlaceholder": "在'安全设置'里生成",
|
||||||
"userId": "用户 ID",
|
"userId": "用户 ID",
|
||||||
"userIdPlaceholder": "例如:114514",
|
"userIdPlaceholder": "例如:114514",
|
||||||
@@ -389,7 +391,7 @@
|
|||||||
"timeoutHint": "范围: 2-30 秒",
|
"timeoutHint": "范围: 2-30 秒",
|
||||||
"timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略",
|
"timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略",
|
||||||
"timeoutCannotBeNegative": "超时时间不能为负数",
|
"timeoutCannotBeNegative": "超时时间不能为负数",
|
||||||
"autoIntervalMinutes": "自动查询间隔(分钟)",
|
"autoIntervalMinutes": "自动查询间隔(分钟,0 表示不自动查询)",
|
||||||
"autoQueryInterval": "自动查询间隔(分钟)",
|
"autoQueryInterval": "自动查询间隔(分钟)",
|
||||||
"autoQueryIntervalHint": "0 表示不自动查询,建议 5-60 分钟",
|
"autoQueryIntervalHint": "0 表示不自动查询,建议 5-60 分钟",
|
||||||
"intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略",
|
"intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export interface DeepLinkImportRequest {
|
|||||||
repo?: string;
|
repo?: string;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
branch?: string;
|
branch?: string;
|
||||||
skillsPath?: string;
|
|
||||||
|
|
||||||
// Config file fields
|
// Config file fields
|
||||||
config?: string;
|
config?: string;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export interface Skill {
|
|||||||
repoOwner?: string;
|
repoOwner?: string;
|
||||||
repoName?: string;
|
repoName?: string;
|
||||||
repoBranch?: string;
|
repoBranch?: string;
|
||||||
skillsPath?: string; // 技能所在的子目录路径,如 "skills"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillRepo {
|
export interface SkillRepo {
|
||||||
@@ -18,7 +17,6 @@ export interface SkillRepo {
|
|||||||
name: string;
|
name: string;
|
||||||
branch: string;
|
branch: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
skillsPath?: string; // 可选:技能所在的子目录路径,如 "skills"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const skillsApi = {
|
export const skillsApi = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const settingsSchema = z.object({
|
|||||||
minimizeToTrayOnClose: z.boolean(),
|
minimizeToTrayOnClose: z.boolean(),
|
||||||
enableClaudePluginIntegration: z.boolean().optional(),
|
enableClaudePluginIntegration: z.boolean().optional(),
|
||||||
launchOnStartup: z.boolean().optional(),
|
launchOnStartup: z.boolean().optional(),
|
||||||
language: z.enum(["en", "zh"]).optional(),
|
language: z.enum(["en", "zh", "ja"]).optional(),
|
||||||
|
|
||||||
// 设备级目录覆盖
|
// 设备级目录覆盖
|
||||||
claudeConfigDir: directorySchema.nullable().optional(),
|
claudeConfigDir: directorySchema.nullable().optional(),
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ export interface Settings {
|
|||||||
// 是否开机自启
|
// 是否开机自启
|
||||||
launchOnStartup?: boolean;
|
launchOnStartup?: boolean;
|
||||||
// 首选语言(可选,默认中文)
|
// 首选语言(可选,默认中文)
|
||||||
language?: "en" | "zh";
|
language?: "en" | "zh" | "ja";
|
||||||
|
|
||||||
// ===== 设备级目录覆盖 =====
|
// ===== 设备级目录覆盖 =====
|
||||||
// 覆盖 Claude Code 配置目录(可选)
|
// 覆盖 Claude Code 配置目录(可选)
|
||||||
|
|||||||
@@ -58,6 +58,29 @@ describe("useSettingsForm Hook", () => {
|
|||||||
expect(changeLanguageSpy).toHaveBeenCalledWith("en");
|
expect(changeLanguageSpy).toHaveBeenCalledWith("en");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should support japanese language preference from server data", async () => {
|
||||||
|
useSettingsQueryMock.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
showInTray: true,
|
||||||
|
minimizeToTrayOnClose: true,
|
||||||
|
enableClaudePluginIntegration: false,
|
||||||
|
claudeConfigDir: "/Users/demo",
|
||||||
|
codexConfigDir: null,
|
||||||
|
language: "ja",
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useSettingsForm());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.settings?.language).toBe("ja");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.initialLanguage).toBe("ja");
|
||||||
|
expect(changeLanguageSpy).toHaveBeenCalledWith("ja");
|
||||||
|
});
|
||||||
|
|
||||||
it("should prioritize reading language from local storage in readPersistedLanguage", () => {
|
it("should prioritize reading language from local storage in readPersistedLanguage", () => {
|
||||||
useSettingsQueryMock.mockReturnValue({
|
useSettingsQueryMock.mockReturnValue({
|
||||||
data: null,
|
data: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user