Compare commits

..

1 Commits

Author SHA1 Message Date
YoVinchen 38bac36960 feat(skill): implement recursive scanning for skill repositories
Add recursive directory scanning to discover SKILL.md files in nested
directories. When a SKILL.md is found, treat sibling directories as
functional folders rather than separate skills.
2025-11-28 11:43:31 +08:00
33 changed files with 408 additions and 1055 deletions
+3 -4
View File
@@ -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,7 +16,6 @@ src/
│ ├── index.ts # 国际化配置文件 │ ├── index.ts # 国际化配置文件
│ └── locales/ │ └── locales/
│ ├── en.json # 英文翻译 │ ├── en.json # 英文翻译
│ ├── ja.json # 日文翻译
│ └── zh.json # 中文翻译 │ └── zh.json # 中文翻译
├── components/ ├── components/
│ └── LanguageSwitcher.tsx # 语言切换组件 │ └── LanguageSwitcher.tsx # 语言切换组件
@@ -25,7 +24,7 @@ src/
## 默认语言设置 ## 默认语言设置
- **默认语言**文 (zh)(无首选时根据浏览器/系统语言选择 zh/en/ja) - **默认语言**文 (en)
- **回退语言**:英文 (en) - **回退语言**:英文 (en)
## 使用方式 ## 使用方式
@@ -58,7 +57,7 @@ src/
## 测试功能 ## 测试功能
应用已添加了语言切换按钮,支持中文、英文、日文三种语言切换,验证国际化功能是否正常工作。 应用已添加了语言切换按钮(地球图标),点击可以在中英文之间切换,验证国际化功能是否正常工作。
## 已更新的组件 ## 已更新的组件
+5 -3
View File
@@ -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<bool, AppError> { fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<(), 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<bool, AppError> { ) -> Result<(), AppError> {
import_default_config_internal(state, app_type) import_default_config_internal(state, app_type)
} }
@@ -102,7 +102,9 @@ 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).map_err(Into::into) import_default_config_internal(&state, app_type)
.map(|_| true)
.map_err(Into::into)
} }
/// 查询供应商用量 /// 查询供应商用量
+1
View File
@@ -90,6 +90,7 @@ pub async fn install_skill(
.clone() .clone()
.unwrap_or_else(|| "main".to_string()), .unwrap_or_else(|| "main".to_string()),
enabled: true, enabled: true,
skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path
}; };
service service
+4 -5
View File
@@ -58,9 +58,7 @@ 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( .prepare("SELECT owner, name, branch, enabled, skills_path FROM skill_repos ORDER BY owner ASC, name ASC")
"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
@@ -70,6 +68,7 @@ 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()))?;
@@ -85,8 +84,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) VALUES (?1, ?2, ?3, ?4)", "INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)",
params![repo.owner, repo.name, repo.branch, repo.enabled], params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path],
).map_err(|e| AppError::Database(e.to_string()))?; ).map_err(|e| AppError::Database(e.to_string()))?;
Ok(()) Ok(())
} }
+2 -2
View File
@@ -202,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) VALUES (?1, ?2, ?3, ?4)", "INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)",
params![repo.owner, repo.name, repo.branch, repo.enabled], params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path],
).map_err(|e| AppError::Database(format!("Migrate skill repo failed: {e}")))?; ).map_err(|e| AppError::Database(format!("Migrate skill repo failed: {e}")))?;
} }
+17 -10
View File
@@ -115,21 +115,28 @@ 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)
} }
} }
+2 -1
View File
@@ -104,6 +104,7 @@ 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)
)", )",
[], [],
@@ -232,7 +233,7 @@ impl Database {
"TEXT NOT NULL DEFAULT 'main'", "TEXT NOT NULL DEFAULT 'main'",
)?; )?;
Self::add_column_if_missing(conn, "skill_repos", "enabled", "BOOLEAN NOT NULL DEFAULT 1")?; Self::add_column_if_missing(conn, "skill_repos", "enabled", "BOOLEAN NOT NULL DEFAULT 1")?;
// 注意: skills_path 字段已被移除,因为现在支持全仓库递归扫描 Self::add_column_if_missing(conn, "skill_repos", "skills_path", "TEXT")?;
Ok(()) Ok(())
} }
+4
View File
@@ -100,8 +100,12 @@ 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
+8
View File
@@ -143,6 +143,7 @@ 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,
@@ -203,6 +204,7 @@ 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,
@@ -260,6 +262,7 @@ 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,
}) })
} }
@@ -284,6 +287,10 @@ 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,
@@ -291,6 +298,7 @@ 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,
+1
View File
@@ -40,6 +40,7 @@ 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
+6 -1
View File
@@ -142,6 +142,7 @@ 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,
@@ -188,6 +189,7 @@ 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,
@@ -229,6 +231,7 @@ 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,
@@ -272,6 +275,7 @@ 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,
@@ -368,11 +372,12 @@ 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"; let url = "ccswitch://v1/import?resource=skill&repo=owner/repo&directory=skills&branch=dev&skills_path=src";
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");
} }
+74 -58
View File
@@ -69,11 +69,6 @@ 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: " (无供应商,请在主界面添加)",
@@ -617,47 +612,54 @@ 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
});
// 1. 初始化默认 Skills 仓库(已有内置检查:表非空则跳过) if need_first_import {
match app_state.db.init_default_skill_repos() { // 数据库为空,尝试从用户现有的配置文件导入数据并初始化默认配置
Ok(count) if count > 0 => { log::info!(
log::info!("✓ Initialized {count} default skill repositories"); "Empty database detected, importing existing configurations and initializing defaults..."
} );
Ok(_) => {} // 表非空,静默跳过
Err(e) => log::warn!("✗ Failed to initialize default skill repos: {e}"),
}
// 2. 导入供应商配置(已有内置检查:该应用已有供应商则跳过 // 1. 初始化默认 Skills 仓库(3个
for app in [ match app_state.db.init_default_skill_repos() {
crate::app_config::AppType::Claude, Ok(count) if count > 0 => {
crate::app_config::AppType::Codex, log::info!("✓ Initialized {count} default skill repositories");
crate::app_config::AppType::Gemini,
] {
match crate::services::provider::ProviderService::import_default_config(
&app_state,
app.clone(),
) {
Ok(true) => {
log::info!("✓ Imported default provider for {}", app.as_str());
} }
Ok(false) => {} // 已有供应商,静默跳过 Ok(_) => log::debug!("No default skill repositories to initialize"),
Err(e) => { Err(e) => log::warn!("✗ Failed to initialize default skill repos: {e}"),
log::debug!( }
"○ No default provider to import for {}: {}",
app.as_str(), // 2. 导入供应商配置(从 live 配置文件)
e for app in [
); crate::app_config::AppType::Claude,
crate::app_config::AppType::Codex,
crate::app_config::AppType::Gemini,
] {
match crate::services::provider::ProviderService::import_default_config(
&app_state,
app.clone(),
) {
Ok(_) => {
log::info!("✓ Imported default provider for {}", app.as_str());
}
Err(e) => {
log::debug!(
"○ No default provider to import for {}: {}",
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");
@@ -681,28 +683,42 @@ 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. 导入提示词文件
if app_state.db.is_prompts_table_empty().unwrap_or(false) { match crate::services::prompt::PromptService::import_from_file_on_first_launch(
log::info!("Prompts table empty, importing from live configurations..."); &app_state,
for app in [
crate::app_config::AppType::Claude, crate::app_config::AppType::Claude,
crate::app_config::AppType::Codex, ) {
crate::app_config::AppType::Gemini, Ok(count) if count > 0 => {
] { log::info!("✓ Imported {count} prompt(s) from Claude");
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
&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 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,
) {
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,
) {
Ok(count) if count > 0 => {
log::info!("✓ Imported {count} prompt(s) from Gemini");
}
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
-6
View File
@@ -176,12 +176,6 @@ 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)?;
// 检查文件是否存在 // 检查文件是否存在
+3 -6
View File
@@ -215,14 +215,11 @@ 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(false); // 已有供应商,跳过 return Ok(());
} }
} }
@@ -301,7 +298,7 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
.db .db
.set_current_provider(app_type.as_str(), &provider.id)?; .set_current_provider(app_type.as_str(), &provider.id)?;
Ok(true) // 真正导入了 Ok(())
} }
/// Write Gemini live configuration with authentication handling /// Write Gemini live configuration with authentication handling
+1 -3
View File
@@ -225,9 +225,7 @@ 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)
} }
+45 -5
View File
@@ -34,6 +34,9 @@ pub struct Skill {
/// 分支名称 /// 分支名称
#[serde(rename = "repoBranch")] #[serde(rename = "repoBranch")]
pub repo_branch: Option<String>, pub repo_branch: Option<String>,
/// 技能所在的子目录路径 (可选, 如 "skills")
#[serde(rename = "skillsPath")]
pub skills_path: Option<String>,
} }
/// 仓库配置 /// 仓库配置
@@ -47,6 +50,9 @@ 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>,
} }
/// 技能安装状态 /// 技能安装状态
@@ -78,18 +84,21 @@ 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 子目录
}, },
], ],
} }
@@ -185,8 +194,25 @@ impl SkillService {
})??; })??;
let mut skills = Vec::new(); let mut skills = Vec::new();
// 扫描仓库根目录(支持全仓库递归扫描) // 确定要扫描的目录路径
let scan_dir = temp_dir.clone(); let scan_dir = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 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()
};
// 递归扫描目录查找所有技能 // 递归扫描目录查找所有技能
self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?; self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?;
@@ -258,7 +284,11 @@ impl SkillService {
let meta = self.parse_skill_metadata(skill_md)?; let meta = self.parse_skill_metadata(skill_md)?;
// 构建 README URL // 构建 README URL
let readme_path = directory.to_string(); let readme_path = if let Some(ref skills_path) = repo.skills_path {
format!("{}/{}", skills_path.trim_matches('/'), directory)
} else {
directory.to_string()
};
Ok(Skill { Ok(Skill {
key: format!("{}/{}:{}", repo.owner, repo.name, directory), key: format!("{}/{}:{}", repo.owner, repo.name, directory),
@@ -273,6 +303,7 @@ impl SkillService {
repo_owner: Some(repo.owner.clone()), repo_owner: Some(repo.owner.clone()),
repo_name: Some(repo.name.clone()), repo_name: Some(repo.name.clone()),
repo_branch: Some(repo.branch.clone()), repo_branch: Some(repo.branch.clone()),
skills_path: repo.skills_path.clone(),
}) })
} }
@@ -374,6 +405,7 @@ impl SkillService {
repo_owner: None, repo_owner: None,
repo_name: None, repo_name: None,
repo_branch: None, repo_branch: None,
skills_path: None,
}); });
} }
@@ -542,8 +574,16 @@ impl SkillService {
)) ))
})??; })??;
// 确定源目录路径(技能相对于仓库根目录的路径) // 根据 skills_path 确定源目录路径
let source = temp_dir.join(&directory); let source = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skills_path,源路径为: temp_dir/skills_path/directory
temp_dir
.join(skills_path.trim_matches('/'))
.join(&directory)
} else {
// 否则源路径为: temp_dir/directory
temp_dir.join(&directory)
};
if !source.exists() { if !source.exists() {
let _ = fs::remove_dir_all(&temp_dir); let _ = fs::remove_dir_all(&temp_dir);
+1 -1
View File
@@ -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" | "ja")) .filter(|s| matches!(*s, "en" | "zh"))
.map(|s| s.to_string()); .map(|s| s.to_string());
} }
+147 -39
View File
@@ -176,16 +176,10 @@ 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;
} }
// 检测 GENERAL 模板(有 apiKey 或 baseUrl return null;
if (existingScript?.apiKey || existingScript?.baseUrl) {
return TEMPLATE_KEYS.GENERAL;
}
// 新配置或无凭证:默认使用 GENERAL(与默认代码模板一致)
return TEMPLATE_KEYS.GENERAL;
}, },
); );
@@ -355,9 +349,14 @@ 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">
<p className="text-base font-medium leading-none text-foreground"> <div className="space-y-1">
{t("usageScript.enableUsageQuery")} <p className="text-sm font-medium leading-none text-foreground">
</p> {t("usageScript.enableUsageQuery")}
</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) =>
@@ -371,9 +370,14 @@ 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">
<Label className="text-base font-medium"> <div className="flex flex-wrap items-center justify-between gap-2">
{t("usageScript.presetTemplate")} <Label className="text-base font-medium">
</Label> {t("usageScript.presetTemplate")}
</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;
@@ -443,9 +447,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="usage-base-url"> <Label htmlFor="usage-base-url">Base URL</Label>
{t("usageScript.baseUrl")}
</Label>
<Input <Input
id="usage-base-url" id="usage-base-url"
type="text" type="text"
@@ -464,9 +466,7 @@ 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"> <Label htmlFor="usage-newapi-base-url">Base URL</Label>
{t("usageScript.baseUrl")}
</Label>
<Input <Input
id="usage-newapi-base-url" id="usage-newapi-base-url"
type="text" type="text"
@@ -545,36 +545,141 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
</div> </div>
</div> </div>
)} )}
</div>
{/* 通用配置(始终显示) */} {/* 脚本配置 */}
<div className="grid gap-4 md:grid-cols-2 pt-4 border-t border-white/10"> <div className="space-y-4 glass rounded-xl border border-white/10 p-6">
{/* 超时时间 */} <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-timeout"> <Label htmlFor="usage-request-url">
{t("usageScript.timeoutSeconds")} {t("usageScript.requestUrl")}
</Label> </Label>
<Input <Input
id="usage-timeout" id="usage-request-url"
type="number" type="text"
min={0} value={script.request?.url || ""}
value={script.timeout ?? 10} onChange={(e) => {
onChange={(e) =>
setScript({ setScript({
...script, ...script,
timeout: validateTimeout(e.target.value), request: { ...script.request, url: e.target.value },
}) });
} }}
onBlur={(e) => placeholder={t("usageScript.requestUrlPlaceholder")}
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")}
@@ -603,6 +708,9 @@ 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>
+16 -5
View File
@@ -30,11 +30,22 @@ export function SkillConfirmation({
</div> </div>
</div> </div>
<div> <div className="grid grid-cols-2 gap-4">
<label className="block text-sm font-medium text-muted-foreground"> <div>
{t("deeplink.skill.branch")} <label className="block text-sm font-medium text-muted-foreground">
</label> {t("deeplink.skill.branch")}
<div className="mt-1 text-sm">{request.branch || "main"}</div> </label>
<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 -7
View File
@@ -2,11 +2,9 @@ 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: LanguageOption; value: "zh" | "en";
onChange: (value: LanguageOption) => void; onChange: (value: "zh" | "en") => void;
} }
export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { export function LanguageSettings({ value, onChange }: LanguageSettingsProps) {
@@ -27,9 +25,6 @@ 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>
); );
+16
View File
@@ -34,6 +34,7 @@ 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) =>
@@ -79,10 +80,12 @@ 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"));
} }
@@ -127,6 +130,13 @@ 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"
@@ -161,6 +171,12 @@ 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),
+34 -11
View File
@@ -26,6 +26,7 @@ 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) =>
@@ -66,10 +67,12 @@ 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"));
} }
@@ -107,17 +110,31 @@ export function RepoManagerPanel({
className="mt-2" className="mt-2"
/> />
</div> </div>
<div> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Label htmlFor="branch" className="text-foreground"> <div>
{t("skills.repo.branch")} <Label htmlFor="branch" className="text-foreground">
</Label> {t("skills.repo.branch")}
<Input </Label>
id="branch" <Input
placeholder={t("skills.repo.branchPlaceholder")} id="branch"
value={branch} placeholder={t("skills.repo.branchPlaceholder")}
onChange={(e) => setBranch(e.target.value)} value={branch}
className="mt-2" onChange={(e) => setBranch(e.target.value)}
/> 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>
@@ -157,6 +174,12 @@ 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),
+1 -1
View File
@@ -12,7 +12,7 @@ import {
} from "./useDirectorySettings"; } from "./useDirectorySettings";
import { useSettingsMetadata } from "./useSettingsMetadata"; import { useSettingsMetadata } from "./useSettingsMetadata";
type Language = "zh" | "en" | "ja"; type Language = "zh" | "en";
interface SaveResult { interface SaveResult {
requiresRestart: boolean; requiresRestart: boolean;
+4 -5
View File
@@ -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" | "ja"; type Language = "zh" | "en";
export type SettingsFormState = Omit<Settings, "language"> & { export type SettingsFormState = Omit<Settings, "language"> & {
language: Language; language: Language;
@@ -11,8 +11,7 @@ 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";
const normalized = lang.toLowerCase(); return lang === "en" ? "en" : "zh";
return normalized === "en" || normalized === "ja" ? normalized : "zh";
}; };
const sanitizeDir = (value?: string | null): string | undefined => { const sanitizeDir = (value?: string | null): string | undefined => {
@@ -52,8 +51,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" || stored === "ja") { if (stored === "en" || stored === "zh") {
return stored as Language; return stored;
} }
} }
return normalizeLanguage(i18n.language); return normalizeLanguage(i18n.language);
+3 -13
View File
@@ -2,18 +2,15 @@ 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";
type Language = "zh" | "en" | "ja"; const DEFAULT_LANGUAGE: "zh" | "en" = "zh";
const DEFAULT_LANGUAGE: Language = "zh"; const getInitialLanguage = (): "zh" | "en" => {
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" || stored === "ja") { if (stored === "zh" || stored === "en") {
return stored; return stored;
} }
} catch (error) { } catch (error) {
@@ -31,10 +28,6 @@ const getInitialLanguage = (): Language => {
return "zh"; return "zh";
} }
if (navigatorLang?.startsWith("ja")) {
return "ja";
}
if (navigatorLang?.startsWith("en")) { if (navigatorLang?.startsWith("en")) {
return "en"; return "en";
} }
@@ -46,9 +39,6 @@ const resources = {
en: { en: {
translation: en, translation: en,
}, },
ja: {
translation: ja,
},
zh: { zh: {
translation: zh, translation: zh,
}, },
+1 -3
View File
@@ -165,7 +165,6 @@
"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",
@@ -377,7 +376,6 @@
"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",
@@ -391,7 +389,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, 0 to disable)", "autoIntervalMinutes": "Auto query interval (minutes)",
"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",
-837
View File
@@ -1,837 +0,0 @@
{
"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 が月額 $280% 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": "プレビュー"
}
}
+2 -4
View File
@@ -165,7 +165,6 @@
"autoReload": "数据将在2秒后自动刷新...", "autoReload": "数据将在2秒后自动刷新...",
"languageOptionChinese": "中文", "languageOptionChinese": "中文",
"languageOptionEnglish": "English", "languageOptionEnglish": "English",
"languageOptionJapanese": "日本語",
"windowBehavior": "窗口行为", "windowBehavior": "窗口行为",
"windowBehaviorHint": "配置窗口最小化与 Claude 插件联动策略。", "windowBehaviorHint": "配置窗口最小化与 Claude 插件联动策略。",
"launchOnStartup": "开机自启", "launchOnStartup": "开机自启",
@@ -377,8 +376,7 @@
"templateGeneral": "通用模板", "templateGeneral": "通用模板",
"templateNewAPI": "NewAPI", "templateNewAPI": "NewAPI",
"credentialsConfig": "凭证配置", "credentialsConfig": "凭证配置",
"baseUrl": "请求地址", "accessToken": "访问令牌",
"accessToken": "访问令牌(在个人安全设置里获取)",
"accessTokenPlaceholder": "在'安全设置'里生成", "accessTokenPlaceholder": "在'安全设置'里生成",
"userId": "用户 ID", "userId": "用户 ID",
"userIdPlaceholder": "例如:114514", "userIdPlaceholder": "例如:114514",
@@ -391,7 +389,7 @@
"timeoutHint": "范围: 2-30 秒", "timeoutHint": "范围: 2-30 秒",
"timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略", "timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略",
"timeoutCannotBeNegative": "超时时间不能为负数", "timeoutCannotBeNegative": "超时时间不能为负数",
"autoIntervalMinutes": "自动查询间隔(分钟0 表示不自动查询", "autoIntervalMinutes": "自动查询间隔(分钟)",
"autoQueryInterval": "自动查询间隔(分钟)", "autoQueryInterval": "自动查询间隔(分钟)",
"autoQueryIntervalHint": "0 表示不自动查询,建议 5-60 分钟", "autoQueryIntervalHint": "0 表示不自动查询,建议 5-60 分钟",
"intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略", "intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略",
+1
View File
@@ -33,6 +33,7 @@ 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;
+2
View File
@@ -10,6 +10,7 @@ export interface Skill {
repoOwner?: string; repoOwner?: string;
repoName?: string; repoName?: string;
repoBranch?: string; repoBranch?: string;
skillsPath?: string; // 技能所在的子目录路径,如 "skills"
} }
export interface SkillRepo { export interface SkillRepo {
@@ -17,6 +18,7 @@ export interface SkillRepo {
name: string; name: string;
branch: string; branch: string;
enabled: boolean; enabled: boolean;
skillsPath?: string; // 可选:技能所在的子目录路径,如 "skills"
} }
export const skillsApi = { export const skillsApi = {
+1 -1
View File
@@ -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", "ja"]).optional(), language: z.enum(["en", "zh"]).optional(),
// 设备级目录覆盖 // 设备级目录覆盖
claudeConfigDir: directorySchema.nullable().optional(), claudeConfigDir: directorySchema.nullable().optional(),
+1 -1
View File
@@ -109,7 +109,7 @@ export interface Settings {
// 是否开机自启 // 是否开机自启
launchOnStartup?: boolean; launchOnStartup?: boolean;
// 首选语言(可选,默认中文) // 首选语言(可选,默认中文)
language?: "en" | "zh" | "ja"; language?: "en" | "zh";
// ===== 设备级目录覆盖 ===== // ===== 设备级目录覆盖 =====
// 覆盖 Claude Code 配置目录(可选) // 覆盖 Claude Code 配置目录(可选)
-23
View File
@@ -58,29 +58,6 @@ 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,