{"record":{"id":"b1adaaebf73d43f7","repo":"farion1231/cc-switch","slug":"skill-no-longer-installed","errorCode":null,"errorMessage":"Skill no longer installed: {}","messagePattern":"Skill no longer installed: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":1352,"sourceCode":"                    });\n                }\n            }\n        }\n\n        Ok(updates)\n    }\n\n    /// 持久化更新后的 Skill 元数据，并重新读取数据库中的权威应用启用状态。\n    ///\n    /// 更新过程包含网络下载，期间用户可能切换启用状态或卸载 Skill。这里必须\n    /// 使用只更新现有记录的 DAO，避免旧快照覆盖 `enabled_*`，也避免已卸载记录\n    /// 被重新插入。\n    fn persist_updated_skill_metadata(\n        db: &Arc<Database>,\n        updated_skill: &InstalledSkill,\n    ) -> Result<InstalledSkill> {\n        if !db.update_skill_metadata(updated_skill)? {\n            return Err(anyhow!(\"Skill no longer installed: {}\", updated_skill.id));\n        }\n\n        db.get_installed_skill(&updated_skill.id)?\n            .ok_or_else(|| anyhow!(\"Skill no longer installed: {}\", updated_skill.id))\n    }\n\n    /// 更新单个 Skill（重新下载并替换本地文件）\n    pub async fn update_skill(&self, db: &Arc<Database>, skill_id: &str) -> Result<InstalledSkill> {\n        let mut skill = db\n            .get_installed_skill(skill_id)?\n            .ok_or_else(|| anyhow!(\"Skill not found: {skill_id}\"))?;\n        skill.apps.pi = Self::skill_exists_in_app(&skill.directory, &AppType::Pi);\n\n        // 本函数后续三种危险操作都用 directory 拼路径：备份源（把任意目录复制进\n        // 备份区并在界面列出）、remove_dir_all（删任意目录）、copy_dir_recursive\n        // （把远端仓库内容写到任意路径）。校验必须在这三者之前。\n        Self::require_valid_directory(&skill.directory)?;\n","sourceCodeStart":1334,"sourceCodeEnd":1370,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L1334-L1370","documentation":"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.","triggerScenarios":"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.","commonSituations":"Update dialog left open while the user uninstalls from the main list; multi-window usage; background sync running concurrently with an update.","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"],"exampleFix":"// before\nconst updated = await invoke('update_skill', { skillId });\n\n// after\ntry {\n  const updated = await invoke('update_skill', { skillId });\n} catch (e) {\n  if (String(e).includes('no longer installed')) {\n    await refreshSkillList(); // row removed mid-update; drop stale state\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"null // the row exists when the update starts; it disappears mid-await, so no pre-check can fully prevent it","typeGuard":null,"tryCatchPattern":"try { await invoke('update_skill', { skillId }); }\ncatch (e) {\n  if (String(e).includes('no longer installed')) { await refreshSkillList(); return null; }\n  throw e;\n}","preventionTips":["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"],"tags":["rust","tauri","race-condition","sqlite","update"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}