# 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 自主管理 skill（CRUD 操作）：

```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')    // 安装 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`/`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 配置" 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_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"
}
```
