# Skill 系统演进设计

> 日期：2026-04-27
> 状态：Draft
> 范围：P0 密钥管理 · P1 运行时执行器 · P2 标准兼容 · P3 碰撞检测与诊断

## 1. 背景与目标

当前 Neta 的 Skill 系统以 SKILL.md 为载体，支持 GitHub/ZIP/本地安装，具备条件激活和附属文件读取能力。但存在以下架构缺口：

- Skill 的 API Key / 密钥无归属，混在主系统环境变量或 prompt 硬编码中
- Skill 自有代码（Python/Node/.NET 脚本）的执行完全依赖 Agent 通过 bash 间接调用，无标准化执行器
- 与 Agent Skills 社区标准（agentskills.io）不完全兼容，社区 skill 安装后可能缺少字段映射
- 多来源安装缺少碰撞检测和诊断，同名冲突无预警
- Agent 读完 SKILL.md 后经常不读 references/ 子文档，导致执行质量下降

### 参考项目

- **pi-mono-main**：纯 prompt skill 系统，多层级发现、碰撞检测、诊断系统、名称规范验证
- **skills-main**（MiniMax Skills）：18 个生产级 skill，涵盖 PDF/DOCX/XLSX/PPTX 文档处理、多模态 API 调用，混合运行时（Python + Node + .NET + bash），系统级依赖（ffmpeg/LibreOffice）

## 2. Skill 分类体系

### 2.1 三种 Skill 类型

| 类型 | 判断条件 | Agent 交互方式 | 典型场景 |
|------|----------|---------------|---------|
| **prompt** | 只有 SKILL.md，无 skill.config.yaml | `read_skill` → 遵循指令 | llm-wiki、社区标准 skill |
| **compute-entry** | config 有 `entrypoint` | `execute_skill` 工具 | OCR 接口封装、单一 API 调用 |
| **compute-toolkit** | config 有 `runtime` 但无 `entrypoint` | `read_skill` → bash 执行脚本 | minimax-pdf、minimax-xlsx |

### 2.2 目录结构

**prompt skill（标准兼容）：**

```
skills/llm-wiki/
├── SKILL.md
└── references/
```

**compute-entry skill：**

```
skills/ocr-reader/
├── SKILL.md
├── skill.config.yaml
├── src/ocr.py
├── requirements.txt
└── .venv/                  ← 安装时自动创建
```

**compute-toolkit skill：**

```
skills/minimax-pdf/
├── SKILL.md
├── skill.config.yaml
├── scripts/
│   ├── palette.py
│   ├── render_cover.js
│   └── make.sh
├── design/
│   └── design.md
├── references/
│   └── *.md
├── requirements.txt
└── .venv/
```

### 2.3 skill.config.yaml 完整 Schema

```yaml
# --- 运行时声明 ---
runtime: python | node | bash | dotnet    # 主运行时
entrypoint: src/ocr.py                    # 可选，有则为 compute-entry，无则为 compute-toolkit
timeout: 30000                            # 执行超时 ms，默认 30s

# --- 环境变量声明 ---
env:
  - name: OCR_API_KEY
    required: true
    description: "OCR 服务 API Key"
  - name: OCR_ENDPOINT
    required: false
    default: "https://api.ocr.com/v1"

# --- 依赖声明 ---
dependencies:
  system:                                 # 系统级依赖
    - name: ffmpeg
      check: "ffmpeg -version"            # 检测命令
    - name: libreoffice
      check: "soffice --version"
  python:
    source: requirements.txt              # 或 inline: ["httpx>=0.27", "pillow"]
  node:
    packages: ["pptxgenjs"]               # npm install 到 skill 目录
  dotnet:
    project: scripts/dotnet/MyProject.Cli # dotnet restore 路径

# --- 安装钩子 ---
setup:
  posix: scripts/setup.sh                 # macOS/Linux
  win32: scripts/setup.ps1                # Windows

# --- compute-entry 专用：接口声明 ---
interface:
  input:
    image_path: { type: string, required: true, description: "图片路径" }
    language: { type: string, default: "auto" }
  output:
    text: { type: string }
    confidence: { type: number }

# --- references 加载策略 ---
references:
  required:                               # 执行前必须读取
    - references/create.md
    - references/format.md
  optional:                               # 按需读取
    - references/tracing.md
  routes:                                 # 任务路由 → 文档映射
    - match: ["create", "generate", "new"]
      required_refs: ["references/create.md", "references/format.md"]
    - match: ["edit", "modify", "fill"]
      required_refs: ["references/edit.md"]
```

