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