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

  1. 先调用 uninstall(skill_id) 移除当前安装的同名 skill,再调用 restore_backup
  2. 若只是想保留当前版本,可先 create_backup 当前目录生成新备份,再 uninstall + restore_backup
  3. 确认是否真的需要恢复:若当前版本就是备份内容,跳过恢复操作

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

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


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)