--- title: 插件开发 description: 用无状态 TypeScript 插件实现 Provider 执行、模型枚举、资源接入与 OAuth 登录。 icon: Blocks --- 插件实现核心定义的三种能力接口:**Provider**(执行一次 LLM 调用)、**Model**(枚举可用模型)、**Resource**(账号等凭证资源)。插件不持有任何持久状态——资源与模型目录由核心存储,每次调用所需数据都通过参数传入。 ## 目录结构 ```text ~/.cursor-byok-v3/plugins/ ├── installed/ │ └── com.example.subscription/ │ ├── plugin.json # 静态身份、入口、图标和 HTTPS 主机白名单 │ ├── main.ts # defineProviderPlugin 组合 providers 与 resources │ └── assets/icon.svg # 本地图标,最大 1 MiB └── data/ └── com.example.subscription/ ├── resources-.json # 核心持久化的资源记录(0600) └── models-.json # 核心持久化的模型目录 ``` 内置插件源码位于 `server/plugins/build-in/`(如 `codex-auth`),随二进制打包并按版本预装进 `plugins/installed/`;Debug 构建下源码目录优先加载,便于热改。 ## 静态清单 ```json { "apiVersion": 1, "id": "com.example.subscription", "name": "Example Subscription", "version": "0.1.0", "author": "@example", "minAppVersion": "0.1.0", "icon": "assets/icon.svg", "entry": "main.ts", "permissions": { "network": ["auth.example.com", "api.example.com"] } } ``` `permissions.network` 只能包含精确主机名。所有插件网络请求都必须是 HTTPS 且命中该白名单。`version` 必填;应用版本低于 `minAppVersion` 时插件会被忽略。内置插件按 `version` 预装进 `plugins/installed/`:版本一致时启动零写盘,版本变化时整目录同步并清理旧文件。 ## 入口与能力 宿主注入 `cursor-byok:plugin`、`cursor-byok:provider`、`cursor-byok:model`、`cursor-byok:resource` 与协议帮助库 `cursor-byok:protocol/openai-responses`。 ```ts import { defineProviderPlugin } from "cursor-byok:plugin"; import { streamOpenAiResponses, HttpError } from "cursor-byok:protocol/openai-responses"; export default defineProviderPlugin({ providers: [{ id: "subscription", displayName: "Example Subscription", providerType: "openai", resourceType: "account", models: { list: async ({ resource }, context) => { // 用首个可用资源发现上游模型;返回值整体替换核心目录。 return [{ id: "model-1", displayName: "Model 1", capabilities: { thinking: true } }]; }, }, invoke: async (input, output, context) => { try { await streamOpenAiResponses({ url: "https://api.example.com/v1/responses", model: input.model.id, request: input.request, headers: { authorization: `Bearer ${token(input.resource)}` }, }, output, context); return { status: "completed" }; } catch (error) { if (error instanceof HttpError && error.status === 401) { return { status: "resource-error", message: error.message, patch: { state: { status: "invalid", message: "sign in again" } }, }; } return { status: "request-error", message: String(error) }; } }, }], resources: [{ type: "account", displayName: "Accounts", add: [{ type: "oauth2.0", id: "device", displayName: "Sign in", begin: async (context) => ({ session: { deviceCode: "..." }, userCode: "ABCD-EFGH", verificationUrl: "https://auth.example.com/device", expiresAtMs: Date.now() + 900_000, pollIntervalMs: 5_000, }), poll: async (session, context) => ({ status: "completed", resources: [{ key: "account:1", privateData: { accessToken: "..." } }], }), }], present: (resource) => ({ displayName: "person@example.com", metrics: [{ id: "weekly", label: "Weekly quota", unit: "percent", value: 75 }], }), refresh: async (resource, context) => ({ privateData: { /* 更新额度 */ } }), }], }); ``` ## 职责边界 - **核心负责**:资源持久化与去重(按 `draft.key` upsert)、资源列表 UI、OAuth 轮询循环(间隔、slow-down、超时)、模型目录存储、每次调用的资源选择(当前取首个可用,冷却到期自动恢复)、调用记录与统计。 - **插件负责**:认证 HTTP 转移(`begin`/`poll`)、凭证解析(`import.parse`)、资源展示投影(`present`)、额度刷新(`refresh`)、协议适配与流式执行(`invoke`)。 `invoke` 接收完整 `LlmRequest`(指令、消息历史、工具、思考配置),边解析上游 SSE 边 `output.emit()` 标准化事件(文本/思考边界、工具参数增量、回放状态、用量、结束原因),最后返回 `completed` 或类型化错误。`resource-error` 携带的 `patch` 会被核心原子应用到选中的资源,是未来负载均衡换资源重试的依据。 稳定模型 ID 为 `plugin://`,每个枚举出的模型都独立进入 Cursor 模型目录。 ## 宿主上下文 - `context.network.fetch(url, init)`:一次性 HTTPS 请求,严格执行清单白名单。 - `context.network.stream(url, init)`:流式响应,按行异步迭代(用于 SSE)。 - `context.signal`:宿主取消本次调用时触发。 ## 沙箱 Deno 进程只能读取自身插件目录和宿主 SDK 目录。远程模块、npm 包、直接网络、环境变量、子进程和文件写入均被禁用。Worker 按请求 ID 多路复用,崩溃时当前请求失败并按需重启。 ## 验证 ```bash deno check --no-config --no-lock --no-npm --no-remote \ --import-map=server/src/plugin/sdk/import-map.json \ server/plugins/build-in/my-plugin/main.ts deno test --no-config --no-lock --no-npm --no-remote \ --import-map=server/src/plugin/sdk/import-map.json \ server/plugins/build-in/my-plugin/plugin_test.ts ```