BigPizzaV3/CodexPlusPlus · error · anyhow::Error
skill 在仓库中的路径不能为空
Error message
skill 在仓库中的路径不能为空
What it means
extract_skill_subtree trims leading/trailing slashes from repo_path and bails when the result is empty. The parameter selects which subtree of the repo zip to extract; an empty value would mean extracting the whole repo (or nothing meaningful), so it is rejected before opening the archive.
Solutions
- 确保传入 repo_path 为仓库内 skill 子目录路径,如 "skills/my-skill"
- 若 skill 位于仓库根目录,不应走 subtree 提取路径,改用根目录安装流程
- 提交前对 subdir 做非空校验(trim 后)。
Example fix
// before
extract_skill_subtree(&zip_bytes, &repo.subdir, &dest)?; // subdir 可能为 ""
// after
let rp = repo.subdir.trim().trim_matches('/');
if rp.is_empty() {
anyhow::bail!("subdir 不能为空");
}
extract_skill_subtree(&zip_bytes, rp, &dest)?; Defensive patterns
Strategy: validation
Validate before calling
fn valid_repo_path(subdir: &str) -> bool {
let t = subdir.trim().trim_matches('/');
!t.is_empty()
} Try / catch
match extract_skill_subtree(&zip, repo_path, &dest) {
Err(e) if e.to_string().contains("路径不能为空") => eprintln!("subdir 未配置,无法按子目录安装"),
other => other?,
} Prevention
- 配置 SkillRepo 时确保 subdir 非空
- skill 位于仓库根时改用根安装流程而非 subtree 提取
- 从 GitHub API 拿 path 时校验字段存在且非空
When it happens
Trigger: 调用 extract_skill_subtree(zip_bytes, repo_path, destination) 时 repo_path 为空串、纯空格或仅由 '/' 组成(trim_matches('/') 后为空)。
Common situations: SkillRepo 的 subdir 字段为空却调用了按子目录安装的流程;从 GitHub trees API 响应取 path 时字段缺失得到空串;把根仓库(skill 在仓库根)的包直接塞进子树提取逻辑。
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/65035b2a0ac970a2.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/skills.rs:776
match std::fs::read_to_string(manifest) {
Ok(text) => parse_skill_frontmatter(&text, fallback_id),
Err(_) => (fallback_id.to_string(), String::new()),
}
}
/// 从仓库 zip 里只解出 `repo_path` 这一棵子树。
///
/// GitHub 的 zip 统一带一层 `repo-ref/` 前缀,`zip_entry_relative_path` 会剥掉它
/// 并同时挡住路径逃逸。符号链接条目一律拒绝——与 codex 内置 skill-installer 的
/// `_validate_skill` 一致,避免装进来的 skill 指到仓库外面。
pub fn extract_skill_subtree(
zip_bytes: &[u8],
repo_path: &str,
destination: &Path,
) -> anyhow::Result<()> {
let repo_path = repo_path.trim_matches('/');
if repo_path.is_empty() {
anyhow::bail!("skill 在仓库中的路径不能为空");
}
let prefix = format!("{repo_path}/");
let mut archive =
zip::ZipArchive::new(Cursor::new(zip_bytes)).context("skill 仓库压缩包无法解析")?;
let mut wrote_manifest = false;
for index in 0..archive.len() {
let mut file = archive
.by_index(index)
.with_context(|| format!("读取压缩包条目 {index} 失败"))?;
if file.is_symlink() {
anyhow::bail!("skill 中不允许包含符号链接:{}", file.name());
}
let Some(relative) = crate::plugin_marketplace::zip_entry_relative_path(file.name()) else {
continue;
};
// zip 内部的分隔符恒为 '/',但上面拿回来的是 PathBuf,在 Windows 上
// to_str() 会渲染成 '\',跟用 '/' 拼出来的 prefix 永远匹配不上——结果就是View on GitHub (pinned to b1ed92e5e4)