{"record":{"id":"710f91841d1d8140","repo":"BigPizzaV3/CodexPlusPlus","slug":"skill-id","errorCode":null,"errorMessage":"{skill_id} 已经装着了，先卸载再从备份恢复","messagePattern":"(.+?) 已经装着了，先卸载再从备份恢复","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":516,"sourceCode":"                backed_up_at,\n            });\n        }\n        backups.sort_by(|a, b| b.id.cmp(&a.id));\n        backups\n    }\n\n    /// 从备份恢复：目录移回 SSOT 并重新建链。\n    pub fn restore_backup(&self, backup_id: &str) -> anyhow::Result<SkillsState> {\n        validate_skill_id(backup_id)?;\n        let backup = self.backups_dir.join(backup_id);\n        if !backup.is_dir() {\n            anyhow::bail!(\"备份不存在：{}\", backup.display());\n        }\n        let (skill_id, _) = split_backup_id(backup_id);\n        validate_skill_id(&skill_id)?;\n        let destination = self.source_dir.join(&skill_id);\n        if destination.exists() {\n            anyhow::bail!(\"{skill_id} 已经装着了，先卸载再从备份恢复\");\n        }\n        std::fs::create_dir_all(&self.source_dir)\n            .with_context(|| format!(\"创建目录失败：{}\", self.source_dir.display()))?;\n        std::fs::rename(&backup, &destination)\n            .with_context(|| format!(\"恢复到 {} 失败\", destination.display()))?;\n\n        let (name, description) =\n            read_skill_manifest(&destination.join(SKILL_MANIFEST_FILE), &skill_id);\n        {\n            let _guard = self.state_lock.lock().unwrap();\n            let mut state = self.load_state_unlocked();\n            state.installed.insert(\n                skill_id.clone(),\n                InstalledSkill {\n                    id: skill_id.clone(),\n                    name,\n                    description,\n                    repo_key: String::new(),","sourceCodeStart":498,"sourceCodeEnd":534,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L498-L534","documentation":"restore_backup splits the backup id into skill id and timestamp, verifies the skill id, then checks whether the skill is currently installed (destination exists in source_dir). Restoring over a live installation would clobber it, so the operation refuses until the skill is uninstalled.","triggerScenarios":"调用 restore_backup(backup_id) 时，split_backup_id(backup_id) 得到的 skill_id 对应的目录已存在于 source_dir 下（即该 skill 当前已安装/已启用）。","commonSituations":"skill 处于安装状态时尝试从备份恢复旧版本；恢复前忘记 uninstall；备份与当前安装的是同一个 skill id；重复点击恢复按钮。","solutions":["先调用 uninstall(skill_id) 移除当前安装的同名 skill，再调用 restore_backup","若只是想保留当前版本，可先 create_backup 当前目录生成新备份，再 uninstall + restore_backup","确认是否真的需要恢复：若当前版本就是备份内容，跳过恢复操作"],"exampleFix":"// before\nstate.restore_backup(&backup_id)?;\n// after\nlet skill_id = \"my-skill\";\nif state.is_installed(skill_id) {\n    state.uninstall(skill_id)?;\n}\nstate.restore_backup(&backup_id)?;","handlingStrategy":"validation","validationCode":"fn can_restore(state: &SkillsState, backup_id: &str) -> anyhow::Result<bool> {\n    let (skill_id, _) = /* split_backup_id 等价逻辑 */ (backup_id.split(\"__\").next().unwrap_or(\"\"), ());\n    Ok(!state.source_dir().join(skill_id).exists())\n}","typeGuard":null,"tryCatchPattern":"match state.restore_backup(backup_id) {\n    Err(e) if e.to_string().contains(\"先卸载再从备份恢复\") => {\n        state.uninstall(&skill_id)?;\n        state.restore_backup(backup_id)?;\n    }\n    other => other?,\n}","preventionTips":["恢复前检查目标 skill 是否已安装","需要保留当前版本时先 create_backup 再 uninstall + restore","UI 层对已安装 skill 的备份恢复按钮做禁用或二次确认提示"],"tags":["backup","conflict","already-exists","skills"],"backgroundTag":"file-already-exists","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}