farion1231/cc-switch · error · anyhow::Error

Skill not found: {skill_id}

Error message

Skill not found: {skill_id}

What it means

Entry lookup of SkillService::update_skill: the installed_skills table has no row with the given skill_id, so the update cannot even start. Unlike errors 42/43/49 this is not a mid-flight race — the id was already stale when the call was made.

Source

Thrown at src-tauri/src/services/skill.rs:1363

    /// 使用只更新现有记录的 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)?;

        let (owner, name, branch) = match (&skill.repo_owner, &skill.repo_name) {
            (Some(o), Some(n)) => (
                o.clone(),
                n.clone(),
                skill
                    .repo_branch
                    .clone()
                    .unwrap_or_else(|| "main".to_string()),
            ),
            _ => return Err(anyhow!("Cannot update local skill: {skill_id}")),
        };

View on GitHub (pinned to 0b5da51016)

Solutions

  1. Re-fetch the installed skills list and only offer Update for ids that still exist
  2. Skip (not fail) missing ids in batch update loops
  3. Refresh after any uninstall before enabling update actions

Example fix

// before
for (const s of cachedSkills) await invoke('update_skill', { id: s.id });

// after: re-read the source of truth, skip gone entries
const current = await invoke<Record<string, { id: string }>>('get_all_installed_skills');
for (const id of Object.keys(current)) {
  await invoke('update_skill', { id });
}
Defensive patterns

Strategy: validation

Validate before calling

const skills = await invoke<Record<string, unknown>>('get_all_installed_skills');
if (!(id in skills)) {
  await refreshSkillList();
  return;
}
await invoke('update_skill', { id });

Try / catch

try {
  await invoke('update_skill', { id });
} catch (e) {
  if (String(e).startsWith('Skill not found')) {
    await refreshSkillList();
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Clicking Update on a skill listed from a stale cache after it was uninstalled elsewhere; passing a wrong or outdated id to the update_skill command; 'update all' iterating a list captured before some rows were removed.

Common situations: UI list not refreshed after uninstall; duplicate windows on one database; batch update loops built from a snapshot.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of farion1231/cc-switch@0b5da51016 (2026-08-20). Data as JSON: /api/errors/773cb653cda5b07b. Report an issue: GitHub.