27 KiB
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
# --- 运行时声明 ---
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 表新增两个字段:
// 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
// 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 注入到子进程。判断逻辑:
// 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
// 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
// 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:
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 示例:
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,只传递白名单:
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 类型:
<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 返回格式改造
// 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 可以在两个位置声明:
skill.config.yaml的references字段(compute skill)- 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 诊断数据结构
// 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 新增:
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() 需要重构:
// 当前签名
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)
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 脚本。
实施时只需:
- 在
entity/skill.ts中添加新字段(secrets、envSchema、skillTypeV2) - 开发环境重启后 TypeORM 自动同步表结构
- 生产环境通过 MCP
execute工具执行 ALTER TABLE(如需要)
现有行新字段为 NULL,不影响现有功能。skillTypeV2 在 scanSkills 时根据有无 skill.config.yaml 自动填充。
10.5 类型同步
shared/types/skill.types.ts 更新:
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)中新增校验逻辑:
// 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() 新增排除列表:
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() 执行完成后写入结构化日志:
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 脚本路径不包含
..(路径穿越防护)
// 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:
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 中则跳过(不产生碰撞诊断,因为是同一物理目录)。
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 中的新实现。不删除文件,避免破坏可能存在的外部引用。后续版本清理。
/**
* @deprecated 使用 tool_resolver.ts 中的 skill 工具注入逻辑替代。
* 此函数不再被主链路调用。
*/
export function buildSkillContext(...) { ... }