//! 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, #[serde(default)] pub resources: Vec, } /// 插件提供的显示文本:纯字符串或 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, 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, #[serde(default)] pub import: Option, 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, 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, pub icon: String, pub providers: Vec, pub resources: Vec, } #[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, pub has_models: bool, /// 已满足调用条件:模型目录非空,且需要资源时至少有一条资源。 pub configured: bool, pub models: Vec, } /// 一个可直接被 Cursor 调用的插件模型。 #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PluginModelDescriptor { /// 稳定模型 ID:`plugin://`。 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, pub icon: String, pub provider_type: String, pub max_output_tokens: Option, 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, pub import: Option, pub can_refresh: bool, pub can_remove: bool, pub resources: Vec, } /// 单条资源的对外投影;凭证保留在核心存储,不进入该结构。 #[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, 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, } /// 插件对一条资源的展示投影(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, } 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); } }