## 3. P0：Skill-Scoped 密钥管理

### 3.1 数据层

`netaclaw_skill` 表新增两个字段：

```typescript
// entity/skill.ts
@Column({ type: 'text', comment: 'AES-256-GCM 加密的 secrets JSON', nullable: true })
secrets: string;

@Column({ type: 'json', comment: 'env 声明 schema', nullable: true })
envSchema: Array<{ name: string; required: boolean; description?: string; default?: string }>;
```

### 3.2 新增服务：SkillSecretService

```typescript
// service/skill_secret.ts
@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillSecretService {
  // AES-256-GCM 加密，密钥来自 process.env.SKILL_SECRET_KEY || process.env.APP_SECRET
  encrypt(plainObj: Record<string, string>): string;
  decrypt(cipherText: string): Record<string, string>;

  // 合并 DB secrets + envSchema defaults，返回完整 env map
  async resolveEnv(skillName: string): Promise<Record<string, string>>;

  // 保存 secrets（加密后写入 DB）
  async saveSecrets(skillName: string, secrets: Record<string, string>): Promise<void>;

  // 获取已配置的 key 列表（不返回明文）
  async getConfiguredKeys(skillName: string): Promise<Array<{ name: string; hasValue: boolean }>>;
}
```

### 3.3 API 变更

Admin controller 新增：

```
GET  /admin/netaclaw/skill/envSchema?name=ocr-reader
  → 返回 envSchema 声明 + 每个 key 的 hasValue 状态

POST /admin/netaclaw/skill/secrets
  body: { name: "ocr-reader", secrets: { "OCR_API_KEY": "sk-xxx" } }
  → 加密后存入 DB，不返回明文
```

### 3.4 前端变更

`skill-detail.vue` 抽屉新增"配置"区域：

- 读取 envSchema 展示每个变量的 name、description、required 标记
- 已配置的显示 `[********]` + 修改按钮
- 未配置且 required 的显示红色提示
- 保存时调用 `/secrets` 接口

### 3.5 运行时 env 注入

**compute-entry 模式：** `SkillExecutorService.execute()` 调用 `resolveEnv(skillName)` 注入到子进程 env。

**compute-toolkit 模式：** bash 工具执行时，检查脚本路径是否落在某个 skill 目录下。如果是，自动从 `SkillSecretService.resolveEnv()` 获取对应 env 注入到子进程。判断逻辑：

```typescript
// tools/builtin/bash.ts execute 方法内
const skillName = skillLoader.resolveSkillByPath(cwd || scriptPath);
if (skillName) {
  const skillEnv = await skillSecretService.resolveEnv(skillName);
  Object.assign(processEnv, skillEnv);
}
```

`SkillLoaderService` 新增 `resolveSkillByPath(absPath)` 方法：检查路径是否以某个 skill 目录为前缀，返回 skill name 或 null。

## 4. P1：Skill Runtime Executor

### 4.1 新增服务：SkillExecutorService

```typescript
// service/skill_executor.ts
interface SkillExecuteParams {
  skillName: string;
  input: Record<string, unknown>;
}

interface SkillExecuteResult {
  success: boolean;
  output?: Record<string, unknown>;
  error?: string;
  duration: number;  // ms
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class SkillExecutorService {
  async execute(params: SkillExecuteParams): Promise<SkillExecuteResult>;
}
```

### 4.2 执行流程

```
Agent 调用 execute_skill({ name: "ocr-reader", input: { image_path: "/tmp/a.png" } })
  → SkillExecutorService.execute()
    → 1. 从 SkillLoaderService 获取 skillMeta，确认是 compute-entry
    → 2. 读取 skill.config.yaml：runtime / entrypoint / timeout / interface
    → 3. 验证 input 满足 interface.input（宽松模式：缺少 optional 字段不报错）
    → 4. SkillSecretService.resolveEnv(skillName) 获取 env map
    → 5. 根据 runtime 构建命令：
         python  → {skillDir}/.venv/bin/python {entrypoint}  (win: .venv\Scripts\python)
         node    → node {entrypoint}
         bash    → bash {entrypoint}
         dotnet  → dotnet run --project {entrypoint}
    → 6. spawn 子进程：
         cwd = skillDir
         env = { ...process.env（白名单）, ...skill-scoped env }
         stdin = JSON.stringify(input)
         timeout = config.timeout || 30000
    → 7. 收集 stdout（期望 JSON），stderr 作为日志
    → 8. 解析 stdout JSON → output，返回 SkillExecuteResult
```

