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 自定义端点列表