
1. 从“plugins”这个词说起为什么它值得单独拎出来聊“plugins”这个词放在任何技术栈里都不算新鲜但放在 Cursor、Codex CLI、Claude Code 这类新一代 AI 编程工具语境下它的分量完全不一样了。过去我们聊插件聊的是编辑器扩展、浏览器扩展、IDE 的 addon本质上是给一个已经成型的软件做功能叠加。而现在聊 plugins聊的是给 AI 编程助手装“外挂能力”——让它能读你的项目规范、能调你的内部工具、能按你团队的约定生成代码、能接入你自己的 CLI 工作流。我最近几个月一直在折腾 Cursor 的插件体系、Codex CLI 的扩展机制以及各种plugin.json配置文件的写法。踩过的坑包括但不限于failed to load plugins web boot: 2 entries did not activate、harness failed to load plugins、插件注册了但 CLI 里死活不生效、TypeScript SDK 版本对不上导致插件加载直接静默失败。这些问题在官方文档里往往只有一句话但实际排查起来能耗掉一整个下午。这篇内容就是把这些经验整理出来。不管你是刚接触 Cursor 想搞清楚“plugins 到底能干什么”还是已经在写自己的plugin.json但被加载失败卡住或者想用 TypeScript SDK 给 CLI 工具做一套可复用的插件体系下面这些内容应该都能直接拿去用。我会从设计思路讲到具体配置再到排查技巧尽量把“为什么这么设计”和“实际怎么操作”都说明白。2. plugins 体系的核心设计思路拆解2.1 为什么 AI 编程工具需要插件机制先想清楚一个问题Cursor 这类工具本身已经能读代码、能补全、能对话了为什么还要搞 plugins答案在于通用能力和项目专属能力之间的鸿沟。通用能力是模型自带的比如“帮我写一个 React 组件”“解释这段 Python”。但项目专属能力是模型不知道的你们团队的 API 命名规范、内部 CLI 的调用方式、特定目录下必须遵守的代码结构、某个私有库的用法。这些东西你不可能每次都写在 prompt 里写了也容易漏。plugins 就是把这些“项目知识”和“工具能力”固化下来让 AI 在需要的时候自动调用。从架构上看plugins 体系通常包含三层声明层plugin.json描述插件元信息、能力层TypeScript SDK 或脚本实现具体逻辑、接入层CLI 或编辑器加载并注册插件。这三层任何一层出问题都会表现为“插件加载失败”或“插件不生效”。2.2 plugin.json 的角色与常见字段设计plugin.json是整个插件体系的入口。它告诉宿主程序这个插件叫什么、版本多少、入口文件在哪、需要什么权限、暴露哪些命令或工具。一个典型的plugin.json大概长这样{ name: team-codegen, version: 1.0.0, description: 团队代码生成规范插件, main: dist/index.js, commands: [ { name: gen-api, description: 按团队规范生成 API 层代码, entry: commands/genApi.js } ], permissions: [read:workspace, exec:internal-cli] }这里有几个字段特别容易出问题。main指向的入口文件如果路径不对或者构建产物没生成宿主加载时会直接报failed to load plugins。commands里的entry是相对路径基准目录是插件根目录不是main所在目录这一点很多人会搞混。permissions字段在不同宿主里名字可能不一样有的叫capabilities有的叫scopes写错了不会报错但插件运行时会被静默拒绝。提示plugin.json里所有路径字段都建议用相对路径并且确保构建产物和源码目录结构一致。用绝对路径在本地能跑换台机器或换 CI 环境大概率挂。2.3 TypeScript SDK 与 CLI 的协作模式TypeScript SDK 是写插件逻辑的主要方式。它提供了一套类型定义和运行时工具让你能注册命令、读取工作区信息、调用宿主暴露的 API。CLI 则是插件的另一种接入形态——有些插件不是给编辑器用的而是给命令行工具用的比如 Codex CLI 或自定义的zcode cli。这两者的协作模式通常是SDK 负责“定义能力”CLI 负责“触发能力”。比如你写了一个插件暴露一个gen-api命令在 Cursor 里可以通过命令面板触发在 CLI 里可以通过your-cli gen-api触发。关键在于命令注册的时机——如果插件在 CLI 启动之后才加载命令就不会被注册表现就是“命令不存在”。所以 CLI 类宿主通常要求插件在启动阶段同步加载异步加载的插件需要显式声明loadPhase: startup之类的字段。3. 核心细节解析与实操要点3.1 插件目录结构怎么定目录结构没有强制标准但有一套经过验证的约定能避免大部分路径问题。我一般这样组织my-plugin/ ├── plugin.json ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts │ └── commands/ │ └── genApi.ts ├── dist/ │ ├── index.js │ └── commands/ │ └── genApi.js └── README.mdsrc放源码dist放构建产物plugin.json里的main和entry全部指向dist。这样做的好处是本地开发和发布产物分离CI 里只需要跑一次tsc就能生成完整产物。很多人图省事直接把main指向src/index.ts本地用ts-node能跑但宿主加载时不会帮你做 TypeScript 编译结果就是failed to load plugins。tsconfig.json里记得把outDir设为distrootDir设为src并且开启declaration这样 SDK 的类型提示才能正常工作。如果插件要发布到团队内部 registrypackage.json里的files字段要包含dist和plugin.json否则安装后缺文件。3.2 命令注册的正确姿势命令注册是插件最核心的功能。以 TypeScript SDK 为例典型写法是import { PluginContext } from cursor/plugin-sdk; export function activate(ctx: PluginContext) { ctx.commands.register(gen-api, async (args) { const name args.name; // 具体逻辑 return { success: true }; }); }这里有几个细节。activate函数是宿主调用的入口必须导出。ctx.commands.register的第一个参数是命令名要和plugin.json里声明的name一致否则会出现“声明了但注册不上”的情况。回调函数可以是异步的但宿主对超时通常有默认限制长时间任务建议拆成“启动任务 查询状态”两步。注意不要在activate里做耗时操作比如读大文件、发网络请求。宿主加载插件时是串行的一个插件卡住会导致后续插件全部加载失败表现就是harness failed to load plugins。3.3 权限声明与运行时校验权限这块是最容易被忽略的。很多插件在本地开发时一切正常因为本地宿主可能默认放开所有权限。但到了 CI 或团队共享环境权限校验会变严插件就会静默失败。常见权限包括read:workspace读工作区文件、write:workspace写工作区文件、exec:shell执行 shell 命令、network网络请求。声明方式各宿主不同但原则是一样的用到什么声明什么不要多声明。多声明会导致审核不通过少声明会导致运行时被拒。如果插件运行时被拒通常不会有明显报错只是命令执行返回空或抛出一个模糊的异常。排查时可以先把权限全部声明上确认功能正常后再逐条删减定位到具体是哪条权限缺失。4. 实操过程与核心环节实现4.1 从零搭一个最小可用插件先搭一个最小插件只做一件事在 Cursor 里注册一个命令输出当前工作区路径。这个例子足够简单能跑通就说明整条链路没问题。第一步初始化项目mkdir my-first-plugin cd my-first-plugin npm init -y npm install -D typescript types/node npm install cursor/plugin-sdk第二步写tsconfig.json{ compilerOptions: { target: ES2020, module: CommonJS, outDir: dist, rootDir: src, declaration: true, strict: true }, include: [src] }第三步写src/index.tsimport { PluginContext } from cursor/plugin-sdk; export function activate(ctx: PluginContext) { ctx.commands.register(show-workspace, async () { const workspace ctx.workspace.rootPath; ctx.ui.showMessage(当前工作区: ${workspace}); return { success: true, workspace }; }); }第四步写plugin.json{ name: my-first-plugin, version: 1.0.0, main: dist/index.js, commands: [ { name: show-workspace, description: 显示当前工作区路径 } ], permissions: [read:workspace] }第五步构建并加载npx tsc然后在 Cursor 的插件目录里软链或复制这个文件夹重启编辑器命令面板里搜show-workspace能执行就说明链路通了。4.2 参数传递与返回值处理命令参数通过args传入类型取决于宿主。Cursor 里通常是对象CLI 里可能是位置参数或--flag。为了兼容两种宿主建议在插件内部做一层参数归一化function normalizeArgs(raw: any) { if (typeof raw string) { return { name: raw }; } return raw; }返回值建议统一成{ success: boolean, data?: any, error?: string }结构。宿主对返回值的处理方式不同有的会直接展示有的会忽略。统一结构能让调试更简单也方便后续接入日志系统。如果命令需要长时间执行比如生成一批文件建议先返回一个任务 ID再通过另一个命令查询进度。这样能避免宿主超时也能让用户看到进度。4.3 在 CLI 环境里加载插件CLI 环境的插件加载和编辑器不太一样。CLI 通常没有“插件目录”的概念而是通过配置文件或环境变量指定插件路径。以 Codex CLI 为例常见做法是在项目根目录放一个.codex/plugins.json列出要加载的插件{ plugins: [ ./plugins/my-first-plugin, /abs/path/to/another-plugin ] }CLI 启动时会按顺序加载这些插件。如果某个插件加载失败CLI 通常会打印failed to load plugins并继续启动但失败的插件命令不可用。排查时可以先只保留一个插件确认能加载后再逐个加回去。提示CLI 环境里插件的main字段必须是编译后的 JS不能是 TS。CLI 不会帮你做编译也不会读tsconfig.json。5. 常见问题与排查技巧实录5.1 加载失败类问题速查现象可能原因排查方法failed to load plugins web boot: 2 entries did not activate插件入口文件不存在或路径错误检查plugin.json的main字段确认dist/index.js存在harness failed to load plugins插件activate函数抛异常或超时在activate里加 try-catch打印日志插件加载了但命令不存在命令名不一致或注册时机太晚对比plugin.json和register里的命令名命令执行返回空权限不足或参数格式不对临时放开所有权限打印args原始值CLI 里插件不生效插件路径配置错误或未编译确认.codex/plugins.json路径确认dist存在5.2 几个我踩过的坑第一个坑是路径大小写。在 macOS 上路径不区分大小写dist/Index.js和dist/index.js都能找到文件。但到了 Linux CI 环境大小写敏感直接加载失败。解决办法是统一用小写文件名并且在plugin.json里严格匹配。第二个坑是依赖版本冲突。插件依赖的 SDK 版本和宿主内置的 SDK 版本不一致时可能出现类型不匹配或运行时错误。建议在package.json里把 SDK 设为peerDependencies让宿主提供而不是自己打包一份。第三个坑是异步加载顺序。如果插件 A 依赖插件 B 提供的命令但 B 加载比 A 慢A 在activate时找不到 B 的命令。解决办法是在plugin.json里声明dependencies让宿主按依赖顺序加载。5.3 日志与调试技巧插件调试最痛苦的是没有明显报错。我的做法是在activate开头和结尾各打一条日志输出插件名和加载耗时。如果只看到开头没看到结尾说明中间卡住了。日志输出到文件比输出到控制台更可靠因为 CLI 环境的控制台可能被宿主占用。import fs from fs; function log(msg: string) { fs.appendFileSync(/tmp/plugin-debug.log, [${new Date().toISOString()}] ${msg}\n); } export function activate(ctx: PluginContext) { log(activate start); // ... log(activate end); }这个日志文件在排查failed to load plugins类问题时特别有用能快速定位是哪个插件、哪一步出的问题。6. 插件体系的扩展方向与个人体会插件体系跑通之后能扩展的方向其实很多。比如把团队内部的代码规范检查做成插件在生成代码时自动校验把常用的 CLI 工具封装成插件命令在编辑器里直接调用甚至可以把插件和 CI 打通让插件在提交前自动跑一遍检查。我个人在实际操作中的体会是插件的第一版一定要足够小。不要一上来就做“全能插件”先做一个命令、跑通链路、确认加载和权限都没问题再逐步加功能。我见过太多插件因为一开始就堆了太多逻辑结果activate超时整个插件体系都加载不起来。另外plugin.json里的字段宁少勿多用不到的权限不要声明用不到的配置不要写减少出错面。最后日志一定要加而且要在第一行就加这样出问题时你至少知道插件有没有被加载。