@clawhub-zqleslie-2c4d73418e
自动检测当前任务类型,动态加载对应的 Skill。当收到新任务时,分析任务意图, 匹配最佳 Skill 并自动加载。支持 Skill 分级保护(core/protected/dynamic), 即插即用零配置,兼容任何 OpenClaw 部署。 触发词:"自动加载skill"、"动态加载"、"智能匹配skill"...
---
name: auto-skill-loader
version: "2.0.1"
level: protected
description: >
自动检测当前任务类型,动态加载对应的 Skill。当收到新任务时,分析任务意图,
匹配最佳 Skill 并自动加载。支持 Skill 分级保护(core/protected/dynamic),
即插即用零配置,兼容任何 OpenClaw 部署。
触发词:"自动加载skill"、"动态加载"、"智能匹配skill",或任何需要判断使用哪个 skill 的场景。
---
# Auto Skill Loader v2.0
> 即插即用 · 核心锁定 · 零配置
## 设计原则
1. **零配置启动** - 安装即用,不需要修改任何参数
2. **核心保护** - core 级 Skill 绝不被自动加载器干扰
3. **通用兼容** - 不假设任何 Agent 名称、路径或环境
4. **最小加载** - 只加载必要的 Skill,节省 token
---
## Skill 保护等级
每个 Skill 可在 SKILL.md frontmatter 中声明 `level`:
```yaml
---
name: my-skill
level: core # core | protected | dynamic(默认 dynamic)
---
```
| 级别 | 标识 | 自动加载器行为 | 适用场景 |
|------|------|---------------|---------|
| 🔒 **core** | `core` | **完全跳过,不触碰** | 内存管理、行为审计、安全监控 |
| 🛡️ **protected** | `protected` | **不自动卸载,加载需显式确认** | 心跳机制、风控、proactive-agent |
| 🔄 **dynamic** | `dynamic` | **自由加载/卸载** | 天气、股票分析、文案生成等 |
**未声明 level 的 Skill 默认为 `dynamic`。**
### 内置 core Skill 白名单
即使 Skill 未声明 level,以下类型的 Skill **始终视为 core**:
- 名称包含 `memory` 的 Skill(如 memory-distiller)
- 名称包含 `audit` 或 `security` 的 Skill
- 名称包含 `proactive` 的 Skill
此白名单可通过配置文件覆盖。
---
## 执行流程
### Step 1: 扫描可用 Skill
按 OpenClaw 标准目录结构扫描,**不硬编码任何路径**:
```
# 优先级从高到低:
L1: {workspace}/.agents/skills/*/SKILL.md # Agent 专属
L2: ~/.agents/skills/*/SKILL.md # 全局共享
L3: {openclaw_install}/skills/*/SKILL.md # OpenClaw 内置
L4: {openclaw_install}/extensions/*/skills/*/SKILL.md # 扩展 Skill
```
**路径解析规则**:
- `{workspace}` = 当前工作目录(自动检测)
- `~` = 用户主目录(跨平台兼容)
- `{openclaw_install}` = OpenClaw 安装目录(通过 `npm root -g` 或环境变量检测)
### Step 2: 解析 Skill 元数据
读取每个 SKILL.md 的 frontmatter:
- `name` - Skill 名称
- `description` - 功能描述(用于意图匹配)
- `level` - 保护等级(默认 dynamic)
- `version` - 版本号(可选)
**只解析 frontmatter,不读取全文(节省 token)。**
### Step 3: 过滤受保护 Skill
```
可加载列表 = 所有 Skill - core Skill - protected Skill(除非显式请求)
```
### Step 4: 意图匹配
根据用户消息,从可加载列表中匹配:
**匹配策略**(优先级从高到低):
| 优先级 | 策略 | 说明 |
|--------|------|------|
| P1 | **触发词匹配** | 消息包含 Skill description 中的触发词 |
| P2 | **语义匹配** | 消息意图与 description 语义相近 |
| P3 | **领域匹配** | 消息领域与 Skill 领域一致 |
**冲突解决**:
- 同名 Skill → 高层级目录优先(L1 > L2 > L3 > L4)
- 多个匹配 → 选最具体的(description 最相关的)
- 无匹配 → 不加载任何 Skill,正常回复
### Step 5: 加载执行
找到匹配 Skill 后:
1. 读取完整 `SKILL.md`
2. 按其指导执行任务
3. 如需要,加载 `scripts/` 或 `references/` 下的资源
### Step 6: Agent 路由(可选)
如果任务不属于当前 Agent 的职责范围,尝试动态路由给其他 Agent:
**路由流程**:
1. 调用 `agents_list` 获取当前可用 Agent 列表
2. 读取各 Agent 的 `SOUL.md` 或 `IDENTITY.md` 判断职责匹配度
3. 找到最匹配的 Agent,通过 `sessions_send` 转发任务
**路由失败处理**:
以下情况视为"无法发现其他 Agent":
- `agents_list` 返回空列表(单 Agent 部署)
- `agents_list` 报错(服务异常)
- 返回的 Agent 列表中只有当前 Agent 自己
- 其他 Agent 的 SOUL.md/IDENTITY.md 无法读取或职责不匹配
**此时应直接回复用户,告知路由失败**:
```
这个任务更适合 [目标 Agent 类型,如:A股量化专家/P哥] 处理。
但当前环境无法自动路由:[具体原因]
- 原因 A:当前是单 Agent 部署,未发现其他 Agent
- 原因 B:其他 Agent 未配置或不在线
- 原因 C:无法读取其他 Agent 的职责描述
**你可以:**
1. 直接 @[目标 Agent 名] 联系他
2. 让我在当前 Agent 尝试处理(可能不够专业)
3. 检查其他 Agent 是否已启动并配置正确
```
**为什么不静默 fallback?**
- 避免用户误以为任务已转发,实际没有
- 诚实透明,不给虚假希望
- 让用户掌握控制权,决定下一步
---
## 配置文件(可选)
文件:`auto-skill-loader.config.yaml`
位置:Skill 目录内 或 workspace 根目录
```yaml
# auto-skill-loader.config.yaml
# 所有字段可选,均有合理默认值
# 额外标记为 core 的 Skill(名称列表)
coreSkills: []
# - my-custom-audit-skill
# - my-critical-skill
# 排除不参与自动加载的 Skill(名称列表)
skipSkills: []
# - deprecated-skill
# 是否启用 Agent 路由(默认 true)
enableRouting: true
# 匹配模式:strict(仅触发词)| normal(触发词+语义)| fuzzy(全部)
matchMode: normal
# 日志级别:silent | info | debug
logLevel: info
```
**不创建此文件 = 使用全部默认值 = 直接能用。**
---
## 使用示例
### 场景 1:用户问天气
```
用户:北京今天天气怎么样?
→ 匹配 weather skill(dynamic)
→ 自动加载 weather/SKILL.md
→ 按其指导获取天气
```
### 场景 2:触及 core Skill
```
用户:关闭记忆管理
→ 检测到 memory-distiller 是 core 级
→ 拒绝操作,提示:此 Skill 为核心级,不可自动卸载
```
### 场景 3:无匹配
```
用户:帮我写一首诗
→ 无 Skill 匹配
→ 正常回复(不加载任何 Skill)
```
---
## Dry Run 模式
预览匹配结果,不实际加载:
在消息中包含 `--dry-run` 或在配置中设置:
```yaml
dryRun: true # 全局 dry run
```
输出格式:
```
🔍 Auto Skill Loader - Dry Run
任务: "北京今天天气怎么样"
匹配: weather (dynamic, L3, score: 0.95)
候选: [weather, summarize]
操作: 将加载 weather/SKILL.md
```
---
## 兼容性
| 环境 | 支持 |
|------|------|
| Windows | ✅ |
| macOS | ✅ |
| Linux | ✅ |
| 单 Agent | ✅ |
| 多 Agent | ✅ |
| ClawHub 安装 | ✅ |
| 手动安装 | ✅ |
---
## 变更日志
### v2.0 (2026-03-19)
- 🔒 新增 Skill 三级保护机制(core/protected/dynamic)
- 🔌 移除所有硬编码(Agent 名称、路由表、路径)
- ⚙️ 新增可选配置文件(零配置也能用)
- 🏷️ 新增 dry-run 预览模式
- 🌍 通用化:兼容任何 OpenClaw 部署
### v1.0 (2026-03-18)
- 初始版本,基本自动加载功能
FILE:auto-skill-loader.config.yaml
# auto-skill-loader.config.yaml
# Auto Skill Loader v2.0 默认配置
# 所有字段可选,均有合理默认值
# 不创建此文件 = 使用全部默认值 = 直接能用
# 额外标记为 core 的 Skill(名称列表)
# 除内置白名单外,你可以在这里添加自定义 core Skill
coreSkills: []
# - my-custom-audit-skill
# 排除不参与自动加载的 Skill(名称列表)
skipSkills: []
# - deprecated-skill
# 是否启用 Agent 路由(默认 true)
# 设为 false 则不尝试发现和转发给其他 Agent
enableRouting: true
# 匹配模式
# strict - 仅触发词精确匹配
# normal - 触发词 + 语义匹配(默认)
# fuzzy - 全部策略(包括模糊领域匹配)
matchMode: normal
# 日志级别
# silent - 不输出
# info - 基本匹配日志(默认)
# debug - 详细匹配过程
logLevel: info
# Dry Run 模式(默认 false)
# 设为 true 则只预览匹配结果,不实际加载
dryRun: false
FILE:package.json
{
"name": "auto-skill-loader",
"version": "2.0.1",
"description": "自动检测任务类型,动态加载对应 Skill。支持 Skill 分级保护(core/protected/dynamic),即插即用零配置。",
"main": "auto-skill-loader.js",
"keywords": [
"openclaw",
"skill",
"auto-load",
"dynamic",
"agent",
"routing"
],
"author": "E姐",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/auto-skill-loader"
},
"engines": {
"openclaw": ">=2026.3.0"
},
"files": [
"SKILL.md",
"auto-skill-loader.js",
"yaml-lite.js",
"auto-skill-loader.config.yaml",
"scripts/"
],
"clawhub": {
"entry": "SKILL.md",
"installPath": "~/.agents/skills/auto-skill-loader"
}
}
FILE:README.md
# Auto Skill Loader v2.0
> 即插即用 · 核心锁定 · 零配置
自动检测当前任务类型,动态加载对应的 Skill。当收到新任务时,分析任务意图,匹配最佳 Skill 并自动加载。
## 特性
- **零配置启动** - 安装即用,不需要修改任何参数
- **核心保护** - core 级 Skill 绝不被自动加载器干扰
- **通用兼容** - 不假设任何 Agent 名称、路径或环境
- **最小加载** - 只加载必要的 Skill,节省 token
- **Agent 路由** - 支持任务转发给其他 Agent
## Skill 保护等级
每个 Skill 可在 SKILL.md frontmatter 中声明 `level`:
```yaml
---
name: my-skill
level: core # core | protected | dynamic(默认 dynamic)
---
```
| 级别 | 自动加载器行为 |
|------|---------------|
| 🔒 **core** | 完全跳过,不触碰 |
| 🛡️ **protected** | 不自动卸载,加载需显式确认 |
| 🔄 **dynamic** | 自由加载/卸载 |
## 安装
```bash
npx clawhub@latest install auto-skill-loader
```
## 使用
触发词:
- "自动加载 skill"
- "动态加载"
- "智能匹配 skill"
或直接描述任务,自动匹配最佳 Skill。
## 配置
可选配置文件 `auto-skill-loader.config.yaml`:
```yaml
# 额外标记为 core 的 Skill
coreSkills: []
# 排除不参与自动加载的 Skill
skipSkills: []
# 匹配模式: strict | normal | fuzzy
matchMode: normal
# 日志级别: silent | info | debug
logLevel: info
```
不创建配置文件 = 使用全部默认值 = 直接能用。
## 设计原则
1. **即插即用** - `clawhub install` 后直接可用
2. **零硬编码** - 不依赖特定 Agent 名、路径、端口
3. **动态发现** - 自动扫描 skills 目录
4. **合理默认** - 所有配置均有默认值
## 版本历史
- v2.0.0 - 重写核心,支持 Agent 路由,优化匹配算法
- v1.0.0 - 初始版本
## 作者
E姐 - OpenClaw 核心贡献者
FILE:scripts/auto-skill-loader.js
#!/usr/bin/env node
/**
* Auto Skill Loader v2.0 - Core Module
*
* 功能:
* 1. 扫描 OpenClaw 标准目录(L1-L4)发现所有 Skill
* 2. 解析 SKILL.md frontmatter(name, level, description)
* 3. 按 level 过滤:core 不碰,protected 需确认,dynamic 自由加载
* 4. 输出可加载 Skill 列表供 Agent 做意图匹配
*
* 设计原则:
* - 零配置启动(所有参数有合理默认值)
* - 不硬编码任何 Agent 名称/路径
* - 跨平台兼容(Windows/macOS/Linux)
*
* 用法:
* node auto-skill-loader.js # 列出所有可加载 Skill
* node auto-skill-loader.js --all # 列出所有 Skill(含 core/protected)
* node auto-skill-loader.js --check <name> # 检查指定 Skill 的保护级别
* node auto-skill-loader.js --dry-run "<msg>" # 模拟匹配(输出候选,不加载)
* node auto-skill-loader.js --json # JSON 格式输出
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const yaml = require('./yaml-lite'); // 轻量 YAML 解析,零依赖
// ============================================================
// 常量
// ============================================================
const CORE_NAME_PATTERNS = [
/memory/i,
/audit/i,
/security/i,
/proactive/i,
];
const DEFAULT_CONFIG = {
coreSkills: [],
skipSkills: [],
enableRouting: true,
matchMode: 'normal', // strict | normal | fuzzy
logLevel: 'info', // silent | info | debug
dryRun: false,
};
// ============================================================
// Frontmatter 解析
// ============================================================
/**
* 从 SKILL.md 内容中提取 YAML frontmatter
* @param {string} content - SKILL.md 文件内容
* @returns {object|null} 解析后的 frontmatter 对象
*/
function parseFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return null;
try {
return yaml.parse(match[1]);
} catch (e) {
return null;
}
}
/**
* 从 SKILL.md 提取 description 正文(frontmatter 之后的第一段非空文本)
* 用于补充 frontmatter 中 description 缺失的情况
*/
function extractDescriptionFromBody(content) {
const body = content.replace(/^---[\s\S]*?---\r?\n?/, '').trim();
// 取第一个非标题、非空行的段落
const lines = body.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('```') && !trimmed.startsWith('|')) {
return trimmed.slice(0, 200); // 限制长度
}
}
return '';
}
// ============================================================
// 目录扫描
// ============================================================
/**
* 获取 OpenClaw 安装目录
* 尝试多种方式检测,保证跨平台兼容
*/
function getOpenClawInstallDir() {
// 方式 1:环境变量
if (process.env.OPENCLAW_HOME) {
return process.env.OPENCLAW_HOME;
}
// 方式 2:常见 npm 全局安装路径
const candidates = [
path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', 'openclaw'), // Windows
path.join('/usr', 'local', 'lib', 'node_modules', 'openclaw'), // macOS/Linux
path.join('/usr', 'lib', 'node_modules', 'openclaw'), // Linux alt
path.join(os.homedir(), '.nvm', 'versions', 'node'), // nvm (需进一步查找)
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
/**
* 获取当前 workspace 目录
* 优先级:环境变量 > cwd > 默认 ~/.openclaw/workspace
*/
function getWorkspaceDir() {
if (process.env.OPENCLAW_WORKSPACE) {
return process.env.OPENCLAW_WORKSPACE;
}
// 检查 cwd 是否在 .openclaw 下
const cwd = process.cwd();
if (cwd.includes('.openclaw')) {
return cwd;
}
// 默认 workspace
const defaultWs = path.join(os.homedir(), '.openclaw', 'workspace');
if (fs.existsSync(defaultWs)) {
return defaultWs;
}
return cwd;
}
/**
* 按 L1-L4 优先级扫描所有 Skill 目录
* @param {string} [workspace] - workspace 路径(可选)
* @returns {Array<{skillDir: string, level: string, priority: number}>}
*/
function scanSkillDirs(workspace) {
workspace = workspace || getWorkspaceDir();
const openclawDir = getOpenClawInstallDir();
const scanPaths = [
// L1: Agent 专属(最高优先级)
{ dir: path.join(workspace, '.agents', 'skills'), priority: 1, label: 'L1-workspace' },
// L2: 全局共享
{ dir: path.join(os.homedir(), '.agents', 'skills'), priority: 2, label: 'L2-global' },
];
// L3: OpenClaw 内置
if (openclawDir) {
scanPaths.push({
dir: path.join(openclawDir, 'skills'),
priority: 3,
label: 'L3-builtin',
});
// L4: 扩展 Skill
scanPaths.push({
dir: path.join(openclawDir, 'extensions'),
priority: 4,
label: 'L4-extensions',
});
}
const results = [];
for (const { dir, priority, label } of scanPaths) {
if (!fs.existsSync(dir)) continue;
if (priority === 4) {
// L4 扩展:需要进入每个扩展目录的 skills/ 子目录
const extensions = safeReaddir(dir);
for (const ext of extensions) {
const extSkillsDir = path.join(dir, ext, 'skills');
if (!fs.existsSync(extSkillsDir)) continue;
const skills = safeReaddir(extSkillsDir);
for (const skill of skills) {
const skillMd = path.join(extSkillsDir, skill, 'SKILL.md');
if (fs.existsSync(skillMd)) {
results.push({ skillDir: path.join(extSkillsDir, skill), skillMd, priority, label: `label/ext` });
}
}
}
} else {
const skills = safeReaddir(dir);
for (const skill of skills) {
const skillMd = path.join(dir, skill, 'SKILL.md');
if (fs.existsSync(skillMd)) {
results.push({ skillDir: path.join(dir, skill), skillMd, priority, label });
}
}
}
}
return results;
}
function safeReaddir(dir) {
try {
return fs.readdirSync(dir).filter(f => {
try {
return fs.statSync(path.join(dir, f)).isDirectory();
} catch { return false; }
});
} catch { return []; }
}
// ============================================================
// Skill 解析 + Level 判定
// ============================================================
/**
* 判断 Skill 的保护级别
* @param {string} name - Skill 名称
* @param {string|undefined} declaredLevel - frontmatter 声明的 level
* @param {object} config - 用户配置
* @returns {'core'|'protected'|'dynamic'}
*/
function resolveLevel(name, declaredLevel, config) {
// 1. 用户配置的 coreSkills 优先
if (config.coreSkills && config.coreSkills.includes(name)) {
return 'core';
}
// 2. frontmatter 显式声明
if (declaredLevel && ['core', 'protected', 'dynamic'].includes(declaredLevel)) {
return declaredLevel;
}
// 3. 内置名称模式匹配(安全兜底)
for (const pattern of CORE_NAME_PATTERNS) {
if (pattern.test(name)) {
return 'core';
}
}
// 4. 默认 dynamic
return 'dynamic';
}
/**
* 解析单个 Skill
*/
function parseSkill(entry, config) {
let content;
try {
content = fs.readFileSync(entry.skillMd, 'utf-8');
} catch {
return null;
}
const fm = parseFrontmatter(content);
const name = (fm && fm.name) || path.basename(entry.skillDir);
const declaredLevel = fm && fm.level;
const description = (fm && fm.description) || extractDescriptionFromBody(content);
const version = (fm && fm.version) || '0.0.0';
const level = resolveLevel(name, declaredLevel, config);
return {
name,
level,
declaredLevel: declaredLevel || null,
description: typeof description === 'string' ? description.trim() : String(description || '').trim(),
version,
priority: entry.priority,
source: entry.label,
path: entry.skillMd,
dir: entry.skillDir,
};
}
// ============================================================
// 配置加载
// ============================================================
function loadConfig(workspace) {
const configPaths = [
path.join(workspace || getWorkspaceDir(), 'auto-skill-loader.config.yaml'),
path.join(workspace || getWorkspaceDir(), '.agents', 'skills', 'auto-skill-loader', 'auto-skill-loader.config.yaml'),
];
for (const configPath of configPaths) {
if (fs.existsSync(configPath)) {
try {
const content = fs.readFileSync(configPath, 'utf-8');
const parsed = yaml.parse(content);
return { ...DEFAULT_CONFIG, ...parsed };
} catch {
// 配置解析失败,用默认值
}
}
}
return { ...DEFAULT_CONFIG };
}
// ============================================================
// 主逻辑
// ============================================================
function loadAllSkills(workspace) {
const config = loadConfig(workspace);
const entries = scanSkillDirs(workspace);
const skills = [];
const seen = new Set(); // 去重:同名 Skill 高优先级覆盖低优先级
for (const entry of entries) {
const skill = parseSkill(entry, config);
if (!skill) continue;
// 同名去重(高优先级保留)
if (seen.has(skill.name)) continue;
seen.add(skill.name);
// skipSkills 过滤
if (config.skipSkills && config.skipSkills.includes(skill.name)) continue;
skills.push(skill);
}
return { skills, config };
}
/**
* 获取可加载 Skill 列表(排除 core,protected 标记但保留)
*/
function getLoadableSkills(workspace) {
const { skills, config } = loadAllSkills(workspace);
return {
loadable: skills.filter(s => s.level === 'dynamic'),
protected: skills.filter(s => s.level === 'protected'),
core: skills.filter(s => s.level === 'core'),
all: skills,
config,
};
}
/**
* 检查指定 Skill 的保护级别
*/
function checkSkill(name, workspace) {
const { skills } = loadAllSkills(workspace);
const skill = skills.find(s => s.name === name);
if (!skill) {
return { found: false, name };
}
return { found: true, ...skill };
}
// ============================================================
// CLI
// ============================================================
function formatSkillLine(s) {
const levelIcon = { core: '🔒', protected: '🛡️', dynamic: '🔄' };
const icon = levelIcon[s.level] || '❓';
const declared = s.declaredLevel ? '' : ' (auto)';
return ` icon s.name.padEnd(30) s.level.padEnd(12)declared [s.source]`;
}
function main() {
const args = process.argv.slice(2);
const jsonMode = args.includes('--json');
const allMode = args.includes('--all');
const checkIdx = args.indexOf('--check');
const dryRunIdx = args.indexOf('--dry-run');
// --check <name>
if (checkIdx !== -1) {
const name = args[checkIdx + 1];
if (!name) {
console.error('Usage: --check <skill-name>');
process.exit(1);
}
const result = checkSkill(name);
if (jsonMode) {
console.log(JSON.stringify(result, null, 2));
} else if (!result.found) {
console.log(`❓ Skill "name" 未找到`);
} else {
const levelIcon = { core: '🔒', protected: '🛡️', dynamic: '🔄' };
console.log(`levelIcon[result.level] result.name: result.level`);
if (result.level === 'core') {
console.log(' ⛔ 此 Skill 为核心级,自动加载器不可操作');
} else if (result.level === 'protected') {
console.log(' ⚠️ 此 Skill 为保护级,需显式确认才能操作');
} else {
console.log(' ✅ 此 Skill 可自由加载/卸载');
}
}
return;
}
// --dry-run "<message>"
if (dryRunIdx !== -1) {
const message = args[dryRunIdx + 1] || '';
const { loadable, protected: prot, core } = getLoadableSkills();
if (jsonMode) {
console.log(JSON.stringify({ message, loadable, protected: prot, core, blocked: core.length }, null, 2));
} else {
console.log(`🔍 Auto Skill Loader - Dry Run`);
console.log(`任务: "message"`);
console.log(`\n可匹配 Skill (loadable.length 个):`);
loadable.forEach(s => console.log(formatSkillLine(s)));
if (prot.length) {
console.log(`\n保护级 Skill (prot.length 个, 需确认):`);
prot.forEach(s => console.log(formatSkillLine(s)));
}
console.log(`\n核心级 Skill (core.length 个, 已屏蔽):`);
core.forEach(s => console.log(formatSkillLine(s)));
console.log(`\n💡 意图匹配由 Agent 完成,此处仅列出候选池`);
}
return;
}
// 默认:列出 Skill
const { loadable, protected: prot, core, all } = getLoadableSkills();
if (jsonMode) {
console.log(JSON.stringify(allMode ? { all } : { loadable, protected: prot, core }, null, 2));
return;
}
if (allMode) {
console.log(`📦 所有 Skill (all.length 个):\n`);
all.forEach(s => console.log(formatSkillLine(s)));
} else {
console.log(`🔄 可加载 Skill (loadable.length 个):\n`);
loadable.forEach(s => console.log(formatSkillLine(s)));
if (prot.length) {
console.log(`\n🛡️ 保护级 (prot.length 个):`);
prot.forEach(s => console.log(formatSkillLine(s)));
}
console.log(`\n🔒 核心级 (core.length 个, 不可操作):`);
core.forEach(s => console.log(formatSkillLine(s)));
}
console.log(`\n总计: all.length 个 Skill | 可加载: loadable.length | 保护: prot.length | 核心: core.length`);
}
// ============================================================
// 导出(供其他脚本调用)
// ============================================================
module.exports = {
parseFrontmatter,
scanSkillDirs,
resolveLevel,
loadAllSkills,
getLoadableSkills,
checkSkill,
loadConfig,
DEFAULT_CONFIG,
};
// CLI 直接运行
if (require.main === module) {
main();
}
FILE:scripts/yaml-lite.js
/**
* yaml-lite.js - 轻量 YAML 解析器
*
* 零外部依赖,仅支持 SKILL.md frontmatter 常用的简单 YAML 格式:
* - key: value(字符串、数字、布尔)
* - key: [a, b, c](内联数组)
* - 多行字符串(> 折叠)
*
* 不支持:嵌套对象、锚点、复杂类型
* 如需完整 YAML,请安装 js-yaml
*/
function parse(text) {
if (!text || typeof text !== 'string') return {};
const result = {};
const lines = text.split(/\r?\n/);
let currentKey = null;
let multilineValue = '';
let isMultiline = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 跳过注释和空行(非多行模式)
if (!isMultiline && (line.trim().startsWith('#') || line.trim() === '')) continue;
// 多行值收集
if (isMultiline) {
if (line.match(/^\S/) && line.includes(':')) {
// 新的 key 开始,结束多行
result[currentKey] = multilineValue.trim();
isMultiline = false;
// 继续解析当前行
} else {
multilineValue += ' ' + line.trim();
continue;
}
}
// key: value 解析
const kvMatch = line.match(/^(\w[\w\s.-]*?):\s*(.*)/);
if (!kvMatch) continue;
const key = kvMatch[1].trim();
let value = kvMatch[2].trim();
// 多行标记 (> 或 |)
if (value === '>' || value === '|') {
currentKey = key;
multilineValue = '';
isMultiline = true;
continue;
}
// 内联数组 [a, b, c]
if (value.startsWith('[') && value.endsWith(']')) {
const inner = value.slice(1, -1);
result[key] = inner.split(',').map(s => parseValue(s.trim()));
continue;
}
// 空数组标记 []
if (value === '[]') {
result[key] = [];
continue;
}
// 去除行内注释
const commentIdx = value.indexOf(' #');
if (commentIdx > 0) {
value = value.slice(0, commentIdx).trim();
}
result[key] = parseValue(value);
}
// 处理最后一个多行值
if (isMultiline && currentKey) {
result[currentKey] = multilineValue.trim();
}
return result;
}
function parseValue(value) {
if (value === '') return '';
// 去引号
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
// 布尔
if (value === 'true') return true;
if (value === 'false') return false;
// null
if (value === 'null' || value === '~') return null;
// 数字
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
return value;
}
function stringify(obj) {
if (!obj || typeof obj !== 'object') return '';
const lines = [];
for (const [key, value] of Object.entries(obj)) {
if (Array.isArray(value)) {
if (value.length === 0) {
lines.push(`key: []`);
} else {
lines.push(`key: [value.map(v => JSON.stringify(v)).join(', ')]`);
}
} else if (typeof value === 'string' && value.includes('\n')) {
lines.push(`key: >`);
lines.push(` value.replace(/\n/g, '\n ')`);
} else {
lines.push(`key: value`);
}
}
return lines.join('\n');
}
module.exports = { parse, stringify };