mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
- 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.
145 lines
5.9 KiB
Plaintext
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
|
|
```
|