17 KiB
Skill 系统迁移设计:对齐 OpenClaw + Hermes 架构
日期: 2026-04-13 状态: 待审核 参考项目: OpenClaw (小龙虾), Hermes Agent
1. 目标
将 Neta 的 skill 系统从"文档占位"状态迁移为完整可用的 prompt-based skill 系统:
- 移除老的 13 个 skill(旧框架产物)及其配套的老组件/字段
- 对齐 OpenClaw 的 SKILL.md 标准、多来源加载、安装机制
- 引入 Hermes 的渐进式披露、条件激活、Agent 自主管理 skill
- 支持 GitHub 地址安装 + Node/Python 生态依赖安装
- 前后端完整实现,前端必须遵循 Cool Admin service 代理规范
2. 核心机制:Prompt-Based Skill
Skill 本质是 prompt 指导,不是可执行工具:
- SKILL.md 内容注入 system prompt(索引模式)或按需加载(完整模式)
- LLM 读取 skill 指令后自主决定如何执行
- 与 OpenClaw、Hermes 机制一致
3. 数据模型
3.1 数据库表 netaclaw_skill(改造)
移除字段(老框架产物,prompt-based skill 不需要):
icon→ 改用 frontmatter 的metadata.emojicategory→ 改用tags分类config→ prompt-based skill 无执行参数配置
保留字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| name | varchar(100) UNIQUE | skill 唯一标识 |
| label | varchar(200) | 显示名称 |
| description | text | 功能描述 |
| skillType | varchar(20) | compute / llm / multimodal |
| tags | json | 标签数组 |
| status | int | 0=禁用 1=启用 |
| version | varchar(20) | 版本号 |
新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| source | varchar(20) | 来源:local / github |
| sourceUrl | varchar(500) | GitHub URL |
| installSpec | json | 安装规格(OpenClaw SkillInstallSpec 兼容) |
| metadata | json | 完整 frontmatter 元数据(emoji/requires/conditions 等) |
| installedAt | datetime | 安装时间 |
| fingerprint | varchar(64) | SHA256 内容指纹(用于更新检测) |
注意:Entity 继承 BaseEntity,
synchronize: true开发环境自动同步表结构。entities.ts无需改动(已注册)。
3.2 文件存储结构
packages/backend/
├── skills/ # skill 安装目录
│ └── {skill-name}/
│ ├── SKILL.md # 主定义文件
│ ├── references/ # 参考文档(可选)
│ ├── templates/ # 模板(可选)
│ └── scripts/ # 脚本(可选)
├── .skillhub/ # 安装管理
│ ├── lock.json # 锁文件
│ └── origins/
│ └── {skill-name}.json # 来源追踪
3.3 SKILL.md Frontmatter 标准
需要引入 js-yaml 库做完整 YAML 解析(现有实现用简单正则只提取 name/description,无法解析嵌套结构)。
---
name: my-skill
description: 技能描述
version: 1.0.0
metadata:
skillType: compute
emoji: "🔧"
tags: [检测, 视觉]
requires:
bins: ["ffmpeg"]
anyBins: ["node", "bun"]
tools: ["bash"]
install:
- id: node-dep
kind: node
package: "some-package"
label: "安装 Node 依赖"
- id: python-dep
kind: uv
package: "some-python-pkg"
label: "安装 Python 依赖"
conditions:
requires_tools: ["bash", "file_read"]
fallback_for_tools: ["custom_tool"]
---
# Skill 标题
## 何时使用
...
## 执行流程
...
3.4 Agent Entity 的 skills 字段(保持简单)
agent.ts 的 skills: string[] 保持不变——只存 skill 名称列表。条件激活规则只在 skill 自身 frontmatter 中定义,不支持 Agent 维度覆盖。理由:降低复杂度,避免 Agent 和 Skill 两处配置冲突。
4. 后端架构
4.1 服务层(新增/改造)
modules/netaclaw/service/
├── skill_loader.ts # 改造:完整 YAML 解析 + 渐进式加载 + 条件过滤
├── skill_installer.ts # 新增:GitHub clone + 依赖安装
└── skill_registry.ts # 新增:lockfile + 指纹 + 来源追踪
依赖新增:js-yaml(YAML 解析,替代现有的正则解析)
SkillLoaderService(改造)
// 多来源扫描
async scanSkills(): Promise<void>
// 渐进式披露:只返回索引(name + description)
buildSkillsIndex(skillNames: string[]): string
// 按需加载完整 SKILL.md 内容
getSkillContent(name: string): string | null
// 条件过滤:根据 agent 可用工具过滤 skill
filterByConditions(skills: SkillEntry[], availableTools: string[]): SkillEntry[]
// 构建 <available_skills> XML
buildSkillsPrompt(skillNames: string[], availableTools: string[]): string
SkillInstallerService(新增)
// GitHub 安装
async installFromGitHub(url: string, options?: { branch?: string; tag?: string }): Promise<SkillMeta>
// 依赖安装(node/uv)
async installDependencies(skillName: string): Promise<{ success: boolean; logs: string[] }>
// 卸载
async uninstall(skillName: string): Promise<void>
// 更新(重新拉取)
async update(skillName: string): Promise<SkillMeta>
// 检查更新
async checkUpdates(): Promise<Array<{ name: string; hasUpdate: boolean }>>
安装器安全校验(对齐 OpenClaw):
- npm 包名正则:
/^(@[a-z0-9._-]+\/)?[a-z0-9._-]+(@[a-z0-9^~>=<.*|-]+)?$/ - uv 包名正则:
/^[a-z0-9][a-z0-9._-]*(\[[a-z0-9,._-]+\])?(([><=!~]=?|===?)[a-z0-9.*_-]+)?$/i - GitHub URL 校验:必须是
github.com域名
SkillRegistryService(新增)
// 读写 lockfile
async readLockfile(): Promise<SkillHubLockfile>
async writeLockfile(lockfile: SkillHubLockfile): Promise<void>
// 来源追踪
async writeOrigin(name: string, origin: SkillOrigin): Promise<void>
async readOrigin(name: string): Promise<SkillOrigin | null>
// 内容指纹
computeFingerprint(skillDir: string): Promise<string>
// 数据库同步
async syncToDatabase(): Promise<void>
4.2 内置工具:skill_manage + read_skill(新增)
skill_manage — Agent 自主管理 skill(CRUD 操作):
// tools/builtin/skill_manage.ts
{
name: 'skill_manage',
description: 'Agent 自主创建/编辑/删除 skill',
parameters: Type.Object({
action: Type.Union([
Type.Literal('create'),
Type.Literal('edit'),
Type.Literal('delete'),
]),
name: Type.String(),
content: Type.Optional(Type.String()), // SKILL.md 完整内容
}),
execute: async (id, params) => { ... }
}
read_skill — Agent 按需读取完整 SKILL.md 内容(渐进式披露的运行时入口):
// tools/builtin/read_skill.ts
{
name: 'read_skill',
description: '读取指定 skill 的完整 SKILL.md 内容',
parameters: Type.Object({
name: Type.String({ description: 'skill 名称' }),
}),
execute: async (id, params) => {
return skillLoader.getSkillContent(params.name);
}
}
设计决策:将管理操作(skill_manage)和运行时读取(read_skill)拆分为两个工具,职责清晰。不复用 file_read 工具,避免 agent 直接访问文件系统路径。
4.3 Controller 改造
现有问题:当前 skill.ts 用原生 @Controller,只有 2 个手写接口。需要改为 @CoolController + 自定义接口混合模式。
@CoolController({
api: ['info', 'list', 'page'], // 自动生成:详情/列表/分页
entity: NetaClawSkillEntity,
pageQueryOp: {
keyWordLikeFields: ['name', 'label', 'description'],
fieldEq: ['status', 'source', 'skillType'],
addOrderBy: { createTime: 'DESC' },
},
})
export class AdminNetaClawSkillController extends BaseController {
// 以下为自定义接口(手写)
@Get('/metas') // 已启用 skill 元数据快照
@Post('/install') // 安装 skill(GitHub URL)
@Post('/uninstall') // 卸载 skill
@Post('/update') // 更新 skill(重新拉取)
@Post('/setStatus') // 启用/禁用
@Get('/content') // 按需获取完整 SKILL.md(运行时用)
@Post('/checkUpdates') // 检查所有 skill 是否有更新
@Post('/installDeps') // 手动触发依赖安装
@Post('/create') // Agent 自主创建 skill
@Post('/edit') // Agent 自主编辑 skill
}
注意:不生成
add/delete/updateCRUD——skill 的增删改通过install/uninstall/update自定义接口处理,语义更清晰。
4.4 API 路由总览
| 方法 | 路径 | 来源 | 说明 |
|---|---|---|---|
| POST | /admin/netaclaw/skill/page |
@CoolController | 分页列表(keyWord/status/source/skillType 筛选) |
| POST | /admin/netaclaw/skill/list |
@CoolController | 全量列表 |
| GET | /admin/netaclaw/skill/info |
@CoolController | 单条详情(query: id) |
| GET | /admin/netaclaw/skill/metas |
自定义 | 已启用 skill 元数据快照 |
| POST | /admin/netaclaw/skill/install |
自定义 | 安装 skill |
| POST | /admin/netaclaw/skill/uninstall |
自定义 | 卸载 skill |
| POST | /admin/netaclaw/skill/update |
自定义 | 更新 skill(重新拉取) |
| POST | /admin/netaclaw/skill/setStatus |
自定义 | 启用/禁用 |
| GET | /admin/netaclaw/skill/content |
自定义 | 按需获取完整 SKILL.md(运行时用) |
| POST | /admin/netaclaw/skill/checkUpdates |
自定义 | 检查更新 |
| POST | /admin/netaclaw/skill/installDeps |
自定义 | 手动触发依赖安装 |
| POST | /admin/netaclaw/skill/create |
自定义 | Agent 自主创建 skill |
| POST | /admin/netaclaw/skill/edit |
自定义 | Agent 自主编辑 skill |
安装请求示例:
{
"url": "https://github.com/user/my-skill",
"source": "github",
"branch": "main",
"version": "v1.0.0"
}
5. 运行时集成
5.1 渐进式披露(对齐 Hermes)
System prompt 只注入 skill 索引(name + description),不注入完整内容:
<available_skills>
<skill>
<name>damage-detector</name>
<description>检测车身视频帧中的旧伤和划痕</description>
</skill>
<skill>
<name>claim-audit</name>
<description>理赔订单审核</description>
</skill>
</available_skills>
当你需要使用某个 skill 时,调用 read_skill 工具读取完整指令后再执行。
Agent 按需加载完整内容的方式:调用 read_skill 内置工具(见 4.2),返回完整 SKILL.md 正文。
与现有实现的差异:现有
getSkillPrompt()把完整 content 拼进 system prompt,需要改为只拼索引。
5.2 条件激活(对齐 Hermes)
Skill frontmatter 可声明:
requires_tools: 必须全部可用才显示在索引中fallback_for_tools: 当指定工具不可用时才显示
SkillLoaderService 在构建 prompt 时根据 agent 配置的工具集过滤。
5.3 双入口统一改造(Gateway + Chat Controller)
现有问题:gateway/server.ts(WebSocket)正确传入 agentInfo.skills,但 chat.ts(HTTP)硬编码空数组 []。两个入口的 skill 加载逻辑不一致。
改造方案:统一为相同的 skill 加载 + 工具注册逻辑:
// 两个入口共用的逻辑(抽取到 service 或 helper)
function buildSkillContext(skillLoader, skillInstaller, agentInfo, defaultTools) {
const skillNames = agentInfo?.skills || [];
const availableToolNames = defaultTools.map(t => t.name);
const skillPrompt = skillLoader.buildSkillsPrompt(skillNames, availableToolNames);
// 注册 skill 相关内置工具
const skillTools = [
createSkillManageTool(skillLoader, skillInstaller),
createReadSkillTool(skillLoader),
];
return { skillPrompt, skillTools };
}
// gateway/server.ts
const { skillPrompt, skillTools } = buildSkillContext(...);
agentConfig.systemPrompt = (agentInfo.systemPrompt || '') + skillPrompt;
tools = [...this.defaultTools, ...memoryTools, ...skillTools];
// chat.ts — 同样的逻辑,不再硬编码空数组
const { skillPrompt, skillTools } = buildSkillContext(...);
6. 前端设计
6.1 API 调用规范修正
现有问题:skills.vue、agent-edit.vue、skill-detail.vue、skill-config.vue 全部用 fetch() 直接调用 API,绕过了 Cool Admin 的 service 代理。违反项目规范。
改造要求:所有 API 调用改为 Cool Admin 方式:
const { service } = useCool();
// @CoolController 自动生成的接口
await service.netaclaw.skill.page({ page: 1, size: 20, keyWord: '...', status: 1 });
await service.netaclaw.skill.info({ id: 1 });
// 自定义接口用 service.request
await service.request({
url: '/admin/netaclaw/skill/install',
method: 'POST',
data: { url: 'https://github.com/user/my-skill' }
});
await service.request({ url: '/admin/netaclaw/skill/metas' });
6.2 Skill 管理页面(/agent/skills 改造)
顶部操作栏:
- 安装按钮 → 弹出对话框输入 GitHub URL
- 筛选:来源(local/github)、状态(启用/禁用)、类型(compute/llm/multimodal)
Skill 卡片列表:
- 卡片:emoji + name + description + skillType 标签 + source 标签 + 状态开关
- 操作:启用/禁用、更新、卸载、查看详情
安装对话框:
- GitHub URL 输入框
- 可选分支/tag
- 安装进度实时反馈(通过 WebSocket 复用现有 Socket.IO 连接,发送
skill_install_progress事件)
详情抽屉(替代老的 skill-detail + skill-config + skill-prompts):
- SKILL.md 渲染(markdown-it)
- 依赖状态列表(已安装/未安装,可手动触发安装)
- 来源信息(URL、安装时间、版本、指纹)
- 条件激活规则展示
6.3 Agent 编辑页面集成(/agent/agent-edit 改造)
现有 "Skill 配置" Tab(Tab 2)改造:
- 左侧:可用 skill 列表(带搜索和分类筛选)
- 右侧:已选 skill 列表(可移除)
- 每个 skill 卡片显示 emoji + name + description + skillType 标签
- API 调用同样改为 Cool Admin service 代理
6.4 安装进度实时反馈
复用现有 Socket.IO 连接,新增事件类型:
| 事件 | 数据 | 说明 |
|---|---|---|
skill_install_progress |
{step, percent, message} |
安装进度 |
skill_install_done |
{name, success, error?} |
安装完成 |
skill_deps_progress |
{name, step, percent} |
依赖安装进度 |
7. 清理工作
7.1 移除老 skill 及相关代码
后端删除:
skills/下所有老 skill 目录(保留目录本身,清空内容)- 数据库
netaclaw_skill表中老数据(清空)
后端改造(非删除):
entity/skill.ts— 移除icon、category、config字段,新增 6 个字段service/skill_loader.ts— 完全重写controller/skill.ts— 改为 @CoolController + 自定义接口
前端删除:
components/skill-config.vue— 老的参数配置表单(prompt-based skill 不需要)components/skill-prompts.vue— 老的提示词编辑(prompt-based skill 不需要)components/skill-model.vue— 老的模型配置(如果存在)
前端改造(非删除):
views/skills.vue— 重写,改用 Cool Admin service 代理views/agent-edit.vue— Skill Tab 改造components/skill-detail.vue— 改为新的详情抽屉
7.2 保留的基础设施
netaclaw_skillEntity 文件(字段改造)entities.ts中的 Entity 注册(无需改动)- skill controller 路由前缀
/admin/netaclaw/skill - 前端
/agent/skills路由和模块配置(config.ts无需改动)
8. 安全考虑
- GitHub clone 只允许
github.com域名 - npm/uv 包名正则白名单校验(防命令注入,对齐 OpenClaw 的 SAFE_*_PACKAGE 正则)
- SKILL.md 文件大小限制(256KB)
- skill 目录数量限制(最多 200 个)
- system prompt 中 skill 索引总量限制(30,000 字符)
- 路径遍历防护(skill 目录不能逃逸出 skills/ 根目录,使用 realpath 校验)
9. GitHub 版本更新检测
机制:checkUpdates() 通过 GitHub API 查询最新 commit hash,与本地 .skillhub/origins/{name}.json 中记录的 commit hash 比较。
// skill_installer.ts
async checkUpdates(): Promise<Array<{ name: string; hasUpdate: boolean; currentHash: string; latestHash: string }>> {
// 1. 遍历所有 source=github 的 skill
// 2. 读取 origin.json 获取 sourceUrl + 安装时的 commit hash
// 3. 调用 GitHub API: GET /repos/{owner}/{repo}/commits?per_page=1
// 4. 比较 commit hash
}
origin.json 结构:
{
"version": 1,
"source": "github",
"url": "https://github.com/user/my-skill",
"branch": "main",
"commitHash": "abc123...",
"installedAt": "2026-04-13T10:00:00Z"
}