BigPizzaV3/CodexPlusPlus · error · anyhow::Error
已经装着了,先卸载再从备份恢复
Error message
{skill_id} 已经装着了,先卸载再从备份恢复 What it means
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.
Solutions
- 先调用 uninstall(skill_id) 移除当前安装的同名 skill,再调用 restore_backup
- 若只是想保留当前版本,可先 create_backup 当前目录生成新备份,再 uninstall + restore_backup
- 确认是否真的需要恢复:若当前版本就是备份内容,跳过恢复操作
Example fix
// before
state.restore_backup(&backup_id)?;
// after
let skill_id = "my-skill";
if state.is_installed(skill_id) {
state.uninstall(skill_id)?;
}
state.restore_backup(&backup_id)?; Defensive patterns
Strategy: validation
Validate before calling
fn can_restore(state: &SkillsState, backup_id: &str) -> anyhow::Result<bool> {
let (skill_id, _) = /* split_backup_id 等价逻辑 */ (backup_id.split("__").next().unwrap_or(""), ());
Ok(!state.source_dir().join(skill_id).exists())
} Try / catch
match state.restore_backup(backup_id) {
Err(e) if e.to_string().contains("先卸载再从备份恢复") => {
state.uninstall(&skill_id)?;
state.restore_backup(backup_id)?;
}
other => other?,
} Prevention
- 恢复前检查目标 skill 是否已安装
- 需要保留当前版本时先 create_backup 再 uninstall + restore
- UI 层对已安装 skill 的备份恢复按钮做禁用或二次确认提示
When it happens
Trigger: 调用 restore_backup(backup_id) 时,split_backup_id(backup_id) 得到的 skill_id 对应的目录已存在于 source_dir 下(即该 skill 当前已安装/已启用)。
Common situations: skill 处于安装状态时尝试从备份恢复旧版本;恢复前忘记 uninstall;备份与当前安装的是同一个 skill id;重复点击恢复按钮。
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- restore conflict: file already exists
- 备份不存在:
- 当前 Codex 配置使用外部 model_catalog_json,无法同时应用每模型窗口、自动压缩或元数据
- Dream Skin theme backup belongs to a different config.toml
- failed to allocate a stale Dream Skin backup path
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/710f91841d1d8140.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/skills.rs:516
backed_up_at,
});
}
backups.sort_by(|a, b| b.id.cmp(&a.id));
backups
}
/// 从备份恢复:目录移回 SSOT 并重新建链。
pub fn restore_backup(&self, backup_id: &str) -> anyhow::Result<SkillsState> {
validate_skill_id(backup_id)?;
let backup = self.backups_dir.join(backup_id);
if !backup.is_dir() {
anyhow::bail!("备份不存在:{}", backup.display());
}
let (skill_id, _) = split_backup_id(backup_id);
validate_skill_id(&skill_id)?;
let destination = self.source_dir.join(&skill_id);
if destination.exists() {
anyhow::bail!("{skill_id} 已经装着了,先卸载再从备份恢复");
}
std::fs::create_dir_all(&self.source_dir)
.with_context(|| format!("创建目录失败:{}", self.source_dir.display()))?;
std::fs::rename(&backup, &destination)
.with_context(|| format!("恢复到 {} 失败", destination.display()))?;
let (name, description) =
read_skill_manifest(&destination.join(SKILL_MANIFEST_FILE), &skill_id);
{
let _guard = self.state_lock.lock().unwrap();
let mut state = self.load_state_unlocked();
state.installed.insert(
skill_id.clone(),
InstalledSkill {
id: skill_id.clone(),
name,
description,
repo_key: String::new(),View on GitHub (pinned to b1ed92e5e4)