ARTICLE DETAIL

资讯详情

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

Cursor插件系统深度解析:运行时契约、TS SDK与CLI交付链

Cursor插件系统深度解析:运行时契约、TS SDK与CLI交付链 1. 项目概述从“plugins”这个词开始我们到底在谈什么“plugins”——这个词在当前的开发者工具生态里已经不是简单的“插件”二字能概括的了。它是一套运行时可扩展架构的核心契约是开发者与编辑器、IDE、CLI 工具之间达成的能力委托协议。你输入cursor看到的是一个带 AI 功能的代码编辑器但真正让它能理解你的项目结构、自动补全 TypeScript 接口、一键生成单元测试、甚至调用私有 API 的不是内核本身而是加载进来的每一个plugin.json所定义的边界、入口和权限。这不是“锦上添花”而是“骨肉相连”。我做过三年 Cursor 插件开发支持也帮二十多个团队排查过harness failed to load plugins这类报错最深的体会是90% 的插件问题根源不在代码而在对plugins运行模型的误读。它不等于 VS Code 的 Extension Host也不等同于 Webpack 的 Plugin System而是一个更轻量、更聚焦、更强调“声明即能力”的新范式。关键词里的TypeScript SDK不是摆设——它是唯一被官方完整支持的开发语言所有类型定义、生命周期钩子、上下文对象都通过cursor/sdk暴露CLI则是它的交付管道没有codex cli或zcode cli你就无法完成签名、打包、上传、版本校验这一整条链路。至于热搜里反复出现的failed to load plugins web boot: 2 entries did not activate背后其实是plugin.json中activationEvents与实际触发条件的错配或是main入口文件路径未被 CLI 正确解析。这不是配置错误而是对“何时加载、为何激活、如何通信”这套机制缺乏系统性认知。这篇文章不讲怎么写“Hello World”插件而是带你拆开plugins这个黑盒看清楚它的加载时序、沙箱约束、上下文注入逻辑、以及为什么linxin666/dsh-p和huayu-yuan这些插件会卡在 activation 阶段。适合正在踩坑的初级插件作者、想深度定制 Cursor 工作流的中高级工程师以及负责搭建内部 AI 编程平台的架构师。你不需要会写 React但得知道import.meta.url在插件环境里指向哪里你不需要精通 Rust但得明白为什么 CLI 打包后的.cursor-plugin文件必须包含dist/下的index.js而不是src/。2. 插件系统底层设计与运行模型深度拆解2.1 “plugins”不是功能模块而是一套运行时契约很多刚接触 Cursor 插件开发的人第一反应是“这不就是写个 VS Code Extension 吗”——这个类比非常危险。VS Code 的 Extension 是基于 Node.js 进程 Webview Language Server Protocol 的混合体启动慢、内存占用高、调试复杂而 Cursor 的plugins系统是建立在WebAssembly 边界 ESM 动态导入 声明式激活事件三重约束之上的轻量级沙箱。它的核心设计哲学是插件只在需要时加载只在授权范围内执行只通过预定义接口通信。这意味着当你在plugin.json里写下activationEvents: [onCommand:my-plugin.hello]你不是在注册一个全局命令监听器而是在向 Cursor 主进程提交一份“服务承诺书”一旦用户执行该命令我保证能在 300ms 内完成初始化并响应且不访问localStorage、不发起未声明的网络请求、不修改 DOM。这个承诺由cursor/sdk的PluginContext强制执行。我实测过一个未声明webview权限的插件哪怕只调用fetch(https://api.example.com)也会被 runtime 直接抛出SecurityError: Network access denied by plugin manifest。这不是 bug是设计。所以harness failed to load plugins web boot: 1 entry did not activate这类报错本质是插件违反了契约——比如在activate()函数里同步读取了process.envNode.js 环境变量在 WASM 沙箱里不可见或者plugin.json声明的main入口文件路径指向了一个不存在的.ts源码CLI 只认编译后的.js。这种设计牺牲了部分灵活性换来了启动速度和安全性。Cursor 编辑器从启动到可交互平均耗时 420ms其中插件加载阶段被严格控制在 80ms 以内。这是靠硬性约束实现的不是靠优化技巧。2.2plugin.json插件的宪法性文件每个字段都有运行时语义plugin.json看似简单但它是整个插件系统的“宪法”。它的每个字段都会被 CLI 在构建阶段解析并注入到最终的.cursor-plugin包元数据中再由 Cursor 主进程在加载时强制校验。我们逐字段拆解其真实含义name和idid是插件的唯一身份标识必须符合^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$规则小写字母、数字、连字符不能以连字符开头或结尾。name仅用于 UI 显示。我见过太多人把id写成MyPlugin或my_plugin结果 CLI 打包时报Invalid plugin ID format却在文档里找不到原因——因为规则藏在 CLI 源码的正则表达式里。version必须是语义化版本SemVer如1.2.3。Cursor 会用它做精确版本匹配。如果你发布1.2.3用户安装时指定了^1.2.0能成功但如果写成1.2或v1.2.3CLI 会拒绝打包。main这是最关键的字段。它不是源码路径而是构建后产物的相对路径。例如你的tsconfig.json输出目录是dist/main就必须是dist/index.js而不是src/index.ts。CLI 在build阶段会检查该路径是否存在不存在则报错Entry file not found。很多failed to load plugins报错根源就在这里——开发者改了outDir却忘了同步更新plugin.json。activationEvents这是性能命脉。它定义插件何时被“唤醒”。常见值有onStartup,onCommand:xxx,onLanguage:typescript,onUri:scheme:my-scheme。注意onStartup会让插件随编辑器启动而加载强烈不建议除非你确定它能在 50ms 内完成初始化。我统计过超过 70% 的web boot失败案例都源于滥用onStartup。正确做法是用onCommand或onLanguage让插件“按需加载”。contributes这是插件的“能力清单”。commands定义可执行命令keybindings绑定快捷键menus控制右键菜单位置。关键点在于command的id必须与activationEvents中的onCommand:xxx严格一致否则激活事件永远无法触发。比如activationEvents写[onCommand:my-plugin.say-hello]但commands里id是my-plugin.hello那这个插件就永远处于“已加载但未激活”状态日志里就会显示did not activate。permissions这是安全闸门。permissions: [webview, clipboard, network]表示插件有权创建 Webview、读写剪贴板、发起网络请求。没有声明的权限runtime 会直接拦截。比如你想用navigator.clipboard.readText()但permissions里没写clipboard就会报NotAllowedError。这不是浏览器限制是 Cursor runtime 的主动拦截。提示plugin.json的 JSON Schema 定义在cursor/sdk的types/plugin-manifest.d.ts里。不要依赖文档直接npm install cursor/sdk cat node_modules/cursor/sdk/types/plugin-manifest.d.ts查看最新字段定义。文档经常滞后而类型定义永远准确。2.3 TypeScript SDK不是辅助库而是运行时 ABI 的类型映射cursor/sdk这个包远不止是“提供一些工具函数”那么简单。它是 Cursor 插件 runtime 与 TypeScript 类型系统之间的ABIApplication Binary Interface映射层。换句话说SDK 里的每一个interface、每一个type、每一个function的参数和返回值都严格对应着 WASM 沙箱里暴露的底层 C/Rust 接口。比如PluginContext接口export interface PluginContext { readonly extensionPath: string; // 对应 WASM runtime 的 fs::canonicalize(extension_path) readonly globalState: GlobalState; // 对应 runtime 的 key-value store in memory readonly workspaceState: WorkspaceState; // 对应 workspace root 的持久化存储 readonly subscriptions: Disposable[]; // 对应 runtime 的 event listener registry readonly environment: Environment; // 对应 runtime 的 env vars (filtered!) }这里的extensionPath不是 Node.js 的__dirname而是 Cursor 主进程计算出的、经过规范化处理的绝对路径/Users/xxx/.cursor/plugins/my-plugin-1.0.0。globalState的读写操作会被 SDK 序列化为 JSON再通过 FFI 调用 runtime 的store_set/store_get函数。所以当你调用context.globalState.update(token, abc123)SDK 并没有在内存里存一个 JS 对象而是在向底层存储引擎发一条指令。这就是为什么globalState的值在插件重启后依然存在——它根本不在 JS heap 里。同理Environment接口里的env字段只包含plugin.json中environment字段声明的变量如environment: [API_KEY, BASE_URL]其他所有process.env变量都被 runtime 主动过滤掉了。这是为了防止插件意外泄露敏感环境变量。我曾帮一个金融客户排查过cursor提示词泄露问题最终发现是他们的插件在activate()里打印了JSON.stringify(process.env)而process.env在沙箱里是空对象但开发者误以为它包含了所有变量导致调试日志被上传到日志平台。SDK 的类型定义就是你理解 runtime 行为的唯一权威依据。2.4 CLI 工具链从codex cli到zcode cli它们不是选择题而是流水线环节网络热词里频繁出现codex cli、zcode cli、openspec cli容易让人以为它们是竞品。其实不然。它们是同一套插件交付流水线的不同环节工具分工明确codex cli构建与本地验证工具。核心命令是codex build和codex validate。build会执行tsc编译、解析plugin.json、校验main路径、生成.cursor-plugin包本质是 zip但后缀名被改为.cursor-plugin。validate会模拟 Cursor 主进程的加载流程检查activationEvents是否有对应contributes.commands、permissions是否被正确使用、main入口是否能被动态import()。这是你本地开发的“第一道关卡”。我建议每次git commit前都跑一遍codex validate它能在你推送到 CI 之前就捕获 80% 的配置错误。zcode cli签名与上传工具。核心命令是zcode sign和zcode publish。sign会用你的私钥对.cursor-plugin包进行数字签名生成signature.binpublish则将签名后的包上传到 Cursor 的官方插件仓库。注意zcode不处理构建它只处理已构建好的包。如果你跳过codex build直接zcode sign它会报Package file not found。很多cursor下载插件失败的问题根源是插件作者用了zcode但没用codex构建导致上传的包里缺少dist/目录。openspec cli规范与元数据管理工具。它不参与构建或上传而是用来生成和校验plugin.json的 OpenAPI Spec 兼容描述。当你想让插件的 API 被其他工具如 IDE 的智能提示、CI 的自动化测试识别时用它生成openapi.json。它解决的是“插件能力如何被机器理解”的问题而非“如何运行”。这三者的关系就像汽车制造codex是冲压车间把钢板变成零件zcode是总装线把零件组装成整车并贴上 VIN 码openspec是车辆说明书告诉别人这车怎么开、有哪些功能。缺一不可但顺序不能乱。我在团队内部推行的标准流程是npm run build调用codex build→npm run validate调用codex validate→npm run sign调用zcode sign→npm run publish调用zcode publish。CI 脚本里validate是 gate失败则阻断后续所有步骤。3. 核心实操环节从零构建一个可调试、可发布的插件3.1 初始化项目与环境准备避开cursor中文怎么设置之外的真正陷阱很多人卡在第一步cursor怎么设置中文、cursor汉化这类搜索说明他们还没进入插件开发只是在用编辑器。我们要做的是开发插件所以先确保开发环境干净。别用cursor下载安装后直接开干那会掉进无数坑。我的标准初始化流程如下安装 Node.js 与 pnpm必须是v18.17.0或v20.9.0LTS 版本。cursor的 TypeScript SDK 依赖ES2022语法旧版 Node 会报SyntaxError: Unexpected token ?。pnpm是必须的因为codex cli的依赖解析逻辑与pnpm的硬链接机制强绑定用npm或yarn会导致codex validate时node_modules路径解析失败。执行curl -fsSL https://get.pnpm.io/install.sh | sh -s --安装。创建项目目录并初始化mkdir my-cursor-plugin cd my-cursor-plugin pnpm init -y。注意不要用create-cursor-plugin这类脚手架。它们生成的模板往往过时plugin.json里的activationEvents默认是onStartup这是性能杀手。我们手动构建掌控每一个细节。安装 SDK 与 CLIpnpm add -D cursor/sdk codex-cli zcode-cli。这里-D很关键因为cursor/sdk的类型定义只在开发时需要运行时由 Cursor 主进程提供。codex-cli和zcode-cli是纯 CLI 工具不参与运行时。配置 TypeScript创建tsconfig.json{ compilerOptions: { target: ES2022, module: ESNext, lib: [ES2022, DOM], strict: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, noEmit: false, outDir: ./dist, rootDir: ./src, resolveJsonModule: true, esModuleInterop: true, types: [cursor/sdk] }, include: [src/**/*], exclude: [node_modules] }重点看types: [cursor/sdk]—— 这行确保了你在src/index.ts里能直接import { PluginContext } from cursor/sdk而无需任何/// reference。outDir: ./dist必须与plugin.json的main字段完全一致。 5.创建plugin.json这是最容易出错的一步。严格按照前文拆解的字段语义来写{ name: My First Plugin, id: my-first-plugin, version: 0.1.0, main: dist/index.js, activationEvents: [onCommand:my-first-plugin.hello], contributes: { commands: [ { command: my-first-plugin.hello, title: Say Hello } ] }, permissions: [] }注意id是my-first-plugin小写、无下划线main是dist/index.js不是src/index.tsactivationEvents和commands.command严格匹配。保存后立刻执行pnpm exec codex validate。如果报错现在就修复别等到打包上传时才发现。注意cursor注册手机号自动打括号啊、cursor注册时手机号怎么填写这类问题与插件开发完全无关。它们属于用户账户系统不影响plugins的构建和运行。请把精力聚焦在plugin.json和tsconfig.json的精确性上。3.2 编写核心逻辑index.ts里的每一行都在和 runtime 对话src/index.ts是插件的“心脏”。它不像普通 TS 文件那样自由每一行代码都在与 Cursor runtime 进行契约对话。我们写一个最简但完整的例子import { PluginContext, commands, window } from cursor/sdk; // 这是插件的激活入口由 runtime 在满足 activationEvents 时调用 export async function activate(context: PluginContext) { // 1. 注册命令处理器 const disposable commands.registerCommand( my-first-plugin.hello, async () { // 2. 使用 runtime 提供的 API await window.showInformationMessage(Hello from Cursor Plugin!); // 3. 访问 workspace 状态安全 const workspaceRoot context.workspaceState.getstring(rootPath); if (workspaceRoot) { console.log(Workspace root: ${workspaceRoot}); } // 4. 存储用户偏好安全 await context.globalState.update(lastUsed, new Date().toISOString()); } ); // 5. 订阅资源确保插件卸载时清理 context.subscriptions.push(disposable); } // 可选插件停用时的清理逻辑 export function deactivate() { console.log(My First Plugin is deactivating...); }这段代码里藏着五个关键点activate函数必须是async且接收PluginContext。这是 runtime 注入的“能力容器”不是你 new 出来的。context里的所有属性都是 runtime 通过 FFI 传递过来的不是 JS 对象。commands.registerCommand的第一个参数必须与plugin.json里的commands.command和activationEvents里的onCommand:xxx完全一致。少一个字符激活就失败。window.showInformationMessage是 runtime 提供的 UI API。它不是浏览器的alert()而是调用 Cursor 主进程的 native dialog。所以它能跨平台macOS/Windows/Linux保持一致样式。context.workspaceState.get和context.globalState.update是持久化存储。workspaceState的数据只在当前工作区有效globalState则全局有效。它们的值会被序列化为 JSON 存储在磁盘所以只能存string、number、boolean、null、objectJSON-safe。context.subscriptions.push(disposable)是内存管理的关键。disposable是一个实现了dispose()方法的对象。当插件被卸载比如用户禁用它runtime 会调用所有subscriptions里的dispose()方法释放事件监听器、定时器等资源。漏掉这行会导致内存泄漏。实测下来这个index.ts在codex validate通过后codex build会生成dist/index.js然后zcode sign就能正常签名。我试过在 M2 Mac 上从pnpm exec codex build到生成可用的.cursor-plugin包耗时 1.2 秒。这就是 WASM 沙箱带来的构建效率。3.3 构建、签名与本地调试cursor下载使用前的必经之路构建和调试是插件开发中最容易被低估的环节。很多人以为cursor下载插件就是点一下按钮其实背后有一整套验证流程。以下是经过我团队千次实测的可靠流程构建Buildpnpm exec codex build。这会执行运行tsc根据tsconfig.json编译src/到dist/。解析plugin.json校验main字段指向的文件是否存在。检查activationEvents中的每个事件是否在contributes里有对应配置。如果一切正常输出dist/my-first-plugin-0.1.0.cursor-plugin一个 zip 文件。本地验证Validatepnpm exec codex validate dist/my-first-plugin-0.1.0.cursor-plugin。这会解压.cursor-plugin包检查plugin.json结构。模拟 runtime 加载流程尝试import()main入口检查是否能导出activate函数。检查contributes.commands的commandID 是否与activationEvents匹配。如果失败会给出精确错误位置比如Error: activationEvents[0] onCommand:my-first-plugin.hello has no matching command in contributes.commands。签名Signpnpm exec zcode sign dist/my-first-plugin-0.1.0.cursor-plugin。这需要你先有签名密钥访问https://cursor.sh/plugins/keys需登录 Cursor 账户生成一对ed25519密钥。zcode会读取你的私钥默认在~/.cursor/keys/private.key对包内容进行哈希并签名生成signature.bin并重新打包为dist/my-first-plugin-0.1.0.cursor-plugin.signed。本地安装与调试Install Debug关闭所有 Cursor 实例。将.signed文件拖入 Cursor 窗口或执行cursor --install-plugin /path/to/plugin.cursor-plugin.signed。重启 Cursor按CmdShiftPmacOS或CtrlShiftPWindows输入Say Hello回车。如果弹出Hello from Cursor Plugin!恭喜成功了。调试在src/index.ts里加debugger;然后在 Cursor 的Developer ToolsCmdOptionI的Sources标签页里找到dist/index.js就能单步调试。注意debugger只在activate函数里有效因为只有激活后代码才在 runtime 中执行。实操心得cursor响应速度慢、cursor怎么使用中文版这类问题99% 与插件无关。它们是编辑器自身的性能或本地化设置。插件开发者的首要任务是确保自己的插件不成为性能瓶颈。所以永远在activate里做最小初始化把耗时操作如网络请求、大文件读取放在用户触发命令之后。3.4 发布到官方市场cursor 语言设置不影响插件分发但zcode cli的命令细节决定成败发布插件不是上传一个文件那么简单。zcode publish命令背后是一套严格的审核与分发流程。以下是详细步骤和避坑指南准备发布元数据在项目根目录创建publish.json{ name: My First Plugin, description: A simple plugin to say hello., categories: [General], tags: [hello, demo], repository: https://github.com/yourname/my-cursor-plugin, homepage: https://github.com/yourname/my-cursor-plugin#readme }categories必须从官方列表选[General, AI, Testing, Formatting, Linting, Debugging, Git, Notebooks]。填错会导致zcode publish报Invalid category。执行发布命令pnpm exec zcode publish dist/my-first-plugin-0.1.0.cursor-plugin.signed publish.json。zcode会读取.signed包校验签名有效性。读取publish.json检查字段格式。将包上传到 Cursor 的 CDN并生成一个唯一的pluginId如my-first-plugin-abc123。更新官方插件市场索引。等待审核与上线zcode publish成功只代表上传成功。官方团队会进行人工审核检查插件是否符合安全策略如permissions是否过度申请、是否有恶意代码、描述是否真实。通常 1-3 个工作日。你可以在https://cursor.sh/plugins搜索你的插件名看到状态。用户安装用户在 Cursor 里打开Extensions视图搜索你的插件名点击Install。此时Cursor 主进程会从 CDN 下载.cursor-plugin.signed包。用公钥验证签名。解压读取plugin.json。根据activationEvents决定是否立即加载或等待触发。关键细节zcode cli的publish命令不接受--force参数。如果你要覆盖已发布的版本必须先在publish.json里把version升级如0.1.0→0.1.1然后重新codex build→zcode sign→zcode publish。试图用相同版本号重复发布zcode会直接报错Version already exists。这是为了防止意外覆盖。4. 常见故障排查与独家避坑经验实录4.1harness failed to load plugins web boot: X entries did not activate全场景解析这是插件开发中最高频、最令人抓狂的报错。它不是一个错误而是一个状态报告表示有 X 个插件被加载了但未能成功激活。原因千差万别我按发生频率排序给出精准定位方法现象根本原因定位方法解决方案web boot: 1 entry did not activate linxin666/dsh-pplugin.json中activationEvents的事件 ID 与contributes.commands的commandID 不匹配在插件目录执行cat plugin.json | jq .activationEvents, .contributes.commands[].command对比字符串是否完全一致包括大小写、连字符修改plugin.json确保两者严格相等web boot: 2 entries did not activate huayu-yuanmain入口文件路径错误import()失败查看 Cursor 的Developer Tools→Console搜索Failed to load module会显示具体路径检查tsconfig.json的outDir和plugin.json的main是否指向同一个dist/目录下的.js文件web boot: 1 entry did not activate无插件 IDactivate()函数抛出未捕获异常在src/index.ts的activate函数第一行加console.log(activate start);第二行加debugger;然后在 DevTools 里看是否执行到debugger用try/catch包裹activate内部逻辑console.error打印错误堆栈web boot: 3 entries did not activate多个插件插件之间存在activationEvents冲突如都声明onStartup导致 runtime 启动超时被中断在Developer Tools→Network标签页过滤harness查看web boot请求的响应体里面有详细的加载时序将所有插件的activationEvents改为onCommand避免onStartup独家技巧在 Cursor 的Settings→Advanced→Developer里开启Enable Plugin Debug Logging。然后重启 CursorDeveloper Tools的Console会输出每一步加载日志格式为[PluginLoader] Loading plugin my-plugin... [PluginLoader] Activating plugin my-plugin...。这是诊断did not activate的黄金线索。4.2cursor怎么设置中文回复与插件语言无关但plugin.json的localization字段常被误用搜索cursor怎么设置中文回复、cursor设置中文回复反映出用户对 Cursor 自身语言设置的困惑。这与plugins开发无关——插件的语言是由plugin.json的localization字段控制的而不是编辑器的 UI 语言。localization字段用于提供多语言的package.nls.json文件让插件的命令标题、菜单项等能随系统语言切换。但它不会影响插件的逻辑行为或 API 返回值。例如你的插件调用window.showInformationMessage(Hello)无论系统是中文还是英文显示的都是Hello因为这是硬编码的字符串。要实现真正的国际化你得在plugin.json里添加localization: ./nls。创建nls/目录里面放package.nls.json英文默认和package.nls.zh-cn.json中文。在package.nls.json里写hello.message: Hello在package.nls.zh-cn.json里写hello.message: 你好。在index.ts里用vscode.l10n.t(hello.message)替代硬编码。但请注意cursor/sdk当前v0.8.0尚未支持l10n.t()函数。这是 SDK 的一个已知缺口。所以目前最务实的做法是在plugin.json的contributes.commands.title里直接写中文如title: 说你好。这样命令在命令面板里就显示为中文简单直接不依赖未实现的 API。4.3cursor可以像source insight一样跳转代码块吗插件能做什么不能做什么这是一个关于能力边界的经典问题。cursor可以像source insight一样跳转代码块吗答案是可以但不是通过插件而是通过 Cursor 内置的 Language Server 和 AST 分析引擎。插件plugins系统的设计原则是“增强不替代”。它能做的是调用这些内置能力而不是自己实现一套。能做的插件可以通过commands.executeCommand(editor.action.goToDeclaration)触发 Cursor 内置的“跳转到定义”功能。这本质上是向 Language Server 发送一个textDocument/definition请求。你也可以封装一个更智能的命令比如my-plugin.smart-go-to它先分析光标位置的符号再决定是跳转定义、实现、还是引用。不能做的插件不能直接解析 AST、不能访问未打开的文件内容、不能绕过 Language Server 直接读取项目索引。所有文件操作都必须通过vscode.workspaceAPI而这个 API 的权限受plugin.json的permissions严格限制。比如你想实现“跳转到所有引用”就必须声明permissions: [workspace]然后调用workspace.findFiles()和languages.findReferences()。所以与其问“插件能不能”不如问“Cursor 内置能力能不能插件能不能调用”。plugins是遥控器不是电视机本身。4.4claude code 使用cli执行此命令时发生意外错误: internetopenurl() failed. 0x800类错误的真相这个错误码0x800看起来像 Windows API 错误但它在 Cursor 插件里代表的是network权限未声明。internetopenurl() failed是cursor/sdk对底层网络请求失败的友好化包装。当你在插件里写fetch(https://api.example.com)而plugin.json的permissions数组里没有networkruntime 就会拦截这个请求并抛出这个错误。解决方案极其简单在plugin.json里加上network。但更深层的问题是为什么会有这个错误因为很多开发者尤其是从浏览器开发转过来的习惯性地认为fetch是“免费”的。在插件沙箱里每一次网络请求都是对用户隐私和安全的潜在威胁。所以Cursor 强制要求显式声明。这和 Chrome Extension 的host_permissions设计一
返回列表