Skip to main content

zerolaunch_plugin_api/plugin/
types.rs

1use crate::config::Configurable;
2use crate::plugin::cached_candidate::CachedCandidateData;
3use crate::services::icon_request::IconRequest;
4use crate::services::parameter::types::ParameterSnapshot;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10pub type CandidateId = u64;
11
12/// 执行目标类型枚举,用于 ActionExecutor 注册和查找。
13/// 序列化键名显式标注(serde-rename 契约),与 as_str() 前端词表一致。
14#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
15pub enum TargetType {
16    /// 文件路径目标(exe/lnk/url 等,由对应 executor 启动)。
17    #[serde(rename = "Path")]
18    Path,
19    /// 应用目标(应用枚举器产物,经 AppLauncher 启动)。
20    #[serde(rename = "App")]
21    App,
22    /// 文件目标(系统关联打开)。
23    #[serde(rename = "File")]
24    File,
25    /// URL 目标(浏览器打开)。
26    #[serde(rename = "Url")]
27    Url,
28    /// 命令目标(shell 执行)。
29    #[serde(rename = "Command")]
30    Command,
31    /// 内置命令目标(宿主 app_command 通道)。
32    #[serde(rename = "BuiltinCommand")]
33    BuiltinCommand,
34    /// 沉浸式插件面板候选 —— 选中即唤醒插件面板(宿主内置 PluginWakeExecutor
35    /// 消费,不注册第三方执行器)。前端词表键名 "Plugin"。
36    #[serde(rename = "Plugin")]
37    Plugin,
38}
39
40impl TargetType {
41    pub fn as_str(&self) -> &'static str {
42        match self {
43            TargetType::Path => "Path",
44            TargetType::App => "App",
45            TargetType::File => "File",
46            TargetType::Url => "Url",
47            TargetType::Command => "Command",
48            TargetType::BuiltinCommand => "BuiltinCommand",
49            TargetType::Plugin => "Plugin",
50        }
51    }
52}
53
54/// 执行目标
55#[derive(Debug, Clone, Serialize, Deserialize, Eq, Hash, PartialEq)]
56pub enum ExecutionTarget {
57    #[serde(rename = "path")]
58    Path(String),
59    #[serde(rename = "app")]
60    App(String),
61    #[serde(rename = "file")]
62    File(String),
63    #[serde(rename = "url")]
64    Url(String),
65    #[serde(rename = "command")]
66    Command(String),
67    #[serde(rename = "builtinCommand")]
68    BuiltinCommand(String),
69    /// 沉浸式插件面板候选 —— 载荷为插件 id,选中后由宿主内置
70    /// PluginWakeExecutor 唤醒其面板(wake_plugin)。
71    #[serde(rename = "plugin")]
72    Plugin(String),
73}
74
75impl ExecutionTarget {
76    pub fn target_type(&self) -> TargetType {
77        match self {
78            ExecutionTarget::Path(_) => TargetType::Path,
79            ExecutionTarget::App(_) => TargetType::App,
80            ExecutionTarget::File(_) => TargetType::File,
81            ExecutionTarget::Url(_) => TargetType::Url,
82            ExecutionTarget::Command(_) => TargetType::Command,
83            ExecutionTarget::BuiltinCommand(_) => TargetType::BuiltinCommand,
84            ExecutionTarget::Plugin(_) => TargetType::Plugin,
85        }
86    }
87
88    pub fn payload(&self) -> &str {
89        match self {
90            ExecutionTarget::Path(s) => s,
91            ExecutionTarget::App(s) => s,
92            ExecutionTarget::File(s) => s,
93            ExecutionTarget::Url(s) => s,
94            ExecutionTarget::Command(s) => s,
95            ExecutionTarget::BuiltinCommand(s) => s,
96            ExecutionTarget::Plugin(s) => s,
97        }
98    }
99}
100
101/// 执行上下文
102///
103/// 可序列化:远端 ActionExecutor 经 RPC 收到完整执行上下文(与进程内一致)。
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ExecutionContext {
106    pub target: ExecutionTarget,
107    pub display_name: String,
108    /// 用户输入的参数列表
109    pub user_args: Vec<String>,
110    /// 系统参数快照(不透明句柄)
111    pub parameter_snapshot: ParameterSnapshot,
112    /// 宿主当前界面语言(如 "zh-Hans");由宿主在执行分发时填充,
113    /// 远端 ActionExecutor 经 RPC 透传进插件上下文(进程内执行器直接用)。
114    pub locale: String,
115}
116
117impl Default for ExecutionContext {
118    fn default() -> Self {
119        Self {
120            target: ExecutionTarget::Path(String::new()),
121            display_name: String::new(),
122            user_args: Vec::new(),
123            parameter_snapshot: ParameterSnapshot::empty(),
124            locale: String::new(),
125        }
126    }
127}
128
129/// 执行错误
130#[derive(Debug, thiserror::Error)]
131pub enum ExecutionError {
132    #[error("Execution failed: {0}")]
133    Failed(String),
134
135    #[error("Executor not found for target type: {0:?}")]
136    NotFound(TargetType),
137
138    #[error("Unsupported action: {0:?}:{1}")]
139    UnsupportedAction(TargetType, String),
140
141    /// 窗口唤醒失败,携带回退目标
142    /// Executor 声明回退策略,Registry 负责执行回退
143    #[error("Window activation failed, fallback to: {fallback_action}")]
144    ActivationFailed { fallback_action: String },
145}
146
147/// 注册错误
148#[derive(Debug, thiserror::Error)]
149pub enum RegistrationError {
150    #[error("Action '{action_id}' for {target_type:?} is already registered")]
151    ActionConflict {
152        target_type: TargetType,
153        action_id: String,
154    },
155}
156
157// 这个是一个搜索候选项
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SearchCandidate {
160    // 候选项的唯一标识符
161    #[serde(rename = "id")]
162    pub id: CandidateId,
163    // 表示用于显示在搜索结果中的名称
164    #[serde(rename = "name")]
165    pub name: String,
166    // 表示用于显示在搜索结果中的图标
167    #[serde(rename = "icon")]
168    pub icon: IconRequest,
169    // 执行目标,替代原 launch_method
170    #[serde(rename = "target")]
171    pub target: ExecutionTarget,
172    // 表示该候选项的关键词,即怎么可以确认用户想要启动这个候选项
173    #[serde(rename = "keywords")]
174    pub keywords: Vec<String>,
175    // 固定的权重偏移,用于在计算分数时考虑该候选项的固定权重。由每个数据源来控制各自的权重
176    #[serde(rename = "bias")]
177    pub bias: f64,
178    /// 触发关键词列表,用于行内模式的精确匹配
179    #[serde(rename = "triggerKeywords")]
180    pub trigger_keywords: Vec<String>,
181}
182
183/// 分数明细的计入方式 —— 标识该项是加权加分还是乘法系数。
184///
185/// 跨 IPC 序列化,由引擎/增强器构造,前端按 kind 渲染明细形态:
186/// 加法项显示 `score × weight = 乘积`,乘法项显示 `× 系数`,
187/// 避免把乘法系数误读为加分项导致总分无法核对。
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
189pub enum ScoreDetailKind {
190    /// 加权加分项:该项的 score × weight 计入总分(默认)。
191    #[default]
192    #[serde(rename = "add")]
193    Add,
194    /// 乘法系数项:该项的 score 乘到当前累计分数上(如长度比率、溢出惩罚、抑制因子)。
195    #[serde(rename = "multiply")]
196    Multiply,
197}
198
199// 这个是一个搜索候选项的详细分数
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ScoreDetail {
202    // 基础分
203    #[serde(rename = "score")]
204    pub score: f64,
205    // 当前权重分
206    #[serde(rename = "weight")]
207    pub weight: f64,
208    // 这个是什么分,以及这个分的来源
209    #[serde(rename = "description")]
210    pub description: String,
211    // 该项的计入方式:add = 加权加分,multiply = 乘法系数
212    #[serde(rename = "kind", default)]
213    pub kind: ScoreDetailKind,
214}
215
216// 这个是一个搜索候选项的分数
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ScoredCandidate {
219    // 表示该候选项的分数
220    #[serde(rename = "candidateId")]
221    pub candidate_id: CandidateId,
222    // 表示该候选项的分数
223    #[serde(rename = "score")]
224    pub score: f64,
225    //表示该候选项得来的详细的分数:加法项按 sum(score × weight) 计入,
226    //乘法项按系数乘入(引擎先乘系数再加加法项,增强器仅产出加法项)
227    #[serde(rename = "detailedScore")]
228    pub detailed_score: Vec<ScoreDetail>,
229}
230
231// 表示一个数据源
232#[async_trait]
233pub trait DataSource: Configurable {
234    async fn fetch_candidates(&self) -> CachedCandidateData;
235}
236
237// 表示对搜索的候选项的搜索关键字做优化的组件,通常是对搜索关键字进行扩展或者优化,以提高搜索的召回率
238#[async_trait]
239pub trait KeywordOptimizer: Configurable {
240    // 根据关键词优化出一组新关键词,通常是对关键词进行分词、扩展或转换
241    async fn optimize(&self, keyword: &str) -> Vec<String>;
242    // 是否对所有已累积的关键词进行优化(true),还是只对原始名称优化
243    fn uses_context(&self) -> bool {
244        false
245    }
246    // 获得优先级,优先级小的优化器会先被调用,优先级相同的优化器会按照注册的顺序被调用
247    fn get_priority(&self) -> u32;
248}
249
250#[async_trait]
251pub trait KeywordInjector: Configurable {
252    /// 根据候选项的完整上下文注入额外关键字。
253    /// 与 KeywordOptimizer 不同,此方法可以访问候选项的 target、icon 等完整信息,
254    /// 用于实现"基于候选身份的关键字注入"(如别名)。
255    async fn inject_keywords(&self, candidate: &SearchCandidate) -> Vec<String>;
256}
257
258// 表示一个搜索引擎,用于计算搜索候选项的分数
259// 用于根据搜索候选项的分数进行排序
260// 搜索引擎通常计算的是一个候选项与用户输入之间的关系
261#[async_trait]
262pub trait SearchEngine: Configurable {
263    async fn calculate_scores(
264        &self,
265        candidates: &CachedCandidateData,
266        query: &str,
267    ) -> Vec<ScoredCandidate>;
268}
269
270// 表示一个分数优化器,用于对搜索候选项的分数进行优化
271// 用于根据搜索候选项的分数进行排序
272// 分数优化器则是计算的是 *所有* 候选项与用户输入之间的关系
273#[async_trait]
274pub trait ScoreBooster: Configurable {
275    // 记录用户输入了这个查询时,选择的是这个候选项
276    async fn record(&self, candidate_id: CandidateId, data: &CachedCandidateData, query: &str);
277    // 根据用户历史输入的查询与选择的候选项,优化当前查询所得到的所有候选项的分数
278    async fn boost(
279        &self,
280        candidates: &mut Vec<ScoredCandidate>,
281        data: &CachedCandidateData,
282        query: &str,
283    );
284}
285
286/// 动作执行器 trait
287/// 每个 Executor 可以声明支持多种 TargetType 和多种 Action
288/// Executor 继承 Configurable,以支持统一配置管理和发现
289#[async_trait]
290pub trait ActionExecutor: Configurable {
291    /// 返回该 Executor 支持的目标类型集合
292    fn supported_target_types(&self) -> Vec<TargetType>;
293
294    /// 返回该 Executor 支持的动作列表
295    fn supported_actions(&self) -> Vec<ResultAction> {
296        vec![ResultAction {
297            id: "execute".to_string(),
298            label: "执行".to_string(),
299            icon: IconRequest::Path(String::new()),
300            is_default: true,
301            shortcut_key: String::new(),
302        }]
303    }
304
305    /// 根据动作 ID 执行对应的操作
306    /// 参数:ctx - 执行上下文;action_id - 动作 ID
307    /// 返回:执行成功返回 Ok(()),失败返回 ExecutionError
308    async fn execute(&self, ctx: &ExecutionContext, action_id: &str) -> Result<(), ExecutionError>;
309}
310
311/// 查询来源通道 — 标识查询进入后端的入口,用于通道间隔离。
312///
313/// 各通道独立维护查询版本计数器,跨通道查询互不使对方过期;
314/// 且仅 GUI 通道允许改写会话状态(会话模式、面板交互事件)与
315/// 插件侧跨查询共享状态(如剪贴板缓存);CLI 与面板通道为只读辅助路径。
316///
317/// 序列化键名显式标注(serde-rename 契约):本枚举经 PluginContext 跨 RPC
318/// 传输到远端插件进程,键名更改会使旧 SDK 二进制反序列化失败。
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
320pub enum QueryChannel {
321    /// 主窗口 GUI 查询(bridge_query,用户搜索栏输入)—— 唯一允许改写会话的通道。
322    #[default]
323    #[serde(rename = "ui")]
324    Ui,
325    /// 本地 CLI HTTP 查询(/v1/query,外部进程只读辅助路径)。
326    #[serde(rename = "cli")]
327    Cli,
328    /// 沉浸式/行内插件面板内查询(bridge_query 显式指定插件)——
329    /// GUI 进程内只读辅助路径:不改写会话、不参与用户输入版本竞争。
330    #[serde(rename = "panel")]
331    Panel,
332}
333
334/// 查询版本门控:宿主在每次查询入口分配单调递增版本号,
335/// 供插件在写入跨查询共享状态(如翻译结果缓存)前判断自身查询
336/// 是否已被更新的查询取代。
337///
338/// 仅宿主进程内使用(内置插件经 PluginContext 注入);不跨 RPC 传输,
339/// 远端插件或直接构造的上下文无门控,此时视为始终为最新。
340#[derive(Debug, Clone)]
341pub struct QueryRevisionGate {
342    /// 当前查询被分配的版本号。
343    revision: u64,
344    /// 宿主侧「最新已分配版本」计数器(每次查询入口 fetch_add)。
345    latest: Arc<AtomicU64>,
346}
347
348impl QueryRevisionGate {
349    /// 创建门控:revision 为当前查询的版本号,latest 为共享的最新版本计数器。
350    pub fn new(revision: u64, latest: Arc<AtomicU64>) -> Self {
351        Self { revision, latest }
352    }
353
354    /// 当前查询的版本号,供日志与追踪使用。
355    pub fn revision(&self) -> u64 {
356        self.revision
357    }
358
359    /// 当前查询是否仍是最新:成立才允许写入跨查询共享状态。
360    pub fn is_current(&self) -> bool {
361        self.latest.load(Ordering::Relaxed) == self.revision
362    }
363}
364
365/// 请求级上下文,在宿主与插件之间共享。
366/// 服务于插件生命周期/查询/动作调用,并携带日志关联 ID。
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct PluginContext {
369    // 当前的请求 ID
370    pub trace_id: String,
371    // 当前的请求 ID
372    pub query_id: Option<String>,
373    // 处理当前请求的插件 ID
374    pub plugin_id: Option<String>,
375    /// 查询版本门控(宿主注入;#[serde(skip)] 不跨 RPC 传输,
376    /// 远端插件或直接构造的上下文为 None,此时视为始终为最新)。
377    #[serde(skip)]
378    pub query_revision_gate: Option<QueryRevisionGate>,
379    /// 查询来源通道(宿主注入;远端插件经 RPC 反序列化时缺省视为 GUI 通道)。
380    #[serde(default)]
381    pub query_channel: QueryChannel,
382    /// 宿主当前界面语言(如 "zh-Hans"),供插件生成本地化文本;
383    /// 远端插件反序列化缺省为空串,可经 host/i18n.get_locale 主动查询。
384    #[serde(default)]
385    pub locale: String,
386}
387
388impl PluginContext {
389    pub fn new(trace_id: &str) -> Self {
390        Self {
391            trace_id: trace_id.to_string(),
392            query_id: None,
393            plugin_id: None,
394            query_revision_gate: None,
395            query_channel: QueryChannel::Ui,
396            locale: String::new(),
397        }
398    }
399
400    pub fn with_query(&mut self, query_id: String) {
401        self.query_id = Some(query_id);
402    }
403
404    pub fn with_plugin_id(&mut self, plugin_id: String) {
405        self.plugin_id = Some(plugin_id);
406    }
407
408    /// 注入查询版本门控(宿主查询入口调用)。
409    pub fn set_query_revision_gate(&mut self, gate: QueryRevisionGate) {
410        self.query_revision_gate = Some(gate);
411    }
412
413    /// 当前查询是否仍是最新;无门控(远端插件、测试、mock)恒为 true。
414    pub fn is_query_current(&self) -> bool {
415        self.query_revision_gate
416            .as_ref()
417            .is_none_or(|g| g.is_current())
418    }
419
420    /// 当前查询的版本号;无门控时为 0,仅供日志使用。
421    pub fn query_revision(&self) -> u64 {
422        self.query_revision_gate
423            .as_ref()
424            .map_or(0, |g| g.revision())
425    }
426}
427
428/// 发送给插件查询处理器的标准化查询载荷。
429/// 服务于查询分发和插件侧搜索逻辑。
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct Query {
432    /// 本次查询的唯一标识,取自 bridge_query 中生成的 trace_id,用于日志关联和插件上下文。
433    pub id: String,
434    /// 用户在搜索栏中输入的原始字符串,未经任何处理,用于触发器匹配和日志记录。
435    pub raw_query: String,
436    /// 派生自 raw_query 的搜索词。普通搜索为全小写形式,插件模式为剥离触发关键词后的剩余部分。
437    pub search_term: String,
438    /// 是否由用户显式确认(如按 Enter)触发的查询。
439    /// 行内插件手动模式(PanelQueryTrigger::OnEnter)用它区分确认查询与预览查询;默认 false。
440    #[serde(rename = "confirm", default)]
441    pub confirm: bool,
442}
443
444/// 插件面板查询触发方式的通用语义。
445/// 服务于宿主判断行内插件模式下输入后是否自动发起查询。
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
447pub enum PanelQueryTrigger {
448    /// 输入后自动触发查询(默认;配合 query_debounce_ms 防抖)。
449    #[default]
450    #[serde(rename = "onInput")]
451    OnInput,
452    /// 输入不自动触发查询,由用户按 Enter 手动触发。
453    #[serde(rename = "onEnter")]
454    OnEnter,
455}
456
457/// 插件面板按键绑定 —— 声明式按键契约的最小单元。
458/// 服务于宿主解释执行:插件声明按键 → 宿主翻译为动作语义。
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
460pub struct PanelKeyBinding {
461    /// 按键格式:"Enter" | "Ctrl+Enter" | "Escape" | "Tab" | "a"。
462    #[serde(rename = "key")]
463    pub key: String,
464    /// 按键触发的动作。
465    #[serde(rename = "action")]
466    pub action: PanelKeyAction,
467}
468
469/// 面板按键动作 —— 宿主解释执行的动作语义。
470/// 服务于插件面板的完整按键权声明(键盘状态机契约)。
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(tag = "kind")]
473pub enum PanelKeyAction {
474    /// 确认当前面板状态(Enter 标准语义,宿主 confirmQuery 三分支):
475    /// 面板有可执行动作时执行默认动作(如复制结果),否则发起确认查询(翻译/计算/失败重试等)。
476    #[serde(rename = "confirm")]
477    Confirm,
478    /// 执行面板动作:None = 执行面板默认动作(不指定);Some(id) = 执行指定动作。
479    #[serde(rename = "executeAction")]
480    ExecuteAction {
481        /// 动作 ID:None = 执行面板默认动作;Some = 执行指定动作。
482        #[serde(rename = "actionId")]
483        action_id: Option<String>,
484    },
485    /// 返回默认面板。
486    #[serde(rename = "goBack")]
487    GoBack,
488    /// 跳转到同一插件内的子面板。
489    #[serde(rename = "gotoPanel")]
490    GotoPanel {
491        #[serde(rename = "panelId")]
492        panel_id: String,
493    },
494    /// 触发插件自定义动作(经面板动作通道回插件,action 即插件动作 ID)。
495    #[serde(rename = "custom")]
496    Custom {
497        #[serde(rename = "action")]
498        action: String,
499        #[serde(rename = "args")]
500        args: serde_json::Value,
501    },
502}
503
504/// 插件面板响应携带的通用交互策略。
505/// 服务于宿主处理输入查询触发时机,不属于插件持久化配置。
506///
507/// 形态语义(行内/沉浸式均为同一份策略,宿主无条件推送):
508/// - `bindings` 对行内与沉浸式**均生效**——沉浸式全屏面板的退出
509///   只能靠插件声明(宿主零兜底),裁剪会导致无法退出;
510/// - `query_trigger` / `query_debounce_ms` 仅行内形态有消费方
511///   (沉浸式隐藏搜索栏,无输入触发路径),闲置时无实害。
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
513pub struct PanelInteraction {
514    /// 查询触发方式:onInput 输入自动触发 / onEnter 由用户按 Enter 手动触发。
515    #[serde(rename = "queryTrigger", default)]
516    pub query_trigger: PanelQueryTrigger,
517    /// 后续输入触发查询前的防抖延迟,单位为毫秒(仅 onInput 模式生效)。
518    #[serde(rename = "queryDebounceMs", default)]
519    pub query_debounce_ms: u64,
520    /// 面板按键绑定列表 —— 声明式按键契约(声明即接管:命中绑定由宿主解释执行,
521    /// 未声明的键一律交还浏览器/输入框,宿主不做兜底)。
522    /// 反序列化缺省为空列表(旧插件未声明时按键全部放行)。
523    #[serde(rename = "bindings", default)]
524    pub bindings: Vec<PanelKeyBinding>,
525}
526
527/// 插件查询响应 —— 一次查询的展示结果契约(跨 IPC 序列化,字段键名与前端
528/// `BridgeQueryResponse.mode` 词表对齐)。
529///
530/// 由 `Plugin::query` / 宿主流程返回,经 SessionDispatcher 路由后包装为
531/// `BridgeQueryResponse` 下发前端;四种变体对应前端不同的展示形态。
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub enum QueryResponse {
534    /// 候选列表结果 —— 默认搜索与插件均可返回,前端按列表渲染。
535    ///
536    /// 空列表即空结果(前端映射 mode "search" + 空数组,展示形态层面
537    /// 不区分 List/Empty)。
538    #[serde(rename = "list")]
539    List {
540        /// 排序后的候选项列表(含动作、占位符统计、触发关键词等展示元数据)。
541        #[serde(rename = "results")]
542        results: Vec<ListItem>,
543    },
544    /// 插件自定义面板 —— 触发式插件接管会话时的渲染结果。
545    ///
546    /// `keep_search_bar` 决定面板形态:true 为行内面板(保留搜索栏,前端
547    /// mode "plugin_panel"),false 为全页面接管(mode "plugin_immersive")。
548    #[serde(rename = "customPanel")]
549    CustomPanel {
550        /// 面板类型标识,前端按此选择面板组件渲染。
551        #[serde(rename = "panelType")]
552        panel_type: String,
553        /// 面板数据(自由 JSON,面板自行定义结构)。
554        #[serde(rename = "data")]
555        data: serde_json::Value,
556        /// 面板动作列表(供 Enter 执行默认动作 / 面板内动作切换)。
557        #[serde(rename = "actions")]
558        actions: Vec<ResultAction>,
559        /// 是否保留搜索栏(true = 行内面板;false = 全页面接管)。
560        #[serde(rename = "keepSearchBar")]
561        keep_search_bar: bool,
562    },
563    /// 空结果 —— 无任何展示内容。
564    ///
565    /// 前端映射 mode "search" + 空数组(与 `List` 空列表行为一致)。
566    /// 注意:插件命中触发词后即使返回 Empty 也是「已处理」,
567    /// 不得继续 fallback 到默认搜索。
568    #[serde(rename = "empty")]
569    Empty,
570    /// 行内参数模式:后端检测到触发关键词+空格后自动进入。
571    /// 前端据此清空搜索栏并展示参数输入 UI。
572    #[serde(rename = "inlineParam")]
573    InlineParam {
574        /// 目标候选项 ID(确认时回传执行)。
575        #[serde(rename = "candidateId")]
576        candidate_id: CandidateId,
577        /// 命中的触发关键词(前端展示 + 退出判定镜像使用)。
578        #[serde(rename = "triggerKeyword")]
579        trigger_keyword: String,
580        /// 该候选项要求的用户参数个数(前端据此校验输入完整性)。
581        #[serde(rename = "userArgCount")]
582        user_arg_count: usize,
583    },
584}
585
586/// 插件返回给宿主的搜索结果项。
587/// 服务于结果聚合、排序与 UI 渲染。
588#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct ListItem {
590    // 这个是候选项的唯一标识符
591    #[serde(rename = "id")]
592    pub id: CandidateId,
593    #[serde(rename = "title")]
594    pub title: String,
595    #[serde(rename = "subtitle")]
596    pub subtitle: String,
597    #[serde(rename = "icon")]
598    pub icon: IconRequest,
599    #[serde(rename = "score")]
600    pub score: f64,
601    // 一个动作列表中只可以有一个默认动作,默认动作会在用户直接按下回车时被触发(由程序员保证)
602    #[serde(rename = "actions")]
603    pub actions: Vec<ResultAction>,
604    /// 目标类型字符串,供前端 ResultItemProvider/ActionInjector 匹配使用
605    #[serde(rename = "targetType")]
606    pub target_type: String,
607    /// 用户参数 {} 的数量
608    #[serde(rename = "userArgCount")]
609    pub user_arg_count: usize,
610    /// 是否包含系统参数({clip}, {hwnd}, {selection})
611    #[serde(rename = "hasSystemParams")]
612    pub has_system_params: bool,
613    /// 触发关键词列表
614    #[serde(rename = "triggerKeywords")]
615    pub trigger_keywords: Vec<String>,
616}
617
618/// 挂载在查询结果上的动作项。
619/// 服务于用户触发后的 Plugin::execute_action 执行流程。
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct ResultAction {
622    // 这个是动作的唯一标识符,通常是一个字符串,由插件定义
623    #[serde(rename = "id")]
624    pub id: String,
625    // 这个是动作的显示名称,用于展示在 UI 上
626    #[serde(rename = "label")]
627    pub label: String,
628    // 这个是该选项的图标,用于展示在 UI 上
629    #[serde(rename = "icon")]
630    pub icon: IconRequest,
631    // 是不是默认的动作,默认的动作会在用户直接按下回车时被触发
632    #[serde(rename = "isDefault")]
633    pub is_default: bool,
634    /// 快捷键提示,格式如 "Shift+Enter"、"Ctrl+Enter"
635    /// 前端根据此字段匹配修饰键到 action 的映射
636    #[serde(rename = "shortcutKey")]
637    pub shortcut_key: String,
638}
639
640/// 单个插件实例的静态元数据描述。
641/// 服务于注册中心索引、触发词路由与插件发现/展示。
642#[derive(Debug, Clone, Serialize, Deserialize)]
643pub struct PluginMetadata {
644    #[serde(rename = "id")]
645    pub id: String,
646    #[serde(rename = "name")]
647    pub name: String,
648    #[serde(rename = "version")]
649    pub version: String,
650    #[serde(rename = "description")]
651    pub description: String,
652    #[serde(rename = "author")]
653    pub author: String,
654    /// 触发关键词列表 —— **语义随形态而变**:
655    /// - 行内形态(Inline):路由触发词,用户输入"触发词 + 空格"命中路由;
656    /// - 沉浸式形态(Panel):**候选搜索关键字**——宿主不将其写入触发词路由,
657    ///   而是注入到该插件的默认搜索候选项匹配关键字中(用户输入该词显示候选项)。
658    #[serde(rename = "triggerKeywords")]
659    pub trigger_keywords: Vec<String>,
660    #[serde(rename = "supportedOs")]
661    pub supported_os: Vec<String>,
662    #[serde(rename = "priority")]
663    pub priority: u32,
664    /// 插件种类(宿主管辖的运行属性):内置代码构造恒为 Builtin;
665    /// 第三方由 plugin-host 加载时强制覆盖为 ThirdParty(防插件谎报)。
666    #[serde(rename = "kind", default)]
667    pub kind: PluginKind,
668    /// 全局唤醒快捷键(如 "Ctrl+E"),可空。
669    #[serde(rename = "hotkey", default)]
670    pub hotkey: Option<String>,
671    /// 插件显示图标(data URL,如 "data:image/png;base64,..."),可空表示无图标。
672    /// 内置与第三方统一为数据,消费方直接透传。
673    #[serde(rename = "icon", default)]
674    pub icon: Option<String>,
675    /// 插件形态:inline = 行内插件(关键词唤醒,保留搜索栏);panel = 完全插件模式
676    /// (trigger 类型,唤醒后接管搜索窗口)。决定热键唤醒资格与图标展示门控。
677    /// 缺省 Panel:旧插件无此字段时按 panel 处理(兼容旧热键插件行为)。
678    #[serde(rename = "mode", default)]
679    pub mode: PluginMode,
680}
681
682/// 插件形态 —— 决定唤醒方式与展示形态。
683/// 与 PluginKind(内置/第三方)正交;序列化键名 "inline"/"panel"。
684///
685/// Inline = 触发词路由(trigger_keywords 生效);Panel = 触发词不参与路由
686/// (转为候选搜索关键字),仅经热键(hotkey)或候选项选中唤醒,且响应必须为
687/// `CustomPanel{keep_search_bar: false}`(全窗口接管)。
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
689pub enum PluginMode {
690    /// 行内插件:仅关键词唤醒,结果/面板嵌入搜索窗口(保留搜索栏)。
691    #[serde(rename = "inline")]
692    Inline,
693    /// 完全插件模式(trigger 类型):唤醒后接管搜索窗口(可全页面)。
694    #[default]
695    #[serde(rename = "panel")]
696    Panel,
697}
698
699/// 插件种类 —— 区分内置(编译进二进制)与第三方(外部子进程)。
700///
701/// 跨 IPC 序列化(InstalledPluginInfo.kind / 前端行模型),
702/// 也用于 PluginManager 内部统一视图 PluginInfo.kind;
703/// 前端以 JSON 键名 "builtin"/"third-party" 作联合类型判断。
704#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
705pub enum PluginKind {
706    /// 编译进二进制、由 inventory 自动发现的内置插件。
707    #[serde(rename = "builtin")]
708    Builtin,
709    /// 外部子进程加载的第三方插件(子进程 + stdio JSON-RPC)。
710    #[default]
711    #[serde(rename = "third-party")]
712    ThirdParty,
713}
714
715/// 插件层统一错误类型。
716/// 服务于生命周期/查询/动作/设置相关错误在宿主与插件间传播。
717#[derive(Debug, thiserror::Error)]
718pub enum PluginError {
719    #[error("Plugin not found: {0}")]
720    NotFound(String),
721
722    #[error("Plugin initialization failed: {0}")]
723    InitFailed(String),
724
725    #[error("Query failed: {0}")]
726    QueryFailed(String),
727
728    #[error("Action execution failed: {0}")]
729    ActionFailed(String),
730
731    #[error("Invalid setting: {0}")]
732    InvalidSetting(String),
733}
734
735#[cfg(test)]
736mod tests {
737    use super::{
738        PanelInteraction, PanelKeyAction, PanelKeyBinding, PanelQueryTrigger, PluginContext,
739        QueryChannel, QueryRevisionGate,
740    };
741    use serde_json::json;
742    use std::sync::atomic::{AtomicU64, Ordering};
743    use std::sync::Arc;
744
745    #[test]
746    /// 验证门控:计数器未越过时当前查询仍有效,越过后失效。
747    fn query_revision_gate_tracks_latest() {
748        let latest = Arc::new(AtomicU64::new(2));
749        let gate = QueryRevisionGate::new(2, latest.clone());
750        assert!(gate.is_current(), "版本号与最新一致时应有效");
751        assert_eq!(gate.revision(), 2);
752
753        latest.fetch_add(1, Ordering::Relaxed);
754        assert!(!gate.is_current(), "更新的查询到达后应失效");
755    }
756
757    #[test]
758    /// 验证 PluginContext 门控默认行为与注入行为。
759    fn plugin_context_gate_defaults_to_current() {
760        let mut ctx = PluginContext::new("trace-1");
761        assert!(ctx.is_query_current(), "无门控时应恒为最新");
762        assert_eq!(ctx.query_revision(), 0);
763        assert_eq!(
764            ctx.query_channel,
765            QueryChannel::Ui,
766            "未显式注入时应缺省为 GUI 通道"
767        );
768
769        let latest = Arc::new(AtomicU64::new(1));
770        ctx.set_query_revision_gate(QueryRevisionGate::new(1, latest));
771        assert!(ctx.is_query_current());
772    }
773
774    #[test]
775    /// 验证门控与句柄字段不参与序列化(不跨 RPC 传输),反序列化后为 None。
776    fn plugin_context_skips_gate_in_serialization() {
777        let mut ctx = PluginContext::new("trace-2");
778        ctx.set_query_revision_gate(QueryRevisionGate::new(1, Arc::new(AtomicU64::new(1))));
779        let json = serde_json::to_string(&ctx).expect("上下文应可序列化");
780        assert!(
781            !json.contains("revision") && !json.contains("gate"),
782            "门控字段不应出现在序列化结果中: {}",
783            json
784        );
785        assert!(
786            !json.contains("handle"),
787            "句柄字段不应出现在序列化结果中: {}",
788            json
789        );
790
791        let roundtrip: PluginContext = serde_json::from_str(&json).expect("上下文应可反序列化");
792        assert!(
793            roundtrip.query_revision_gate.is_none(),
794            "反序列化后门控应为 None"
795        );
796        assert!(roundtrip.is_query_current());
797        assert_eq!(
798            roundtrip.query_channel,
799            QueryChannel::Ui,
800            "通道字段应参与序列化且缺省为 GUI 通道"
801        );
802    }
803
804    #[test]
805    /// 验证通道与目标类型的序列化键名契约(serde-rename)。
806    fn query_channel_and_target_type_serialize_with_stable_keys() {
807        assert_eq!(
808            serde_json::to_value(QueryChannel::Ui).unwrap(),
809            json!("ui"),
810            "GUI 通道键名应为 ui"
811        );
812        assert_eq!(
813            serde_json::to_value(QueryChannel::Cli).unwrap(),
814            json!("cli"),
815            "CLI 通道键名应为 cli"
816        );
817        assert_eq!(
818            serde_json::to_value(QueryChannel::Panel).unwrap(),
819            json!("panel"),
820            "面板通道键名应为 panel"
821        );
822
823        use super::TargetType;
824        assert_eq!(
825            serde_json::to_value(TargetType::Plugin).unwrap(),
826            json!("Plugin"),
827            "插件目标类型键名应与前端词表一致"
828        );
829        assert_eq!(
830            serde_json::to_value(TargetType::Path).unwrap(),
831            json!("Path")
832        );
833    }
834
835    #[test]
836    /// 验证面板交互策略的 JSON 字段名、枚举值和默认值。
837    fn panel_interaction_serializes_with_stable_contract() {
838        let interaction = PanelInteraction {
839            query_trigger: PanelQueryTrigger::OnEnter,
840            query_debounce_ms: 300,
841            bindings: vec![PanelKeyBinding {
842                key: "Enter".to_string(),
843                action: PanelKeyAction::Confirm,
844            }],
845        };
846        let value = serde_json::to_value(&interaction).expect("交互策略应可序列化");
847        assert_eq!(
848            value,
849            json!({
850                "queryTrigger": "onEnter",
851                "queryDebounceMs": 300,
852                // 完整传输契约:bindings 始终序列化(no-skip-serializing-if),缺省为空列表
853                "bindings": [{"key": "Enter", "action": {"kind": "confirm"}}],
854            })
855        );
856
857        let default_value: PanelInteraction =
858            serde_json::from_value(json!({})).expect("缺失交互策略字段时应使用默认值");
859        assert_eq!(default_value, PanelInteraction::default());
860    }
861}