### 4.3 新增 Agent 工具：execute_skill

```typescript
// tools/builtin/execute_skill.ts
const ExecuteSkillParams = Type.Object({
  name: Type.String({ description: 'compute skill 名称' }),
  input: Type.Record(Type.String(), Type.Unknown(), { description: '输入参数 JSON' }),
});
```

注册到 catalog：

```typescript
registerSchema({
  name: 'execute_skill',
  toolset: 'skill',
  description: '执行 compute skill',
  capability: 'compute',
  visibility: 'skill',
});
```

工具注入条件：`buildSkillContext()` 检查 Agent 配置的 skills 中是否存在 compute-entry 类型，有则注入 `execute_skill` 工具。

### 4.4 Skill 端协议

entrypoint 脚本遵循 stdin/stdout JSON 协议：

- 从 stdin 读取 JSON 输入
- 环境变量中获取 secrets（如 `os.environ["OCR_API_KEY"]`）
- 将 JSON 结果写入 stdout
- 非零退出码 = 失败，stderr 内容作为错误信息

Python 示例：

```python
import sys, json, os

def main():
    data = json.loads(sys.stdin.read())
    api_key = os.environ["OCR_API_KEY"]
    # ... 业务逻辑 ...
    print(json.dumps({"text": "识别结果", "confidence": 0.95}))

if __name__ == "__main__":
    main()
```

### 4.5 依赖安装改造

`SkillInstallerService.installDependencies()` 重构：

| 依赖类型 | 当前行为 | 改造后 |
|---------|---------|--------|
| `python` | `uv tool install` 全局 | skill 目录下 `uv venv .venv && uv pip install -r requirements.txt` |
| `node` | `pnpm add` 到主项目 | skill 目录下 `npm install` |
| `dotnet` | 不支持 | skill 目录下 `dotnet restore` |
| `system` | 不支持 | 执行 `check` 命令检测，未安装则报诊断 warning |
| `setup` | 不支持 | 首次安装后执行 `setup.posix` 或 `setup.win32` 脚本 |

### 4.6 环境变量白名单

spawn 子进程时不继承完整 `process.env`，只传递白名单：

```typescript
const ENV_WHITELIST = [
  'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'TZ',
  'TEMP', 'TMP', 'TMPDIR',
  'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY',
];
```

加上 skill-scoped env，确保主系统的敏感变量（DB 密码等）不泄露给 skill 代码。

## 5. P2：Agent Skills 标准兼容

### 5.1 字段映射

| Agent Skills 标准字段 | Neta 映射 | 说明 |
|---|---|---|
| `name` | 直接使用 | 新增命名规范验证 |
| `description` | 直接使用 | 最大 1024 字符 |
| `disable-model-invocation` | 新增支持 | hidden skill，不进入 prompt 索引 |
| `allowed-tools` | → `metadata.conditions.requires_tools` | 语义等价 |
| `compatibility` | → `metadata.compatibility` | 环境要求声明 |
| `license` | → `metadata.license` | 直接存储 |

### 5.2 名称规范验证

新增 `validateSkillName(name: string)` 函数，对齐 Agent Skills 标准：

- 只允许 `[a-z0-9-]`
- 1-64 字符
- 不允许首尾连字符、连续连字符
- 名称必须匹配父目录名

验证时机：安装（GitHub/ZIP）、创建（skill_manage 工具 / REST API）。不合规则拒绝并返回具体错误。已存在的不合规 skill 在 scanSkills 时产生 warning 诊断但仍加载。

### 5.3 Prompt 索引变更

`buildSkillsPrompt()` 输出区分 skill 类型：

```xml
<available_skills>
  <skill name="llm-wiki" type="prompt" category="知识库">
    知识库管理（用 read_skill 加载完整指令）
  </skill>
  <skill name="ocr-reader" type="compute-entry" category="多模态">
    OCR 图片识别（用 execute_skill 调用）
    输入: image_path(string,必填), language(string,默认auto)
  </skill>
  <skill name="minimax-pdf" type="compute-toolkit" category="文档生成">
    PDF 生成/填表/重排版（用 read_skill 加载指令后通过 bash 执行脚本）
  </skill>
</available_skills>
```

