GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md

766 lines
27 KiB
Markdown
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
# 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 Skills18 个生产级 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. P0Skill-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. P1Skill 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.yamlruntime / 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期望 JSONstderr 作为日志
→ 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. P2Agent 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 | 缺少 descriptionskill 不加载 |
| `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
- 详情抽屉新增"配置"tabenv 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 的 skillminimax-pdf 等)安装后,如果没有 skill.config.yaml 也按 prompt skill 处理;后续可逐步补充 config 文件升级为 compute-toolkit
## 8. 新增文件清单
| 文件 | 说明 |
|------|------|
| `service/skill_secret.ts` | P0密钥加密存储与解析 |
| `service/skill_executor.ts` | P1compute-entry 执行器 |
| `service/skill_config.ts` | skill.config.yaml 解析器 |
| `tools/builtin/execute_skill.ts` | P1Agent 工具 |
| `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 {
// 执行 setuptimeout 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(...) { ... }
```