From 5e54656d45b9f2f7f8d6e956b5941c1887bbd88a Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 21 Nov 2025 15:02:01 +0800 Subject: [PATCH 1/4] fix(skills): resolve third-party skills installation failure (#268) - Add skills_path field to Skill struct - Use skills_path to construct correct source path during installation - Fix installation for repos with custom skill subdirectories --- src-tauri/src/commands/skill.rs | 2 +- src-tauri/src/services/skill.rs | 20 +++++++++++++++++--- src/lib/api/skills.ts | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/skill.rs b/src-tauri/src/commands/skill.rs index 64a5c18..fc5a544 100644 --- a/src-tauri/src/commands/skill.rs +++ b/src-tauri/src/commands/skill.rs @@ -62,7 +62,7 @@ pub async fn install_skill( .clone() .unwrap_or_else(|| "main".to_string()), enabled: true, - skills_path: None, // 安装时使用默认路径 + skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path }; service diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index 2af22b3..f32a186 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -32,6 +32,9 @@ pub struct Skill { /// 分支名称 #[serde(rename = "repoBranch")] pub repo_branch: Option, + /// 技能所在的子目录路径 (可选, 如 "skills") + #[serde(rename = "skillsPath")] + pub skills_path: Option, } /// 仓库配置 @@ -234,6 +237,7 @@ impl SkillService { repo_owner: Some(repo.owner.clone()), repo_name: Some(repo.name.clone()), repo_branch: Some(repo.branch.clone()), + skills_path: repo.skills_path.clone(), }); } Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e), @@ -312,6 +316,7 @@ impl SkillService { repo_owner: None, repo_name: None, repo_branch: None, + skills_path: None, }); } } @@ -442,12 +447,21 @@ impl SkillService { .await .map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??; - // 复制到安装目录 - let source = temp_dir.join(&directory); + // 根据 skills_path 确定源目录路径 + let source = if let Some(ref skills_path) = repo.skills_path { + // 如果指定了 skills_path,源路径为: temp_dir/skills_path/directory + temp_dir.join(skills_path.trim_matches('/')).join(&directory) + } else { + // 否则源路径为: temp_dir/directory + temp_dir.join(&directory) + }; if !source.exists() { let _ = fs::remove_dir_all(&temp_dir); - return Err(anyhow::anyhow!("技能目录不存在")); + return Err(anyhow::anyhow!( + "技能目录不存在: {}", + source.display() + )); } // 删除旧版本 diff --git a/src/lib/api/skills.ts b/src/lib/api/skills.ts index c0ddb87..a455401 100644 --- a/src/lib/api/skills.ts +++ b/src/lib/api/skills.ts @@ -10,6 +10,7 @@ export interface Skill { repoOwner?: string; repoName?: string; repoBranch?: string; + skillsPath?: string; // 技能所在的子目录路径,如 "skills" } export interface SkillRepo { From 7fa0a7b16648e99ef956d18c01f686dd50e843ed Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 21 Nov 2025 16:20:01 +0800 Subject: [PATCH 2/4] feat(skills): enhance error messages with i18n support - Add structured error format with error codes and context - Create skillErrorParser to format errors for user-friendly display - Add comprehensive i18n keys for all skill-related errors (zh/en) - Extend download timeout from 15s to 60s to reduce false positives - Fix: Pass correct error title based on operation context (load/install/uninstall) Error improvements: - SKILL_NOT_FOUND: Show skill directory name - DOWNLOAD_TIMEOUT: Display repo info and timeout duration with network suggestion - DOWNLOAD_FAILED: Show HTTP status code with specific suggestions (403/404/429) - SKILL_DIR_NOT_FOUND: Show full path with URL check suggestion - EMPTY_ARCHIVE: Suggest checking repository URL Users will now see detailed error messages instead of generic "Error". --- src-tauri/src/commands/skill.rs | 25 ++++++- src-tauri/src/error.rs | 25 +++++++ src-tauri/src/services/skill.rs | 63 +++++++++++++--- src/components/skills/SkillsPage.tsx | 61 ++++++++++++++-- src/i18n/locales/en.json | 28 ++++++++ src/i18n/locales/zh.json | 28 ++++++++ src/lib/errors/skillErrorParser.ts | 104 +++++++++++++++++++++++++++ 7 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 src/lib/errors/skillErrorParser.ts diff --git a/src-tauri/src/commands/skill.rs b/src-tauri/src/commands/skill.rs index fc5a544..6b12850 100644 --- a/src-tauri/src/commands/skill.rs +++ b/src-tauri/src/commands/skill.rs @@ -1,3 +1,4 @@ +use crate::error::format_skill_error; use crate::services::skill::SkillState; use crate::services::{Skill, SkillRepo, SkillService}; use crate::store::AppState; @@ -45,18 +46,36 @@ pub async fn install_skill( let skill = skills .iter() .find(|s| s.directory.eq_ignore_ascii_case(&directory)) - .ok_or_else(|| "技能不存在".to_string())?; + .ok_or_else(|| { + format_skill_error( + "SKILL_NOT_FOUND", + &[("directory", &directory)], + Some("checkRepoUrl"), + ) + })?; if !skill.installed { let repo = SkillRepo { owner: skill .repo_owner .clone() - .ok_or_else(|| "缺少仓库信息".to_string())?, + .ok_or_else(|| { + format_skill_error( + "MISSING_REPO_INFO", + &[("directory", &directory), ("field", "owner")], + None, + ) + })?, name: skill .repo_name .clone() - .ok_or_else(|| "缺少仓库信息".to_string())?, + .ok_or_else(|| { + format_skill_error( + "MISSING_REPO_INFO", + &[("directory", &directory), ("field", "name")], + None, + ) + })?, branch: skill .repo_branch .clone() diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 455e907..d9b0262 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -94,3 +94,28 @@ impl From for String { err.to_string() } } + +/// 格式化为 JSON 错误字符串,前端可解析为结构化错误 +pub fn format_skill_error( + code: &str, + context: &[(&str, &str)], + suggestion: Option<&str>, +) -> String { + use serde_json::json; + + let mut ctx_map = serde_json::Map::new(); + for (key, value) in context { + ctx_map.insert(key.to_string(), json!(value)); + } + + let error_obj = json!({ + "code": code, + "context": ctx_map, + "suggestion": suggestion, + }); + + serde_json::to_string(&error_obj).unwrap_or_else(|_| { + // 如果 JSON 序列化失败,返回简单格式 + format!("ERROR:{}", code) + }) +} diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index f32a186..1c06f73 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -7,6 +7,8 @@ use std::fs; use std::path::{Path, PathBuf}; use tokio::time::timeout; +use crate::error::format_skill_error; + /// 技能对象 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Skill { @@ -133,7 +135,11 @@ impl SkillService { } fn get_install_dir() -> Result { - let home = dirs::home_dir().context("无法获取用户主目录")?; + let home = dirs::home_dir().context(format_skill_error( + "GET_HOME_DIR_FAILED", + &[], + Some("checkPermission"), + ))?; Ok(home.join(".claude").join("skills")) } } @@ -173,9 +179,19 @@ impl SkillService { /// 从仓库获取技能列表 async fn fetch_repo_skills(&self, repo: &SkillRepo) -> Result> { // 为单个仓库加载增加整体超时,避免无效链接长时间阻塞 - let temp_dir = timeout(std::time::Duration::from_secs(15), self.download_repo(repo)) + let temp_dir = timeout(std::time::Duration::from_secs(60), self.download_repo(repo)) .await - .map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??; + .map_err(|_| { + anyhow!(format_skill_error( + "DOWNLOAD_TIMEOUT", + &[ + ("owner", &repo.owner), + ("name", &repo.name), + ("timeout", "60") + ], + Some("checkNetwork"), + )) + })??; let mut skills = Vec::new(); // 确定要扫描的目录路径 @@ -379,7 +395,17 @@ impl SkillService { // 下载 ZIP let response = self.http_client.get(url).send().await?; if !response.status().is_success() { - return Err(anyhow::anyhow!("下载失败: {}", response.status())); + let status = response.status().as_u16().to_string(); + return Err(anyhow::anyhow!(format_skill_error( + "DOWNLOAD_FAILED", + &[("status", &status)], + match status.as_str() { + "403" => Some("http403"), + "404" => Some("http404"), + "429" => Some("http429"), + _ => Some("checkNetwork"), + }, + ))); } let bytes = response.bytes().await?; @@ -394,7 +420,11 @@ impl SkillService { let name = first_file.name(); name.split('/').next().unwrap_or("").to_string() } else { - return Err(anyhow::anyhow!("空的压缩包")); + return Err(anyhow::anyhow!(format_skill_error( + "EMPTY_ARCHIVE", + &[], + Some("checkRepoUrl"), + ))); }; // 解压所有文件 @@ -441,11 +471,21 @@ impl SkillService { // 下载仓库时增加总超时,防止无效链接导致长时间卡住安装过程 let temp_dir = timeout( - std::time::Duration::from_secs(15), + std::time::Duration::from_secs(60), self.download_repo(&repo), ) .await - .map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??; + .map_err(|_| { + anyhow!(format_skill_error( + "DOWNLOAD_TIMEOUT", + &[ + ("owner", &repo.owner), + ("name", &repo.name), + ("timeout", "60") + ], + Some("checkNetwork"), + )) + })??; // 根据 skills_path 确定源目录路径 let source = if let Some(ref skills_path) = repo.skills_path { @@ -458,10 +498,11 @@ impl SkillService { if !source.exists() { let _ = fs::remove_dir_all(&temp_dir); - return Err(anyhow::anyhow!( - "技能目录不存在: {}", - source.display() - )); + return Err(anyhow::anyhow!(format_skill_error( + "SKILL_DIR_NOT_FOUND", + &[("path", &source.display().to_string())], + Some("checkRepoUrl"), + ))); } // 删除旧版本 diff --git a/src/components/skills/SkillsPage.tsx b/src/components/skills/SkillsPage.tsx index eed361e..21c5fd3 100644 --- a/src/components/skills/SkillsPage.tsx +++ b/src/components/skills/SkillsPage.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { SkillCard } from "./SkillCard"; import { RepoManager } from "./RepoManager"; import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills"; +import { formatSkillError } from "@/lib/errors/skillErrorParser"; interface SkillsPageProps { onClose?: () => void; @@ -27,9 +28,22 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) { afterLoad(data); } } catch (error) { - toast.error(t("skills.loadFailed"), { - description: error instanceof Error ? error.message : t("common.error"), + const errorMessage = + error instanceof Error ? error.message : String(error); + + // 传入 "skills.loadFailed" 作为标题 + const { title, description } = formatSkillError( + errorMessage, + t, + "skills.loadFailed" + ); + + toast.error(title, { + description, + duration: 8000, }); + + console.error("Load skills failed:", error); } finally { setLoading(false); } @@ -54,8 +68,26 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) { toast.success(t("skills.installSuccess", { name: directory })); await loadSkills(); } catch (error) { - toast.error(t("skills.installFailed"), { - description: error instanceof Error ? error.message : t("common.error"), + const errorMessage = + error instanceof Error ? error.message : String(error); + + // 使用错误解析器格式化错误,传入 "skills.installFailed" + const { title, description } = formatSkillError( + errorMessage, + t, + "skills.installFailed" + ); + + toast.error(title, { + description, + duration: 10000, // 延长显示时间让用户看清 + }); + + // 打印到控制台方便调试 + console.error("Install skill failed:", { + directory, + error, + message: errorMessage, }); } }; @@ -66,8 +98,25 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) { toast.success(t("skills.uninstallSuccess", { name: directory })); await loadSkills(); } catch (error) { - toast.error(t("skills.uninstallFailed"), { - description: error instanceof Error ? error.message : t("common.error"), + const errorMessage = + error instanceof Error ? error.message : String(error); + + // 使用错误解析器格式化错误,传入 "skills.uninstallFailed" + const { title, description } = formatSkillError( + errorMessage, + t, + "skills.uninstallFailed" + ); + + toast.error(title, { + description, + duration: 10000, + }); + + console.error("Uninstall skill failed:", { + directory, + error, + message: errorMessage, }); } }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f92f297..536692e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -675,6 +675,34 @@ "installFailed": "Failed to install", "uninstallSuccess": "Skill {{name}} uninstalled", "uninstallFailed": "Failed to uninstall", + "error": { + "skillNotFound": "Skill not found: {{directory}}", + "missingRepoInfo": "Missing repository info (owner or name)", + "downloadTimeout": "Download repository {{owner}}/{{name}} timeout ({{timeout}}s)", + "downloadTimeoutHint": "Please check network connection or retry later", + "skillPathNotFound": "Skill path '{{path}}' not found in repository {{owner}}/{{name}}", + "skillDirNotFound": "Skill directory not found: {{path}}", + "emptyArchive": "Downloaded archive is empty", + "downloadFailed": "Download failed: HTTP {{status}}", + "allBranchesFailed": "All branches failed, tried: {{branches}}", + "httpError": "HTTP error {{status}}", + "http403": "GitHub access restricted, possibly rate limited", + "http404": "Repository or branch not found, please check URL", + "http429": "Too many requests, please wait and retry", + "parseMetadataFailed": "Failed to parse skill metadata", + "getHomeDirFailed": "Unable to get user home directory", + "networkError": "Network error", + "fsError": "File system error", + "unknownError": "Unknown error", + "suggestion": { + "checkNetwork": "Please check network connection", + "checkProxy": "Consider configuring HTTP proxy", + "retryLater": "Please retry later", + "checkRepoUrl": "Please check repository URL and branch name", + "checkDiskSpace": "Please check disk space", + "checkPermission": "Please check directory permissions" + } + }, "repo": { "title": "Manage Skill Repositories", "description": "Add or remove GitHub skill repository sources", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 1e135a6..190a58e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -675,6 +675,34 @@ "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": "仓库或分支不存在,请检查地址", + "http429": "请求过于频繁,请等待后重试", + "parseMetadataFailed": "解析技能元数据失败", + "getHomeDirFailed": "无法获取用户主目录", + "networkError": "网络错误", + "fsError": "文件系统错误", + "unknownError": "未知错误", + "suggestion": { + "checkNetwork": "请检查网络连接", + "checkProxy": "建议配置 HTTP 代理", + "retryLater": "请稍后重试", + "checkRepoUrl": "请检查仓库地址和分支名称", + "checkDiskSpace": "请检查磁盘空间", + "checkPermission": "请检查目录权限" + } + }, "repo": { "title": "管理技能仓库", "description": "添加或删除 GitHub 技能仓库源", diff --git a/src/lib/errors/skillErrorParser.ts b/src/lib/errors/skillErrorParser.ts new file mode 100644 index 0000000..929611a --- /dev/null +++ b/src/lib/errors/skillErrorParser.ts @@ -0,0 +1,104 @@ +import { TFunction } from "i18next"; + +/** + * 结构化错误对象 + */ +export interface SkillError { + code: string; + context: Record; + suggestion?: string; +} + +/** + * 尝试解析后端返回的错误字符串 + * 如果是 JSON 格式,返回结构化错误;否则返回 null + */ +export function parseSkillError(errorString: string): SkillError | null { + try { + const parsed = JSON.parse(errorString); + if (parsed.code && parsed.context) { + return parsed as SkillError; + } + } catch { + // 不是 JSON 格式,返回 null + } + return null; +} + +/** + * 将错误码映射到 i18n key + */ +function getErrorI18nKey(code: string): string { + const mapping: Record = { + SKILL_NOT_FOUND: "skills.error.skillNotFound", + MISSING_REPO_INFO: "skills.error.missingRepoInfo", + DOWNLOAD_TIMEOUT: "skills.error.downloadTimeout", + DOWNLOAD_FAILED: "skills.error.downloadFailed", + SKILL_DIR_NOT_FOUND: "skills.error.skillDirNotFound", + EMPTY_ARCHIVE: "skills.error.emptyArchive", + GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed", + }; + + return mapping[code] || "skills.error.unknownError"; +} + +/** + * 将建议码映射到 i18n key + */ +function getSuggestionI18nKey(suggestion: string): string { + const mapping: Record = { + checkNetwork: "skills.error.suggestion.checkNetwork", + checkProxy: "skills.error.suggestion.checkProxy", + retryLater: "skills.error.suggestion.retryLater", + checkRepoUrl: "skills.error.suggestion.checkRepoUrl", + checkPermission: "skills.error.suggestion.checkPermission", + http403: "skills.error.http403", + http404: "skills.error.http404", + http429: "skills.error.http429", + }; + + return mapping[suggestion] || suggestion; +} + +/** + * 格式化技能错误为用户友好的消息 + * @param errorString 后端返回的错误字符串 + * @param t i18next 翻译函数 + * @param defaultTitle 默认标题的 i18n key(如 "skills.installFailed") + * @returns 包含标题和描述的对象 + */ +export function formatSkillError( + errorString: string, + t: TFunction, + defaultTitle: string = "skills.installFailed" +): { title: string; description: string } { + const parsedError = parseSkillError(errorString); + + if (!parsedError) { + // 如果不是结构化错误,返回原始错误字符串 + return { + title: t(defaultTitle), + description: errorString || t("common.error"), + }; + } + + const { code, context, suggestion } = parsedError; + + // 获取错误消息的 i18n key + const errorKey = getErrorI18nKey(code); + + // 构建描述(错误消息 + 建议) + let description = t(errorKey, context); + + // 如果有建议,追加到描述中 + if (suggestion) { + const suggestionKey = getSuggestionI18nKey(suggestion); + const suggestionText = t(suggestionKey); + description += `\n\n${suggestionText}`; + } + + return { + title: t(defaultTitle), + description, + }; +} From ba336fc416b9c7971bbbda1d862d1e38faadc42a Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 21 Nov 2025 23:23:35 +0800 Subject: [PATCH 3/4] feat(settings): add auto-launch on system startup feature Implement auto-launch functionality with proper state synchronization and error handling across Windows, macOS, and Linux platforms. Key changes: - Add auto_launch module using auto-launch crate 0.5 - Define typed errors (AutoLaunchPathError, AutoLaunchEnableError, etc.) - Sync system state with settings.json on app startup - Only call system API when auto-launch state actually changes - Add UI toggle in Window Settings panel - Add i18n support for auto-launch settings (en/zh) Implementation details: - Settings file (settings.json) is the single source of truth - On startup, system state is synced to match settings.json - Error handling uses Rust type system with proper error propagation - Frontend optimized to avoid unnecessary system API calls Platform support: - Windows: HKEY_CURRENT_USER registry modification - macOS: AppleScript-based launch item (configurable to Launch Agent) - Linux: XDG autostart desktop file --- src-tauri/Cargo.lock | 41 ++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/auto_launch.rs | 39 ++++++++++++++++++++ src-tauri/src/commands/settings.rs | 17 +++++++++ src-tauri/src/error.rs | 8 +++++ src-tauri/src/lib.rs | 27 ++++++++++++++ src-tauri/src/settings.rs | 4 +++ src/components/settings/WindowSettings.tsx | 7 ++++ src/hooks/useSettings.ts | 15 ++++++++ src/hooks/useSettingsForm.ts | 3 ++ src/i18n/locales/en.json | 3 ++ src/i18n/locales/zh.json | 3 ++ src/lib/api/settings.ts | 8 +++++ src/types.ts | 2 ++ 14 files changed, 178 insertions(+) create mode 100644 src-tauri/src/auto_launch.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7bcb55c..c8b8f84 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -291,6 +291,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -598,6 +609,7 @@ name = "cc-switch" version = "3.7.0" dependencies = [ "anyhow", + "auto-launch", "chrono", "dirs 5.0.1", "futures", @@ -982,6 +994,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1000,6 +1021,17 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -6397,6 +6429,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2a2dd53..530d258 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,7 @@ tauri-plugin-updater = "2" tauri-plugin-dialog = "2" tauri-plugin-store = "2" tauri-plugin-deep-link = "2" +auto-launch = "0.5" dirs = "5.0" toml = "0.8" toml_edit = "0.22" diff --git a/src-tauri/src/auto_launch.rs b/src-tauri/src/auto_launch.rs new file mode 100644 index 0000000..43bef82 --- /dev/null +++ b/src-tauri/src/auto_launch.rs @@ -0,0 +1,39 @@ +use crate::error::AppError; +use auto_launch::AutoLaunch; + +/// 初始化 AutoLaunch 实例 +fn get_auto_launch() -> Result { + let app_name = "CC Switch"; + let app_path = std::env::current_exe().map_err(AppError::AutoLaunchPathError)?; + + let auto_launch = AutoLaunch::new(app_name, &app_path.to_string_lossy(), false, &[] as &[&str]); + Ok(auto_launch) +} + +/// 启用开机自启 +pub fn enable_auto_launch() -> Result<(), AppError> { + let auto_launch = get_auto_launch()?; + auto_launch + .enable() + .map_err(|e| AppError::AutoLaunchEnableError(e.to_string()))?; + log::info!("Auto-launch enabled"); + Ok(()) +} + +/// 禁用开机自启 +pub fn disable_auto_launch() -> Result<(), AppError> { + let auto_launch = get_auto_launch()?; + auto_launch + .disable() + .map_err(|e| AppError::AutoLaunchDisableError(e.to_string()))?; + log::info!("Auto-launch disabled"); + Ok(()) +} + +/// 检查是否已启用开机自启 +pub fn is_auto_launch_enabled() -> Result { + let auto_launch = get_auto_launch()?; + auto_launch + .is_enabled() + .map_err(|e| AppError::AutoLaunchCheckError(e.to_string())) +} diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index ee76526..eeca097 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -37,3 +37,20 @@ pub async fn set_app_config_dir_override( crate::app_store::set_app_config_dir_to_store(&app, path.as_deref())?; Ok(true) } + +/// 设置开机自启 +#[tauri::command] +pub async fn set_auto_launch(enabled: bool) -> Result { + if enabled { + crate::auto_launch::enable_auto_launch().map_err(|e| e.to_string())?; + } else { + crate::auto_launch::disable_auto_launch().map_err(|e| e.to_string())?; + } + Ok(true) +} + +/// 获取开机自启状态 +#[tauri::command] +pub async fn get_auto_launch_status() -> Result { + crate::auto_launch::is_auto_launch_enabled().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index d9b0262..2912d8e 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -50,6 +50,14 @@ pub enum AppError { zh: String, en: String, }, + #[error("Failed to get application path for auto-launch: {0}")] + AutoLaunchPathError(#[source] std::io::Error), + #[error("Failed to enable auto-launch: {0}")] + AutoLaunchEnableError(String), + #[error("Failed to disable auto-launch: {0}")] + AutoLaunchDisableError(String), + #[error("Failed to check auto-launch status: {0}")] + AutoLaunchCheckError(String), } impl AppError { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0ed21ab..5ac537e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod app_config; mod app_store; +mod auto_launch; mod claude_mcp; mod claude_plugin; mod codex_config; @@ -559,6 +560,30 @@ pub fn run() { // 启动阶段不再无条件保存,避免意外覆盖用户配置。 + // 同步开机自启状态:以 settings.json 为准,保持系统项一致 + { + let settings = crate::settings::get_settings(); + let system_enabled = crate::auto_launch::is_auto_launch_enabled().unwrap_or(false); + + if settings.launch_on_startup != system_enabled { + log::info!( + "开机自启状态不一致:settings={}, system={},以 settings 为准", + settings.launch_on_startup, + system_enabled + ); + + let sync_result = if settings.launch_on_startup { + crate::auto_launch::enable_auto_launch() + } else { + crate::auto_launch::disable_auto_launch() + }; + + if let Err(e) = sync_result { + log::warn!("同步开机自启状态失败: {}", e); + } + } + } + // 注册 deep-link URL 处理器(使用正确的 DeepLinkExt API) log::info!("=== Registering deep-link URL handler ==="); @@ -653,6 +678,8 @@ pub fn run() { commands::get_settings, commands::save_settings, commands::restart_app, + commands::set_auto_launch, + commands::get_auto_launch_status, commands::check_for_updates, commands::is_portable_mode, commands::get_claude_plugin_status, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 75b4c3c..518e1cb 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -49,6 +49,9 @@ pub struct AppSettings { pub gemini_config_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub language: Option, + /// 是否开机自启 + #[serde(default)] + pub launch_on_startup: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub security: Option, /// Claude 自定义端点列表 @@ -77,6 +80,7 @@ impl Default for AppSettings { codex_config_dir: None, gemini_config_dir: None, language: None, + launch_on_startup: false, security: None, custom_endpoints_claude: HashMap::new(), custom_endpoints_codex: HashMap::new(), diff --git a/src/components/settings/WindowSettings.tsx b/src/components/settings/WindowSettings.tsx index 94ec79a..06f0d39 100644 --- a/src/components/settings/WindowSettings.tsx +++ b/src/components/settings/WindowSettings.tsx @@ -19,6 +19,13 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {

+ onChange({ launchOnStartup: value })} + /> + { + return await invoke("set_auto_launch", { enabled }); + }, + + async getAutoLaunchStatus(): Promise { + return await invoke("get_auto_launch_status"); + }, }; diff --git a/src/types.ts b/src/types.ts index b4cb57f..f94a737 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,6 +101,8 @@ export interface Settings { geminiConfigDir?: string; // 首选语言(可选,默认中文) language?: "en" | "zh"; + // 是否开机自启 + launchOnStartup?: boolean; // Claude 自定义端点列表 customEndpointsClaude?: Record; // Codex 自定义端点列表 From eb46ac85925d822311262f6d70ecf0fec6501bf8 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 21 Nov 2025 23:30:56 +0800 Subject: [PATCH 4/4] Revert "feat(settings): add auto-launch on system startup feature" This reverts commit ba336fc416b9c7971bbbda1d862d1e38faadc42a. Reason: Found issues that need to be addressed before reintroducing the auto-launch feature. Will reimplement with fixes. --- src-tauri/Cargo.lock | 41 ---------------------- src-tauri/Cargo.toml | 1 - src-tauri/src/auto_launch.rs | 39 -------------------- src-tauri/src/commands/settings.rs | 17 --------- src-tauri/src/error.rs | 8 ----- src-tauri/src/lib.rs | 27 -------------- src-tauri/src/settings.rs | 4 --- src/components/settings/WindowSettings.tsx | 7 ---- src/hooks/useSettings.ts | 15 -------- src/hooks/useSettingsForm.ts | 3 -- src/i18n/locales/en.json | 3 -- src/i18n/locales/zh.json | 3 -- src/lib/api/settings.ts | 8 ----- src/types.ts | 2 -- 14 files changed, 178 deletions(-) delete mode 100644 src-tauri/src/auto_launch.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c8b8f84..7bcb55c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -291,17 +291,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto-launch" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" -dependencies = [ - "dirs 4.0.0", - "thiserror 1.0.69", - "winreg 0.10.1", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -609,7 +598,6 @@ name = "cc-switch" version = "3.7.0" dependencies = [ "anyhow", - "auto-launch", "chrono", "dirs 5.0.1", "futures", @@ -994,15 +982,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys 0.3.7", -] - [[package]] name = "dirs" version = "5.0.1" @@ -1021,17 +1000,6 @@ dependencies = [ "dirs-sys 0.5.0", ] -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "dirs-sys" version = "0.4.1" @@ -6429,15 +6397,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.52.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 530d258..2a2dd53 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,7 +34,6 @@ tauri-plugin-updater = "2" tauri-plugin-dialog = "2" tauri-plugin-store = "2" tauri-plugin-deep-link = "2" -auto-launch = "0.5" dirs = "5.0" toml = "0.8" toml_edit = "0.22" diff --git a/src-tauri/src/auto_launch.rs b/src-tauri/src/auto_launch.rs deleted file mode 100644 index 43bef82..0000000 --- a/src-tauri/src/auto_launch.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::error::AppError; -use auto_launch::AutoLaunch; - -/// 初始化 AutoLaunch 实例 -fn get_auto_launch() -> Result { - let app_name = "CC Switch"; - let app_path = std::env::current_exe().map_err(AppError::AutoLaunchPathError)?; - - let auto_launch = AutoLaunch::new(app_name, &app_path.to_string_lossy(), false, &[] as &[&str]); - Ok(auto_launch) -} - -/// 启用开机自启 -pub fn enable_auto_launch() -> Result<(), AppError> { - let auto_launch = get_auto_launch()?; - auto_launch - .enable() - .map_err(|e| AppError::AutoLaunchEnableError(e.to_string()))?; - log::info!("Auto-launch enabled"); - Ok(()) -} - -/// 禁用开机自启 -pub fn disable_auto_launch() -> Result<(), AppError> { - let auto_launch = get_auto_launch()?; - auto_launch - .disable() - .map_err(|e| AppError::AutoLaunchDisableError(e.to_string()))?; - log::info!("Auto-launch disabled"); - Ok(()) -} - -/// 检查是否已启用开机自启 -pub fn is_auto_launch_enabled() -> Result { - let auto_launch = get_auto_launch()?; - auto_launch - .is_enabled() - .map_err(|e| AppError::AutoLaunchCheckError(e.to_string())) -} diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index eeca097..ee76526 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -37,20 +37,3 @@ pub async fn set_app_config_dir_override( crate::app_store::set_app_config_dir_to_store(&app, path.as_deref())?; Ok(true) } - -/// 设置开机自启 -#[tauri::command] -pub async fn set_auto_launch(enabled: bool) -> Result { - if enabled { - crate::auto_launch::enable_auto_launch().map_err(|e| e.to_string())?; - } else { - crate::auto_launch::disable_auto_launch().map_err(|e| e.to_string())?; - } - Ok(true) -} - -/// 获取开机自启状态 -#[tauri::command] -pub async fn get_auto_launch_status() -> Result { - crate::auto_launch::is_auto_launch_enabled().map_err(|e| e.to_string()) -} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 2912d8e..d9b0262 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -50,14 +50,6 @@ pub enum AppError { zh: String, en: String, }, - #[error("Failed to get application path for auto-launch: {0}")] - AutoLaunchPathError(#[source] std::io::Error), - #[error("Failed to enable auto-launch: {0}")] - AutoLaunchEnableError(String), - #[error("Failed to disable auto-launch: {0}")] - AutoLaunchDisableError(String), - #[error("Failed to check auto-launch status: {0}")] - AutoLaunchCheckError(String), } impl AppError { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5ac537e..0ed21ab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,5 @@ mod app_config; mod app_store; -mod auto_launch; mod claude_mcp; mod claude_plugin; mod codex_config; @@ -560,30 +559,6 @@ pub fn run() { // 启动阶段不再无条件保存,避免意外覆盖用户配置。 - // 同步开机自启状态:以 settings.json 为准,保持系统项一致 - { - let settings = crate::settings::get_settings(); - let system_enabled = crate::auto_launch::is_auto_launch_enabled().unwrap_or(false); - - if settings.launch_on_startup != system_enabled { - log::info!( - "开机自启状态不一致:settings={}, system={},以 settings 为准", - settings.launch_on_startup, - system_enabled - ); - - let sync_result = if settings.launch_on_startup { - crate::auto_launch::enable_auto_launch() - } else { - crate::auto_launch::disable_auto_launch() - }; - - if let Err(e) = sync_result { - log::warn!("同步开机自启状态失败: {}", e); - } - } - } - // 注册 deep-link URL 处理器(使用正确的 DeepLinkExt API) log::info!("=== Registering deep-link URL handler ==="); @@ -678,8 +653,6 @@ pub fn run() { commands::get_settings, commands::save_settings, commands::restart_app, - commands::set_auto_launch, - commands::get_auto_launch_status, commands::check_for_updates, commands::is_portable_mode, commands::get_claude_plugin_status, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 518e1cb..75b4c3c 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -49,9 +49,6 @@ pub struct AppSettings { pub gemini_config_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub language: Option, - /// 是否开机自启 - #[serde(default)] - pub launch_on_startup: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub security: Option, /// Claude 自定义端点列表 @@ -80,7 +77,6 @@ impl Default for AppSettings { codex_config_dir: None, gemini_config_dir: None, language: None, - launch_on_startup: false, security: None, custom_endpoints_claude: HashMap::new(), custom_endpoints_codex: HashMap::new(), diff --git a/src/components/settings/WindowSettings.tsx b/src/components/settings/WindowSettings.tsx index 06f0d39..94ec79a 100644 --- a/src/components/settings/WindowSettings.tsx +++ b/src/components/settings/WindowSettings.tsx @@ -19,13 +19,6 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {

- onChange({ launchOnStartup: value })} - /> - { - return await invoke("set_auto_launch", { enabled }); - }, - - async getAutoLaunchStatus(): Promise { - return await invoke("get_auto_launch_status"); - }, }; diff --git a/src/types.ts b/src/types.ts index f94a737..b4cb57f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,8 +101,6 @@ export interface Settings { geminiConfigDir?: string; // 首选语言(可选,默认中文) language?: "en" | "zh"; - // 是否开机自启 - launchOnStartup?: boolean; // Claude 自定义端点列表 customEndpointsClaude?: Record; // Codex 自定义端点列表