compute-entry 的索引包含 `interface.input` 摘要，Agent 无需 read_skill 即可直接调用 execute_skill。

hidden skill（`disable-model-invocation: true`）不出现在索引中，但仍可通过 `read_skill` 显式加载。

### 5.4 `read_skill` 返回格式改造

```typescript
// tools/builtin/read_skill.ts
async execute(_id, params) {
  const skill = skillLoader.getSkill(params.name);
  let result = skill.content;

  // 附属文件：区分必读和可选
  const config = skillLoader.getSkillConfig(params.name);
  const requiredRefs = config?.references?.required || [];
  const optionalRefs = (skill.files || []).filter(f => !requiredRefs.includes(f));

  if (requiredRefs.length > 0) {
    result += `\n\n<skill_required_references>`;
    result += `\n⚠️ 执行此 skill 的任务前，你必须先用 read_skill_file 读取以下文档：`;
    for (const ref of requiredRefs) {
      result += `\n- ${ref}`;
    }
    result += `\n未读取这些文档就执行操作会导致错误。`;
    result += `\n</skill_required_references>`;
  }

  if (optionalRefs.length > 0) {
    result += `\n\n<skill_optional_references>`;
    result += `\n以下文档可按需读取：`;
    for (const ref of optionalRefs) {
      result += `\n- ${ref}`;
    }
    result += `\n</skill_optional_references>`;
  }

  return textResult(result);
}
```

### 5.5 references 声明优先级

references 可以在两个位置声明：

1. `skill.config.yaml` 的 `references` 字段（compute skill）
2. SKILL.md frontmatter 的 `metadata.references` 字段（prompt skill 兼容）

优先级：`skill.config.yaml` > `SKILL.md frontmatter`。如果两者都存在，以 config 为准。对于纯 prompt skill（无 config），从 frontmatter 读取。如果两者都没有，fallback 到现有行为（列出所有附属文件为 optional）。

### 5.6 系统 prompt 层面的约束

`buildSkillsPrompt()` 的引导文本增加：

```
读取 skill 后，如果返回中包含 <skill_required_references>，你必须在执行任何操作前
先用 read_skill_file 逐一读取列出的所有文档。这不是建议，是强制要求。
```

## 6. P3：碰撞检测与诊断系统

### 6.1 诊断数据结构

```typescript
// service/skill_diagnostic.ts
interface SkillDiagnostic {
  level: 'error' | 'warning' | 'info';
  code: string;           // 机器可读的诊断码
  skillName: string;
  message: string;
  path?: string;
  detail?: Record<string, unknown>;
}
```

### 6.2 诊断码清单

| code | level | 触发条件 |
|------|-------|---------|
| `NAME_COLLISION` | warning | 两个 skill 目录解析出相同 name |
| `NAME_INVALID` | warning | 名称不符合 `[a-z0-9-]` 规范 |
| `NAME_MISMATCH` | warning | frontmatter name 与目录名不一致 |
| `DESC_MISSING` | error | 缺少 description，skill 不加载 |
| `DESC_TOO_LONG` | warning | description 超过 1024 字符 |
| `FINGERPRINT_CHANGED` | info | 文件内容变更但未通过正式更新流程 |
| `ENV_NOT_CONFIGURED` | warning | compute skill 声明了 required env 但 DB 无对应 secret |
| `RUNTIME_UNAVAILABLE` | error | 声明 python/node/dotnet 但系统未安装 |
| `SYSTEM_DEP_MISSING` | warning | 系统依赖（ffmpeg 等）check 命令失败 |
| `VENV_MISSING` | warning | Python skill 缺少 .venv 目录 |
| `CONFIG_PARSE_ERROR` | error | skill.config.yaml 解析失败 |

### 6.3 碰撞检测

在 `scanSkills()` 中实现，逻辑参考 pi-mono：

- 维护 `Map<name, skillPath>` 和 `Set<realPath>`
- 同名 skill：保留先发现的（winner），后发现的记录 `NAME_COLLISION` 诊断
- symlink 去重：通过 `fs.realpath` 追踪，同一物理文件不重复加载

### 6.4 诊断收集与暴露

`SkillLoaderService` 新增：

