Skip to main content

zerolaunch_plugin_host/adapter/
remote_component.rs

1//! RemoteComponent — 远程插件组件的统一承载结构。
2//!
3//! 一个逻辑组件对应一个 `RemoteComponent` 实例,同时实现 `Configurable`、
4//! `DataSource`、`ActionExecutor`、`Plugin` 四个 trait,消除原先多 adapter
5//! 之间对 `RemoteConfigurableAdapter` 的机械委托。
6
7use async_trait::async_trait;
8use parking_lot::RwLock;
9use std::sync::Arc;
10use std::time::Duration;
11use zerolaunch_plugin_api::config::{
12    ComponentCore, ComponentType, ConfigActionDef, ConfigError, Configurable, SettingDefinition,
13};
14use zerolaunch_plugin_api::{
15    keyword_trigger_match, ActionExecutor, CachedCandidateData, CandidateId, DataSource,
16    ExecutionContext, ExecutionError, KeywordInjector, KeywordInputSource, KeywordOptimizer,
17    PanelInteraction, Plugin, PluginContext, PluginError, PluginHandle, PluginMetadata, Query,
18    QueryResponse, ResultAction, ScoreBooster, ScoredCandidate, SearchCandidate, SearchEngine,
19    TargetType,
20};
21
22use crate::client::JsonRpcClient;
23use zerolaunch_plugin_protocol::messages::*;
24use zerolaunch_plugin_protocol::methods::plugin as plugin_methods;
25use zerolaunch_plugin_protocol::{codes, ProtocolError};
26
27/// `plugin/match_query` 的 RPC 超时(宿主路由热路径专用)。
28///
29/// 使用范围:仅 `RemoteComponent::match_query`。故意远小于协议约定的"其他方法 5s"——
30/// 本调用在每次按键的路由阶段执行,超时即输入延迟;超时按"不接管"处理并告警。
31const MATCH_QUERY_TIMEOUT: Duration = Duration::from_millis(100);
32
33/// `plugin/shutdown`(退出钩子)的 RPC 超时上限。
34///
35/// 使用范围:仅 `RemoteComponent::on_shutdown`。超时后调用方放弃等待并继续关闭进程,
36/// 与内置插件钩子"执行完再退出"的语义对齐(内置无超时,远端受进程边界保护)。
37const SHUTDOWN_HOOK_TIMEOUT: Duration = Duration::from_secs(3);
38
39/// 远程插件组件的种类与专属数据。
40///
41/// 判断字段归属的标准:
42/// - 只有某个种类需要 → 放入对应 variant;
43/// - 所有种类都需要(如 Configurable 相关的缓存)→ 保留在 `RemoteComponent` struct 层面。
44#[derive(Debug)]
45pub enum RemoteComponentKind {
46    DataSource,
47    ActionExecutor {
48        target_types: Vec<TargetType>,
49        result_actions: Vec<ResultAction>,
50    },
51    /// 搜索引擎 —— 参与搜索管道(与内置引擎互斥,仅允许一个启用)。
52    SearchEngine,
53    Plugin {
54        /// 插件级元数据 —— 与 `PluginRegistration.metadata` 共享同一 `Arc`(唯一源),
55        /// 由 build_components 一次性构造后不可变。
56        metadata: Arc<PluginMetadata>,
57        /// 交互策略缓存:discover 拉初始值,查询/设置变更时经 RPC 刷新
58        /// (PanelInteraction 是插件级语义——仅 Plugin 组件持有;
59        /// 内置插件在每次会话推送时同步求值,远端以此对齐)。
60        interaction_policy: RwLock<PanelInteraction>,
61    },
62    /// 分数增强器 —— 引擎打分后追加分数修正(多个可同时启用)。
63    ScoreBooster,
64    /// 关键词优化器 —— 候选管道中扩展关键词。
65    /// 优化属性(input_source / priority)为声明属性,经 keyword_optimizer_info
66    /// RPC 拉取缓存(discover 初始值),设置变更时经 RPC 刷新。
67    KeywordOptimizer {
68        info: RwLock<KeywordOptimizerInfo>,
69    },
70    /// 关键词注入器 —— 基于候选项完整上下文注入额外关键词。
71    KeywordInjector,
72}
73
74pub struct RemoteComponent {
75    /// 身份核心。内置插件与远程插件共享同一身份模型。
76    pub core: ComponentCore,
77
78    // ── 通信 ──
79    pub client: Arc<JsonRpcClient>,
80
81    // ── 私有缓存 ──
82    cached_settings: RwLock<serde_json::Value>,
83    cached_schema: RwLock<Vec<SettingDefinition>>,
84    /// 配置动作缓存。`config_actions()` 是 `Configurable` trait 的通用方法,
85    /// 所有组件类型均可能使用,因此放在 struct 层面而非 kind variant 内部。
86    cached_actions: RwLock<Vec<ConfigActionDef>>,
87    /// 默认启用状态:discover 时经 RPC 拉取(与内置 `default_enabled()` 语义一致)。
88    cached_default_enabled: RwLock<bool>,
89
90    // ── 种类与专属数据 ──
91    pub kind: RemoteComponentKind,
92}
93
94/// Helper: convert a ProtocolError to a ConfigError.
95fn to_config_error(e: ProtocolError) -> ConfigError {
96    ConfigError::ApplyFailed(e.to_string())
97}
98impl std::fmt::Debug for RemoteComponent {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.debug_struct("RemoteComponent")
101            .field("core", &self.core)
102            .field("cached_settings", &self.cached_settings.read())
103            .field("cached_schema", &self.cached_schema.read())
104            .field("cached_actions", &self.cached_actions.read())
105            .field("kind", &self.kind)
106            .finish()
107    }
108}
109
110impl RemoteComponent {
111    #[allow(clippy::too_many_arguments)]
112    pub fn new(
113        component_id: String,
114        component_name: String,
115        component_description: String,
116        component_type: ComponentType,
117        priority: u32,
118        client: Arc<JsonRpcClient>,
119        schema: Vec<SettingDefinition>,
120        settings: serde_json::Value,
121        actions: Vec<ConfigActionDef>,
122        default_enabled: bool,
123        kind: RemoteComponentKind,
124    ) -> Self {
125        Self {
126            core: ComponentCore::new(
127                component_id,
128                component_name,
129                component_description,
130                component_type,
131                priority,
132            ),
133            client,
134            cached_settings: RwLock::new(settings),
135            cached_schema: RwLock::new(schema),
136            cached_actions: RwLock::new(actions),
137            cached_default_enabled: RwLock::new(default_enabled),
138            kind,
139        }
140    }
141
142    /// 将自身转换为 `DataSource` trait object,仅在 kind 为 DataSource 时成功。
143    pub fn as_data_source(self: Arc<Self>) -> Option<Arc<dyn DataSource>> {
144        matches!(self.kind, RemoteComponentKind::DataSource).then(|| self as Arc<dyn DataSource>)
145    }
146
147    /// 将自身转换为 `ActionExecutor` trait object,仅在 kind 为 ActionExecutor 时成功。
148    pub fn as_action_executor(self: Arc<Self>) -> Option<Arc<dyn ActionExecutor>> {
149        matches!(self.kind, RemoteComponentKind::ActionExecutor { .. })
150            .then(|| self as Arc<dyn ActionExecutor>)
151    }
152
153    /// 将自身转换为 `Plugin` trait object,仅在 kind 为 Plugin 时成功。
154    pub fn as_plugin(self: Arc<Self>) -> Option<Arc<dyn Plugin>> {
155        matches!(self.kind, RemoteComponentKind::Plugin { .. }).then(|| self as Arc<dyn Plugin>)
156    }
157    /// 将自身转换为 `SearchEngine` trait object,仅在 kind 为 SearchEngine 时成功。
158    pub fn as_search_engine(self: Arc<Self>) -> Option<Arc<dyn SearchEngine>> {
159        matches!(self.kind, RemoteComponentKind::SearchEngine)
160            .then(|| self as Arc<dyn SearchEngine>)
161    }
162
163    /// 将自身转换为 `ScoreBooster` trait object,仅在 kind 为 ScoreBooster 时成功。
164    pub fn as_score_booster(self: Arc<Self>) -> Option<Arc<dyn ScoreBooster>> {
165        matches!(self.kind, RemoteComponentKind::ScoreBooster)
166            .then(|| self as Arc<dyn ScoreBooster>)
167    }
168
169    /// 将自身转换为 `KeywordOptimizer` trait object,仅在 kind 为 KeywordOptimizer 时成功。
170    pub fn as_keyword_optimizer(self: Arc<Self>) -> Option<Arc<dyn KeywordOptimizer>> {
171        matches!(self.kind, RemoteComponentKind::KeywordOptimizer { .. })
172            .then(|| self as Arc<dyn KeywordOptimizer>)
173    }
174
175    /// 将自身转换为 `KeywordInjector` trait object,仅在 kind 为 KeywordInjector 时成功。
176    pub fn as_keyword_injector(self: Arc<Self>) -> Option<Arc<dyn KeywordInjector>> {
177        matches!(self.kind, RemoteComponentKind::KeywordInjector)
178            .then(|| self as Arc<dyn KeywordInjector>)
179    }
180
181    pub fn is_data_source(&self) -> bool {
182        matches!(self.kind, RemoteComponentKind::DataSource)
183    }
184
185    pub fn is_action_executor(&self) -> bool {
186        matches!(self.kind, RemoteComponentKind::ActionExecutor { .. })
187    }
188
189    pub fn is_plugin(&self) -> bool {
190        matches!(self.kind, RemoteComponentKind::Plugin { .. })
191    }
192    /// 是否为搜索引擎组件。
193    pub fn is_search_engine(&self) -> bool {
194        matches!(self.kind, RemoteComponentKind::SearchEngine)
195    }
196
197    /// 是否为分数增强器组件。
198    pub fn is_score_booster(&self) -> bool {
199        matches!(self.kind, RemoteComponentKind::ScoreBooster)
200    }
201
202    /// 是否为关键词优化器组件。
203    pub fn is_keyword_optimizer(&self) -> bool {
204        matches!(self.kind, RemoteComponentKind::KeywordOptimizer { .. })
205    }
206
207    /// 是否为关键词注入器组件。
208    pub fn is_keyword_injector(&self) -> bool {
209        matches!(self.kind, RemoteComponentKind::KeywordInjector)
210    }
211}
212#[async_trait]
213impl Configurable for RemoteComponent {
214    fn core(&self) -> &ComponentCore {
215        &self.core
216    }
217
218    fn setting_schema(&self) -> Vec<SettingDefinition> {
219        self.cached_schema.read().clone()
220    }
221
222    fn get_settings(&self) -> serde_json::Value {
223        self.cached_settings.read().clone()
224    }
225
226    fn config_actions(&self) -> Vec<ConfigActionDef> {
227        self.cached_actions.read().clone()
228    }
229
230    /// 默认启用状态来自插件自声明(discover 时经 RPC 拉取),与内置组件一致。
231    fn default_enabled(&self) -> bool {
232        *self.cached_default_enabled.read()
233    }
234
235    async fn apply_settings(&self, settings: serde_json::Value) -> Result<(), ConfigError> {
236        let client = self.client.clone();
237        let component_id = self.core.component_id().to_string();
238        let settings_clone = settings.clone();
239        client
240            .call::<_, serde_json::Value>(
241                plugin_methods::APPLY_SETTINGS,
242                ApplySettingsParams {
243                    component_id,
244                    settings: settings_clone,
245                },
246                Duration::from_secs(5),
247            )
248            .await
249            .map_err(to_config_error)?;
250        *self.cached_settings.write() = settings;
251        // 设置变更可能改变交互策略(如 on-enter 模式开关):
252        // 应用成功后立即刷新策略缓存,避免 reemit_current_session 推送旧策略
253        // (内置插件每次推送同步求值,远端以此对齐)。
254        self.refresh_interaction_policy().await;
255        if self.is_keyword_optimizer() {
256            self.refresh_keyword_optimizer_info().await;
257        }
258        Ok(())
259    }
260
261    async fn validate_settings(&self, settings: &serde_json::Value) -> Result<(), ConfigError> {
262        // 1. 宿主侧 Schema 校验(key 合法性、类型、对象结构、数值/数组约束等)
263        //    宿主 Schema 是最终权威,插件校验只能补充不能取代。
264        let contribution = self.settings_contribution()?;
265        contribution
266            .validate_values(settings)
267            .map_err(ConfigError::ValidationFailed)?;
268
269        // 2. 插件侧业务校验(通过 RPC 委托远程插件)
270        let client = self.client.clone();
271        let component_id = self.core.component_id().to_string();
272        let settings_clone = settings.clone();
273        let result: ValidateSettingsResult = client
274            .call(
275                plugin_methods::VALIDATE_SETTINGS,
276                ValidateSettingsParams {
277                    component_id,
278                    settings: settings_clone,
279                },
280                Duration::from_secs(5),
281            )
282            .await
283            .map_err(to_config_error)?;
284        if let Some(error) = result.error {
285            return Err(ConfigError::ValidationFailed(error));
286        }
287        Ok(())
288    }
289
290    async fn execute_config_action(
291        &self,
292        action: &str,
293        params: &serde_json::Value,
294    ) -> Result<serde_json::Value, String> {
295        let client = self.client.clone();
296        let component_id = self.core.component_id().to_string();
297        let action = action.to_string();
298        let params = params.clone();
299        let result: Result<serde_json::Value, ProtocolError> = client
300            .call::<_, serde_json::Value>(
301                plugin_methods::EXECUTE_CONFIG_ACTION,
302                ExecuteConfigActionParams {
303                    component_id,
304                    action,
305                    params,
306                },
307                Duration::from_secs(10),
308            )
309            .await;
310        result.map_err(|e| e.to_string())
311    }
312}
313
314#[async_trait]
315impl DataSource for RemoteComponent {
316    async fn fetch_candidates(&self) -> CachedCandidateData {
317        assert!(
318            matches!(self.kind, RemoteComponentKind::DataSource),
319            "RemoteComponent {} is not a DataSource but fetch_candidates() was called",
320            self.core.component_id()
321        );
322
323        let result: Result<FetchCandidatesResult, _> = self
324            .client
325            .call(
326                plugin_methods::FETCH_CANDIDATES,
327                FetchCandidatesParams {
328                    component_id: self.core.component_id().to_string(),
329                },
330                Duration::from_secs(30),
331            )
332            .await;
333
334        match result {
335            Ok(data) => {
336                let mut cache = CachedCandidateData::new();
337                for candidate in data.candidates {
338                    cache.add_candidate(candidate);
339                }
340                cache
341            }
342            Err(e) => {
343                tracing::warn!(
344                    "DataSource {} fetch_candidates failed: {}",
345                    self.core.component_id(),
346                    e
347                );
348                CachedCandidateData::new()
349            }
350        }
351    }
352}
353
354#[async_trait]
355impl ActionExecutor for RemoteComponent {
356    fn supported_target_types(&self) -> Vec<TargetType> {
357        match &self.kind {
358            RemoteComponentKind::ActionExecutor { target_types, .. } => target_types.clone(),
359            _ => panic!(
360                "RemoteComponent {} is not an ActionExecutor but supported_target_types() was called",
361                self.core.component_id()
362            ),
363        }
364    }
365
366    fn supported_actions(&self) -> Vec<ResultAction> {
367        match &self.kind {
368            RemoteComponentKind::ActionExecutor { result_actions, .. } => result_actions.clone(),
369            _ => panic!(
370                "RemoteComponent {} is not an ActionExecutor but supported_actions() was called",
371                self.core.component_id()
372            ),
373        }
374    }
375
376    async fn execute(&self, ctx: &ExecutionContext, action_id: &str) -> Result<(), ExecutionError> {
377        assert!(
378            matches!(self.kind, RemoteComponentKind::ActionExecutor { .. }),
379            "RemoteComponent {} is not an ActionExecutor but execute() was called",
380            self.core.component_id()
381        );
382
383        // 完整执行上下文原样透传(与进程内 ActionExecutor 一致),
384        // 不再重建伪 PluginContext(旧实现丢弃 target 等字段并伪造 trace_id)。
385        let result: Result<ExecutorExecuteResult, _> = self
386            .client
387            .call(
388                plugin_methods::EXECUTOR_EXECUTE,
389                ExecutorExecuteParams {
390                    component_id: self.core.component_id().to_string(),
391                    execution_ctx: ctx.clone(),
392                    action_id: action_id.to_string(),
393                },
394                Duration::from_secs(30),
395            )
396            .await;
397
398        match result {
399            Ok(r) => {
400                if let Some(error) = r.error {
401                    Err(ExecutionError::Failed(error))
402                } else {
403                    Ok(())
404                }
405            }
406            Err(e) => Err(ExecutionError::Failed(e.to_string())),
407        }
408    }
409}
410
411#[async_trait]
412impl Plugin for RemoteComponent {
413    async fn init(
414        &self,
415        ctx: &PluginContext,
416        _handle: Option<Arc<PluginHandle>>,
417    ) -> Result<(), PluginError> {
418        assert!(
419            matches!(self.kind, RemoteComponentKind::Plugin { .. }),
420            "RemoteComponent {} is not a Plugin but init() was called",
421            self.core.component_id()
422        );
423        // 远端进程无宿主句柄(跨进程不可序列化),init 语义为通知插件进程完成初始化;
424        // 平台能力由插件侧经 host() 的 host/* RPC 访问。
425        // METHOD_NOT_FOUND(旧 SDK 插件无 init 方法)= 无初始化需求,静默完成
426        // (与引入 init RPC 前的 no-op 行为一致);其他错误向上传播。
427        let metadata = match &self.kind {
428            RemoteComponentKind::Plugin { metadata, .. } => metadata,
429            _ => unreachable!("kind 已在上面断言为 Plugin"),
430        };
431        let result: Result<serde_json::Value, _> = self
432            .client
433            .call(
434                plugin_methods::INIT,
435                InitParams {
436                    plugin_id: metadata.id.clone(),
437                    ctx: ctx.clone(),
438                },
439                Duration::from_secs(10),
440            )
441            .await;
442        match result {
443            Ok(_) => Ok(()),
444            Err(zerolaunch_plugin_protocol::ProtocolError::Rpc { code, .. })
445                if code == codes::METHOD_NOT_FOUND =>
446            {
447                Ok(())
448            }
449            Err(e) => Err(PluginError::InitFailed(e.to_string())),
450        }
451    }
452
453    /// 交互策略:返回查询时刷新的缓存值(与内置每次查询同步求值语义对齐)。
454    /// 仅 Plugin 组件有策略(kind 已断言为 Plugin)。
455    fn interaction_policy(&self) -> PanelInteraction {
456        match &self.kind {
457            RemoteComponentKind::Plugin {
458                interaction_policy, ..
459            } => interaction_policy.read().clone(),
460            _ => panic!(
461                "RemoteComponent {} is not a Plugin but interaction_policy() was called",
462                self.core.component_id()
463            ),
464        }
465    }
466
467    async fn query(
468        &self,
469        ctx: &PluginContext,
470        query: &Query,
471    ) -> Result<QueryResponse, PluginError> {
472        let metadata = match &self.kind {
473            RemoteComponentKind::Plugin { metadata, .. } => metadata,
474            _ => panic!(
475                "RemoteComponent {} is not a Plugin but query() was called",
476                self.core.component_id()
477            ),
478        };
479
480        // 每次查询先刷新交互策略缓存(内置插件在会话推送时同步求值,远端以此对齐)。
481        self.refresh_interaction_policy().await;
482
483        self.client
484            .call::<_, QueryResponse>(
485                plugin_methods::QUERY,
486                QueryParams {
487                    plugin_id: metadata.id.clone(),
488                    ctx: ctx.clone(),
489                    query: query.clone(),
490                },
491                Duration::from_secs(30),
492            )
493            .await
494            .map_err(|e| PluginError::QueryFailed(e.to_string()))
495    }
496
497    /// 查询匹配:经 RPC 询问插件是否接管当前输入(响应为布尔值)。
498    ///
499    /// 超时故意远小于常规 5s 约定——本方法在按键热路径上执行,慢判定直接等于输入延迟。
500    /// - 旧 SDK 未实现该方法(`METHOD_NOT_FOUND`)→ 用同一份框架关键词判定兜底(与改动前行为一致);
501    /// - 其他错误/超时 → 按不命中处理并告警,不阻塞输入。
502    async fn match_query(&self, raw_query: &str, declared_trigger_keywords: &[String]) -> bool {
503        if !matches!(self.kind, RemoteComponentKind::Plugin { .. }) {
504            panic!(
505                "RemoteComponent {} is not a Plugin but match_query() was called",
506                self.core.component_id()
507            );
508        }
509        let result: Result<bool, _> = self
510            .client
511            .call(
512                plugin_methods::MATCH_QUERY,
513                MatchQueryParams {
514                    raw_query: raw_query.to_string(),
515                    trigger_keywords: declared_trigger_keywords.to_vec(),
516                },
517                MATCH_QUERY_TIMEOUT,
518            )
519            .await;
520        match result {
521            Ok(verdict) => verdict,
522            Err(ProtocolError::Rpc { code, .. }) if code == codes::METHOD_NOT_FOUND => {
523                keyword_trigger_match(declared_trigger_keywords, raw_query).is_some()
524            }
525            Err(e) => {
526                tracing::warn!(
527                    component_id = self.core.component_id(),
528                    error = %e,
529                    "插件查询匹配失败,按不命中处理"
530                );
531                false
532            }
533        }
534    }
535
536    async fn execute_action(
537        &self,
538        ctx: &PluginContext,
539        action_id: &str,
540        payload: serde_json::Value,
541    ) -> Result<(), PluginError> {
542        let metadata = match &self.kind {
543            RemoteComponentKind::Plugin { metadata, .. } => metadata,
544            _ => panic!(
545                "RemoteComponent {} is not a Plugin but execute_action() was called",
546                self.core.component_id()
547            ),
548        };
549
550        self.client
551            .call::<_, serde_json::Value>(
552                plugin_methods::EXECUTE_ACTION,
553                ExecuteActionParams {
554                    plugin_id: metadata.id.clone(),
555                    ctx: ctx.clone(),
556                    action_id: action_id.to_string(),
557                    payload,
558                },
559                Duration::from_secs(30),
560            )
561            .await
562            .map_err(|e| PluginError::ActionFailed(e.to_string()))?;
563        Ok(())
564    }
565
566    /// 退出钩子:把内置插件同款的 `Plugin::on_shutdown` 统一翻译为 `plugin/shutdown` RPC。
567    ///
568    /// 旧 SDK 未实现该方法(`METHOD_NOT_FOUND`)视为无收尾逻辑,返回 Ok;
569    /// 其他错误向上传播,由调用方(应用退出清理 / plugin-host teardown)记录后继续关闭。
570    async fn on_shutdown(&self, ctx: &PluginContext) -> Result<(), PluginError> {
571        if !matches!(self.kind, RemoteComponentKind::Plugin { .. }) {
572            panic!(
573                "RemoteComponent {} is not a Plugin but on_shutdown() was called",
574                self.core.component_id()
575            );
576        }
577        match self
578            .client
579            .call::<_, serde_json::Value>(
580                plugin_methods::SHUTDOWN,
581                ShutdownParams { ctx: ctx.clone() },
582                SHUTDOWN_HOOK_TIMEOUT,
583            )
584            .await
585        {
586            Ok(_) => Ok(()),
587            Err(ProtocolError::Rpc { code, .. }) if code == codes::METHOD_NOT_FOUND => Ok(()),
588            Err(e) => Err(PluginError::ShutdownFailed(e.to_string())),
589        }
590    }
591}
592
593impl RemoteComponent {
594    /// 经 RPC 刷新交互策略缓存(宿主在会话推送时同步读取)。
595    /// 策略为插件级语义:非 Plugin 组件(DataSource/Executor)无策略,直接跳过。
596    /// 失败静默:策略保持上次值(新协议方法,老 SDK 插件可能回 METHOD_NOT_FOUND)。
597    async fn refresh_interaction_policy(&self) {
598        let RemoteComponentKind::Plugin {
599            interaction_policy, ..
600        } = &self.kind
601        else {
602            return;
603        };
604        let result: Result<PanelInteraction, _> = self
605            .client
606            .call(
607                plugin_methods::INTERACTION_POLICY,
608                InteractionPolicyParams {
609                    component_id: self.core.component_id().to_string(),
610                },
611                Duration::from_secs(5),
612            )
613            .await;
614        if let Ok(policy) = result {
615            *interaction_policy.write() = policy;
616        }
617    }
618    /// 经 RPC 刷新优化器属性缓存(input_source / priority 为声明字段)。
619    /// 仅 KeywordOptimizer 组件有属性;失败静默保持上次值。
620    async fn refresh_keyword_optimizer_info(&self) {
621        let RemoteComponentKind::KeywordOptimizer { info } = &self.kind else {
622            return;
623        };
624        let result: Result<KeywordOptimizerInfo, _> = self
625            .client
626            .call(
627                plugin_methods::KEYWORD_OPTIMIZER_INFO,
628                KeywordOptimizerInfoParams {
629                    component_id: self.core.component_id().to_string(),
630                },
631                Duration::from_secs(5),
632            )
633            .await;
634        match result {
635            Ok(meta) => *info.write() = meta,
636            Err(e) => {
637                // 刷新失败保留上次值(候选管道按旧属性排序),告警便于诊断,不阻断设置流程。
638                tracing::warn!(
639                    "KeywordOptimizer {} 属性刷新失败: {}",
640                    self.core.component_id(),
641                    e
642                );
643            }
644        }
645    }
646}
647#[async_trait]
648impl SearchEngine for RemoteComponent {
649    async fn calculate_scores(
650        &self,
651        candidates: &CachedCandidateData,
652        query: &str,
653    ) -> Vec<ScoredCandidate> {
654        assert!(
655            matches!(self.kind, RemoteComponentKind::SearchEngine),
656            "RemoteComponent {} is not a SearchEngine but calculate_scores() was called",
657            self.core.component_id()
658        );
659        let result: Result<Vec<ScoredCandidate>, _> = self
660            .client
661            .call(
662                plugin_methods::CALCULATE_SCORES,
663                CalculateScoresParams {
664                    component_id: self.core.component_id().to_string(),
665                    candidates: candidates.to_data(),
666                    query: query.to_string(),
667                },
668                Duration::from_secs(5),
669            )
670            .await;
671        match result {
672            Ok(scored) => scored,
673            Err(e) => {
674                // 远端引擎故障:本次查询退化为空结果(插件禁用即恢复),不静默回退本地引擎。
675                tracing::error!(
676                    "SearchEngine {} calculate_scores failed: {}",
677                    self.core.component_id(),
678                    e
679                );
680                Vec::new()
681            }
682        }
683    }
684}
685
686#[async_trait]
687impl ScoreBooster for RemoteComponent {
688    async fn record(&self, candidate_id: CandidateId, data: &CachedCandidateData, query: &str) {
689        assert!(
690            matches!(self.kind, RemoteComponentKind::ScoreBooster),
691            "RemoteComponent {} is not a ScoreBooster but record() was called",
692            self.core.component_id()
693        );
694        let result: Result<serde_json::Value, _> = self
695            .client
696            .call(
697                plugin_methods::BOOSTER_RECORD,
698                BoosterRecordParams {
699                    component_id: self.core.component_id().to_string(),
700                    candidate_id,
701                    candidates: data.to_data(),
702                    query: query.to_string(),
703                },
704                Duration::from_secs(2),
705            )
706            .await;
707        if let Err(e) = result {
708            tracing::warn!(
709                "ScoreBooster {} record failed: {}",
710                self.core.component_id(),
711                e
712            );
713        }
714    }
715
716    async fn boost(
717        &self,
718        candidates: &mut Vec<ScoredCandidate>,
719        data: &CachedCandidateData,
720        query: &str,
721    ) {
722        assert!(
723            matches!(self.kind, RemoteComponentKind::ScoreBooster),
724            "RemoteComponent {} is not a ScoreBooster but boost() was called",
725            self.core.component_id()
726        );
727        let result: Result<Vec<ScoredCandidate>, _> = self
728            .client
729            .call(
730                plugin_methods::BOOSTER_BOOST,
731                BoosterBoostParams {
732                    component_id: self.core.component_id().to_string(),
733                    candidates: data.to_data(),
734                    scored: candidates.clone(),
735                    query: query.to_string(),
736                },
737                Duration::from_secs(5),
738            )
739            .await;
740        match result {
741            Ok(scored) => *candidates = scored,
742            Err(e) => {
743                // 增强失败:保留引擎原分数(无增强,结果不劣于无该组件)。
744                tracing::warn!(
745                    "ScoreBooster {} boost failed: {}",
746                    self.core.component_id(),
747                    e
748                );
749            }
750        }
751    }
752}
753
754#[async_trait]
755impl KeywordOptimizer for RemoteComponent {
756    async fn optimize(&self, keyword: &str) -> Vec<String> {
757        assert!(
758            matches!(self.kind, RemoteComponentKind::KeywordOptimizer { .. }),
759            "RemoteComponent {} is not a KeywordOptimizer but optimize() was called",
760            self.core.component_id()
761        );
762        let result: Result<Vec<String>, _> = self
763            .client
764            .call(
765                plugin_methods::KEYWORD_OPTIMIZE,
766                KeywordOptimizeParams {
767                    component_id: self.core.component_id().to_string(),
768                    keyword: keyword.to_string(),
769                    input_source: self.input_source(),
770                },
771                Duration::from_secs(10),
772            )
773            .await;
774        match result {
775            Ok(keywords) => keywords,
776            Err(e) => {
777                // 优化失败:跳过该组件(召回略降,候选仍可搜索)。
778                tracing::warn!(
779                    "KeywordOptimizer {} optimize failed: {}",
780                    self.core.component_id(),
781                    e
782                );
783                Vec::new()
784            }
785        }
786    }
787
788    fn input_source(&self) -> KeywordInputSource {
789        match &self.kind {
790            RemoteComponentKind::KeywordOptimizer { info } => info.read().input_source.clone(),
791            _ => panic!(
792                "RemoteComponent {} is not a KeywordOptimizer but input_source() was called",
793                self.core.component_id()
794            ),
795        }
796    }
797
798    fn get_priority(&self) -> u32 {
799        match &self.kind {
800            RemoteComponentKind::KeywordOptimizer { info } => info.read().priority,
801            _ => panic!(
802                "RemoteComponent {} is not a KeywordOptimizer but get_priority() was called",
803                self.core.component_id()
804            ),
805        }
806    }
807}
808
809#[async_trait]
810impl KeywordInjector for RemoteComponent {
811    async fn inject_keywords(&self, candidate: &SearchCandidate) -> Vec<String> {
812        assert!(
813            matches!(self.kind, RemoteComponentKind::KeywordInjector),
814            "RemoteComponent {} is not a KeywordInjector but inject_keywords() was called",
815            self.core.component_id()
816        );
817        let result: Result<Vec<String>, _> = self
818            .client
819            .call(
820                plugin_methods::KEYWORD_INJECT,
821                KeywordInjectParams {
822                    component_id: self.core.component_id().to_string(),
823                    candidate: candidate.clone(),
824                },
825                Duration::from_secs(10),
826            )
827            .await;
828        match result {
829            Ok(keywords) => keywords,
830            Err(e) => {
831                // 注入失败:跳过该组件(召回略降,候选仍可搜索)。
832                tracing::warn!(
833                    "KeywordInjector {} inject_keywords failed: {}",
834                    self.core.component_id(),
835                    e
836                );
837                Vec::new()
838            }
839        }
840    }
841}