mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 12:13:05 +08:00
231 lines
6.9 KiB
Rust
231 lines
6.9 KiB
Rust
//! Defines serializable plugin capability definitions and desktop descriptors.
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::state::{ResourceRecord, ResourceState, StoredModel};
|
|
|
|
/// 由 collect.ts 输出的能力摘要;不含任何可执行内容。
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct PluginModuleDefinition {
|
|
pub providers: Vec<ProviderDefinition>,
|
|
#[serde(default)]
|
|
pub resources: Vec<ResourceDefinition>,
|
|
}
|
|
|
|
/// 插件提供的显示文本:纯字符串或 locale → 文本映射;核心原样透传,由前端解析。
|
|
pub type LocalizedText = serde_json::Value;
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ProviderDefinition {
|
|
pub id: String,
|
|
pub display_name: LocalizedText,
|
|
#[serde(default)]
|
|
pub description: LocalizedText,
|
|
pub provider_type: String,
|
|
#[serde(default)]
|
|
pub resource_type: Option<String>,
|
|
pub has_models: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ResourceDefinition {
|
|
#[serde(rename = "type")]
|
|
pub resource_type: String,
|
|
pub display_name: LocalizedText,
|
|
#[serde(default)]
|
|
pub add: Vec<AddMethodDefinition>,
|
|
#[serde(default)]
|
|
pub import: Option<ImportDefinition>,
|
|
pub can_refresh: bool,
|
|
pub can_remove: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct AddMethodDefinition {
|
|
#[serde(rename = "type")]
|
|
pub method_type: String,
|
|
pub id: String,
|
|
pub display_name: LocalizedText,
|
|
#[serde(default)]
|
|
pub description: LocalizedText,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ImportDefinition {
|
|
pub display_name: LocalizedText,
|
|
#[serde(default)]
|
|
pub description: LocalizedText,
|
|
pub accept: Vec<String>,
|
|
pub multiple: bool,
|
|
}
|
|
|
|
pub const OAUTH2_ADD_METHOD: &str = "oauth2.0";
|
|
|
|
/// 桌面端看到的插件全貌。
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PluginDescriptor {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub version: String,
|
|
pub author: Option<String>,
|
|
pub icon: String,
|
|
pub providers: Vec<PluginProviderDescriptor>,
|
|
pub resources: Vec<PluginResourceDescriptor>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PluginProviderDescriptor {
|
|
pub id: String,
|
|
pub plugin_id: String,
|
|
pub display_name: LocalizedText,
|
|
pub description: LocalizedText,
|
|
pub provider_type: String,
|
|
pub resource_type: Option<String>,
|
|
pub has_models: bool,
|
|
/// 已满足调用条件:模型目录非空,且需要资源时至少有一条资源。
|
|
pub configured: bool,
|
|
pub models: Vec<PluginModelDescriptor>,
|
|
}
|
|
|
|
/// 一个可直接被 Cursor 调用的插件模型。
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PluginModelDescriptor {
|
|
/// 稳定模型 ID:`plugin:<plugin>/<provider>/<model>`。
|
|
pub id: String,
|
|
pub plugin_id: String,
|
|
pub plugin_name: String,
|
|
pub provider_id: String,
|
|
pub model_id: String,
|
|
pub display_name: String,
|
|
pub description: Option<String>,
|
|
pub icon: String,
|
|
pub provider_type: String,
|
|
pub max_output_tokens: Option<u64>,
|
|
pub images: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PluginResourceDescriptor {
|
|
#[serde(rename = "type")]
|
|
pub resource_type: String,
|
|
pub display_name: LocalizedText,
|
|
pub add: Vec<AddMethodDefinition>,
|
|
pub import: Option<ImportDefinition>,
|
|
pub can_refresh: bool,
|
|
pub can_remove: bool,
|
|
pub resources: Vec<PluginResourceView>,
|
|
}
|
|
|
|
/// 单条资源的对外投影;凭证保留在核心存储,不进入该结构。
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PluginResourceView {
|
|
pub id: String,
|
|
pub state: ResourceState,
|
|
pub display_name: String,
|
|
pub description: LocalizedText,
|
|
pub metrics: Vec<ResourceMetric>,
|
|
pub created_at_ms: i64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ResourceMetric {
|
|
pub id: String,
|
|
pub label: LocalizedText,
|
|
pub unit: String,
|
|
pub value: f64,
|
|
#[serde(default)]
|
|
pub reset_at_ms: Option<i64>,
|
|
}
|
|
|
|
/// 插件对一条资源的展示投影(resource.present 的返回值)。
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ResourcePresentation {
|
|
pub display_name: String,
|
|
#[serde(default)]
|
|
pub description: LocalizedText,
|
|
#[serde(default)]
|
|
pub metrics: Vec<ResourceMetric>,
|
|
}
|
|
|
|
impl PluginResourceView {
|
|
pub fn from_record(record: &ResourceRecord, presentation: ResourcePresentation) -> Self {
|
|
Self {
|
|
id: record.id.clone(),
|
|
state: record.state.clone(),
|
|
display_name: presentation.display_name,
|
|
description: presentation.description,
|
|
metrics: presentation.metrics,
|
|
created_at_ms: record.created_at_ms,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const ADAPTER_ID_PREFIX: &str = "plugin:";
|
|
|
|
pub fn model_id(plugin_id: &str, provider_id: &str, model_id: &str) -> String {
|
|
format!("{ADAPTER_ID_PREFIX}{plugin_id}/{provider_id}/{model_id}")
|
|
}
|
|
|
|
/// 解析稳定模型 ID;上游模型段允许包含 `/`。
|
|
pub fn parse_model_id(value: &str) -> Option<(&str, &str, &str)> {
|
|
let rest = value.strip_prefix(ADAPTER_ID_PREFIX)?;
|
|
let (plugin_id, rest) = rest.split_once('/')?;
|
|
let (provider_id, model_id) = rest.split_once('/')?;
|
|
(!plugin_id.is_empty() && !provider_id.is_empty() && !model_id.is_empty()).then_some((
|
|
plugin_id,
|
|
provider_id,
|
|
model_id,
|
|
))
|
|
}
|
|
|
|
impl PluginModelDescriptor {
|
|
pub fn new(
|
|
plugin_id: &str,
|
|
plugin_name: &str,
|
|
icon: &str,
|
|
provider: &ProviderDefinition,
|
|
model: &StoredModel,
|
|
) -> Self {
|
|
Self {
|
|
id: model_id(plugin_id, &provider.id, &model.id),
|
|
plugin_id: plugin_id.to_owned(),
|
|
plugin_name: plugin_name.to_owned(),
|
|
provider_id: provider.id.clone(),
|
|
model_id: model.id.clone(),
|
|
display_name: model.display_name.clone(),
|
|
description: model.description.clone(),
|
|
icon: icon.to_owned(),
|
|
provider_type: provider.provider_type.clone(),
|
|
max_output_tokens: model.max_output_tokens,
|
|
images: model.images,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_stable_model_ids_with_slashes() {
|
|
let id = model_id("dev.example", "codex", "org/gpt-5");
|
|
assert_eq!(
|
|
parse_model_id(&id),
|
|
Some(("dev.example", "codex", "org/gpt-5"))
|
|
);
|
|
assert_eq!(parse_model_id("plugin:only/one"), None);
|
|
assert_eq!(parse_model_id("model-hash"), None);
|
|
}
|
|
}
|