```typescript
private diagnostics: SkillDiagnostic[] = [];

getDiagnostics(): SkillDiagnostic[] { return this.diagnostics; }

// scanSkills() 执行时清空并重新收集
async scanSkills(): Promise<void> {
  this.diagnostics = [];
  // ... 扫描过程中 push 诊断 ...
}
```

### 6.5 API

```
GET /admin/netaclaw/skill/diagnostics
  → 返回当前所有诊断信息列表
  → 支持 ?level=error 过滤
```

### 6.6 前端展示

`skills.vue` 页面顶部新增诊断横幅：

- error 级别：红色警告条，显示数量和摘要
- warning 级别：黄色提示条
- 点击展开查看详细诊断列表（skill 名称 + 诊断码 + 消息）

`skill-detail.vue` 抽屉新增"诊断"区域：只显示该 skill 相关的诊断。

## 7. 对现有系统的兼容性影响

### 7.1 skill_manage 工具

- 创建 skill 时新增名称规范验证，不合规则拒绝
- 如果 content 的 frontmatter 包含 `metadata.env`，自动在 DB 创建 envSchema 记录
- 不影响现有 create/edit/delete 流程

### 7.2 skill 管理页面

- 卡片新增 type 标签（prompt / compute-entry / compute-toolkit）
- 详情抽屉新增"配置"tab（env secrets 管理）
- 详情抽屉新增"诊断"tab
- 安装对话框不变

### 7.3 Agent 运行时

- `buildSkillsPrompt()` 输出格式微调（增加 type 属性），向后兼容
- `buildSkillContext()` 返回的 `skillTools` 条件性新增 `execute_skill`
- hidden skill 不进入 prompt 索引
- bash 工具执行 skill 目录下脚本时自动注入 skill-scoped env

### 7.4 现有 skill 零迁移

- `playwright-cli` 和 `llm-wiki` 没有 `skill.config.yaml`，自动识别为 prompt skill，行为不变
- skills-main 的 skill（minimax-pdf 等）安装后，如果没有 skill.config.yaml 也按 prompt skill 处理；后续可逐步补充 config 文件升级为 compute-toolkit

## 8. 新增文件清单

| 文件 | 说明 |
|------|------|
| `service/skill_secret.ts` | P0：密钥加密存储与解析 |
| `service/skill_executor.ts` | P1：compute-entry 执行器 |
| `service/skill_config.ts` | skill.config.yaml 解析器 |
| `tools/builtin/execute_skill.ts` | P1：Agent 工具 |
| `service/skill_diagnostic.ts` | P3：诊断收集（可合并到 skill_loader） |

## 9. 修改文件清单

| 文件 | 变更 |
|------|------|
| `entity/skill.ts` | 新增 secrets、envSchema 字段 |
| `service/skill_loader.ts` | 加载 skill.config.yaml、碰撞检测、诊断收集、resolveSkillByPath |
| `service/skill_installer.ts` | 依赖安装改造（skill 级 venv/node_modules）、setup 脚本执行 |
| `service/tool_resolver.ts` | execute_skill 工具实例化与条件注入（替代 skill_context.ts） |
| `tools/builtin/read_skill.ts` | 返回格式改造（required/optional references） |
| `tools/builtin/bash.ts` | 重构 createLocalBashOperations 增加 envOverride 参数，skill 路径检测注入 env |
| `controller/admin/skill.ts` | 新增 envSchema/secrets/diagnostics 端点 |
| `service/skill_loader.ts:buildSkillsPrompt` | 输出格式调整（type 属性、required references 提示） |
| `frontend: skills.vue` | 诊断横幅、type 标签 |
| `frontend: skill-detail.vue` | 重构为 tab 布局（基本信息 / 配置 / 诊断） |
| `shared/types/skill.types.ts` | 新增 SkillConfig、SkillDiagnostic，扩展 runtime 枚举 |

## 10. 架构评审补充

### 10.1 工具注入链路修正

`execute_skill` 的注入目标从 `skill_context.ts` 改为 `tool_resolver.ts`。在 `resolve()` 方法中，当检测到 Agent 配置的 skills 包含 compute-entry 类型时，实例化 `execute_skill` 工具并加入工具列表。逻辑位置在现有 `read_skill` 注入点（约 line 603）之后。

`skill_context.ts` 的 `buildSkillContext()` 函数标记为 deprecated，后续迁移到 tool_resolver 内部。

