mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 05:12:03 +08:00
feat(desktop): add plugin resource actions
This commit is contained in:
@@ -172,6 +172,10 @@ pub fn api_router(service: ControlService) -> Router {
|
||||
"/__byok-api__/api/plugins/{plugin_id}/resources/{resource_type}/{resource_id}",
|
||||
axum::routing::delete(plugins::delete_resource),
|
||||
)
|
||||
.route(
|
||||
"/__byok-api__/api/plugins/{plugin_id}/resources/{resource_type}/{resource_id}/actions/{action_id}",
|
||||
post(plugins::action),
|
||||
)
|
||||
.route(
|
||||
"/__byok-api__/api/plugins/{plugin_id}/resources/{resource_type}/{resource_id}/refresh",
|
||||
post(plugins::refresh_resource),
|
||||
|
||||
@@ -87,6 +87,23 @@ pub async fn refresh_resource(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn action(
|
||||
State(service): State<ControlService>,
|
||||
Path((plugin_id, resource_type, resource_id, action_id)): Path<(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
)>,
|
||||
Json(input): Json<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>> {
|
||||
Ok(Json(
|
||||
service
|
||||
.plugin_resource_action(&plugin_id, &resource_type, &resource_id, &action_id, input)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn delete_resource(
|
||||
State(service): State<ControlService>,
|
||||
Path((plugin_id, resource_type, resource_id)): Path<(String, String, String)>,
|
||||
|
||||
@@ -226,6 +226,19 @@ impl ControlService {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn plugin_resource_action(
|
||||
&self,
|
||||
plugin_id: &str,
|
||||
resource_type: &str,
|
||||
resource_id: &str,
|
||||
action_id: &str,
|
||||
input: serde_json::Value,
|
||||
) -> Result<serde_json::Value> {
|
||||
self.plugins
|
||||
.resource_action(plugin_id, resource_type, resource_id, action_id, input)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn plugin_delete_resource(
|
||||
&self,
|
||||
plugin_id: &str,
|
||||
|
||||
@@ -38,10 +38,25 @@ pub struct ResourceDefinition {
|
||||
pub add: Vec<AddMethodDefinition>,
|
||||
#[serde(default)]
|
||||
pub import: Option<ImportDefinition>,
|
||||
#[serde(default)]
|
||||
pub actions: Vec<ResourceActionDefinition>,
|
||||
pub can_refresh: bool,
|
||||
pub can_remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ResourceActionDefinition {
|
||||
pub id: String,
|
||||
pub display_name: LocalizedText,
|
||||
#[serde(default)]
|
||||
pub description: LocalizedText,
|
||||
#[serde(default)]
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub destructive: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct AddMethodDefinition {
|
||||
@@ -132,6 +147,7 @@ pub struct PluginResourceDescriptor {
|
||||
pub display_name: LocalizedText,
|
||||
pub add: Vec<AddMethodDefinition>,
|
||||
pub import: Option<ImportDefinition>,
|
||||
pub actions: Vec<ResourceActionDefinition>,
|
||||
pub can_refresh: bool,
|
||||
pub can_remove: bool,
|
||||
pub resources: Vec<PluginResourceView>,
|
||||
@@ -171,6 +187,61 @@ pub struct ResourcePresentation {
|
||||
pub metrics: Vec<ResourceMetric>,
|
||||
}
|
||||
|
||||
/// 插件资源操作返回的安全详情;patch 只在核心内部应用,不会回传给桌面端。
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ResourceActionResult {
|
||||
pub title: LocalizedText,
|
||||
#[serde(default)]
|
||||
pub description: Option<LocalizedText>,
|
||||
#[serde(default)]
|
||||
pub cards: Vec<ResourceActionCard>,
|
||||
#[serde(default, skip_serializing)]
|
||||
pub patch: Option<super::state::ResourcePatch>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ResourceActionCard {
|
||||
pub id: String,
|
||||
pub title: LocalizedText,
|
||||
#[serde(default)]
|
||||
pub status: Option<LocalizedText>,
|
||||
#[serde(default)]
|
||||
pub granted_at_ms: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub expires_at_ms: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub fields: Vec<ResourceActionField>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ResourceActionField {
|
||||
pub id: String,
|
||||
pub label: LocalizedText,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// 返回给桌面端的资源操作结果,明确排除插件私有 patch。
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourceActionResponse {
|
||||
pub title: LocalizedText,
|
||||
pub description: Option<LocalizedText>,
|
||||
pub cards: Vec<ResourceActionCard>,
|
||||
}
|
||||
|
||||
impl From<ResourceActionResult> for ResourceActionResponse {
|
||||
fn from(result: ResourceActionResult) -> Self {
|
||||
Self {
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
cards: result.cards,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginResourceView {
|
||||
pub fn from_record(record: &ResourceRecord, presentation: ResourcePresentation) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -13,8 +13,9 @@ use super::{
|
||||
data::PluginDataStore,
|
||||
descriptor::{
|
||||
parse_model_id, PluginDescriptor, PluginModelDescriptor, PluginProviderDescriptor,
|
||||
PluginResourceDescriptor, PluginResourceView, ProviderDefinition, ResourceDefinition,
|
||||
ResourcePresentation, OAUTH2_ADD_METHOD, OAUTH2_AUTHORIZATION_CODE_ADD_METHOD,
|
||||
PluginResourceDescriptor, PluginResourceView, ProviderDefinition, ResourceActionResponse,
|
||||
ResourceActionResult, ResourceDefinition, ResourcePresentation, OAUTH2_ADD_METHOD,
|
||||
OAUTH2_AUTHORIZATION_CODE_ADD_METHOD,
|
||||
},
|
||||
oauth_callback::{self, CallbackHandle, CallbackOutcome, CallbackRequest},
|
||||
runtime::PluginRuntime,
|
||||
@@ -838,6 +839,58 @@ impl PluginRegistry {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resource_action(
|
||||
&self,
|
||||
plugin_id: &str,
|
||||
resource_type: &str,
|
||||
resource_id: &str,
|
||||
action_id: &str,
|
||||
input: serde_json::Value,
|
||||
) -> Result<serde_json::Value> {
|
||||
let executable = self.executable()?;
|
||||
let entry = self.find_entry(&executable, plugin_id).await?;
|
||||
let resource = find_resource(&entry, resource_type)?;
|
||||
let action = resource
|
||||
.actions
|
||||
.iter()
|
||||
.find(|action| action.id == action_id)
|
||||
.ok_or_else(|| {
|
||||
Error::Config(format!(
|
||||
"plugin '{plugin_id}' resource '{resource_type}' does not define action '{action_id}'"
|
||||
))
|
||||
})?;
|
||||
if !matches!(action.target.as_str(), "resource" | "card") {
|
||||
return Err(Error::Config(format!(
|
||||
"plugin '{plugin_id}' resource action '{action_id}' has an invalid target"
|
||||
)));
|
||||
}
|
||||
let record = self
|
||||
.find_record(plugin_id, resource_type, resource_id)
|
||||
.await?;
|
||||
let value = self
|
||||
.worker(&entry, &executable)
|
||||
.await
|
||||
.invoke(
|
||||
"resource.action",
|
||||
serde_json::json!({
|
||||
"resourceType": resource_type,
|
||||
"actionId": action_id,
|
||||
"resource": record.snapshot(resource_type),
|
||||
"input": input,
|
||||
}),
|
||||
CancellationToken::new(),
|
||||
)
|
||||
.await?;
|
||||
let result: ResourceActionResult = serde_json::from_value(value)?;
|
||||
if let Some(patch) = result.patch.clone() {
|
||||
self.inner
|
||||
.state
|
||||
.apply_patch(plugin_id, resource_type, resource_id, patch)
|
||||
.await?;
|
||||
}
|
||||
Ok(serde_json::to_value(ResourceActionResponse::from(result))?)
|
||||
}
|
||||
|
||||
pub async fn delete_resource(
|
||||
&self,
|
||||
plugin_id: &str,
|
||||
@@ -961,6 +1014,7 @@ impl PluginRegistry {
|
||||
display_name: definition.display_name.clone(),
|
||||
add: definition.add.clone(),
|
||||
import: definition.import.clone(),
|
||||
actions: definition.actions.clone(),
|
||||
can_refresh: definition.can_refresh,
|
||||
can_remove: definition.can_remove,
|
||||
resources: views,
|
||||
|
||||
@@ -99,6 +99,13 @@ export function __descriptor(definition: ProviderPluginDefinition) {
|
||||
multiple: resource.import.multiple ?? false,
|
||||
}
|
||||
: null,
|
||||
actions: (resource.actions ?? []).map((action) => ({
|
||||
id: action.id,
|
||||
displayName: action.displayName,
|
||||
description: action.description ?? null,
|
||||
target: action.target ?? "resource",
|
||||
destructive: action.destructive ?? false,
|
||||
})),
|
||||
canRefresh: resource.refresh !== undefined,
|
||||
canRemove: resource.remove !== undefined,
|
||||
})),
|
||||
|
||||
@@ -44,6 +44,45 @@ export type ResourceMetric = {
|
||||
resetAtMs?: number;
|
||||
};
|
||||
|
||||
export type ResourceActionTarget = "resource" | "card";
|
||||
|
||||
export type ResourceAction = {
|
||||
id: string;
|
||||
displayName: LocalizedText;
|
||||
description?: LocalizedText;
|
||||
target?: ResourceActionTarget;
|
||||
destructive?: boolean;
|
||||
run(
|
||||
resource: ResourceSnapshot,
|
||||
input: JsonValue,
|
||||
context: PluginContext,
|
||||
): Promise<ResourceActionResult>;
|
||||
};
|
||||
|
||||
export type ResourceActionField = {
|
||||
id: string;
|
||||
label: LocalizedText;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/** 资源操作返回的通用详情卡片;不得包含凭证。 */
|
||||
export type ResourceActionCard = {
|
||||
id: string;
|
||||
title: LocalizedText;
|
||||
status?: LocalizedText;
|
||||
grantedAtMs?: number;
|
||||
expiresAtMs?: number;
|
||||
fields?: ResourceActionField[];
|
||||
};
|
||||
|
||||
export type ResourceActionResult = {
|
||||
title: LocalizedText;
|
||||
description?: LocalizedText;
|
||||
cards?: ResourceActionCard[];
|
||||
/** 消费类操作可用它更新宿主保存的资源状态。 */
|
||||
patch?: ResourcePatch;
|
||||
};
|
||||
|
||||
/** 单条资源的用户可见投影;不得泄露凭证。displayName 是数据(如邮箱),保持纯字符串。 */
|
||||
export type ResourceView = {
|
||||
displayName: string;
|
||||
@@ -145,6 +184,7 @@ export type ResourceSupport = {
|
||||
add?: ResourceAddMethod[];
|
||||
import?: ResourceImportSupport;
|
||||
present(resource: ResourceSnapshot): ResourceView;
|
||||
actions?: ResourceAction[];
|
||||
/** 用户主动触发时重新读取上游状态(额度、凭证有效性)。 */
|
||||
refresh?(resource: ResourceSnapshot, context: PluginContext): Promise<ResourcePatch>;
|
||||
/** 可选的上游撤销;宿主随后删除本地记录。 */
|
||||
|
||||
@@ -122,6 +122,17 @@ async function dispatch(message: { id: string; method: string; params?: JsonValu
|
||||
result = await support.refresh(params.resource as never, context);
|
||||
break;
|
||||
}
|
||||
case "resource.action": {
|
||||
const support = resourceSupport(params.resourceType);
|
||||
const action = (support.actions ?? []).find((item) => item.id === params.actionId);
|
||||
if (!action) throw new Error(`resource ${params.resourceType} has no action ${params.actionId}`);
|
||||
result = await action.run(
|
||||
params.resource as never,
|
||||
params.input ?? null,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "resource.remove": {
|
||||
const support = resourceSupport(params.resourceType);
|
||||
await support.remove?.(params.resource as never, context);
|
||||
|
||||
Reference in New Issue
Block a user