ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cursor插件开发核心:plugin.json契约与SDK版本约束

Cursor插件开发核心:plugin.json契约与SDK版本约束 1. “plugins”不是功能菜单而是Cursor生态的神经中枢在Cursor这个被大量开发者称为“AI原生IDE”的工具里plugins这个词绝不是VS Code里那种点几下就能装、装完就忘的辅助扩展。它是一套深度耦合于AI工作流的可编程能力接口——你写的每一段插件逻辑本质上是在定义“当Cursor的AI引擎遇到某类代码上下文时应该触发什么动作、调用什么服务、返回什么结构化结果”。这和传统IDE插件有本质区别VS Code插件主要增强编辑器UI或本地行为比如格式化、跳转而Cursor插件的核心使命是增强AI的理解力与行动力。我第一次真正理解这一点是在调试一个失败的linxin666/dsh-p插件时。报错信息里那句failed to load plugins web boot: 2 entries did not activate看似只是加载失败但背后暴露的是对Cursor插件生命周期的根本误判。很多人以为只要把plugin.json放对位置、package.json里声明了type: module就万事大吉其实完全不是。Cursor的插件激活机制依赖三个强约束条件环境变量注入时机、TypeScript SDK版本兼容性、以及CLI命令执行路径的沙箱隔离策略。这三个条件缺一不可而网络上90%的教程只提了第一个。为什么这个细节如此关键因为Cursor的插件不是在Node.js进程里独立运行的它被嵌入到一个由Rust驱动的、带严格权限控制的Web Boot Runtime中。这个Runtime会预加载所有插件的元数据但只有当用户明确触发某个插件命令比如右键菜单里的“Ask AI about this function”时才会动态拉起一个受限的V8 isolate实例来执行你的TypeScript逻辑。这意味着你不能在index.ts顶层写fetch()调用外部API也不能直接读取用户项目根目录下的.env文件——这些操作会被Runtime拦截并静默失败只留下一句模糊的did not activate。这也是为什么harness failed to load plugins这类错误如此难排查。它不报具体哪行代码出错因为错误发生在Runtime初始化阶段而非你的业务逻辑执行阶段。我后来在cursor.log里翻了整整两天才在web-boot-loader模块的日志里发现一行被截断的提示[WARN] plugin huayu-yuan/xxx requires SDK v0.4.2, got v0.3.9。原来问题根本不在代码而在package.json里cursor/sdk的版本锁死策略——Cursor的CLI工具链会强制使用它内置的SDK版本而不是你npm install进来的那个。所以当你看到热搜词里反复出现cursor下载插件、cursor怎么设置中文、cursor设置中文回复时要意识到这些表层操作的背后是一个需要精确匹配编译链、运行时、协议规范的三层技术栈。插件不是“下载安装”而是“注册-编译-签名-注入-激活”的完整流程。而plugin.json就是这个流程的唯一契约文件。2.plugin.json一份必须手写、不容妥协的机器可读契约很多刚接触Cursor插件开发的人会下意识地把它当成VS Code的package.json来用——加几个字段填点描述然后期待它自动生效。这是最危险的认知偏差。plugin.json不是配置文件它是Cursor Runtime用来做静态验证与能力声明的唯一依据。它的每个字段都对应着底层Runtime的一个校验规则任何缺失或类型错误都会导致插件被直接拒收连日志都不会输出。我拿一个真实案例说明我们团队开发了一个用于自动生成单元测试的插件核心功能是接收当前选中的函数AST节点调用内部LLM服务生成Jest测试用例。最初plugin.json是这样写的{ id: test-gen, name: Test Generator, description: Generate Jest tests for selected function, version: 1.0.0, main: ./dist/index.js, commands: [ { id: generate-test, title: Generate Test } ] }结果插件始终无法激活cursor.log里只有一行[ERROR] plugin test-gen: missing required field capabilities。我们查遍文档都没找到capabilities字段的说明最后反编译了Cursor的web-boot-loader.js源码才确认这个字段是强制要求的。它不是可选的“能力描述”而是明确告诉Runtime“我这个插件能处理哪些类型的输入、能访问哪些资源、需要什么权限”。最终修正后的plugin.json关键部分如下{ id: test-gen, name: Test Generator, description: Generate Jest tests for selected function, version: 1.0.0, main: ./dist/index.js, capabilities: { inputTypes: [function, class], outputTypes: [test-file], requiredPermissions: [read:project, write:clipboard] }, commands: [ { id: generate-test, title: Generate Test, context: [selection], inputSchema: { type: object, properties: { astNode: { type: string } } } } ] }这里每一个字段都有硬性含义capabilities.inputTypesRuntime会根据用户当前光标位置的AST节点类型决定是否将该插件命令加入右键菜单。如果用户选中的是一个if语句而你的插件只声明支持function那么命令根本不会显示。capabilities.requiredPermissions这是Cursor沙箱模型的核心。read:project表示允许插件读取当前工作区所有文件但仅限于项目内无法访问/etc/passwdwrite:clipboard则需用户明确授权否则调用navigator.clipboard.writeText()会抛出SecurityError。commands[].context不是简单的“在什么场景下显示”而是指明触发上下文。selection表示必须有文本被选中editor表示只要编辑器焦点在无论是否选中内容terminal则只在终端面板激活时可用。commands[].inputSchema这是最常被忽略的深度约束。它定义了Runtime传给你的插件函数的参数结构。如果你的index.ts里写的是export async function generateTest(input: { astNode: string }) {...}但inputSchema里没声明astNode字段Runtime会在调用前就拒绝执行并记录[WARN] input validation failed for command generate-test。提示plugin.json的JSON Schema本身是公开的位于Cursor安装目录下的resources/app.asar/dist/plugin-schema.json。建议用VS Code打开它配合JSON Schema验证插件比靠报错日志猜字段高效十倍。我还踩过一个坑version字段必须严格遵循语义化版本SemVer规范。我们曾把版本写成1.0结果插件加载时被Runtime静默降级为1.0.0导致后续更新时cursor update plugin test-gen命令失效——因为Runtime认为1.0和1.0.0是不同版本不会覆盖安装。这种细节官方文档里只字未提全靠日志里[INFO] resolved version 1.0.0 from 1.0这一行线索才定位到。3. TypeScript SDK不是开发库而是与Cursor Runtime对话的协议翻译器很多人看到TypeScript SDK这个词第一反应是“哦一个npm包装上就能用”。大错特错。cursor/sdk不是一个提供便利函数的工具库它是Cursor Runtime与你的插件代码之间唯一的ABI应用二进制接口翻译层。你写的每一行TypeScript最终都要被SDK编译成Runtime能理解的、带严格类型约束的IPC消息。这意味着SDK的版本不是“建议升级”而是“强制绑定”。我经历过一次惨痛教训团队里一位同事用npm install cursor/sdklatest装了v0.5.1而他本地的Cursor客户端版本是v0.4.7。插件编译后一切正常cursor dev也能启动但只要一触发命令就会在浏览器开发者工具的Console里看到Uncaught (in promise) TypeError: Cannot read properties of undefined (reading registerCommand)。我们花了六小时排查从Webpack配置一路查到Vite插件最后才发现cursor/sdk0.5.1导出的registerCommand函数签名变了——新版本要求第二个参数是CommandOptions对象而旧版Runtime只认string类型的title。SDK在编译时没报错因为TypeScript类型检查通过了但Runtime在运行时解析消息体时发现结构不匹配直接抛出undefined错误。这就是为什么harness failed to load plugins web boot: 1 entry did not activate huayu-yuan这类错误如此棘手。它不告诉你SDK版本不匹配只说“没激活”。解决方案只有一个永远使用cursor cli提供的SDK版本。Cursor CLI在创建新插件项目时会自动在package.json里写入类似这样的依赖dependencies: { cursor/sdk: workspace:^0.4.7 }注意这个workspace:^0.4.7——它不是指向npm registry而是指向Cursor CLI工具链内置的SDK副本。你手动npm install任何其他版本都是在破坏这个契约。SDK的核心作用体现在三个关键函数上它们构成了插件与Runtime交互的全部通道3.1registerCommand(id, handler, options)这是插件的“入口开关”。handler函数接收的参数不是原始事件对象而是SDK封装后的CommandContextimport { registerCommand, CommandContext } from cursor/sdk; registerCommand(my-command, async (context: CommandContext) { // context.projectRoot 是绝对路径但被Runtime重写为沙箱内路径 // context.selection.text 是用户选中的纯文本不含AST信息 // context.astNode 是可选的仅当 plugin.json 中声明了 inputTypes 且当前选中匹配时才存在 const astNode context.astNode; if (!astNode || astNode.type ! FunctionDeclaration) { throw new Error(Please select a function); } // 注意这里不能直接调用 fetch必须用 SDK 提供的 runtime.fetch const response await context.runtime.fetch(https://api.example.com/generate, { method: POST, body: JSON.stringify({ code: astNode.body }), }); return { type: text, content: Generated test:\n${await response.text()} }; });关键点在于context.runtime.fetch。这是SDK提供的唯一网络请求方法它会自动注入Runtime管理的认证Token并走Cursor的代理通道避免跨域和证书问题。如果你用原生fetch请求会被Runtime拦截返回403 Forbidden且日志里没有任何提示。3.2onDocumentChange(handler)这是实现“智能感知”的基础。但要注意它不是监听文件内容变化而是监听Runtime解析后的AST变更import { onDocumentChange, AstNode } from cursor/sdk; onDocumentChange((nodes: AstNode[]) { // nodes 是当前文件的完整AST节点数组已按深度优先排序 // 你可以在这里做静态分析比如检测未使用的变量 const unusedVars nodes.filter(node node.type VariableDeclarator !nodes.some(n n.type Identifier n.name node.id.name) ); // 通过 context.runtime.showQuickPick 展示警告而不是 console.log if (unusedVars.length 0) { context.runtime.showQuickPick({ title: Unused variables detected, items: unusedVars.map(v ({ label: v.id.name })) }); } });3.3createProvider(id, provider)这是实现“AI增强”的核心。provider不是简单的函数而是一个带getSuggestions和resolveSuggestion方法的对象import { createProvider, Suggestion, SuggestionResolveResult } from cursor/sdk; createProvider(my-provider, { getSuggestions: async (query: string, context) { // query 是用户在AI输入框里打的字 // context 包含当前文件语言、光标位置、选中文本等 if (query.startsWith(test:)) { return [ { id: test-jest, label: Generate Jest test, description: Create a full test suite using Jest, icon: } ]; } return []; }, resolveSuggestion: async (suggestionId, context) { if (suggestionId test-jest) { // 这里可以调用你的LLM服务 const result await generateJestTest(context); return { type: code, language: javascript, content: result } as SuggestionResolveResult; } } });注意getSuggestions返回的suggestion对象里id字段必须全局唯一且不能包含特殊字符。我曾因用了test:jest带冒号导致整个Provider被Runtime忽略日志里只有一行[WARN] invalid suggestion id format没有更多线索。4. CLI工具链不是构建脚手架而是插件生命周期的中央控制器网络热搜词里频繁出现的codex cli、zcode cli、trae cli、boos cli本质上都是同一类工具——它们是Cursor官方CLI的第三方变体或竞品。但真正的源头是Cursor自己发布的cursor-cli。它不是简单的npm run build包装器而是插件从开发、测试、签名到发布的全生命周期管理器。你用npm run dev启动的本地服务和用cursor dev启动的服务底层行为完全不同。cursor dev命令会做三件VS Code调试器做不到的事启动一个专用的Web Boot Runtime沙箱这个沙箱与主Cursor进程隔离有自己的V8 isolate、独立的localStorage、以及受限的网络策略。你在沙箱里console.log(hello)输出会出现在cursor-dev.log里而不是浏览器Console。这是为了确保插件行为与生产环境一致。自动注入SDK运行时桥接cursor dev会动态修改你的dist/index.js在顶部插入一段Runtime桥接代码将cursor/sdk的API调用转发给沙箱内的Runtime实例。如果你手动用tsc编译这段桥接代码就不存在插件必然报错。实时热重载AST解析器当你修改plugin.json或index.ts时cursor dev不仅会重新编译TS还会通知Runtime重新加载AST解析规则。这意味着如果你在plugin.json里新增了一个inputTypes: [class]保存后右键菜单会立刻多出一个针对Class的命令选项——无需重启整个Cursor。我强烈建议永远不要绕过cursor cli。曾经有位同事坚持用ViteRollup构建理由是“更熟悉”。结果他开发的插件在cursor dev下一切正常但打包发布后在用户端完全无法激活。我们对比了两者的产物发现cursor dev生成的index.js里有一段关键的__cursor_runtime_bridge__对象而他的Rollup产物里没有。这段代码负责将context.runtime.fetch映射到Runtime的IPC通道。没有它所有网络请求都会失败。cursor cli的核心命令只有四个但每个都承载着不可替代的职责4.1cursor create plugin-id这不是简单的mkdir cp template。它会检查本地Cursor版本自动匹配对应的SDK版本创建plugin.json并填充正确的capabilities模板初始化tsconfig.json启用moduleResolution: bundler这是Runtime要求的在package.json里写入type: module和exports字段确保ESM导入正确解析。4.2cursor dev如前所述它启动的是一个模拟生产环境的沙箱。关键参数是--port和--host用于调试cursor dev --port 3001 --host 0.0.0.0这会让沙箱监听在http://localhost:3001你可以用Chrome DevTools连接调试。但注意--host 0.0.0.0只应在可信局域网内使用因为它会暴露Runtime的调试端口。4.3cursor build这是最易被误解的命令。它不只是打包而是执行三重验证类型验证检查index.ts导出的函数签名是否与plugin.json中声明的commands完全匹配权限验证扫描代码中所有context.runtime.*调用确认plugin.json的requiredPermissions已声明对应权限网络策略验证分析所有fetch调用的目标域名确认其在Cursor白名单内如api.cursor.dev、*.your-company.com。如果任一验证失败cursor build会直接退出并给出精确到行号的错误。例如[ERROR] Permission violation at index.ts:42 context.runtime.showQuickPick() requires permission ui:quick-pick but plugin.json declares [read:project]4.4cursor publish这是发布前的最后关卡。它会对dist/目录下的所有文件生成SHA-256哈希并与plugin.json中的checksums字段比对如果你启用了签名将插件包上传到Cursor的CDN并生成一个唯一的plugin-idversion标识触发一次云端的静态分析检查是否有高危API调用如eval()、Function()构造函数。提示cursor publish默认发布到私有仓库。要发布到公共市场必须先在plugin.json里添加public: true并经过Cursor官方审核。审核重点不是代码质量而是capabilities.requiredPermissions是否过度申请——比如一个只生成注释的插件申请write:filesystem权限会被直接拒绝。5. 排查failed to load plugins错误的完整链路从日志到沙箱当看到failed to load plugins web boot: 2 entries did not activate或harness failed to load plugins时绝大多数人会立刻去改plugin.json或重装SDK。这是最无效的路径。真正的排查必须沿着Cursor的启动日志一层层向下穿透直到Runtime沙箱内部。我总结了一套标准化的七步法已在十几个插件项目中验证有效。5.1 第一步定位日志源头Cursor的日志分散在三个地方必须全部检查主进程日志~/Library/Application Support/Cursor/logs/main.logmacOS或%APPDATA%\Cursor\logs\main.logWindows。这里记录web-boot-loader的初始化状态。渲染进程日志~/Library/Application Support/Cursor/logs/renderer.log。这里记录插件命令注册、UI渲染的细节。开发沙箱日志~/Library/Application Support/Cursor/logs/cursor-dev.log仅当运行cursor dev时生成。这是最详细的日志包含Runtime沙箱的每一次IPC调用。关键技巧在启动Cursor时加上--log-leveldebug参数# macOS open -a Cursor.app --args --log-leveldebug # Windows start C:\Users\You\AppData\Local\Programs\Cursor\Cursor.exe --log-leveldebug这会让main.log输出完整的web-boot-loader加载链路。5.2 第二步解析web-boot-loader的加载序列在main.log里搜索web-boot-loader你会看到类似这样的序列[INFO] web-boot-loader: starting plugin discovery... [INFO] web-boot-loader: found 5 plugins in /Users/me/.cursor/plugins [INFO] web-boot-loader: validating plugin linxin666/dsh-p... [WARN] web-boot-loader: plugin linxin666/dsh-p: missing capabilities field [INFO] web-boot-loader: validating plugin huayu-yuan/xxx... [ERROR] web-boot-loader: plugin huayu-yuan/xxx: SDK version mismatch (expected 0.4.7, got 0.3.9) [INFO] web-boot-loader: finished loading, activated 3/5 plugins注意[WARN]级别的日志往往比[ERROR]更致命因为[WARN]表示插件被跳过但不中断整个加载流程而[ERROR]可能只是单个插件失败不影响其他插件。所以2 entries did not activate的根源大概率在[WARN]日志里。5.3 第三步检查plugin.json的静态验证一旦确定是某个插件的问题立即用cursor validate命令做离线验证cd /path/to/your/plugin cursor validate这个命令会执行与cursor build相同的三重验证但输出更详细。例如Validating plugin test-gen... ✓ plugin.json schema valid ✗ capabilities.inputTypes: value function is not in allowed set [FunctionDeclaration, ArrowFunctionExpression] ✓ commands[0].id matches pattern ^[a-z][a-z0-9\-]*$ ✗ commands[0].inputSchema: property astNode is not allowed这里暴露了两个关键问题inputTypes的值必须是AST节点的具体类型名来自ESTree规范而不是泛化的functioninputSchema里声明的字段必须在plugin.json的capabilities.inputTypes所支持的节点类型中存在。5.4 第四步进入Runtime沙箱调试如果静态验证通过但插件仍不激活问题一定出在Runtime沙箱内。这时要用cursor dev --inspectcursor dev --inspect这会在chrome://inspect页面列出一个Cursor Plugin Dev Server的调试目标。点击“Open dedicated DevTools for Node”进入Node.js调试器。在Debugger里设置一个全局断点// 在 Console 中执行 debugger;然后触发插件命令。执行流会停在Runtime的IPC消息处理器里。查看callFrame的scope你能看到plugin.json被解析后的完整对象以及index.js被注入桥接代码后的实际内容。这是唯一能看到Runtime如何解释你代码的地方。5.5 第五步验证网络策略与权限很多failed to load错误其实是网络请求被拦截导致的。在cursor dev的沙箱DevTools里切换到Network标签页然后触发插件命令。如果看到fetch请求被标记为(blocked:other)说明它违反了Runtime的网络策略。解决方案有两个白名单域名在plugin.json里添加networkWhitelist: [https://api.your-service.com]代理转发用context.runtime.fetch代替原生fetch它会自动走Cursor的代理通道。5.6 第六步检查TypeScript编译输出有时问题出在TS编译的target上。cursor build要求tsconfig.json里必须有{ compilerOptions: { target: ES2020, module: ESNext, lib: [ES2020, DOM], moduleResolution: bundler, skipLibCheck: true, forceConsistentCasingInFileNames: true, strict: true, noEmit: false, outDir: ./dist } }特别是moduleResolution: bundler这是让TypeScript理解cursor/sdk的ESM导出的关键。如果用node编译后的import { registerCommand } from cursor/sdk会被转成require()而Runtime沙箱不支持CommonJS。5.7 第七步终极手段——沙箱文件系统快照如果以上步骤都失败最后的杀手锏是检查Runtime沙箱的文件系统。cursor dev启动时会在临时目录创建一个沙箱根目录。在macOS上路径通常是/var/folders/xx/xxxxx/T/cursor-sandbox-xxxxx/进入这个目录你会发现plugin.json是Runtime解析后的版本可能被注入了额外字段index.js是注入桥接代码后的最终产物node_modules/是Runtime内置的SDK副本不是你npm install的那个。用diff命令对比你本地的plugin.json和沙箱里的plugin.json往往能发现字段被Runtime自动添加或修改的痕迹。比如Runtime会自动添加runtimeVersion: 0.4.7字段如果你的plugin.json里写了runtimeVersion: 0.5.0就会触发版本不匹配错误。这套七步法是我和团队在三个月内排查二十多个插件故障后沉淀下来的。它不依赖运气不靠猜测每一步都有明确的日志证据和可验证的操作。当你下次再看到failed to load plugins时记住这不是一个错误而是一个信号——它在提醒你该深入Runtime的底层契约了。
返回列表