### 10.2 bash 工具 env 注入重构

`bash.ts` 的 `createLocalBashOperations()` 需要重构：

```typescript
// 当前签名
function createLocalBashOperations(shellConfig: ShellConfig): BashOperations

// 改造后
interface BashEnvProvider {
  getAdditionalEnv(cwd: string): Promise<Record<string, string>>;
}

function createLocalBashOperations(
  shellConfig: ShellConfig,
  envProvider?: BashEnvProvider,
): BashOperations
```

`SkillLoaderService` 实现 `BashEnvProvider` 接口：根据 cwd 判断是否在 skill 目录下，是则返回 skill-scoped env。判断策略：严格前缀匹配 `skillsDir + sep + skillName + sep`，不做模糊匹配。

### 10.3 加密方案细化

secrets 字段的线格式：`base64(IV:16bytes || ciphertext || authTag:16bytes)`

```typescript
class SkillSecretService {
  private readonly algorithm = 'aes-256-gcm';
  private readonly ivLength = 16;

  encrypt(plainObj: Record<string, string>): string {
    const iv = crypto.randomBytes(this.ivLength);
    const cipher = crypto.createCipheriv(this.algorithm, this.deriveKey(), iv);
    const encrypted = Buffer.concat([cipher.update(JSON.stringify(plainObj), 'utf8'), cipher.final()]);
    const authTag = cipher.getAuthTag();
    return Buffer.concat([iv, encrypted, authTag]).toString('base64');
  }

  private deriveKey(): Buffer {
    const raw = process.env.SKILL_SECRET_KEY || process.env.APP_SECRET;
    if (!raw) throw new Error('SKILL_SECRET_KEY or APP_SECRET must be set');
    return crypto.createHash('sha256').update(raw).digest(); // 32 bytes
  }
}
```

密钥轮换：提供 `POST /admin/netaclaw/skill/rotateSecrets` 端点，用旧 key 解密所有 secrets 后用新 key 重新加密。密钥丢失时所有 secrets 不可恢复，需管理员重新配置。

### 10.4 DB 迁移

项目使用 TypeORM `synchronize: true`（开发环境）自动同步 entity 变更到数据库。生产环境通过数据库 MCP 工具直接操作，不需要编写 migration SQL 脚本。

实施时只需：
1. 在 `entity/skill.ts` 中添加新字段（`secrets`、`envSchema`、`skillTypeV2`）
2. 开发环境重启后 TypeORM 自动同步表结构
3. 生产环境通过 MCP `execute` 工具执行 ALTER TABLE（如需要）

现有行新字段为 NULL，不影响现有功能。`skillTypeV2` 在 scanSkills 时根据有无 skill.config.yaml 自动填充。

### 10.5 类型同步

`shared/types/skill.types.ts` 更新：

```typescript
export type SkillRuntime = 'node' | 'python' | 'bash' | 'dotnet';
export type SkillClassification = 'prompt' | 'compute-entry' | 'compute-toolkit';
```

DB 的 `skillType` 字段（compute/llm/multimodal）保留不变，它描述的是 skill 的能力类别。新增 `skill_type_v2` 字段存储三分类，两者正交。

### 10.6 Agent 分配 skill 时的类型校验

Agent 编辑页（`agent-edit.vue`）的 skill 选择器改造：

**前端改造：**
- 可选 Skill 列表中每个 skill 显示分类标签（prompt / compute-entry / compute-toolkit）
- 已选择列表中同样显示分类标签
- 当选择 compute-entry 或 compute-toolkit skill 时，如果该 Agent 缺少对应工具权限，显示黄色 warning 提示

**后端校验：**

在 Agent 保存接口（`POST /admin/netaclaw/agent/update`）中新增校验逻辑：

```typescript
// controller/agent.ts 的 update 方法中
if (body.skills?.length) {
  const warnings: string[] = [];
  for (const skillName of body.skills) {
    const classification = this.skillLoader.getSkillClassification(skillName);
    if (classification === 'compute-entry') {
      // 检查 Agent 的 tool governance 是否允许 execute_skill
      // 通过 tool_resolver 的 catalog 检查 execute_skill 是否在 Agent 可用工具列表中
      const toolNames = collectToolNames({ hasSkills: true });
      if (!toolNames.includes('execute_skill')) {
        warnings.push(`Skill "${skillName}" 是 compute-entry 类型，但 execute_skill 工具未启用`);
      }
    }
    if (classification === 'compute-toolkit') {
      const toolNames = collectToolNames({ hasSkills: true });
      if (!toolNames.includes('bash')) {
        warnings.push(`Skill "${skillName}" 是 compute-toolkit 类型，但 bash 工具未启用`);
      }
    }
  }
  // warnings 不阻塞保存，随响应返回
}
```

