{"record":{"id":"34cfd09c165fc965","repo":"BigPizzaV3/CodexPlusPlus","slug":"repo-path-skill-md-skill","errorCode":null,"errorMessage":"{repo_path} 下没有 SKILL.md，不是一个有效的 skill","messagePattern":"(.+?) 下没有 SKILL\\.md，不是一个有效的 skill","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/skills.rs","lineNumber":828,"sourceCode":"                .with_context(|| format!(\"创建目录失败：{}\", output_path.display()))?;\n            continue;\n        }\n        if let Some(parent) = output_path.parent() {\n            std::fs::create_dir_all(parent)\n                .with_context(|| format!(\"创建目录失败：{}\", parent.display()))?;\n        }\n        let mut contents = Vec::new();\n        file.read_to_end(&mut contents)\n            .with_context(|| format!(\"读取压缩包条目 {} 失败\", file.name()))?;\n        std::fs::write(&output_path, contents)\n            .with_context(|| format!(\"写入 {} 失败\", output_path.display()))?;\n        if inner == SKILL_MANIFEST_FILE {\n            wrote_manifest = true;\n        }\n    }\n\n    if !wrote_manifest {\n        anyhow::bail!(\"{repo_path} 下没有 SKILL.md，不是一个有效的 skill\");\n    }\n    Ok(())\n}\n\nfn safe_relative_path(value: &str) -> anyhow::Result<PathBuf> {\n    let mut relative = PathBuf::new();\n    for component in Path::new(value).components() {\n        match component {\n            std::path::Component::Normal(part) => relative.push(part),\n            std::path::Component::CurDir => {}\n            _ => anyhow::bail!(\"压缩包条目越界：{value}\"),\n        }\n    }\n    if relative.as_os_str().is_empty() {\n        anyhow::bail!(\"压缩包条目路径为空\");\n    }\n    Ok(relative)\n}","sourceCodeStart":810,"sourceCodeEnd":846,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/skills.rs#L810-L846","documentation":"After extracting all entries, extract_skill_subtree verifies at least one written file was SKILL.md (wrote_manifest flag); otherwise it bails. A subtree without a manifest is not a valid skill and installing it would produce a broken link in the SSOT directory.","triggerScenarios":"zip 包中 repo_path 子树内不存在名为 SKILL.md 的文件——通常是 repo_path 填错了子目录，或该目录确实不是 skill 目录。","commonSituations":"SkillRepo.subdir 指向仓库中非 skill 目录（如文档目录）；skill 结构不符合规范（清单用了别的文件名）；仓库版本变更后目录结构变化导致旧 subdir 失效；zip 下载不完整。","solutions":["核对 subdir 是否正确指向包含 SKILL.md 的目录，在 GitHub 上确认该路径下确实有 SKILL.md","若仓库结构更新，更新 SkillRepo 的 subdir 后重试","确认仓库遵循 skill 规范（SKILL.md 位于 skill 根）；不规范仓库无法安装"],"exampleFix":"// before\n// subdir = \"skills/old-name\" 但仓库已改名为 \"skills/new-name\"\nextract_skill_subtree(&zip, \"skills/old-name\", &dest)?;\n// after\nextract_skill_subtree(&zip, \"skills/new-name\", &dest)?; // 含 SKILL.md 的目录","handlingStrategy":"try-catch","validationCode":"// 安装前可先核对仓库结构：\n// GET https://api.github.com/repos/{owner}/{name}/git/trees/{branch}?recursive=1\n// 确认 subdir 下存在 SKILL.md","typeGuard":null,"tryCatchPattern":"match extract_skill_subtree(&zip, repo_path, &dest) {\n    Err(e) if e.to_string().contains(\"SKILL.md\") => eprintln!(\"{} 不是有效的 skill 目录\", repo_path),\n    other => other?,\n}","preventionTips":["配置 subdir 前在 GitHub 上确认该目录含 SKILL.md","仓库结构变更后同步更新 SkillRepo.subdir","安装失败时检查 zip 是否完整下载（对比大小或 git tree）"],"tags":["zip","validation","missing-manifest","skills"],"backgroundTag":"missing-required-config-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"}