{"record":{"id":"6c42a81acc1c4ec3","repo":"BigPizzaV3/CodexPlusPlus","slug":"skills","errorCode":null,"errorMessage":"备份不存在：{}","messagePattern":"备份不存在：(.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":510,"sourceCode":"            let manifest = path.join(SKILL_MANIFEST_FILE);\n            let (name, _) = read_skill_manifest(&manifest, &skill_id);\n            backups.push(SkillBackup {\n                id: id.to_string(),\n                skill_id,\n                name,\n                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(","sourceCodeStart":492,"sourceCodeEnd":528,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L492-L528","documentation":"restore_backup takes a backup_id and immediately runs validate_skill_id on it; this bail fires when the id is empty. Since the id is used to build the backups_dir path, an empty value would resolve to the backups directory itself, so it is rejected before any filesystem check.","triggerScenarios":"调用 SkillsState::restore_backup(backup_id) 时传入的 backup_id 在 backups_dir 下没有对应的目录（backup.is_dir() 为 false），例如传入了从未创建过的备份 id、备份已被恢复（目录被移走）或已被 delete_backup 删除。","commonSituations":"重复恢复同一个备份（第一次成功后目录已被移回 source_dir）；手工清理或迁移 backups 目录后 id 失效；拼写错误的 backup_id；换机器或换 profile 后备份目录不存在。","solutions":["先调用列出备份的接口（或直接检查 backups_dir/<backup_id> 是否存在）确认 backup_id 有效，再调用 restore_backup","如果备份已成功恢复过一次，目录已不在 backups 下，无需也无法再次恢复；目标 skill 已存在时会改报“已经装着了”","检查是否传错字符串：backup_id 格式为 <skill_id>__<timestamp> 之类，由 split_backup_id 约定，需原样传入","确认运行环境指向的根目录（backups_dir 所在）与创建备份时一致"],"exampleFix":"// before\nstate.restore_backup(\"my-skill__20260101\"); // 目录可能已被恢复掉\n// after\nlet backup_path = state.backups_dir().join(\"my-skill__20260101\");\nif backup_path.is_dir() {\n    state.restore_backup(\"my-skill__20260101\")?;\n}","handlingStrategy":"validation","validationCode":"fn backup_exists(backups_dir: &Path, backup_id: &str) -> bool {\n    backups_dir.join(backup_id).is_dir()\n}\nif !backup_exists(&state.backups_dir(), backup_id) { return; }","typeGuard":null,"tryCatchPattern":"match state.restore_backup(backup_id) {\n    Err(e) if e.to_string().contains(\"备份不存在\") => eprintln!(\"备份 {} 不存在，跳过\", backup_id),\n    other => other?,\n}","preventionTips":["恢复前先列出备份并确认 id 存在","记住备份是一次性的：恢复成功后即从 backups 目录消失","避免手工增删 backups 目录内容"],"tags":["backup","file-not-found","skills"],"backgroundTag":"resource-not-found","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"}