`/admin/netaclaw/skill/metas` 端点返回数据新增 `classification` 字段，前端据此渲染标签。

### 10.7 基础设施文件过滤

`collectFiles()` 新增排除列表：

```typescript
const INFRA_FILES = new Set([
  'skill.config.yaml', 'requirements.txt', 'package.json',
  'package-lock.json', 'tsconfig.json', '.env',
]);
```

这些文件不出现在 `read_skill` 返回的 `<skill_files>` 列表中，Agent 不会误读。

### 10.8 Skill 执行审计日志

`SkillExecutorService.execute()` 执行完成后写入结构化日志：

```typescript
this.logger.info('[SkillExecutor] %s executed by agent=%s duration=%dms success=%s',
  params.skillName, agentId, result.duration, result.success);
```

失败时额外记录 stderr 摘要（截断到 500 字符）。

### 10.9 setup 脚本安全约束

安装时执行 setup 脚本增加限制：

**后端（skill_installer.ts）：**
- timeout: 120s
- 只允许 `source === 'github'` 或 `source === 'zip'`（管理员手动上传）的 skill 执行 setup
- `source === 'local'`（通过 skill_manage 工具创建）的 skill 不允许 setup 脚本
- 执行前检查 setup 脚本路径不包含 `..`（路径穿越防护）

```typescript
// skill_installer.ts installDependencies 中
const setupKey = process.platform === 'win32' ? 'win32' : 'posix';
const setupScript = config?.setup?.[setupKey];
if (setupScript) {
  const origin = await this.registry.readOrigin(skillName);
  const allowedSources = ['github', 'zip'];
  if (!origin || !allowedSources.includes(origin.source)) {
    logs.push(`[setup] 跳过: 仅 GitHub/ZIP 安装的 skill 允许执行 setup 脚本`);
  } else if (setupScript.includes('..')) {
    logs.push(`[setup] 跳过: setup 脚本路径包含路径穿越`);
  } else {
    // 执行 setup，timeout 120s
  }
}
```

**前端（skills.vue 安装对话框）：**
- 安装完成后，如果 skill 包含 setup 脚本，弹出确认对话框：
  "此 Skill 包含安装脚本（{scriptName}），是否执行？"
- 用户确认后才调用 `/installDeps` 端点

### 10.10 references.routes 的实现策略

routes 匹配在 server 端执行。`read_skill` 工具新增可选参数 `task`：

```typescript
const ReadSkillParams = Type.Object({
  name: Type.String(),
  task: Type.Optional(Type.String({ description: '当前任务描述，用于自动匹配需要读取的文档' })),
});
```

server 端对 `task` 做关键词匹配（`match` 数组中的词是否出现在 task 中），命中则将对应 `required_refs` 的内容直接拼接到返回结果中，Agent 无需二次调用 `read_skill_file`。未命中则 fallback 到 `<skill_required_references>` 列表提示。

### 10.11 Symlink 去重

`scanSkills()` 中维护 `Set<string>` 存储已加载 skill 的 realpath。对每个 skill 目录调用 `fs.realpath()` 获取真实路径，如果已在 set 中则跳过（不产生碰撞诊断，因为是同一物理目录）。

```typescript
const realPathSet = new Set<string>();
// 在加载每个 skill 时
const realPath = await fs.realpath(path.join(this.skillsDir, entry.name)).catch(() => null);
if (realPath && realPathSet.has(realPath)) continue; // 静默跳过 symlink 重复
if (realPath) realPathSet.add(realPath);
```

### 10.12 skill_context.ts 废弃

`skill_context.ts` 的 `buildSkillContext()` 函数标记为 `@deprecated`，添加注释指向 `tool_resolver.ts` 中的新实现。不删除文件，避免破坏可能存在的外部引用。后续版本清理。

```typescript
/**
 * @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
 * 此函数不再被主链路调用。
 */
export function buildSkillContext(...) { ... }
```
