Files
cursor-byok/apps/docs/content/docs/plugin-development.mdx
T
leookun a5bbe67845 refactor: replace Button with TruncatedButton in CursorModelCards and PluginManagementPage
- Updated the UI components in CursorModelCards and PluginManagementPage to use TruncatedButton for better text handling and display.
- Adjusted styles in CursorSettings and PluginManagementPage to ensure proper button layout and responsiveness.
- Added new ActionMenu component for handling additional actions in PluginManagementPage.
- Enhanced localization files to include new strings for the ActionMenu and TruncatedButton components.
2026-08-30 20:45:27 +08:00

145 lines
5.9 KiB
Plaintext

---
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-<type>.json # 核心持久化的资源记录(0600)
└── models-<provider>.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:<plugin-id>/<provider-id>/<model-id>`,每个枚举出的模型都独立进入 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
```