farion1231/cc-switch · error · anyhow::Error
Skill not found: {id}
Error message
Skill not found: {id} What it means
Uninstall looks up the skill row by id in SQLite via db.get_installed_skill before touching the filesystem; when the query returns None there is nothing to uninstall and the error is raised. It is a plain not-found guard at the top of SkillService::uninstall, before any directory is validated or deleted.
Source
Thrown at src-tauri/src/services/skill.rs:962
current_app
);
Ok(installed_skill)
}
/// 卸载 Skill
///
/// 流程:
/// 1. 从所有应用目录删除
/// 2. 从 SSOT 删除
/// 3. 从数据库删除
pub fn uninstall(db: &Arc<Database>, id: &str) -> Result<SkillUninstallResult> {
let _state_guard = skill_state_write_guard();
// 获取 skill 信息
let skill = db
.get_installed_skill(id)?
.ok_or_else(|| anyhow!("Skill not found: {id}"))?;
// DB 行可能被同步导入污染(远端快照 raw SQL 直接灌库,绕过安装期校验),
// 也可能是 v3.11.0 引入 sanitize_install_name 之前留下的存量脏值
// (当年扫描不过滤点开头目录,`.github/SKILL.md` 会存成 `.github`)。
//
// 守卫失败时**跳过全部文件系统操作、但仍删除 DB 行**:`db.delete_skill`
// 全项目只有这一处调用且未暴露为命令,若在此直接返回 Err,用户就再也无法
// 从界面删掉这条记录,只能手改 SQLite。安全目标是「不碰危险路径」,
// 不是「把用户锁在坏状态里」。
let (backup_path, preserved_pi_path, pi_cleanup_incomplete) =
match Self::require_valid_directory(&skill.directory) {
Ok(directory) => {
let ssot_dir = Self::get_ssot_dir()?;
let source = ssot_dir.join(&directory);
let mut preserved_pi_path: Option<PathBuf> = None;
let mut pi_cleanup_incomplete = false;
let mut pi_removal_path = None;
View on GitHub (pinned to a2e22f3302)
Solutions
- Refresh the installed-skills list and re-render before retrying
- Confirm you are passing the installed skill's id (InstalledSkill.id), not the marketplace/repo identifier
- If the row keeps disappearing, check cloud-sync import logs — a remote snapshot may be deleting rows under you
Example fix
// before
await invoke('uninstall_skill', { id });
// after
const skills = await invoke('list_installed_skills');
if (!skills.some(s => s.id === id)) {
await refreshSkillList(); // stale view — nothing to uninstall
} else {
await invoke('uninstall_skill', { id });
} Defensive patterns
Strategy: validation
Validate before calling
const skills = await invoke('list_installed_skills');
if (!skills.some(s => s.id === id)) {
await refreshSkillList();
return; // nothing to uninstall
} Type guard
const isInstalledSkill = (s: unknown, id: string): s is InstalledSkill => typeof s === 'object' && s !== null && (s as any).id === id;
Try / catch
try { await invoke('uninstall_skill', { id }); }
catch (e) {
if (String(e).includes('Skill not found')) { await refreshSkillList(); return; }
throw e;
} Prevention
- Always render uninstall actions from a freshly fetched installed list
- Pass InstalledSkill.id, never marketplace identifiers
When it happens
Trigger: Calling the uninstall command with an id that is not in the installed_skills table: the skill was already uninstalled from another window/device, the UI list is stale after a cloud-sync import replaced rows, or the id string is malformed/truncated.
Common situations: Stale list rendered before a background sync import wiped and re-inserted rows; double-click on uninstall where the first call already removed the row; passing a marketplace skill id instead of the installed skill id.
Related errors
- Skill no longer installed: {}
- Skill directory changed during install; please retry
- Unsupported URL scheme
- Invalid URL
- Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {}
AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16).
Data as JSON: /api/errors/10183ff0ed4e07fd.
Report an issue: GitHub.