From b02bf6fefb0a287fd280999d34978f900eb3e24b Mon Sep 17 00:00:00 2001 From: zhang Date: Wed, 18 Feb 2026 10:12:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(install):=20=E8=87=AA=E5=8A=A8=E4=B8=BA=20u?= =?UTF-8?q?ser-invocable=20skill=20=E7=94=9F=E6=88=90=E6=96=9C=E6=9D=A0?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: commit 0eb9f45 重组 skills 目录结构后,SKILL.md 被移入 tools/ 和 domains/ 子目录,超出 Claude Code 的一级发现范围 (~/.claude/skills/*/SKILL.md),导致所有斜杠命令不可用。 修复: - 安装时自动扫描全部 SKILL.md,解析 YAML frontmatter - 为 user-invocable: true 的 skill 在 ~/.claude/commands/ 下 生成对应的斜杠命令包装文件 - 有 scripts/ 的 skill (tools/) 生成 run_skill.js 调用 - 无 scripts/ 的 skill (domains/) 生成知识引用 - 采用文件级合并安装,不影响用户已有的自定义命令 - 卸载时精准删除生成的命令文件并恢复备份 其他修复: - config/CLAUDE.md 修复 10 处 skills 路径引用错误 - skills/SKILL.md sage 总纲 user-invocable 改为 false(纯路由索引) - bin/lib/utils.js 新增 parseFrontmatter() 工具函数 --- bin/install.js | 130 ++++++++++++++++++++++++++++++++++++++++++++-- bin/lib/utils.js | 18 ++++++- config/CLAUDE.md | 20 +++---- package-lock.json | 4 +- skills/SKILL.md | 2 +- 5 files changed, 157 insertions(+), 17 deletions(-) diff --git a/bin/install.js b/bin/install.js index 72ef80f..69d3a6b 100755 --- a/bin/install.js +++ b/bin/install.js @@ -15,7 +15,7 @@ if (parseInt(process.versions.node) < parseInt(MIN_NODE)) { process.exit(1); } const PKG_ROOT = fs.realpathSync(path.join(__dirname, '..')); -const { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog } = +const { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter } = require(path.join(__dirname, 'lib', 'utils.js')); const { detectCclineBin, installCcline: _installCcline } = require(path.join(__dirname, 'lib', 'ccline.js')); @@ -193,6 +193,122 @@ function runUninstall(tgt) { // ── 安装核心 ── +/** + * 递归扫描 skills 目录,找出所有 user-invocable: true 的 SKILL.md + * @param {string} skillsDir - skills 源目录绝对路径 + * @returns {Array<{meta: Object, relPath: string, hasScripts: boolean}>} + */ +function scanInvocableSkills(skillsDir) { + const results = []; + function scan(dir) { + const skillMd = path.join(dir, 'SKILL.md'); + if (fs.existsSync(skillMd)) { + try { + const content = fs.readFileSync(skillMd, 'utf8'); + const meta = parseFrontmatter(content); + if (meta && meta['user-invocable'] === 'true' && meta.name) { + const relPath = path.relative(skillsDir, dir); + const scriptsDir = path.join(dir, 'scripts'); + const hasScripts = fs.existsSync(scriptsDir) && + fs.readdirSync(scriptsDir).some(f => f.endsWith('.js')); + results.push({ meta, relPath, hasScripts }); + } + } catch (e) { /* 解析失败跳过 */ } + } + try { + fs.readdirSync(dir).forEach(sub => { + const subPath = path.join(dir, sub); + if (fs.statSync(subPath).isDirectory() && !shouldSkip(sub) && sub !== 'scripts') { + scan(subPath); + } + }); + } catch (e) { /* 读取失败跳过 */ } + } + scan(skillsDir); + return results; +} + +/** + * 根据 SKILL.md 元数据生成 command .md 内容 + * @param {Object} meta - parseFrontmatter 返回的元数据 + * @param {string} skillRelPath - 相对于 skills/ 的路径(如 'tools/gen-docs') + * @param {boolean} hasScripts - 是否有可执行脚本 + * @returns {string} command .md 文件内容 + */ +function generateCommandContent(meta, skillRelPath, hasScripts) { + const name = meta.name; + const desc = (meta.description || '').replace(/"/g, '\\"'); + const argHint = meta['argument-hint']; + const tools = meta['allowed-tools'] || 'Read'; + const skillPath = skillRelPath + ? `~/.claude/skills/${skillRelPath}/SKILL.md` + : '~/.claude/skills/SKILL.md'; + + const lines = [ + '---', + `name: ${name}`, + `description: "${desc}"`, + ]; + if (argHint) lines.push(`argument-hint: "${argHint}"`); + lines.push(`allowed-tools: ${tools}`); + lines.push('---'); + lines.push(''); + lines.push('## 执行指南'); + lines.push(''); + lines.push('先读取完整的 Skill 定义获取详细规范:'); + lines.push(''); + lines.push('```'); + lines.push(skillPath); + lines.push('```'); + + if (hasScripts) { + lines.push(''); + lines.push('然后执行:'); + lines.push(''); + lines.push('```bash'); + lines.push(`node ~/.claude/skills/run_skill.js ${name} $ARGUMENTS`); + lines.push('```'); + } else { + lines.push(''); + lines.push('根据秘典内容为用户提供专业指导。'); + } + + lines.push(''); + return lines.join('\n'); +} + +/** + * 扫描 skills 并为 user-invocable 的 skill 生成 command 包装,文件级合并安装 + */ +function installGeneratedCommands(skillsSrcDir, targetDir, backupDir, manifest) { + const skills = scanInvocableSkills(skillsSrcDir); + if (skills.length === 0) return 0; + + const cmdsDir = path.join(targetDir, 'commands'); + fs.mkdirSync(cmdsDir, { recursive: true }); + + skills.forEach(({ meta, relPath, hasScripts }) => { + const fileName = `${meta.name}.md`; + const destFile = path.join(cmdsDir, fileName); + const relFile = path.posix.join('commands', fileName); + + if (fs.existsSync(destFile)) { + const cmdsBackupDir = path.join(backupDir, 'commands'); + fs.mkdirSync(cmdsBackupDir, { recursive: true }); + fs.copyFileSync(destFile, path.join(cmdsBackupDir, fileName)); + manifest.backups.push(relFile); + info(`备份: ${c.d(relFile)}`); + } + + const content = generateCommandContent(meta, relPath, hasScripts); + fs.writeFileSync(destFile, content); + manifest.installed.push(relFile); + }); + + ok(`commands/ ${c.d(`(自动生成 ${skills.length} 个斜杠命令)`)}`); + return skills.length; +} + function installCore(tgt) { const targetDir = path.join(HOME, `.${tgt}`); const backupDir = path.join(targetDir, '.sage-backup'); @@ -205,7 +321,7 @@ function installCore(tgt) { { src: 'config/CLAUDE.md', dest: tgt === 'claude' ? 'CLAUDE.md' : null }, { src: 'config/AGENTS.md', dest: tgt === 'codex' ? 'AGENTS.md' : null }, { src: 'output-styles', dest: tgt === 'claude' ? 'output-styles' : null }, - { src: 'skills', dest: 'skills' } + { src: 'skills', dest: 'skills' }, ].filter(f => f.dest !== null); const manifest = { @@ -223,6 +339,7 @@ function installCore(tgt) { } warn(`跳过: ${src}`); return; } + if (fs.existsSync(destPath)) { const bp = path.join(backupDir, dest); rmSafe(bp); copyRecursive(destPath, bp); manifest.backups.push(dest); @@ -232,6 +349,12 @@ function installCore(tgt) { rmSafe(destPath); copyRecursive(srcPath, destPath); manifest.installed.push(dest); }); + // 为 Claude 目标自动生成 user-invocable 斜杠命令 + if (tgt === 'claude') { + const skillsSrc = path.join(PKG_ROOT, 'skills'); + installGeneratedCommands(skillsSrc, targetDir, backupDir, manifest); + } + const settingsPath = path.join(targetDir, 'settings.json'); let settings = {}; if (fs.existsSync(settingsPath)) { @@ -426,5 +549,6 @@ if (require.main === module) { module.exports = { deepMergeNew, detectClaudeAuth, detectCodexAuth, - detectCclineBin, copyRecursive, shouldSkip, SETTINGS_TEMPLATE + detectCclineBin, copyRecursive, shouldSkip, SETTINGS_TEMPLATE, + scanInvocableSkills, generateCommandContent, installGeneratedCommands }; diff --git a/bin/lib/utils.js b/bin/lib/utils.js index a3c130b..e95977f 100644 --- a/bin/lib/utils.js +++ b/bin/lib/utils.js @@ -58,4 +58,20 @@ function printMergeLog(log, c) { }); } -module.exports = { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, SKIP }; +/** + * 解析 Markdown 文件的 YAML frontmatter + * @param {string} content - 文件内容 + * @returns {Object|null} 解析后的键值对,无 frontmatter 返回 null + */ +function parseFrontmatter(content) { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!match) return null; + const meta = {}; + match[1].split('\n').forEach(line => { + const m = line.match(/^([\w][\w-]*)\s*:\s*(.+)/); + if (m) meta[m[1]] = m[2].trim().replace(/^["']|["']$/g, ''); + }); + return meta; +} + +module.exports = { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter, SKIP }; diff --git a/config/CLAUDE.md b/config/CLAUDE.md index 3a357df..ed13013 100644 --- a/config/CLAUDE.md +++ b/config/CLAUDE.md @@ -54,7 +54,7 @@ | ❄ 玄冰 | 镇魔之盾,护佑安宁 | 蓝队、告警、IOC、应急、取证、SIEM、EDR | | ⚡ 紫霄 | 攻守一体,方为大道 | 紫队、ATT&CK、TTP、检测验证、规则调优 | -详细攻防技术见 `skills/security/` 各秘典。 +详细攻防技术见 `skills/domains/security/` 各秘典。 --- @@ -156,15 +156,15 @@ | 化身 | 秘典 | 触发场景 | |------|------|----------| -| 🔥 赤焰 | `skills/security/red-team.md` | 渗透、红队、exploit、C2 | -| ❄ 玄冰 | `skills/security/blue-team.md` | 蓝队、告警、IOC、应急 | -| ⚡ 紫霄 | `skills/security/` | ATT&CK、TTP、攻防演练 | -| 📜 符箓 | `skills/development/` | 语言开发任务 | -| 👁 天眼 | `skills/security/threat-intel.md` | OSINT、威胁情报 | -| 🔮 丹鼎 | `skills/ai/` | RAG、Agent、LLM | -| 🕸 天罗 | `skills/multi-agent/` | TeamCreate、多Agent协同 | -| 🏗 阵法 | `skills/architecture/` | 架构、API、云原生、缓存、合规 | -| 🔧 炼器 | `skills/devops/` | Git、测试、数据库、性能、可观测性 | +| 🔥 赤焰 | `skills/domains/security/red-team.md` | 渗透、红队、exploit、C2 | +| ❄ 玄冰 | `skills/domains/security/blue-team.md` | 蓝队、告警、IOC、应急 | +| ⚡ 紫霄 | `skills/domains/security/` | ATT&CK、TTP、攻防演练 | +| 📜 符箓 | `skills/domains/development/` | 语言开发任务 | +| 👁 天眼 | `skills/domains/security/threat-intel.md` | OSINT、威胁情报 | +| 🔮 丹鼎 | `skills/domains/ai/` | RAG、Agent、LLM | +| 🕸 天罗 | `skills/orchestration/multi-agent/SKILL.md` | TeamCreate、多Agent协同 | +| 🏗 阵法 | `skills/domains/architecture/` | 架构、API、云原生、缓存、合规 | +| 🔧 炼器 | `skills/domains/devops/` | Git、测试、数据库、性能、可观测性 | **校验关卡**(自动触发,不可跳过): diff --git a/package-lock.json b/package-lock.json index f6fe6c3..161de14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-abyss", - "version": "1.7.0", + "version": "1.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-abyss", - "version": "1.7.0", + "version": "1.7.2", "license": "MIT", "dependencies": { "@inquirer/prompts": "^7.10.1" diff --git a/skills/SKILL.md b/skills/SKILL.md index e2ea6a1..7b7f7a9 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -2,7 +2,7 @@ name: sage description: 邪修红尘仙·神通秘典总纲。智能路由到专业秘典。当魔尊需要任何开发、安全、架构、DevOps、AI 相关能力时,通过此入口路由到最匹配的专业秘典。 license: MIT -user-invocable: true +user-invocable: false disable-model-invocation: false ---