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

  1. Refresh the skill list — if the skill is gone it was intentionally uninstalled; nothing to update
  2. Reinstall from the marketplace if you still want it (update cannot resurrect a deleted row by design)
  3. 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

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


AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16). Data as JSON: /api/errors/b1adaaebf73d43f7. Report an issue: GitHub.