farion1231/cc-switch · error · anyhow::Error
Skill no longer installed: {}
Error message
Skill no longer installed: {} What it means
SkillService::update_skill performs a network download with the state lock released, so metadata is persisted through update_skill_metadata — a DAO that only UPDATEs an existing row and returns false when zero rows matched. A false return means the DB row was deleted during the download window (user uninstalled, or a cloud-sync import replaced the table), so the stale snapshot must not be re-inserted.
Source
Thrown at src-tauri/src/services/skill.rs:1352
});
}
}
}
Ok(updates)
}
/// 持久化更新后的 Skill 元数据,并重新读取数据库中的权威应用启用状态。
///
/// 更新过程包含网络下载,期间用户可能切换启用状态或卸载 Skill。这里必须
/// 使用只更新现有记录的 DAO,避免旧快照覆盖 `enabled_*`,也避免已卸载记录
/// 被重新插入。
fn persist_updated_skill_metadata(
db: &Arc<Database>,
updated_skill: &InstalledSkill,
) -> Result<InstalledSkill> {
if !db.update_skill_metadata(updated_skill)? {
return Err(anyhow!("Skill no longer installed: {}", updated_skill.id));
}
db.get_installed_skill(&updated_skill.id)?
.ok_or_else(|| anyhow!("Skill no longer installed: {}", updated_skill.id))
}
/// 更新单个 Skill(重新下载并替换本地文件)
pub async fn update_skill(&self, db: &Arc<Database>, skill_id: &str) -> Result<InstalledSkill> {
let mut skill = db
.get_installed_skill(skill_id)?
.ok_or_else(|| anyhow!("Skill not found: {skill_id}"))?;
skill.apps.pi = Self::skill_exists_in_app(&skill.directory, &AppType::Pi);
// 本函数后续三种危险操作都用 directory 拼路径:备份源(把任意目录复制进
// 备份区并在界面列出)、remove_dir_all(删任意目录)、copy_dir_recursive
// (把远端仓库内容写到任意路径)。校验必须在这三者之前。
Self::require_valid_directory(&skill.directory)?;
View on GitHub (pinned to a2e22f3302)
Solutions
- Refresh the skill list — if the skill is gone it was intentionally uninstalled; nothing to update
- Reinstall from the marketplace if you still want it (update cannot resurrect a deleted row by design)
- Serialize destructive skill operations in the UI (single flight queue) to avoid update/uninstall races
Example fix
// before
const updated = await invoke('update_skill', { skillId });
// after
try {
const updated = await invoke('update_skill', { skillId });
} catch (e) {
if (String(e).includes('no longer installed')) {
await refreshSkillList(); // row removed mid-update; drop stale state
return;
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
null // the row exists when the update starts; it disappears mid-await, so no pre-check can fully prevent it
Try / catch
try { await invoke('update_skill', { skillId }); }
catch (e) {
if (String(e).includes('no longer installed')) { await refreshSkillList(); return null; }
throw e;
} Prevention
- Disable uninstall/update for the same row while an update command is in flight
- Treat 'no longer installed' during update as benign — refresh and move on
When it happens
Trigger: Running update_skill while the same skill is uninstalled from another window during the up-to-60s download; or a remote cloud-snapshot raw-SQL import deletes/recreates rows while an update is in flight.
Common situations: Update dialog left open while the user uninstalls from the main list; multi-window usage; background sync running concurrently with an update.
Related errors
- Skill directory changed during install; please retry
- Skill not found: {id}
- Unsupported URL scheme
- Invalid URL
- Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {}
AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16).
Data as JSON: /api/errors/b1adaaebf73d43f7.
Report an issue: GitHub.