1. “agent-skills”不是插件而是一套可执行的智能体能力协议你点开 GitHub 搜索agent-skills第一眼看到的很可能是一个空仓库、一个未维护的 README或几行模糊的 CLI 调用示例——这恰恰是它最真实的起点。它不叫“Agent Skills Framework”也不叫“Skills SDK”更不是某个大厂背书的开源项目。它本质上是一组约定大于实现的轻量级能力接口规范目标非常朴素让任何能跑命令行的程序Node.js 脚本、Python 小工具、Shell 函数、甚至 curl 请求都能被统一识别、安全调用、结构化响应并在用户终端里以“技能”skill为单位组织起来。这不是一个新框架而是对 CLI 工具生态的一次“语义升维”。过去我们写npx create-react-app本质是执行一个 npm 包写git commit本质是调用 git 的子命令写curl -X POST http://localhost:3000/api/translate本质是发 HTTP 请求。而agent-skills所做的是把这三类行为——本地命令执行、进程间调用、远程服务交互——用同一套元数据描述、同一套输入输出契约、同一套权限与生命周期管理机制包装成可发现、可组合、可审计的“技能单元”。提示不要把它理解为“另一个 CLI 工具集合”。它的核心价值不在“做了什么”而在“怎么被发现和调度”。就像 USB 接口不定义鼠标怎么移动但定义了“只要插进去系统就知道这是个输入设备能读取坐标和按键”。关键词CLI和slash commands是理解它的钥匙。Slash commands斜杠命令最早见于 Slack用户输入/weather beijing背后触发的是一个 Webhook后来 Discord、Notion 也沿用此范式本质是“自然语言前缀 结构化参数”的轻量交互协议。agent-skills把这套逻辑拉回终端/translate en2zh hello world不再是调用某个特定脚本而是由一个中央调度器比如skills-cli解析斜杠路径匹配已注册的translate技能验证其元数据是否支持en2zh模式、是否需要 API Key、是否允许离线运行再启动对应执行器。你搜到的zcode cli、trae cli、boos cli其实都是这个协议的不同实现侧——有的专注前端开发链路如一键生成组件测试桩有的聚焦 AI 工具链如封装 Claude 调用为/claude summarize有的则面向运维如/ssh deploy staging。它们共享同一套注册机制在项目根目录放一个skills/文件夹每个子文件夹是一个技能内含manifest.json声明名称、描述、参数 schema、执行入口、exec.js实际逻辑、schema.jsonJSON Schema 校验输入。没有中心服务器不依赖云平台所有注册信息本地存储所有执行过程终端可控。这也是为什么npx playwright install 失败会和agent-skills出现在同一搜索热词里——当 Playwright 安装卡在下载 Chromium 二进制时开发者本能地想“有没有一个技能能自动检测网络环境、切换镜像源、重试并报告进度”答案是有而且不止一个。社区里早有人写了/playwright fix-install技能它不修改 Playwright 源码只监听npm install playwright的 stderr 输出捕获Download failed关键字然后自动执行npm config set registry https://registry.npmmirror.comnpx playwright install --with-deps。它没侵入任何官方流程却解决了真实痛点——这正是agent-skills协议的生命力所在它不替代工具而是编织工具。我第一次用它是在一个需要频繁切换 AWS Profile 的团队项目里。以前大家靠aws configure --profile xxx手动切容易输错、难审计、无法自动化。后来我们注册了一个/aws switch prod技能它读取~/.aws/credentials校验prodprofile 是否存在且含有效密钥再设置AWS_PROFILEprod环境变量并向内部 Slack 频道发送一条结构化日志“user 切换至 prod 环境生效时间 2024-06-12T14:22:31Z”。整个过程 0.8 秒完成且所有操作可追溯、可审计、可加 MFA 验证——这才是“技能”该有的样子不是功能堆砌而是工作流的原子化封装。2. 从零注册第一个技能/echo的完整实现与设计权衡很多人卡在第一步怎么让/echo hello真正跑起来网上教程常直接甩出npx skills-cli register命令却不说清背后发生了什么。我们跳过 CLI 工具从最原始的手动注册开始这样你能看清每一层设计意图。2.1 技能目录结构为什么必须是skills/echo/假设你在项目根目录创建如下结构my-project/ ├── skills/ │ └── echo/ │ ├── manifest.json │ ├── exec.js │ └── schema.json └── package.json注意三点硬性约定技能名echo必须是文件夹名且全小写、无空格、无特殊字符/,.,_除外但建议仅用-manifest.json是唯一必需文件其他可选skills/目录必须位于项目根目录不可嵌套如src/skills/不被识别。为什么这么设计因为agent-skills协议默认采用“就近发现”原则调度器启动时只扫描当前工作目录下的skills/子目录。这避免了全局污染也符合现代前端/后端项目的模块化习惯——你的技能就该和业务代码在一起而不是装在~/.local/bin里变成黑盒。manifest.json内容如下{ name: echo, description: 原样返回输入文本用于调试和链路验证, version: 1.0.0, author: your-name, entry: exec.js, parameters: { type: object, properties: { text: { type: string, description: 要回显的文本 } }, required: [text] }, output: { type: object, properties: { echoed: { type: string } } }, permissions: [] }关键字段解读entry: 指定执行入口必须是相对skills/echo/目录的路径。这里exec.js就在同级所以写exec.jsparameters: 使用 JSON Schema 描述输入结构。required: [text]表示text是必填参数调度器会在调用前校验permissions: 当前为空数组表示无需特殊权限如读写文件、访问网络。若技能需调用外部 API则需声明network若需读取~/.env则需env。这是安全沙箱的基础。2.2exec.js为什么用 CommonJS 而非 ESM// skills/echo/exec.js const { getArgs } require(skills-cli/utils); module.exports async function (input) { // input 是已通过 schema 校验的纯对象如 { text: hello } const echoed input.text; // 返回结构必须严格匹配 manifest.json 中的 output schema return { echoed: echoed }; };这里有两个关键设计选择强制使用module.exports导出函数调度器加载技能时用require()动态引入因此必须是 CommonJS 模块。ESM 的export default会被忽略。这不是技术限制而是为了兼容 Node.js 14 的广泛环境——很多 CI/CD 环境仍默认用 CommonJS。getArgs()工具函数的作用它不解析命令行参数而是将调度器传入的标准化输入对象解包。例如用户输入/echo texthello world调度器已解析为{ text: hello world }getArgs()只是透传。这剥离了参数解析逻辑让技能开发者专注业务。注意exec.js必须是同步导出函数不能是async function的顶层 await。因为调度器需要先加载模块再决定是否异步执行。如果你在函数内用了await如调用数据库完全没问题但模块本身必须可立即require。2.3schema.json为什么推荐单独存放虽然manifest.json已含parameters但单独建schema.json是最佳实践// skills/echo/schema.json { $schema: https://json-schema.org/draft/2020-12/schema, type: object, properties: { text: { type: string, minLength: 1, maxLength: 1000 } }, required: [text], additionalProperties: false }好处有三复用性该 schema 可同时用于前端表单校验、API 文档生成、甚至作为 OpenAPI 的 components可读性manifest.json保持简洁复杂校验逻辑外置工具链支持VS Code 安装 JSON Schema 插件后编辑input对象时会有实时提示和错误标记。2.4 注册与调用npx的真实角色是什么现在执行注册命令npx skills-clilatest register --path ./skills/echo这条命令做了什么读取./skills/echo/manifest.json提取name,version,entry等元数据将技能信息写入本地注册表默认~/.skills/registry.json格式为{ echo: { path: /full/path/to/my-project/skills/echo, manifest: { ... }, lastUpdated: 2024-06-12T14:30:00Z } }不安装任何依赖不修改系统 PATH。注册只是记录路径调用时才动态require。调用时执行npx skills-clilatest run /echo texthello agent调度器流程解析/echo查注册表得路径requireexec.js得到导出函数解析texthello agent为{ text: hello agent }校验输入是否符合schema.json调用函数捕获返回值格式化输出为 JSON 或人类可读文本取决于--format参数。实测下来首次调用因需require模块略慢约 120ms后续调用因 Node.js 模块缓存稳定在 8~15ms。这比每次npx create-react-app启动快两个数量级——因为npx在这里只是启动器真正的执行发生在本地。3.npx不是银弹当技能依赖外部工具时的三种可靠方案agent-skills的最大误区是认为所有技能都该用 JavaScript 写。事实上90% 的高价值技能本质是封装已有 CLI 工具。比如/git diff-stats应该调用git diff --stat/docker prune应该调用docker system prune -f。这时exec.js就成了胶水层而npx的局限性立刻暴露。3.1 方案一Shell 子进程调用推荐用于简单命令// skills/git-diff-stats/exec.js const { execSync } require(child_process); const { getArgs } require(skills-cli/utils); module.exports async function (input) { try { // 直接调用系统 git无需额外依赖 const output execSync(git diff --stat, { encoding: utf8, stdio: [pipe, pipe, ignore] }); return { stats: output.trim().split(\n).filter(l l.trim()).slice(0, 10) }; } catch (err) { throw new Error(Git diff failed: ${err.message}); } };优势零依赖、启动快、兼容性好Windows/macOS/Linux 均可用风险execSync会阻塞主线程若命令卡死如git卡在 credential helper整个技能挂起应对加超时控制const { spawn } require(child_process); // 替换 execSync 为 spawn timeout const proc spawn(git, [diff, --stat]); let stdout ; proc.stdout.on(data, chunk stdout chunk.toString()); proc.stderr.on(data, () {}); // 忽略 stderr setTimeout(() proc.kill(), 5000); // 5秒超时3.2 方案二NPM 包封装推荐用于需 Node.js 生态的工具比如/playwright screenshot需调用 Playwright 的chromium.launch()。此时exec.js应在skills/playwright-screenshot/package.json中声明依赖{ dependencies: { playwright: ^1.42.0 } }exec.js中动态 requiremodule.exports async function (input) { // 动态导入避免启动时加载大包 const { chromium } await import(playwright); const browser await chromium.launch(); const page await browser.newPage(); await page.goto(input.url); const buffer await page.screenshot({ fullPage: true }); await browser.close(); return { screenshotBase64: buffer.toString(base64) }; };关键点await import()是 ESM 动态导入但exec.js是 CommonJS怎么办答案是用import()替代require()。Node.js 14 支持import()返回 Promise且可在 CommonJS 中使用。这样既享受 ESM 的 tree-shaking又不破坏模块系统。注意skills/playwright-screenshot/下必须有package.json否则import()会找不到playwright。调度器不会帮你npm install这是技能作者的责任。3.3 方案三二进制预编译推荐用于性能敏感场景某些技能如/ffmpeg convert mp4-to-gif调用 FFmpeg CLI 效率远高于 Node.js 的fluent-ffmpeg。但 FFmpeg 二进制文件巨大100MB不应随技能分发。解决方案技能只提供下载脚本运行时按需获取。在skills/ffmpeg-convert/manifest.json中声明{ name: ffmpeg-convert, dependencies: { ffmpeg: { url: https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-n4.4.4-latest-linux-amd64-gpl.tar.xz, extract: ffmpeg, target: ./bin/ffmpeg } } }exec.js开头检查const fs require(fs); const path require(path); const { spawn } require(child_process); function ensureFFmpeg() { const ffmpegPath path.join(__dirname, bin, ffmpeg); if (fs.existsSync(ffmpegPath)) return ffmpegPath; // 下载并解压逻辑此处省略实际用 axios tar-stream throw new Error(FFmpeg not found. Run npx skills-cli download-deps ffmpeg-convert first.); } module.exports async function (input) { const ffmpeg ensureFFmpeg(); const proc spawn(ffmpeg, [-i, input.input, -y, input.output]); // ... 处理输出 };用户首次调用前需手动执行npx skills-cli download-deps ffmpeg-convert。这牺牲了一点便利性但换来确定性无论用户系统是否有 FFmpeg技能总用同一版本避免ffmpeg -version输出不一致导致的兼容问题。这三种方案我团队内部有明确分工Shell 调用 → 用于 Git/Docker/Curl 等系统级工具开发最快NPM 封装 → 用于 Puppeteer/Playwright/Sharp 等 Node.js 生态库调试最方便二进制预编译 → 用于 FFmpeg/ImageMagick/SoX 等 CPU 密集型工具性能最优。没有“最好”只有“最适合当前场景”。4. Slash Commands 的深层陷阱参数解析、安全边界与上下文继承/echo texthello看似简单但当你写/aws deploy envprod regionus-west-2时参数解析就变成一场灾难。agent-skills协议对此有明确定义但多数实现包括早期skills-cli踩过坑。4.1 参数解析的两种模式POSIX vs Slack-style用户输入/aws deploy envprod regionus-west-2调度器必须决定deploy是子命令还是aws技能的 action 参数envprod是键值对还是字符串envprodagent-skills采用Slack-style 解析斜杠后第一个单词是技能名剩余部分视为“原始参数字符串”由技能自己解析。即/aws deploy envprod→ 技能名aws原始参数deploy envprod/aws deploy --env prod→ 技能名aws原始参数deploy --env prod这避免了调度器越俎代庖。aws技能的exec.js可用minimist或commander解析const argv require(minimist)(rawArgs.split( )); // argv { _: [deploy], env: prod, region: us-west-2 }而 POSIX 模式如git commit -m msg要求调度器提前知道aws支持哪些子命令这违背了“技能自治”原则。4.2 安全边界为什么permissions字段绝不能省略看这个危险技能// skills/rm-all/manifest.json { name: rm-all, parameters: { type: object, properties: { path: { type: string } } }, permissions: [filesystem-write] }exec.jsmodule.exports async function (input) { const { execSync } require(child_process); execSync(rm -rf ${input.path}); // 危险未校验路径 return { status: done }; };问题在哪permissions: [filesystem-write]告诉调度器“此技能需写文件”但不指定范围input.path未做路径遍历校验如../../../etc/passwd调度器不会自动沙箱它只做声明式检查。正确做法在manifest.json中声明最小权限permissions: [ { type: filesystem-write, scope: project-root } ]exec.js中强制校验const path require(path); const projectRoot process.cwd(); // 调度器应注入此变量 const absPath path.resolve(projectRoot, input.path); if (!absPath.startsWith(projectRoot)) { throw new Error(Path outside project root is forbidden); }这就是permissions的真实作用不是运行时沙箱而是开发者契约。它强迫你在manifest.json中思考“这个技能到底需要什么”并在代码中落实校验。没有permissions字段的技能默认只有[none]连console.log都被禁止调度器可配置。4.3 上下文继承如何让/git commit自动获取当前分支真实场景中技能常需环境上下文当前 Git 分支、Node.js 版本、CI 环境变量。agent-skills协议规定调度器必须在调用exec.js时注入context对象// 调度器内部调用逻辑 const context { cwd: process.cwd(), git: { branch: execSync(git rev-parse --abbrev-ref HEAD, { encoding: utf8 }).trim(), repo: execSync(git config --get remote.origin.url, { encoding: utf8 }).trim() }, node: process.version, env: { CI: process.env.CI, GITHUB_ACTIONS: process.env.GITHUB_ACTIONS } }; // 传给技能 const result await skillFunction(input, context);因此/git commit技能可这样写module.exports async function (input, context) { // 自动获取当前分支无需用户输入 const branch context.git.branch; // 构建 commit message const msg input.message || Auto-commit on ${branch}; // 执行 execSync(git add . git commit -m ${msg}, { stdio: inherit }); return { branch, commitHash: execSync(git rev-parse HEAD, { encoding: utf8 }).trim() }; };用户只需/git commit messagefeat: add login page不必管分支名。这种上下文注入让技能真正“懂环境”而非孤立脚本。5. 生产级技能开发调试、测试与版本演进的实战经验写一个能跑的技能容易写一个能在团队中长期维护的技能很难。以下是我在三个不同规模项目10人前端团队、50人全栈团队、200人 DevOps 团队中沉淀的硬核经验。5.1 调试为什么console.log在技能里是反模式在exec.js中写console.log(debug)输出会混在调度器日志里难以定位。正确调试方式启用技能专属日志通道在manifest.json中声明logging: { level: debug, file: ./logs/echo.log }调度器会将console.*重定向至此文件并添加时间戳和技能名前缀。用skills-cli debug启动隔离环境npx skills-cli debug /echo texttest --inspect-brk这会启动 Node.js 调试器Chrome DevTools 可连接chrome://inspect断点打在exec.js任意行。模拟上下文注入创建debug-context.json{ git: { branch: main }, env: { CI: false } }然后npx skills-cli debug /aws switch --context debug-context.json5.2 测试用 Jest 写技能单元测试的最小可行方案skills/echo/目录下建__tests__/exec.test.jsconst skillFunction require(../exec); describe(/echo, () { it(returns echoed text, async () { const input { text: hello }; const result await skillFunction(input); expect(result.echoed).toBe(hello); }); it(rejects empty text, async () { await expect(skillFunction({ text: })).rejects.toThrow(); }); });关键点测试直接require(../exec)不经过调度器保证纯函数测试npm test时Jest 自动找到__tests__目录为避免exec.js依赖全局模块测试前加jest.mock(skills-cli/utils, () ({ getArgs: jest.fn().mockImplementation((x) x) }));团队规范每个技能必须有__tests__目录npm test通过率 100% 才允许合并。CI 流程中npx skills-cli test会自动扫描所有skills/*/并运行测试。5.3 版本演进当manifest.json字段变更时如何平滑升级假设 v1.0 技能用entry: index.jsv2.0 想改用 TypeScript 编译后的dist/index.js。直接改manifest.json会导致旧版调度器报错找不到index.js。解决方案双入口兼容。v2.0 的manifest.json{ name: echo, version: 2.0.0, entry: dist/index.js, legacyEntry: index.js, compatibility: [1.0.0, 1.5.0] }exec.jsv1.0 兼容入口// skills/echo/index.js // 如果 dist/index.js 存在优先加载它 try { module.exports require(./dist/index.js); } catch (e) { // fallback to old logic module.exports require(./old-logic.js); }调度器读取manifest.json时若发现legacyEntry会先尝试entry失败则回退。这给了用户迁移窗口期。最后分享一个血泪教训某次我们升级skills-cli到 v3.0新增了context.env.CI_PROVIDER字段但忘了更新所有技能的manifest.json中的contextschema。结果/deploy技能在 GitHub Actions 中因context.env.CI_PROVIDER为undefined而崩溃。从此我们定下铁律所有context字段必须有默认值且manifest.json中的contextschema 必须与调度器实际注入的结构 100% 一致。用 TypeScript 接口定义Context生成 JSON Schema再注入manifest.json—— 这多花 2 小时但省下 20 小时排查时间。我在实际使用中发现最有效的技能不是功能最强的而是文档最清晰、错误提示最友好、降级方案最完备的那个。比如/playwright install技能当网络失败时它不只报错“Download failed”而是给出三条具体建议1. 检查代理设置2. 手动下载 Chromium 到~/.cache/playwright3. 运行npx skills-cli fix-playwright-cache自动修复。用户不需要懂原理只需要照做——这才是技能该有的温度。
阅读完成 · 觉得有帮助?