{"record":{"id":"cb91bf95f269cb37","repo":"BigPizzaV3/CodexPlusPlus","slug":"skill-id-skills","errorCode":null,"errorMessage":"skill id 不能为空","messagePattern":"skill id 不能为空","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":622,"sourceCode":"        owner,\n        name,\n        branch,\n        subdir,\n        enabled: repo.enabled,\n    })\n}\n\nfn is_safe_repo_segment(value: &str) -> bool {\n    !value.is_empty()\n        && value\n            .chars()\n            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))\n}\n\n/// skill id 直接参与拼路径，必须挡住 `..` 和分隔符。\nfn validate_skill_id(id: &str) -> anyhow::Result<()> {\n    if id.is_empty() {\n        anyhow::bail!(\"skill id 不能为空\");\n    }\n    if id == \".\" || id == \"..\" || id.contains('/') || id.contains('\\\\') {\n        anyhow::bail!(\"非法的 skill id：{id}\");\n    }\n    Ok(())\n}\n\n/// GitHub trees API 的响应 → 该仓库里的 skill 清单。\n///\n/// 一个目录只要直接含 `SKILL.md` 就算一个 skill；`content_hash` 用子树里\n/// 每个文件的 blob sha 算，远端内容一变哈希就变，不用下载就能判断有没有更新。\npub fn parse_skills_from_tree(repo: &SkillRepo, tree: &Value) -> Vec<RemoteSkill> {\n    let Some(items) = tree.get(\"tree\").and_then(Value::as_array) else {\n        return Vec::new();\n    };\n    let prefix = if repo.subdir.is_empty() {\n        String::new()\n    } else {","sourceCodeStart":604,"sourceCodeEnd":640,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L604-L640","documentation":"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.","triggerScenarios":"任一接受 skill_id 的公开方法传入空字符串 \"\"，例如 uninstall(\"\")、set_enabled(\"\", true)、restore_backup(\"\")、delete_backup(\"\")。","commonSituations":"前端列表为空时仍触发了操作；skill 清单里 name 字段缺失或为空串；上游数据未回填 id 就调用接口。","solutions":["调用前检查 skill_id 非空（且非纯空白）再执行操作","排查 id 来源：从 zip 的 SKILL.md 或仓库清单解析时，manifest 的 name 字段可能为空，需先补齐","UI 层在 skill 未选中时禁用 uninstall/delete 等按钮"],"exampleFix":"// before\nstate.uninstall(&skill_id)?; // skill_id 可能为 \"\"\n// after\nif skill_id.trim().is_empty() {\n    anyhow::bail!(\"请先选择要操作的 skill\");\n}\nstate.uninstall(&skill_id)?;","handlingStrategy":"validation","validationCode":"fn ensure_skill_id_present(id: &str) -> Result<(), String> {\n    if id.trim().is_empty() { Err(\"skill id 不能为空\".into()) } else { Ok(()) }\n}","typeGuard":null,"tryCatchPattern":"match state.uninstall(&skill_id) {\n    Err(e) if e.to_string().contains(\"skill id 不能为空\") => eprintln!(\"未选择 skill，忽略操作\"),\n    other => other?,\n}","preventionTips":["UI 在未选中 skill 时禁用相关操作按钮","解析 SKILL.md/仓库清单时校验 name 非空并回填 id","对所有 skill_id 入参统一走一个入口校验包装"],"tags":["validation","empty-field","skills"],"backgroundTag":"empty-required-field","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"}