BigPizzaV3/CodexPlusPlus · error · anyhow::Error

skill id 不能为空

Error message

skill id 不能为空

What it means

validate_skill_id is a path-safety guard for ids used directly in filesystem paths (SSOT dir, backups). It bails on an empty id before the traversal checks; callers like uninstall/restore_backup pass user-supplied ids, so empty input is caught here.

Solutions

  1. 调用前检查 skill_id 非空(且非纯空白)再执行操作
  2. 排查 id 来源:从 zip 的 SKILL.md 或仓库清单解析时,manifest 的 name 字段可能为空,需先补齐
  3. UI 层在 skill 未选中时禁用 uninstall/delete 等按钮

Example fix

// before
state.uninstall(&skill_id)?; // skill_id 可能为 ""
// after
if skill_id.trim().is_empty() {
    anyhow::bail!("请先选择要操作的 skill");
}
state.uninstall(&skill_id)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_skill_id_present(id: &str) -> Result<(), String> {
    if id.trim().is_empty() { Err("skill id 不能为空".into()) } else { Ok(()) }
}

Try / catch

match state.uninstall(&skill_id) {
    Err(e) if e.to_string().contains("skill id 不能为空") => eprintln!("未选择 skill,忽略操作"),
    other => other?,
}

Prevention

When it happens

Trigger: 任一接受 skill_id 的公开方法传入空字符串 "",例如 uninstall("")、set_enabled("", true)、restore_backup("")、delete_backup("")。

Common situations: 前端列表为空时仍触发了操作;skill 清单里 name 字段缺失或为空串;上游数据未回填 id 就调用接口。

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/cb91bf95f269cb37. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/skills.rs:622

        owner,
        name,
        branch,
        subdir,
        enabled: repo.enabled,
    })
}

fn is_safe_repo_segment(value: &str) -> bool {
    !value.is_empty()
        && value
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}

/// skill id 直接参与拼路径,必须挡住 `..` 和分隔符。
fn validate_skill_id(id: &str) -> anyhow::Result<()> {
    if id.is_empty() {
        anyhow::bail!("skill id 不能为空");
    }
    if id == "." || id == ".." || id.contains('/') || id.contains('\\') {
        anyhow::bail!("非法的 skill id:{id}");
    }
    Ok(())
}

/// GitHub trees API 的响应 → 该仓库里的 skill 清单。
///
/// 一个目录只要直接含 `SKILL.md` 就算一个 skill;`content_hash` 用子树里
/// 每个文件的 blob sha 算,远端内容一变哈希就变,不用下载就能判断有没有更新。
pub fn parse_skills_from_tree(repo: &SkillRepo, tree: &Value) -> Vec<RemoteSkill> {
    let Some(items) = tree.get("tree").and_then(Value::as_array) else {
        return Vec::new();
    };
    let prefix = if repo.subdir.is_empty() {
        String::new()
    } else {

View on GitHub (pinned to b1ed92e5e4)