GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-13-skill-system-migration-design.md
2026-05-20 21:39:12 +08:00

471 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.emoji`
- `category` → 改用 `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无法解析嵌套结构
```yaml
---
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改造
```typescript
// 多来源扫描
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新增
```typescript
// 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新增
```typescript
// 读写 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 自主管理 skillCRUD 操作):
```typescript
// 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 内容(渐进式披露的运行时入口):
```typescript
// 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` + 自定义接口混合模式。
```typescript
@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') // 安装 skillGitHub 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`/`update` CRUD——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 |
**安装请求示例:**
```json
{
"url": "https://github.com/user/my-skill",
"source": "github",
"branch": "main",
"version": "v1.0.0"
}
```
## 5. 运行时集成
### 5.1 渐进式披露(对齐 Hermes
System prompt 只注入 skill 索引name + description不注入完整内容
```xml
<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 加载 + 工具注册逻辑:
```typescript
// 两个入口共用的逻辑(抽取到 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 方式:
```typescript
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 配置" TabTab 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_skill` Entity 文件(字段改造)
- `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 比较。
```typescript
// 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 结构**
```json
{
"version": 1,
"source": "github",
"url": "https://github.com/user/my-skill",
"branch": "main",
"commitHash": "abc123...",
"installedAt": "2026-04-13T10:00:00Z"
}
```